MainUnit.pas 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. unit MainUnit;
  2. (* ***** BEGIN LICENSE BLOCK *****
  3. * Version: MPL 1.1 or LGPL 2.1 with linking exception
  4. *
  5. * The contents of this file are subject to the Mozilla Public License Version
  6. * 1.1 (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. * http://www.mozilla.org/MPL/
  9. *
  10. * Software distributed under the License is distributed on an "AS IS" basis,
  11. * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12. * for the specific language governing rights and limitations under the
  13. * License.
  14. *
  15. * Alternatively, the contents of this file may be used under the terms of the
  16. * Free Pascal modified version of the GNU Lesser General Public License
  17. * Version 2.1 (the "FPC modified LGPL License"), in which case the provisions
  18. * of this license are applicable instead of those above.
  19. * Please see the file LICENSE.txt for additional information concerning this
  20. * license.
  21. *
  22. * The Original Code is Particle Swarm example for Graphics32
  23. *
  24. * The Initial Developer of the Original Code is
  25. * Anders Melander <[email protected]>
  26. *
  27. *
  28. * Portions created by the Initial Developer are Copyright (C) 2024
  29. * the Initial Developer. All Rights Reserved.
  30. *
  31. * ***** END LICENSE BLOCK ***** *)
  32. interface
  33. {$include GR32.inc}
  34. {-$define FADE_BLEND}
  35. {$define PARTICLE_AA}
  36. uses
  37. Messages,
  38. SysUtils, Classes,
  39. Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, Types,
  40. GR32,
  41. GR32_System,
  42. GR32_Image,
  43. GR32.Noise.Simplex;
  44. //------------------------------------------------------------------------------
  45. //
  46. // Particle Swarm example
  47. //
  48. //------------------------------------------------------------------------------
  49. // Control the motion of a swarm of particles using 3D Simplex Noise, the third
  50. // dimension being time.
  51. //------------------------------------------------------------------------------
  52. //
  53. // Based on ideas by:
  54. //
  55. // - Mattias Fagerlund
  56. // https://lotsacode.wordpress.com/2013/04/14/colored-perlin-particle-field/
  57. //
  58. // - Jake Weary
  59. // https://codepen.io/thepheer/pen/VwqqQG
  60. //
  61. // - Sadik Mussah
  62. // https://gist.github.com/smussah/118ff6b385feac2bde349dd21053e75d
  63. //
  64. //------------------------------------------------------------------------------
  65. const
  66. ParamParticlesCount = 5000; // Initial number of particles
  67. ParamFade: Byte = 240; // Fade-to-black factor: [0..255], 255 = no fade.
  68. ParamColorAlpha: Byte = 192; // Alpha of paint color
  69. ParamColorHueShift = 0.002; // Hue shift per frame
  70. ParamColorSaturation = 0.75;
  71. ParamColorLightness = 0.5;
  72. ParamParticleSpaceFactor = 0.003; // How much does the current position affect the simplex noise
  73. ParamParticleTimeFactor = 0.001; // How much does the current time affect the simplex noise
  74. ParamParticleVectorFactor = 0.25; // Amount of randomness in vector
  75. ParamParticleSpeedFactor = 0.95; // Velocity decay; <=1, 1=none
  76. type
  77. TParticleControl = (pcNone, pcAttract, pcRepulse, pcSlowmo);
  78. TParticle = record
  79. const
  80. MinLifetime = 1000;
  81. MaxLifetime = 10000;
  82. private
  83. FPosition: TFloatPoint;
  84. FTrail: TFloatPoint;
  85. FVelocity: TFloatPoint;
  86. FNoise: TSimplexNoise;
  87. FIteration: integer;
  88. FLifetime: integer;
  89. public
  90. procedure Initialize(ANoise: TSimplexNoise);
  91. procedure Reset(const ABounds: TRect);
  92. procedure Step(const ABounds: TRect; ParticleControl: TParticleControl; const ControlPoint: TPoint);
  93. procedure Render(Buffer: TBitmap32);
  94. end;
  95. const
  96. MSG_AFTERSHOW = WM_USER;
  97. type
  98. TFormMain = class(TForm)
  99. PaintBox: TPaintBox32;
  100. TimerFrameRate: TTimer;
  101. procedure TimerFrameRateTimer(Sender: TObject);
  102. procedure PaintBoxResize(Sender: TObject);
  103. procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  104. procedure FormShow(Sender: TObject);
  105. private
  106. FOptionAnimateColors: boolean;
  107. FOptionFade: boolean;
  108. FOptionRenderNoise: boolean;
  109. FHue: Single;
  110. FParticles: TArray<TParticle>;
  111. FNoise: TSimplexNoise;
  112. FFrameCount: integer;
  113. FFrameRateStopwatch: TStopwatch;
  114. FBenchmark: boolean;
  115. FIteration: integer;
  116. FFormHelp: TForm;
  117. procedure SetParticleCount(Value: integer);
  118. procedure AppEventsIdle(Sender: TObject; var Done: Boolean);
  119. procedure Render;
  120. procedure MsgAfterShow(var Msg: TMessage); message MSG_AFTERSHOW;
  121. public
  122. constructor Create(AOwner: TComponent); override;
  123. destructor Destroy; override;
  124. end;
  125. var
  126. FormMain: TFormMain;
  127. implementation
  128. {$R *.dfm}
  129. uses
  130. Windows,
  131. Math,
  132. GR32_Blend,
  133. GR32_LowLevel,
  134. {$ifdef FPC}
  135. GR32_Geometry,
  136. {$endif FPC}
  137. HelpUnit;
  138. { TFormMain }
  139. constructor TFormMain.Create(AOwner: TComponent);
  140. begin
  141. inherited;
  142. FNoise := TSimplexNoise.Create;
  143. FOptionAnimateColors := True;
  144. FOptionFade := True;
  145. FFrameRateStopwatch := TStopwatch.StartNew;
  146. FBenchmark := FindCmdLineSwitch('benchmark');
  147. Application.OnIdle := AppEventsIdle;
  148. FFormHelp := TFormHelp.Create(Self);
  149. end;
  150. destructor TFormMain.Destroy;
  151. begin
  152. FNoise.Free;
  153. inherited;
  154. end;
  155. procedure TFormMain.AppEventsIdle(Sender: TObject; var Done: Boolean);
  156. begin
  157. Render;
  158. end;
  159. procedure TFormMain.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  160. begin
  161. case Key of
  162. VK_ESCAPE:
  163. Close;
  164. VK_F1:
  165. FFormHelp.Visible := not FFormHelp.Visible;
  166. VK_ADD:
  167. SetParticleCount(Length(FParticles) + 500);
  168. VK_SUBTRACT:
  169. if (Length(FParticles) > 500) then
  170. SetParticleCount(Length(FParticles) - 500);
  171. Ord('C'):
  172. FOptionAnimateColors := not FOptionAnimateColors;
  173. Ord('F'):
  174. FOptionFade := not FOptionFade;
  175. Ord('N'):
  176. FOptionRenderNoise := not FOptionRenderNoise;
  177. end;
  178. end;
  179. procedure TFormMain.FormShow(Sender: TObject);
  180. begin
  181. PostMessage(Handle, MSG_AFTERSHOW, 0, 0);
  182. end;
  183. procedure TFormMain.MsgAfterShow(var Msg: TMessage);
  184. var
  185. r: TRect;
  186. begin
  187. r := ClientToScreen(ClientRect);
  188. FFormHelp.Top := r.Top;
  189. FFormHelp.left := r.Right - FFormHelp.Width;
  190. ShowWindow(FFormHelp.Handle, SW_SHOWNA);
  191. FFormHelp.Visible := True;
  192. SetParticleCount(ParamParticlesCount);
  193. end;
  194. procedure TFormMain.PaintBoxResize(Sender: TObject);
  195. begin
  196. // Clear to some color other than black so we avoid the ghosts caused
  197. // by fade using blend never reaching complete black.
  198. {$ifdef FADE_BLEND}
  199. PaintBox.Buffer.Clear(HSLtoRGB(0.0, 0.75, 0.5, 255));
  200. {$endif}
  201. end;
  202. procedure TFormMain.Render;
  203. var
  204. i: integer;
  205. x, y: integer;
  206. z: Single;
  207. Pixel: PColor32;
  208. ParticleControl: TParticleControl;
  209. MousePos: TPoint;
  210. begin
  211. Inc(FFrameCount);
  212. Inc(FIteration);
  213. PaintBox.Buffer.BeginUpdate;
  214. try
  215. if (FOptionRenderNoise) then
  216. begin
  217. // Display 3D Simplex Noise as color Hue where time is the third dimension
  218. z := GetTickCount * ParamParticleTimeFactor;
  219. Pixel := PColor32(PaintBox.Buffer.Bits); // We could have used PaintBox.Buffer.Pixel[x, y] here
  220. // but the loop is slow enough without it...
  221. for y := 0 to PaintBox.Buffer.Height-1 do
  222. for x := 0 to PaintBox.Buffer.Width-1 do
  223. begin
  224. Pixel^ := HSLtoRGB(
  225. (FNoise.Noise(X*ParamParticleSpaceFactor, Y*ParamParticleSpaceFactor, z) + 0.5) / 2,
  226. ParamColorSaturation,
  227. ParamColorLightness,
  228. 255);
  229. Inc(Pixel);
  230. end;
  231. PaintBox.Buffer.Changed;
  232. end else
  233. begin
  234. // Fade to black
  235. // Ideally we would fade by adjusting the L channel of a HSL color but that is far too expensive
  236. if (FOptionFade) then
  237. {$ifdef FADE_BLEND}
  238. // We fade out the existing image by blending black onto it. The alpha controls how fast we fade.
  239. BlendMems($09000000, PColor32(PaintBox.Buffer.Bits), PaintBox.Buffer.Width*PaintBox.Buffer.Height);
  240. {$else}
  241. // Fade out by scaling the RGB: Faded = Colors * Weight / 255
  242. ScaleMems(PColor32(PaintBox.Buffer.Bits), PaintBox.Buffer.Width*PaintBox.Buffer.Height, ParamFade);
  243. {$endif}
  244. // Color cycle
  245. PaintBox.Buffer.PenColor := HSLtoRGB(FHue, ParamColorSaturation, ParamColorLightness, ParamColorAlpha);
  246. if (FOptionAnimateColors) then
  247. begin
  248. FHue := FHue + ParamColorHueShift;
  249. FHue := FHue - Floor(FHue);
  250. end;
  251. // Live user interaction
  252. MousePos := PaintBox.ScreenToClient(Mouse.CursorPos);
  253. if (GetAsyncKeyState(VK_LBUTTON) and $8000 <> 0) then
  254. ParticleControl := pcAttract
  255. else
  256. if (GetAsyncKeyState(VK_RBUTTON) and $8000 <> 0) then
  257. ParticleControl := pcRepulse
  258. else
  259. if (GetAsyncKeyState(VK_MBUTTON) and $8000 <> 0) then
  260. ParticleControl := pcSlowmo
  261. else
  262. ParticleControl := pcNone;
  263. // Move and render particles
  264. for i := 0 to High(FParticles) do
  265. begin
  266. FParticles[i].Step(PaintBox.Buffer.BoundsRect, ParticleControl, MousePos);
  267. FParticles[i].Render(PaintBox.Buffer);
  268. end;
  269. if (FOptionFade) then
  270. PaintBox.ForceFullInvalidate;
  271. end;
  272. finally
  273. PaintBox.Buffer.EndUpdate;
  274. end;
  275. if (FBenchmark) and (FIteration > 10000) then
  276. Application.Terminate;
  277. end;
  278. procedure TFormMain.SetParticleCount(Value: integer);
  279. var
  280. Count: integer;
  281. begin
  282. Count := Length(FParticles);
  283. SetLength(FParticles, Value);
  284. while (Count < Value) do
  285. begin
  286. FParticles[Count].Initialize(FNoise);
  287. FParticles[Count].Reset(PaintBox.Buffer.BoundsRect);
  288. Inc(Count);
  289. end;
  290. PaintBox.Invalidate;
  291. end;
  292. procedure TFormMain.TimerFrameRateTimer(Sender: TObject);
  293. var
  294. FPS: Single;
  295. begin
  296. FFrameRateStopwatch.Stop;
  297. TTimer(Sender).Enabled := False;
  298. if (FFrameRateStopwatch.ElapsedMilliseconds <> 0) then
  299. FPS := 1000 * FFrameCount / FFrameRateStopwatch.ElapsedMilliseconds
  300. else
  301. FPS := 0;
  302. Caption := Format('%.0n particles @ %.0n fps', [Length(FParticles)*1.0, FPS]);
  303. FFrameCount := 0;
  304. TTimer(Sender).Enabled := True;
  305. FFrameRateStopwatch := TStopwatch.StartNew;
  306. end;
  307. { TParticle }
  308. procedure TParticle.Initialize(ANoise: TSimplexNoise);
  309. begin
  310. FNoise := ANoise;
  311. end;
  312. procedure TParticle.Render(Buffer: TBitmap32);
  313. begin
  314. {$if defined(PARTICLE_AA)}
  315. Buffer.MoveToF(FTrail.X, FTrail.Y);
  316. Buffer.LineToFS(FPosition.X, FPosition.Y);
  317. {$else}
  318. Buffer.MoveTo(Round(FTrail.X), Round(FTrail.Y));
  319. Buffer.LineToS(Round(FPosition.X), Round(FPosition.Y));
  320. {$ifend}
  321. end;
  322. procedure TParticle.Reset(const ABounds: TRect);
  323. begin
  324. FPosition.X := Random(ABounds.Width);
  325. FPosition.Y := Random(ABounds.Height);
  326. FTrail := FPosition;
  327. FVelocity.X := 1;
  328. FVelocity.Y := 1;
  329. FIteration := 0;
  330. FLifetime := MinLifetime + Random(MaxLifetime-MinLifetime);
  331. end;
  332. procedure TParticle.Step(const ABounds: TRect; ParticleControl: TParticleControl; const ControlPoint: TPoint);
  333. var
  334. x, y, z: Single;
  335. Angle: Single;
  336. Factor: Single;
  337. Wrapped: boolean;
  338. MouseDistance: Single;
  339. begin
  340. Inc(FIteration);
  341. if (FIteration > FLifetime) then
  342. Reset(ABounds); // "Respawn"
  343. x := FPosition.X * ParamParticleSpaceFactor;
  344. y := FPosition.Y * ParamParticleSpaceFactor;
  345. z := GetTickCount * ParamParticleTimeFactor;
  346. Angle := Random * 2 * PI;
  347. Factor := Random * ParamParticleVectorFactor;
  348. // Calculate the new velocity based on the noise; Random velocity in a random direction
  349. FVelocity.X := FVelocity.X + Factor * Sin(Angle) + FNoise.Noise(x, y, -z);
  350. FVelocity.Y := FVelocity.Y + Factor * Cos(Angle) + FNoise.Noise(x, y, z);
  351. // Alter the vector according to user interaction
  352. case ParticleControl of
  353. pcAttract:
  354. // Add a difference between mouse pos and particle pos (a fraction of it) to the velocity.
  355. begin
  356. FVelocity.X := FVelocity.X + (ControlPoint.X - FPosition.X) * 0.00085;
  357. FVelocity.Y := FVelocity.Y + (ControlPoint.Y - FPosition.Y) * 0.00085;
  358. end;
  359. pcRepulse:
  360. // Repulse the particles if the right mouse button is down and the distance between
  361. // the mouse and particle is below an arbitrary value between 200 and 250.
  362. begin
  363. {$ifndef FPC}
  364. MouseDistance := FPosition.Distance(ControlPoint);
  365. {$else FPC}
  366. MouseDistance := Distance(FPosition, FloatPoint(Mouse.CursorPos));
  367. {$endif FPC}
  368. if (MouseDistance < 200+Random(50)) then
  369. begin
  370. FVelocity.X := FVelocity.X + (FPosition.X - ControlPoint.X) * 0.02;
  371. FVelocity.Y := FVelocity.Y + (FPosition.Y - ControlPoint.Y) * 0.02;
  372. end;
  373. end;
  374. pcSlowmo:
  375. // Time dilation field, stuff moves slower here, depending on distance
  376. begin
  377. {$ifndef FPC}
  378. MouseDistance := FPosition.Distance(ControlPoint);
  379. {$else FPC}
  380. MouseDistance := Distance(FPosition, FloatPoint(Mouse.CursorPos));
  381. {$endif FPC}
  382. Factor := MouseDistance / (200 + Random * 50);
  383. if (Factor < 1) then
  384. begin
  385. FVelocity.X := FVelocity.X * Factor;
  386. FVelocity.Y := FVelocity.Y * Factor;
  387. end;
  388. end;
  389. end;
  390. // Update position
  391. FTrail := FPosition;
  392. FPosition.X := FPosition.X + FVelocity.X;
  393. FPosition.Y := FPosition.Y + FVelocity.Y;
  394. // Slow down the velocity slightly
  395. FVelocity.X := FVelocity.X * ParamParticleSpeedFactor;
  396. FVelocity.Y := FVelocity.Y * ParamParticleSpeedFactor;
  397. // Wrap around the edges
  398. Wrapped := False;
  399. if (FPosition.X < ABounds.Left) then
  400. begin
  401. FPosition.X := ABounds.Right;
  402. Wrapped := True;
  403. end else
  404. if (FPosition.X > ABounds.Right) then
  405. begin
  406. FPosition.X := ABounds.Left;
  407. Wrapped := True;
  408. end;
  409. if (FPosition.Y < ABounds.Top) then
  410. begin
  411. FPosition.Y := ABounds.Bottom;
  412. Wrapped := True;
  413. end else
  414. if (FPosition.Y > ABounds.Bottom) then
  415. begin
  416. FPosition.Y := ABounds.Top;
  417. Wrapped := True;
  418. end;
  419. if (Wrapped) then
  420. FTrail := FPosition;
  421. end;
  422. end.