CParticle.cpp 85 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302
  1. #include "CParticle.h"
  2. #include "WallpaperEngine/Data/Model/Property.h"
  3. #include "WallpaperEngine/Logging/Log.h"
  4. #include "WallpaperEngine/Maths.h"
  5. #include "WallpaperEngine/Render/Utils/NoiseUtils.h"
  6. #include <GL/glew.h>
  7. #include <algorithm>
  8. #include <cmath>
  9. #include <glm/gtc/constants.hpp>
  10. #include <glm/gtc/matrix_transform.hpp>
  11. extern float g_Time;
  12. extern float g_RealTime;
  13. using namespace WallpaperEngine::Render::Objects;
  14. using namespace WallpaperEngine::Render::Utils;
  15. using namespace WallpaperEngine::Data::Model;
  16. CParticle::CParticle (Wallpapers::CScene& scene, const Particle& particle) :
  17. CObject (scene, particle), CRenderable (scene, particle, *particle.material->material),
  18. ScriptableObject (scene, particle), m_particle (particle) {
  19. this->registerProperty ("scale", *particle.scale->value);
  20. this->registerProperty ("angles", *particle.angles->value);
  21. this->registerProperty ("visible", *particle.visible->value);
  22. this->registerProperty ("parallaxDepth", *particle.parallaxDepth->value);
  23. this->detectTexture ();
  24. std::random_device rd;
  25. m_rng.seed (rd ());
  26. // Read renderer config early - buffer sizing below depends on it
  27. if (!m_particle.renderers.empty ()) {
  28. const auto& renderer = m_particle.renderers[0];
  29. if (renderer.name == "rope" || renderer.name == "ropetrail") {
  30. // Both rope and ropetrail use genericropeparticle shader
  31. m_useRopeRenderer = true;
  32. m_ropeSubdivision = std::max (0, static_cast<int> (renderer.subdivision));
  33. m_ropeUVScale = renderer.uvScale;
  34. m_ropeUVScrolling = renderer.uvScrolling;
  35. m_ropeUVSmoothing = renderer.uvSmoothing;
  36. if (renderer.name == "ropetrail") {
  37. m_useTrailRenderer = true;
  38. m_trailLength = renderer.length;
  39. m_ropeSegments = std::max (2, static_cast<int> (renderer.segments));
  40. }
  41. } else if (renderer.name == "spritetrail") {
  42. // spritetrail uses genericparticle with TRAILRENDERER combo
  43. m_useTrailRenderer = true;
  44. m_trailLength = renderer.length;
  45. m_trailMaxLength = renderer.maxLength;
  46. m_trailMinLength = renderer.minLength;
  47. }
  48. }
  49. float countMultiplier = particle.instanceOverride.count->value->getFloat ();
  50. uint32_t adjustedMaxCount = static_cast<uint32_t> (particle.maxCount * countMultiplier);
  51. // Use wallpaper's specified count, or default if maxCount is 0
  52. m_maxParticles = (adjustedMaxCount > 0) ? adjustedMaxCount : DEFAULT_MAX_PARTICLES;
  53. m_particles.resize (m_maxParticles);
  54. if (m_useRopeRenderer) {
  55. // Rope: N particles connect via (N-1) segments, each subdivided into sub-segments
  56. const int subdivision = std::max (1, m_ropeSubdivision);
  57. const int maxSubSegments = std::max (1, static_cast<int> (m_maxParticles - 1)) * subdivision;
  58. m_vertices.resize (maxSubSegments * 4 * ROPE_FLOATS_PER_VERTEX);
  59. m_indices.resize (maxSubSegments * 6);
  60. } else {
  61. // 4 vertices, 6 indices per particle
  62. const int verticesPerParticle = 4;
  63. const int indicesPerParticle = 6;
  64. m_vertices.resize (m_maxParticles * verticesPerParticle * SPRITE_FLOATS_PER_VERTEX);
  65. m_indices.resize (m_maxParticles * indicesPerParticle);
  66. }
  67. }
  68. CParticle::~CParticle () {
  69. delete m_pass;
  70. if (m_vao != 0) {
  71. glDeleteVertexArrays (1, &m_vao);
  72. }
  73. if (m_vbo != 0) {
  74. glDeleteBuffers (1, &m_vbo);
  75. }
  76. if (m_ebo != 0) {
  77. glDeleteBuffers (1, &m_ebo);
  78. }
  79. m_vertices.clear ();
  80. m_indices.clear ();
  81. }
  82. void CParticle::setup () {
  83. if (m_initialized) {
  84. return;
  85. }
  86. // Convert origin from screen space (0,0 top-left) to centered space, matching the
  87. // ortho(-width/2, width/2, -height/2, height/2) projection
  88. m_lastScreenWidth = getScene ().getCamera ().getWidth ();
  89. m_lastScreenHeight = getScene ().getCamera ().getHeight ();
  90. glm::vec3 origin = m_particle.origin->value->getVec3 ();
  91. origin.x -= m_lastScreenWidth / 2.0f;
  92. origin.y = m_lastScreenHeight / 2.0f - origin.y;
  93. m_transformedOrigin = origin;
  94. if (m_particle.material && m_particle.material->material && !m_particle.material->material->passes.empty ()) {
  95. auto& firstPass = *m_particle.material->material->passes.begin ();
  96. // Overbright: brightness multiplier for additive particles
  97. auto overbrightIt = firstPass->constants.find ("ui_editor_properties_overbright");
  98. if (overbrightIt != firstPass->constants.end ()) {
  99. m_overbright = overbrightIt->second->value->getFloat ();
  100. }
  101. }
  102. // TextureParser computes the spritesheet grid from TEXS frame data (animated textures) or
  103. // .tex-json metadata (static textures). GIF-style animated textures (separate GL texture per
  104. // frame) get 0 cols/rows since a 1x1 grid can't hold all frames - no SPRITESHEET mode needed,
  105. // frame switching happens via texture ID instead.
  106. if (const auto texture = getTexture ()) {
  107. m_spritesheetCols = static_cast<int> (texture->getSpritesheetCols ());
  108. m_spritesheetRows = static_cast<int> (texture->getSpritesheetRows ());
  109. m_spritesheetFrames = static_cast<int> (texture->getSpritesheetFrames ());
  110. m_spritesheetDuration = texture->getSpritesheetDuration ();
  111. }
  112. setupEmitters ();
  113. setupInitializers ();
  114. setupOperators ();
  115. setupPass ();
  116. m_controlPoints.resize (8);
  117. for (const auto& cp : m_particle.controlPoints) {
  118. if (cp.id >= 0 && cp.id < 8) {
  119. m_controlPoints[cp.id].offset = cp.offset;
  120. // flags bit 0 = linkMouse
  121. m_controlPoints[cp.id].linkMouse = (cp.flags & 1) != 0;
  122. m_controlPoints[cp.id].worldSpace = (cp.flags & 2) != 0;
  123. if (m_controlPoints[cp.id].linkMouse) {
  124. m_hasMouseControlPoint = true;
  125. }
  126. // Mouse-linked CPs get their position from update() instead
  127. if (!m_controlPoints[cp.id].linkMouse) {
  128. if (m_controlPoints[cp.id].worldSpace) {
  129. // World space: offset is in screen-centered coords, convert to particle local space
  130. m_controlPoints[cp.id].position = cp.offset - m_transformedOrigin;
  131. } else {
  132. // Local space: offset is already relative to particle system center
  133. m_controlPoints[cp.id].position = cp.offset;
  134. }
  135. }
  136. }
  137. }
  138. m_initialized = true;
  139. }
  140. void CParticle::render () {
  141. if (!m_initialized) {
  142. return;
  143. }
  144. const auto& appContext = this->getScene ().getContext ().getApp ().getContext ();
  145. const auto visibility = appContext.resolveObjectVisibility (this->getId (), this->getObject ().name);
  146. if (!visibility.value_or (m_particle.visible->value->getBool ())) {
  147. return;
  148. }
  149. syncTransformedOrigin ();
  150. // stop() drops every particle, and a later play() starts emitting from scratch
  151. const auto playback = this->getPlayback ();
  152. if (playback == Playback::Stopped) {
  153. m_particleCount = 0;
  154. } else if (m_lastPlayback == Playback::Stopped) {
  155. m_emitters.clear ();
  156. setupEmitters ();
  157. }
  158. m_lastPlayback = playback;
  159. const float currentTime = m_hasMouseControlPoint ? g_RealTime : g_Time;
  160. // Initialize time on first render to avoid a huge dt spike, and skip the update
  161. // that frame to avoid an initial burst
  162. if (m_time == 0.0) {
  163. m_time = currentTime;
  164. // "starttime" prewarms the system so it starts already populated instead of every
  165. // particle visibly leaving the emitter at once
  166. if (!m_prewarmed && m_particle.startTime > 0.0f && playback == Playback::Playing) {
  167. m_prewarmed = true;
  168. constexpr float step = 1.0f / 30.0f;
  169. m_time = currentTime - m_particle.startTime;
  170. for (float left = m_particle.startTime; left > 0.0f; left -= step) {
  171. const float dt = std::min (step, left);
  172. m_time += dt;
  173. update (dt);
  174. }
  175. m_time = currentTime;
  176. }
  177. if (m_useRopeRenderer) {
  178. renderRope ();
  179. } else {
  180. renderSprites ();
  181. }
  182. return;
  183. }
  184. float dt = currentTime - static_cast<float> (m_time);
  185. m_time = currentTime;
  186. if (dt > 0.0f && playback != Playback::Stopped) {
  187. // Cap dt to prevent simulation instability across different FPS
  188. dt = std::min (dt, 0.1f);
  189. update (dt);
  190. }
  191. if (m_particleCount > 0 && m_particle.material) {
  192. if (m_useRopeRenderer) {
  193. renderRope ();
  194. } else {
  195. renderSprites ();
  196. }
  197. }
  198. }
  199. bool CParticle::isPlaying () const {
  200. const auto playback = this->getPlayback ();
  201. return playback == Playback::Playing || (playback == Playback::Paused && m_particleCount > 0);
  202. }
  203. // scripts can move the system every frame (e.g. an origin that follows the cursor)
  204. void CParticle::syncTransformedOrigin () {
  205. const float screenWidth = static_cast<float> (getScene ().getWidth ());
  206. const float screenHeight = static_cast<float> (getScene ().getHeight ());
  207. glm::vec3 origin = m_particle.origin->value->getVec3 ();
  208. origin.x -= screenWidth / 2.0f;
  209. origin.y = screenHeight / 2.0f - origin.y;
  210. if (origin == m_transformedOrigin && screenWidth == m_lastScreenWidth && screenHeight == m_lastScreenHeight) {
  211. return;
  212. }
  213. m_transformedOrigin = origin;
  214. m_lastScreenWidth = screenWidth;
  215. m_lastScreenHeight = screenHeight;
  216. for (auto& cp : m_controlPoints) {
  217. if (!cp.linkMouse && cp.worldSpace) {
  218. cp.position = cp.offset - m_transformedOrigin;
  219. }
  220. }
  221. }
  222. void CParticle::update (float dt) {
  223. float screenWidth = static_cast<float> (getScene ().getWidth ());
  224. float screenHeight = static_cast<float> (getScene ().getHeight ());
  225. const glm::vec2* mousePos = getScene ().getMousePositionNormalized ();
  226. if (mousePos) {
  227. for (auto& cp : m_controlPoints) {
  228. if (cp.linkMouse) {
  229. // Convert mouse position from normalized [0,1] to centered screen space
  230. glm::vec3 position;
  231. position.x = (mousePos->x * screenWidth) - (screenWidth / 2.0f);
  232. position.y = (screenHeight / 2.0f) - (mousePos->y * screenHeight);
  233. position.z = 0.0f;
  234. position += cp.offset;
  235. // Subtract transformed origin to keep in particle local space (avoids
  236. // double transformation by the model matrix)
  237. cp.position = position - m_transformedOrigin;
  238. }
  239. }
  240. }
  241. // pause() stops emission but keeps simulating what is already alive
  242. if (this->getPlayback () == Playback::Playing) {
  243. for (auto& emitter : m_emitters) {
  244. emitter (m_particles, m_particleCount, dt);
  245. }
  246. }
  247. for (uint32_t i = 0; i < m_particleCount; i++) {
  248. m_particles[i].age += dt;
  249. }
  250. for (auto& op : m_operators) {
  251. op (m_particles, m_particleCount, m_controlPoints, static_cast<float> (m_time), dt);
  252. }
  253. for (uint32_t i = 0; i < m_particleCount; i++) {
  254. auto& p = m_particles[i];
  255. if (m_spritesheetFrames > 0) {
  256. float lifetimePos = p.getLifetimePos ();
  257. float animSpeed = m_particle.sequenceMultiplier > 0.0f ? m_particle.sequenceMultiplier : 1.0f;
  258. if (m_particle.animationMode == "randomframe") {
  259. if (p.frame < 0.0f) {
  260. std::mt19937 particleRng (
  261. static_cast<std::mt19937::result_type> (reinterpret_cast<uintptr_t> (&p))
  262. );
  263. std::uniform_int_distribution<int> dist (0, m_spritesheetFrames - 1);
  264. p.frame = static_cast<float> (dist (particleRng));
  265. }
  266. } else if (m_particle.animationMode == "once") {
  267. p.frame = std::min (
  268. lifetimePos * m_spritesheetFrames * animSpeed, static_cast<float> (m_spritesheetFrames - 1)
  269. );
  270. } else {
  271. if (m_spritesheetDuration > 0.0f) {
  272. float timeInCycle = std::fmod (p.age * animSpeed, m_spritesheetDuration);
  273. float cyclePos = timeInCycle / m_spritesheetDuration;
  274. p.frame = std::fmod (cyclePos * m_spritesheetFrames, static_cast<float> (m_spritesheetFrames));
  275. } else {
  276. p.frame = std::fmod (
  277. lifetimePos * m_spritesheetFrames * animSpeed, static_cast<float> (m_spritesheetFrames)
  278. );
  279. }
  280. }
  281. }
  282. }
  283. // Order-preserving compaction: particles only die from lifetime expiry (never from
  284. // size, since size can oscillate), and index 0 must stay the oldest particle
  285. uint32_t writeIdx = 0;
  286. for (uint32_t readIdx = 0; readIdx < m_particleCount; readIdx++) {
  287. if (m_particles[readIdx].isAlive ()) {
  288. if (writeIdx != readIdx) {
  289. m_particles[writeIdx] = m_particles[readIdx];
  290. }
  291. writeIdx++;
  292. }
  293. }
  294. m_particleCount = writeIdx;
  295. }
  296. const Particle& CParticle::getParticle () const { return m_particle; }
  297. const float& CParticle::getBrightness () const { return m_overbright; }
  298. const float& CParticle::getUserAlpha () const { return m_particle.instanceOverride.alpha->value->getFloat (); }
  299. const float& CParticle::getAlpha () const { return m_particle.instanceOverride.alpha->value->getFloat (); }
  300. const glm::vec3& CParticle::getColor () const {
  301. static const glm::vec3 defaultColor (1.0f);
  302. if (m_particle.instanceOverride.color && m_particle.instanceOverride.color->value) {
  303. return m_particle.instanceOverride.color->value->getVec3 ();
  304. }
  305. return defaultColor;
  306. }
  307. const glm::vec4& CParticle::getColor4 () const {
  308. static const glm::vec4 defaultColor (1.0f);
  309. if (m_particle.instanceOverride.color && m_particle.instanceOverride.color->value) {
  310. return m_particle.instanceOverride.color->value->getVec4 ();
  311. }
  312. return defaultColor;
  313. }
  314. const glm::vec3& CParticle::getCompositeColor () const { return getColor (); }
  315. // ========== EMITTERS ==========
  316. void CParticle::setupEmitters () {
  317. for (const auto& emitter : m_particle.emitters) {
  318. EmitterFunc func;
  319. if (emitter.name == "boxrandom") {
  320. func = createBoxEmitter (emitter);
  321. } else if (emitter.name == "sphererandom") {
  322. func = createSphereEmitter (emitter);
  323. } else {
  324. sLog.out ("Unknown emitter type: ", emitter.name);
  325. continue;
  326. }
  327. if (func) {
  328. m_emitters.push_back (std::move (func));
  329. }
  330. }
  331. }
  332. float CParticle::sampleAudio (
  333. int mode, const glm::vec2& bounds, float exponent, int frequencyStart, int frequencyEnd
  334. ) const {
  335. // same curve as wallpaper64.exe. Modes 1/2/3 pick left/right/averaged channels there, the recorder is mono
  336. if (mode == 0) {
  337. return 1.0f;
  338. }
  339. int first = std::clamp (frequencyStart, 0, 15);
  340. int last = std::clamp (frequencyEnd, 0, 15);
  341. if (last < first) {
  342. std::swap (first, last);
  343. }
  344. const auto& recorder = this->getScene ().getAudioContext ().getRecorder ();
  345. float peak = 0.0f;
  346. recorder.lock ();
  347. for (int i = first; i <= last; i++) {
  348. peak = std::max (peak, recorder.audio16[i]);
  349. }
  350. recorder.unlock ();
  351. float t = (peak - bounds.x) / (bounds.y - bounds.x);
  352. // NaN from equal bounds ends up as 0 like the original
  353. t = t >= 1.0f ? 1.0f : (t >= 0.0f ? t : 0.0f);
  354. const float response = std::pow (t * t * (3.0f - 2.0f * t), exponent);
  355. return response >= 1.0f ? 1.0f : (response >= 0.0f ? response : 0.0f);
  356. }
  357. EmitterFunc CParticle::createBoxEmitter (const ParticleEmitter& emitter) {
  358. float rate = emitter.rate * m_particle.instanceOverride.rate->value->getFloat ();
  359. glm::vec3 transformedEmitterOrigin = emitter.origin;
  360. transformedEmitterOrigin.y = -transformedEmitterOrigin.y;
  361. int controlPointIndex = emitter.controlPoint;
  362. if (controlPointIndex == -1 && !m_particle.controlPoints.empty ()) {
  363. const auto& cp0 = m_particle.controlPoints[0];
  364. if ((cp0.flags & 1) != 0) {
  365. controlPointIndex = 0;
  366. }
  367. }
  368. glm::vec3 flippedDirections = emitter.directions;
  369. flippedDirections.y = -flippedDirections.y;
  370. bool limitOnePerFrame = (emitter.flags & 2) != 0;
  371. bool randomPeriodicEmission = (emitter.flags & 4) != 0;
  372. return
  373. [this, emitter, transformedEmitterOrigin, controlPointIndex, rate, flippedDirections, limitOnePerFrame,
  374. randomPeriodicEmission, emissionTimer = 0.0f, delayTimer = emitter.delay, durationTimer = 0.0f,
  375. periodicTimer = 0.0f, periodicDuration = 0.0f, periodicDelay = 0.0f, emitting = false,
  376. instantaneousEmitted = false] (std::vector<ParticleInstance>& particles, uint32_t& count, float dt) mutable {
  377. if (count >= particles.size ()) {
  378. return;
  379. }
  380. if (delayTimer > 0.0f) {
  381. delayTimer -= dt;
  382. return;
  383. }
  384. if (emitter.duration > 0.0f) {
  385. durationTimer += dt;
  386. if (durationTimer >= emitter.duration) {
  387. return;
  388. }
  389. }
  390. if (randomPeriodicEmission) {
  391. periodicTimer += dt;
  392. if (!emitting) {
  393. if (periodicTimer >= periodicDelay) {
  394. emitting = true;
  395. periodicTimer = 0.0f;
  396. periodicDuration = WallpaperEngine::Maths::randomFloat (
  397. m_rng, emitter.minPeriodicDuration, emitter.maxPeriodicDuration
  398. );
  399. } else {
  400. return;
  401. }
  402. } else {
  403. if (periodicTimer >= periodicDuration) {
  404. emitting = false;
  405. periodicTimer = 0.0f;
  406. periodicDelay = WallpaperEngine::Maths::randomFloat (
  407. m_rng, emitter.minPeriodicDelay, emitter.maxPeriodicDelay
  408. );
  409. return;
  410. }
  411. }
  412. }
  413. uint32_t toEmit = 0;
  414. if (emitter.instantaneous > 0 && !instantaneousEmitted) {
  415. toEmit = emitter.instantaneous;
  416. instantaneousEmitted = true;
  417. }
  418. if (emitter.rate > 0.0f) {
  419. const float audio = sampleAudio (
  420. emitter.audioProcessingMode, emitter.audioProcessingBounds, emitter.audioProcessingExponent,
  421. emitter.audioProcessingFrequencyStart, emitter.audioProcessingFrequencyEnd
  422. );
  423. emissionTimer += dt * rate * audio;
  424. uint32_t rateEmit = static_cast<uint32_t> (emissionTimer);
  425. emissionTimer -= static_cast<float> (rateEmit);
  426. // limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
  427. if (limitOnePerFrame && rateEmit > 1) {
  428. rateEmit = 1;
  429. }
  430. toEmit += rateEmit;
  431. }
  432. for (uint32_t i = 0; i < toEmit && count < particles.size (); i++) {
  433. auto& p = particles[count];
  434. glm::vec3 spawnOrigin = transformedEmitterOrigin;
  435. if (controlPointIndex >= 0 && controlPointIndex < static_cast<int> (m_controlPoints.size ())) {
  436. spawnOrigin += m_controlPoints[controlPointIndex].position;
  437. }
  438. // Random position within the box volume (hollow box if distanceMin > 0)
  439. glm::vec3 randomPos;
  440. for (int axis = 0; axis < 3; axis++) {
  441. float minDist = emitter.distanceMin[axis];
  442. float maxDist = emitter.distanceMax[axis];
  443. float dist = WallpaperEngine::Maths::randomFloat (m_rng, minDist, maxDist);
  444. // Randomly flip sign to center the distribution
  445. if (WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) < 0.5f) {
  446. dist = -dist;
  447. }
  448. randomPos[axis] = dist;
  449. }
  450. randomPos *= flippedDirections;
  451. p.position = spawnOrigin + randomPos;
  452. // Emitter does not set velocity - initializers handle that
  453. p.velocity = glm::vec3 (0.0f);
  454. p.acceleration = glm::vec3 (0.0f);
  455. p.rotation = glm::vec3 (0.0f);
  456. p.angularVelocity = glm::vec3 (0.0f);
  457. p.angularAcceleration = glm::vec3 (0.0f);
  458. p.color = glm::vec3 (1.0f) * m_particle.instanceOverride.colorn->value->getVec3 ();
  459. p.alpha = 1.0f * m_particle.instanceOverride.alpha->value->getFloat ();
  460. p.size = 20.0f * m_particle.instanceOverride.size->value->getFloat ();
  461. p.lifetime = 1.0f * m_particle.instanceOverride.lifetime->value->getFloat ();
  462. p.age = 0.0f;
  463. p.alive = true;
  464. p.frame = -1.0f;
  465. p.initial.color = p.color;
  466. p.initial.alpha = p.alpha;
  467. p.initial.size = p.size;
  468. p.initial.lifetime = p.lifetime;
  469. // Reset oscillator state for reused particles
  470. p.oscillateAlpha = {};
  471. p.oscillateSize = {};
  472. p.oscillatePosition = {};
  473. for (auto& init : m_initializers) {
  474. init (p);
  475. }
  476. count++;
  477. }
  478. };
  479. }
  480. EmitterFunc CParticle::createSphereEmitter (const ParticleEmitter& emitter) {
  481. float rate = emitter.rate * m_particle.instanceOverride.rate->value->getFloat ();
  482. float lifetime = 1.0f * m_particle.instanceOverride.lifetime->value->getFloat ();
  483. // Convert emitter origin from screen space (Y down) to centered space (Y up)
  484. glm::vec3 transformedEmitterOrigin = emitter.origin;
  485. transformedEmitterOrigin.y = -transformedEmitterOrigin.y;
  486. int controlPointIndex = emitter.controlPoint;
  487. // Auto-detect control point 0 if not specified and CP0 has linkMouse
  488. if (controlPointIndex == -1 && !m_particle.controlPoints.empty ()) {
  489. const auto& cp0 = m_particle.controlPoints[0];
  490. if ((cp0.flags & 1) != 0) { // bit 0 = linkMouse
  491. controlPointIndex = 0;
  492. }
  493. }
  494. bool limitOnePerFrame = (emitter.flags & 2) != 0;
  495. return [this, emitter, transformedEmitterOrigin, controlPointIndex, rate, lifetime, limitOnePerFrame,
  496. emissionTimer = 0.0f,
  497. remaining
  498. = emitter.instantaneous] (std::vector<ParticleInstance>& particles, uint32_t& count, float dt) mutable {
  499. if (count >= particles.size ()) {
  500. return;
  501. }
  502. const float audio = sampleAudio (
  503. emitter.audioProcessingMode, emitter.audioProcessingBounds, emitter.audioProcessingExponent,
  504. emitter.audioProcessingFrequencyStart, emitter.audioProcessingFrequencyEnd
  505. );
  506. emissionTimer += dt * rate * audio;
  507. uint32_t toEmit = static_cast<uint32_t> (emissionTimer);
  508. emissionTimer -= static_cast<float> (toEmit);
  509. // limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
  510. if (limitOnePerFrame && toEmit > 1) {
  511. toEmit = 1;
  512. }
  513. if (remaining > 0) {
  514. toEmit = remaining;
  515. remaining = 0;
  516. }
  517. for (uint32_t i = 0; i < toEmit && count < particles.size (); i++) {
  518. auto& p = particles[count];
  519. glm::vec3 spawnOrigin = transformedEmitterOrigin;
  520. if (controlPointIndex >= 0 && controlPointIndex < static_cast<int> (m_controlPoints.size ())) {
  521. spawnOrigin += m_controlPoints[controlPointIndex].position;
  522. }
  523. glm::vec3 randomPos;
  524. // flags & 4 == 0: orthographic particles use a 2D disk distribution in X/Y
  525. // flags & 4 != 0: perspective particles use a 3D spherical shell distribution
  526. if ((m_particle.flags & 4) == 0) {
  527. float angle = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, glm::two_pi<float> ());
  528. float minRadius = emitter.distanceMin.x;
  529. float maxRadius = emitter.distanceMax.x;
  530. // Use sqrt for uniform area distribution in annulus
  531. float minRadiusSq = minRadius * minRadius;
  532. float maxRadiusSq = maxRadius * maxRadius;
  533. float radiusXY = std::sqrt (WallpaperEngine::Maths::randomFloat (m_rng, minRadiusSq, maxRadiusSq));
  534. randomPos = glm::vec3 (
  535. radiusXY * std::cos (angle), radiusXY * std::sin (angle),
  536. WallpaperEngine::Maths::randomFloat (m_rng, -maxRadius, maxRadius)
  537. );
  538. randomPos *= emitter.directions;
  539. } else {
  540. float theta = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, glm::two_pi<float> ());
  541. float cosTheta = WallpaperEngine::Maths::randomFloat (m_rng, -1.0f, 1.0f);
  542. float sinTheta = std::sqrt (1.0f - cosTheta * cosTheta);
  543. randomPos = glm::vec3 (sinTheta * std::cos (theta), sinTheta * std::sin (theta), cosTheta);
  544. // Use cubic root for uniform volume distribution
  545. float minRadius = emitter.distanceMin.x;
  546. float maxRadius = emitter.distanceMax.x;
  547. float minRadiusCubed = minRadius * minRadius * minRadius;
  548. float maxRadiusCubed = maxRadius * maxRadius * maxRadius;
  549. float radius = std::cbrt (WallpaperEngine::Maths::randomFloat (m_rng, minRadiusCubed, maxRadiusCubed));
  550. randomPos *= radius;
  551. randomPos *= emitter.directions;
  552. }
  553. // sign property forces per-axis polarity: 0 = both, 1 = positive only, -1 = negative only
  554. for (int i = 0; i < 3; i++) {
  555. if (emitter.sign[i] == 1) {
  556. randomPos[i] = std::abs (randomPos[i]);
  557. } else if (emitter.sign[i] == -1) {
  558. randomPos[i] = -std::abs (randomPos[i]);
  559. }
  560. }
  561. p.position = spawnOrigin + randomPos;
  562. // Set velocity only if emitter specifies speed (otherwise use initializers)
  563. if (emitter.speedMax > 0.0f || emitter.speedMin != 0.0f) {
  564. // Velocity pointing outward from ellipsoid (randomPos already includes directions scaling)
  565. glm::vec3 direction
  566. = glm::length (randomPos) > 0.0f ? glm::normalize (randomPos) : glm::vec3 (0.0f, 1.0f, 0.0f);
  567. float speed = WallpaperEngine::Maths::randomFloat (m_rng, emitter.speedMin, emitter.speedMax);
  568. p.velocity = direction * speed;
  569. } else {
  570. p.velocity = glm::vec3 (0.0f);
  571. }
  572. p.acceleration = glm::vec3 (0.0f);
  573. p.rotation = glm::vec3 (0.0f);
  574. p.angularVelocity = glm::vec3 (0.0f);
  575. p.angularAcceleration = glm::vec3 (0.0f);
  576. p.color = glm::vec3 (1.0f) * m_particle.instanceOverride.colorn->value->getVec3 ();
  577. p.alpha = 1.0f * m_particle.instanceOverride.alpha->value->getFloat ();
  578. p.size = 20.0f * m_particle.instanceOverride.size->value->getFloat ();
  579. p.lifetime = lifetime;
  580. p.age = 0.0f;
  581. p.alive = true;
  582. p.frame = -1.0f;
  583. p.initial.color = p.color;
  584. p.initial.alpha = p.alpha;
  585. p.initial.size = p.size;
  586. p.initial.lifetime = p.lifetime;
  587. // Reset oscillator state for reused particles
  588. p.oscillateAlpha = {};
  589. p.oscillateSize = {};
  590. p.oscillatePosition = {};
  591. for (auto& init : m_initializers) {
  592. init (p);
  593. }
  594. count++;
  595. }
  596. };
  597. }
  598. // ========== INITIALIZERS ==========
  599. void CParticle::setupInitializers () {
  600. for (const auto& initializer : m_particle.initializers) {
  601. if (!initializer) {
  602. continue;
  603. }
  604. InitializerFunc func;
  605. if (initializer->is<ColorRandomInitializer> ()) {
  606. func = createColorRandomInitializer (*initializer->as<ColorRandomInitializer> ());
  607. } else if (initializer->is<SizeRandomInitializer> ()) {
  608. func = createSizeRandomInitializer (*initializer->as<SizeRandomInitializer> ());
  609. } else if (initializer->is<AlphaRandomInitializer> ()) {
  610. func = createAlphaRandomInitializer (*initializer->as<AlphaRandomInitializer> ());
  611. } else if (initializer->is<LifetimeRandomInitializer> ()) {
  612. const auto& lifeInit = *initializer->as<LifetimeRandomInitializer> ();
  613. m_uniformLifetimes = (lifeInit.min->value->getFloat () == lifeInit.max->value->getFloat ());
  614. func = createLifetimeRandomInitializer (lifeInit);
  615. } else if (initializer->is<VelocityRandomInitializer> ()) {
  616. func = createVelocityRandomInitializer (*initializer->as<VelocityRandomInitializer> ());
  617. } else if (initializer->is<RotationRandomInitializer> ()) {
  618. func = createRotationRandomInitializer (*initializer->as<RotationRandomInitializer> ());
  619. } else if (initializer->is<AngularVelocityRandomInitializer> ()) {
  620. func = createAngularVelocityRandomInitializer (*initializer->as<AngularVelocityRandomInitializer> ());
  621. } else if (initializer->is<TurbulentVelocityRandomInitializer> ()) {
  622. func = createTurbulentVelocityRandomInitializer (*initializer->as<TurbulentVelocityRandomInitializer> ());
  623. } else if (initializer->is<MapSequenceAroundControlPointInitializer> ()) {
  624. func = createMapSequenceAroundControlPointInitializer (
  625. *initializer->as<MapSequenceAroundControlPointInitializer> ()
  626. );
  627. } else {
  628. sLog.out ("Unknown initializer type");
  629. }
  630. if (func) {
  631. m_initializers.push_back (std::move (func));
  632. }
  633. }
  634. }
  635. InitializerFunc CParticle::createColorRandomInitializer (const ColorRandomInitializer& init) {
  636. DynamicValue* minValue = init.min->value.get ();
  637. DynamicValue* maxValue = init.max->value.get ();
  638. DynamicValue* colorOverride = m_particle.instanceOverride.colorn->value.get ();
  639. return [this, minValue, maxValue, colorOverride] (ParticleInstance& p) {
  640. p.color = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
  641. * colorOverride->getVec3 ();
  642. p.initial.color = p.color;
  643. };
  644. }
  645. InitializerFunc CParticle::createSizeRandomInitializer (const SizeRandomInitializer& init) {
  646. DynamicValue* minValue = init.min->value.get ();
  647. DynamicValue* maxValue = init.max->value.get ();
  648. DynamicValue* exponentValue = init.exponent->value.get ();
  649. DynamicValue* sizeOverride = m_particle.instanceOverride.size->value.get ();
  650. return [this, minValue, maxValue, exponentValue, sizeOverride] (ParticleInstance& p) {
  651. float t = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f);
  652. float exponent = exponentValue->getFloat ();
  653. float min = minValue->getFloat ();
  654. float max = maxValue->getFloat ();
  655. // Apply exponent for non-linear distribution
  656. float adjustedT = std::pow (t, exponent);
  657. p.size = (min + adjustedT * (max - min)) * sizeOverride->getFloat () / 2.0f;
  658. p.initial.size = p.size;
  659. };
  660. }
  661. InitializerFunc CParticle::createAlphaRandomInitializer (const AlphaRandomInitializer& init) {
  662. DynamicValue* minValue = init.min->value.get ();
  663. DynamicValue* maxValue = init.max->value.get ();
  664. DynamicValue* alphaOverride = m_particle.instanceOverride.alpha->value.get ();
  665. return [this, minValue, maxValue, alphaOverride] (ParticleInstance& p) {
  666. p.alpha = WallpaperEngine::Maths::randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ())
  667. * alphaOverride->getFloat ();
  668. p.initial.alpha = p.alpha;
  669. };
  670. }
  671. InitializerFunc CParticle::createLifetimeRandomInitializer (const LifetimeRandomInitializer& init) {
  672. DynamicValue* minValue = init.min->value.get ();
  673. DynamicValue* maxValue = init.max->value.get ();
  674. DynamicValue* lifetimeOverride = m_particle.instanceOverride.lifetime->value.get ();
  675. return [this, minValue, maxValue, lifetimeOverride] (ParticleInstance& p) {
  676. p.lifetime = WallpaperEngine::Maths::randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ())
  677. * lifetimeOverride->getFloat ();
  678. p.initial.lifetime = p.lifetime;
  679. };
  680. }
  681. InitializerFunc CParticle::createVelocityRandomInitializer (const VelocityRandomInitializer& init) {
  682. DynamicValue* minValue = init.min->value.get ();
  683. DynamicValue* maxValue = init.max->value.get ();
  684. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  685. return [this, minValue, maxValue, speedOverride] (ParticleInstance& p) {
  686. glm::vec3 vel = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
  687. * speedOverride->getFloat ();
  688. vel.y = -vel.y;
  689. p.velocity += vel;
  690. };
  691. }
  692. InitializerFunc CParticle::createRotationRandomInitializer (const RotationRandomInitializer& init) {
  693. DynamicValue* minValue = init.min->value.get ();
  694. DynamicValue* maxValue = init.max->value.get ();
  695. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  696. return [this, minValue, maxValue, speedOverride] (ParticleInstance& p) {
  697. p.rotation = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
  698. * speedOverride->getFloat ();
  699. };
  700. }
  701. InitializerFunc CParticle::createAngularVelocityRandomInitializer (const AngularVelocityRandomInitializer& init) {
  702. DynamicValue* minValue = init.min->value.get ();
  703. DynamicValue* maxValue = init.max->value.get ();
  704. DynamicValue* exponentValue = init.exponent->value.get ();
  705. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  706. return [this, minValue, maxValue, exponentValue, speedOverride] (ParticleInstance& p) {
  707. glm::vec3 minVec = minValue->getVec3 ();
  708. glm::vec3 maxVec = maxValue->getVec3 ();
  709. float exponent = exponentValue->getFloat ();
  710. // exponent = 1: uniform; exponent -> 0: bias towards max; exponent >= 2: bias towards min
  711. glm::vec3 result;
  712. for (int i = 0; i < 3; i++) {
  713. float t = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f);
  714. t = std::pow (t, exponent);
  715. result[i] = minVec[i] + t * (maxVec[i] - minVec[i]);
  716. }
  717. p.angularVelocity = result * speedOverride->getFloat ();
  718. };
  719. }
  720. InitializerFunc CParticle::createTurbulentVelocityRandomInitializer (const TurbulentVelocityRandomInitializer& init) {
  721. DynamicValue* speedMin = init.speedMin->value.get ();
  722. DynamicValue* speedMax = init.speedMax->value.get ();
  723. DynamicValue* offsetVal = init.offset->value.get ();
  724. DynamicValue* scaleVal = init.scale->value.get ();
  725. DynamicValue* forwardVal = init.forward->value.get ();
  726. DynamicValue* timeScaleVal = init.timeScale->value.get ();
  727. DynamicValue* phaseMinVal = init.phaseMin->value.get ();
  728. DynamicValue* phaseMaxVal = init.phaseMax->value.get ();
  729. DynamicValue* rightVal = init.right->value.get ();
  730. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  731. return [this, speedMin, speedMax, offsetVal, scaleVal, forwardVal, timeScaleVal, phaseMinVal, phaseMaxVal, rightVal,
  732. speedOverride] (ParticleInstance& p) {
  733. glm::vec3 forward = forwardVal->getVec3 ();
  734. glm::vec3 right = rightVal->getVec3 ();
  735. // Y-flip for coordinate system conversion
  736. forward.y = -forward.y;
  737. right.y = -right.y;
  738. if (glm::length (forward) > 0.0001f) {
  739. forward = glm::normalize (forward);
  740. } else {
  741. // Default forward direction when not specified (up in centered space)
  742. forward = glm::vec3 (0.0f, 1.0f, 0.0f);
  743. }
  744. if (glm::length (right) > 0.0001f) {
  745. right = glm::normalize (right);
  746. } else {
  747. right = glm::vec3 (1.0f, 0.0f, 0.0f);
  748. }
  749. float speed = WallpaperEngine::Maths::randomFloat (m_rng, speedMin->getFloat (), speedMax->getFloat ());
  750. float scale = scaleVal->getFloat ();
  751. float offset = offsetVal->getFloat ();
  752. float timeScale = timeScaleVal->getFloat ();
  753. float phaseMin = phaseMinVal->getFloat ();
  754. float phaseMax = phaseMaxVal->getFloat ();
  755. // Sample noise at position + time offset: timescale shifts the field over time so
  756. // particles spawned at different times drift differently (evolving vapor stream);
  757. // the position term gives spatial coherence between nearby particles.
  758. glm::vec3 noisePos = p.position * 0.1f;
  759. noisePos += glm::vec3 (static_cast<float> (m_time) * timeScale);
  760. // Phase adds per-particle randomization to noise position
  761. float phase = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax);
  762. glm::vec3 samplePos = noisePos + glm::vec3 (phase, phase * 0.7f, phase * 1.3f);
  763. glm::vec3 result = curlNoise (samplePos);
  764. float len = glm::length (result);
  765. if (len < 0.0001f) {
  766. result = forward;
  767. } else {
  768. result = result / len;
  769. }
  770. // Scale limits how far direction can deviate from forward
  771. if (scale < 2.0f) {
  772. float cosAngle = glm::dot (result, forward);
  773. float angle = std::acos (glm::clamp (cosAngle, -1.0f, 1.0f)) / glm::pi<float> ();
  774. float maxAngle = scale / 2.0f;
  775. if (angle > maxAngle && maxAngle > 0.0001f) {
  776. glm::vec3 axis = glm::cross (result, forward);
  777. float axisLen = glm::length (axis);
  778. if (axisLen > 0.0001f) {
  779. axis = axis / axisLen;
  780. float rotAngle = (angle - maxAngle) * glm::pi<float> ();
  781. glm::mat3 rot = glm::mat3 (glm::rotate (glm::mat4 (1.0f), rotAngle, axis));
  782. result = rot * result;
  783. }
  784. }
  785. }
  786. // Offset rotates result around right axis (tilts up/down)
  787. if (std::abs (offset) > 0.0001f) {
  788. glm::mat3 rot = glm::mat3 (glm::rotate (glm::mat4 (1.0f), -offset, right));
  789. result = rot * result;
  790. }
  791. // 2D/orthographic particles (flags & 4 == 0): project onto XY. curlNoise is 3D but
  792. // z-drift is meaningless here and makes rope segments diverge in depth.
  793. if ((m_particle.flags & 4) == 0) {
  794. result.z = 0.0f;
  795. float len2d = glm::length (result);
  796. if (len2d > 0.0001f) {
  797. result /= len2d;
  798. }
  799. }
  800. glm::vec3 finalVel = result * speed * speedOverride->getFloat ();
  801. p.velocity += finalVel;
  802. };
  803. }
  804. InitializerFunc
  805. CParticle::createMapSequenceAroundControlPointInitializer (const MapSequenceAroundControlPointInitializer& init) {
  806. DynamicValue* controlPointValue = init.controlPoint->value.get ();
  807. DynamicValue* countValue = init.count->value.get ();
  808. DynamicValue* speedMinValue = init.speedMin->value.get ();
  809. DynamicValue* speedMaxValue = init.speedMax->value.get ();
  810. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  811. // Sequence counter is shared (closure state) across all particles spawned by this
  812. // initializer, giving each one a distinct angle around the circle
  813. int sequenceIndex = 0;
  814. return [this, controlPointValue, countValue, speedMinValue, speedMaxValue, sequenceIndex,
  815. speedOverride] (ParticleInstance& p) mutable {
  816. int controlPoint = static_cast<int> (controlPointValue->getFloat ());
  817. int count = static_cast<int> (countValue->getFloat ());
  818. if (count < 1) {
  819. count = 1;
  820. }
  821. float angle = (static_cast<float> (sequenceIndex) / static_cast<float> (count)) * glm::two_pi<float> ();
  822. sequenceIndex = (sequenceIndex + 1) % count;
  823. glm::vec3 centerPos = glm::vec3 (0.0f);
  824. if (controlPoint >= 0 && controlPoint < static_cast<int> (m_controlPoints.size ())) {
  825. centerPos = m_controlPoints[controlPoint].position;
  826. }
  827. p.position = centerPos;
  828. glm::vec3 speedMin = speedMinValue->getVec3 ();
  829. glm::vec3 speedMax = speedMaxValue->getVec3 ();
  830. glm::vec3 speed = WallpaperEngine::Maths::randomVec3 (m_rng, speedMin, speedMax);
  831. // Flip Y before rotation to convert to centered space
  832. speed.y = -speed.y;
  833. // Rotating by the sequence angle gives the outward radial/circular pattern
  834. glm::mat3 rotationMatrix = glm::mat3 (
  835. std::cos (angle), -std::sin (angle), 0.0f, std::sin (angle), std::cos (angle), 0.0f, 0.0f, 0.0f, 1.0f
  836. );
  837. glm::vec3 rotatedSpeed = rotationMatrix * speed * speedOverride->getFloat ();
  838. p.velocity = rotatedSpeed;
  839. };
  840. }
  841. // ========== OPERATORS ==========
  842. void CParticle::setupOperators () {
  843. for (const auto& op : m_particle.operators) {
  844. if (!op) {
  845. continue;
  846. }
  847. OperatorFunc func;
  848. if (op->is<MovementOperator> ()) {
  849. func = createMovementOperator (*op->as<MovementOperator> ());
  850. } else if (op->is<AngularMovementOperator> ()) {
  851. func = createAngularMovementOperator (*op->as<AngularMovementOperator> ());
  852. } else if (op->is<AlphaFadeOperator> ()) {
  853. func = createAlphaFadeOperator (*op->as<AlphaFadeOperator> ());
  854. } else if (op->is<SizeChangeOperator> ()) {
  855. func = createSizeChangeOperator (*op->as<SizeChangeOperator> ());
  856. } else if (op->is<AlphaChangeOperator> ()) {
  857. func = createAlphaChangeOperator (*op->as<AlphaChangeOperator> ());
  858. } else if (op->is<ColorChangeOperator> ()) {
  859. func = createColorChangeOperator (*op->as<ColorChangeOperator> ());
  860. } else if (op->is<TurbulenceOperator> ()) {
  861. func = createTurbulenceOperator (*op->as<TurbulenceOperator> ());
  862. } else if (op->is<VortexOperator> ()) {
  863. func = createVortexOperator (*op->as<VortexOperator> ());
  864. } else if (op->is<ControlPointAttractOperator> ()) {
  865. func = createControlPointAttractOperator (*op->as<ControlPointAttractOperator> ());
  866. } else if (op->is<OscillateAlphaOperator> ()) {
  867. func = createOscillateAlphaOperator (*op->as<OscillateAlphaOperator> ());
  868. } else if (op->is<OscillateSizeOperator> ()) {
  869. func = createOscillateSizeOperator (*op->as<OscillateSizeOperator> ());
  870. } else if (op->is<OscillatePositionOperator> ()) {
  871. func = createOscillatePositionOperator (*op->as<OscillatePositionOperator> ());
  872. } else {
  873. sLog.out ("Unknown operator type");
  874. }
  875. if (func) {
  876. m_operators.push_back (std::move (func));
  877. }
  878. }
  879. }
  880. OperatorFunc CParticle::createMovementOperator (const MovementOperator& op) {
  881. DynamicValue* dragValue = op.drag->value.get ();
  882. DynamicValue* gravityValue = op.gravity->value.get ();
  883. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  884. return [dragValue, gravityValue, speedOverride] (
  885. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  886. float dt
  887. ) {
  888. float speed = speedOverride->getFloat ();
  889. float drag = dragValue->getFloat ();
  890. glm::vec3 gravity = gravityValue->getVec3 ();
  891. // Flip gravity Y for centered space
  892. gravity.y = -gravity.y;
  893. for (uint32_t i = 0; i < count; i++) {
  894. auto& p = particles[i];
  895. if (!p.alive) {
  896. continue;
  897. }
  898. // Integrate position from current velocity (already speed-scaled) before
  899. // updating velocity for next frame
  900. p.position += p.velocity * dt;
  901. p.velocity += gravity * dt * speed;
  902. // Drag decay, clamped so drag*dt > 1.0 can't reverse velocity
  903. float dragFactor = 1.0f - (drag * dt);
  904. if (dragFactor < 0.0f) {
  905. dragFactor = 0.0f;
  906. }
  907. p.velocity *= dragFactor;
  908. }
  909. };
  910. }
  911. OperatorFunc CParticle::createAngularMovementOperator (const AngularMovementOperator& op) {
  912. DynamicValue* dragValue = op.drag->value.get ();
  913. DynamicValue* forceValue = op.force->value.get ();
  914. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  915. return [dragValue, forceValue, speedOverride] (
  916. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  917. float dt
  918. ) {
  919. float drag = dragValue->getFloat ();
  920. float speed = speedOverride->getFloat ();
  921. glm::vec3 force = forceValue->getVec3 ();
  922. for (uint32_t i = 0; i < count; i++) {
  923. auto& p = particles[i];
  924. if (!p.alive) {
  925. continue;
  926. }
  927. p.rotation += p.angularVelocity * dt * speed;
  928. p.angularVelocity += force * dt * speed;
  929. // Positive drag slows down, negative speeds up; clamped so drag*dt > 1.0 can't reverse it
  930. float dragFactor = 1.0f - (drag * dt);
  931. if (dragFactor < 0.0f) {
  932. dragFactor = 0.0f;
  933. }
  934. p.angularVelocity *= dragFactor;
  935. // Wrap rotation to prevent floating-point precision issues
  936. const float pi = glm::pi<float> ();
  937. const float two_pi = glm::two_pi<float> ();
  938. for (int j = 0; j < 3; j++) {
  939. while (p.rotation[j] > pi) {
  940. p.rotation[j] -= two_pi;
  941. }
  942. while (p.rotation[j] < -pi) {
  943. p.rotation[j] += two_pi;
  944. }
  945. }
  946. }
  947. };
  948. }
  949. OperatorFunc CParticle::createAlphaFadeOperator (const AlphaFadeOperator& op) {
  950. DynamicValue* fadeInTimeValue = op.fadeInTime->value.get ();
  951. DynamicValue* fadeOutTimeValue = op.fadeOutTime->value.get ();
  952. return
  953. [fadeInTimeValue, fadeOutTimeValue] (
  954. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  955. ) {
  956. float fadeInTime = fadeInTimeValue->getFloat ();
  957. float fadeOutTime = fadeOutTimeValue->getFloat ();
  958. for (uint32_t i = 0; i < count; i++) {
  959. auto& p = particles[i];
  960. if (!p.alive) {
  961. continue;
  962. }
  963. float life = p.getLifetimePos ();
  964. if (life <= fadeInTime) {
  965. float fade = WallpaperEngine::Maths::fadeValue (life, 0.0f, fadeInTime, 0.0f, 1.0f);
  966. p.alpha = p.initial.alpha * fade;
  967. } else if (life > fadeOutTime) {
  968. float fade = 1.0f - WallpaperEngine::Maths::fadeValue (life, fadeOutTime, 1.0f, 0.0f, 1.0f);
  969. p.alpha = p.initial.alpha * fade;
  970. } else {
  971. p.alpha = p.initial.alpha;
  972. }
  973. // Update oscillator base so oscillateAlpha combines properly
  974. p.oscillateAlpha.base = p.alpha;
  975. }
  976. };
  977. }
  978. OperatorFunc CParticle::createSizeChangeOperator (const SizeChangeOperator& op) {
  979. DynamicValue* startTimeValue = op.startTime->value.get ();
  980. DynamicValue* endTimeValue = op.endTime->value.get ();
  981. DynamicValue* startValueValue = op.startValue->value.get ();
  982. DynamicValue* endValueValue = op.endValue->value.get ();
  983. return
  984. [startTimeValue, endTimeValue, startValueValue, endValueValue] (
  985. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  986. ) {
  987. float startTime = startTimeValue->getFloat ();
  988. float endTime = endTimeValue->getFloat ();
  989. float startValue = startValueValue->getFloat ();
  990. float endValue = endValueValue->getFloat ();
  991. for (uint32_t i = 0; i < count; i++) {
  992. auto& p = particles[i];
  993. if (!p.alive) {
  994. continue;
  995. }
  996. float life = p.getLifetimePos ();
  997. float multiplier = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue, endValue);
  998. p.size = p.initial.size * multiplier;
  999. // Update oscillator base so oscillateSize combines properly
  1000. p.oscillateSize.base = p.size;
  1001. }
  1002. };
  1003. }
  1004. OperatorFunc CParticle::createAlphaChangeOperator (const AlphaChangeOperator& op) {
  1005. DynamicValue* startTimeValue = op.startTime->value.get ();
  1006. DynamicValue* endTimeValue = op.endTime->value.get ();
  1007. DynamicValue* startValueValue = op.startValue->value.get ();
  1008. DynamicValue* endValueValue = op.endValue->value.get ();
  1009. return
  1010. [startTimeValue, endTimeValue, startValueValue, endValueValue] (
  1011. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  1012. ) {
  1013. float startTime = startTimeValue->getFloat ();
  1014. float endTime = endTimeValue->getFloat ();
  1015. float startValue = startValueValue->getFloat ();
  1016. float endValue = endValueValue->getFloat ();
  1017. for (uint32_t i = 0; i < count; i++) {
  1018. auto& p = particles[i];
  1019. if (!p.alive) {
  1020. continue;
  1021. }
  1022. float life = p.getLifetimePos ();
  1023. float multiplier = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue, endValue);
  1024. p.alpha = p.initial.alpha * multiplier;
  1025. // Update oscillator base so oscillateAlpha combines properly
  1026. p.oscillateAlpha.base = p.alpha;
  1027. }
  1028. };
  1029. }
  1030. OperatorFunc CParticle::createColorChangeOperator (const ColorChangeOperator& op) {
  1031. DynamicValue* startTimeValue = op.startTime->value.get ();
  1032. DynamicValue* endTimeValue = op.endTime->value.get ();
  1033. DynamicValue* startValueValue = op.startValue->value.get ();
  1034. DynamicValue* endValueValue = op.endValue->value.get ();
  1035. return
  1036. [startTimeValue, endTimeValue, startValueValue, endValueValue] (
  1037. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  1038. ) {
  1039. float startTime = startTimeValue->getFloat ();
  1040. float endTime = endTimeValue->getFloat ();
  1041. glm::vec3 startValue = startValueValue->getVec3 ();
  1042. glm::vec3 endValue = endValueValue->getVec3 ();
  1043. for (uint32_t i = 0; i < count; i++) {
  1044. auto& p = particles[i];
  1045. if (!p.alive) {
  1046. continue;
  1047. }
  1048. float life = p.getLifetimePos ();
  1049. glm::vec3 color;
  1050. color.r = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.r, endValue.r);
  1051. color.g = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.g, endValue.g);
  1052. color.b = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.b, endValue.b);
  1053. p.color = p.initial.color * color;
  1054. }
  1055. };
  1056. }
  1057. OperatorFunc CParticle::createTurbulenceOperator (const TurbulenceOperator& op) {
  1058. DynamicValue* scaleValue = op.scale->value.get ();
  1059. DynamicValue* speedMinValue = op.speedMin->value.get ();
  1060. DynamicValue* speedMaxValue = op.speedMax->value.get ();
  1061. DynamicValue* timeScaleValue = op.timeScale->value.get ();
  1062. DynamicValue* maskValue = op.mask->value.get ();
  1063. DynamicValue* phaseMinValue = op.phaseMin->value.get ();
  1064. DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
  1065. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1066. DynamicValue* audioModeValue = op.audioProcessingMode->value.get ();
  1067. DynamicValue* audioBoundsValue = op.audioProcessingBounds->value.get ();
  1068. DynamicValue* audioExponentValue = op.audioProcessingExponent->value.get ();
  1069. DynamicValue* audioStartValue = op.audioProcessingFrequencyStart->value.get ();
  1070. DynamicValue* audioEndValue = op.audioProcessingFrequencyEnd->value.get ();
  1071. // Phase and speed are randomized once per operator instance, not per particle
  1072. const float phase
  1073. = WallpaperEngine::Maths::randomFloat (m_rng, phaseMinValue->getFloat (), phaseMaxValue->getFloat ());
  1074. const float turbSpeed
  1075. = WallpaperEngine::Maths::randomFloat (m_rng, speedMinValue->getFloat (), speedMaxValue->getFloat ());
  1076. return [this, scaleValue, timeScaleValue, maskValue, speedOverride, audioModeValue, audioBoundsValue,
  1077. audioExponentValue, audioStartValue, audioEndValue, phase, baseTurbSpeed = turbSpeed] (
  1078. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&,
  1079. float currentTime, float dt
  1080. ) {
  1081. const float noiseScale = scaleValue->getFloat () * 2.0f;
  1082. const float timeScale = timeScaleValue->getFloat ();
  1083. const glm::vec3 mask = maskValue->getVec3 ();
  1084. const float speed = speedOverride->getFloat ();
  1085. const float audio = sampleAudio (
  1086. audioModeValue->getInt (), audioBoundsValue->getVec2 (), audioExponentValue->getFloat (),
  1087. audioStartValue->getInt (), audioEndValue->getInt ()
  1088. );
  1089. const float turbSpeed = baseTurbSpeed * audio;
  1090. if (turbSpeed <= 0.0001f) {
  1091. return;
  1092. }
  1093. for (size_t i = 0; i < count; ++i) {
  1094. ParticleInstance& p = particles[i];
  1095. if (!p.alive) {
  1096. continue;
  1097. }
  1098. glm::vec3 noisePos = p.position;
  1099. noisePos.x += phase + timeScale * currentTime;
  1100. noisePos *= noiseScale;
  1101. glm::vec3 curlDir = curlNoise (noisePos);
  1102. const float len = glm::length (curlDir);
  1103. if (len > 0.0001f) {
  1104. curlDir = (curlDir / len) * turbSpeed;
  1105. }
  1106. curlDir *= mask;
  1107. p.velocity += curlDir * dt * speed;
  1108. }
  1109. };
  1110. }
  1111. OperatorFunc CParticle::createVortexOperator (const VortexOperator& op) {
  1112. int controlPoint = op.controlPoint;
  1113. int flags = op.flags;
  1114. DynamicValue* axisValue = op.axis->value.get ();
  1115. DynamicValue* offsetValue = op.offset->value.get ();
  1116. DynamicValue* distanceInnerValue = op.distanceInner->value.get ();
  1117. DynamicValue* distanceOuterValue = op.distanceOuter->value.get ();
  1118. DynamicValue* speedInnerValue = op.speedInner->value.get ();
  1119. DynamicValue* speedOuterValue = op.speedOuter->value.get ();
  1120. DynamicValue* centerForceValue = op.centerForce->value.get ();
  1121. DynamicValue* ringRadiusValue = op.ringRadius->value.get ();
  1122. DynamicValue* ringWidthValue = op.ringWidth->value.get ();
  1123. DynamicValue* ringPullDistanceValue = op.ringPullDistance->value.get ();
  1124. DynamicValue* ringPullForceValue = op.ringPullForce->value.get ();
  1125. DynamicValue* audioModeValue = op.audioProcessingMode->value.get ();
  1126. DynamicValue* audioBoundsValue = op.audioProcessingBounds->value.get ();
  1127. DynamicValue* audioExponentValue = op.audioProcessingExponent->value.get ();
  1128. DynamicValue* audioStartValue = op.audioProcessingFrequencyStart->value.get ();
  1129. DynamicValue* audioEndValue = op.audioProcessingFrequencyEnd->value.get ();
  1130. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1131. bool infiniteAxis = (flags & 1) != 0;
  1132. bool maintainDistance = (flags & 2) != 0;
  1133. bool ringShape = (flags & 4) != 0;
  1134. return [controlPoint, axisValue, offsetValue, distanceInnerValue, distanceOuterValue, speedInnerValue,
  1135. speedOuterValue, centerForceValue, ringRadiusValue, ringWidthValue, ringPullDistanceValue,
  1136. ringPullForceValue, audioModeValue, audioBoundsValue, audioExponentValue, audioStartValue, audioEndValue,
  1137. infiniteAxis, maintainDistance, ringShape, speedOverride, this] (
  1138. std::vector<ParticleInstance>& particles, uint32_t count,
  1139. const std::vector<ControlPointData>& controlPoints, float, float dt
  1140. ) {
  1141. const float audioResponse = sampleAudio (
  1142. audioModeValue->getInt (), audioBoundsValue->getVec2 (), audioExponentValue->getFloat (),
  1143. audioStartValue->getInt (), audioEndValue->getInt ()
  1144. );
  1145. if (audioResponse <= 0.0f) {
  1146. return;
  1147. }
  1148. glm::vec3 axis = axisValue->getVec3 ();
  1149. glm::vec3 offset = offsetValue->getVec3 ();
  1150. float distanceInner = distanceInnerValue->getFloat ();
  1151. float distanceOuter = distanceOuterValue->getFloat ();
  1152. float speedInner = speedInnerValue->getFloat ();
  1153. float speedOuter = speedOuterValue->getFloat ();
  1154. float centerForce = centerForceValue->getFloat ();
  1155. float ringRadius = ringRadiusValue->getFloat ();
  1156. float ringWidth = ringWidthValue->getFloat ();
  1157. float ringPullDistance = ringPullDistanceValue->getFloat ();
  1158. float ringPullForce = ringPullForceValue->getFloat ();
  1159. speedInner *= audioResponse;
  1160. speedOuter *= audioResponse;
  1161. glm::vec3 center = glm::vec3 (0.0f);
  1162. if (controlPoint >= 0 && controlPoint < static_cast<int> (controlPoints.size ())) {
  1163. center = controlPoints[controlPoint].position + offset;
  1164. } else {
  1165. center = offset;
  1166. }
  1167. if (glm::length (axis) > 0.0f) {
  1168. axis = glm::normalize (axis);
  1169. } else {
  1170. axis = glm::vec3 (0.0f, 0.0f, 1.0f);
  1171. }
  1172. for (uint32_t i = 0; i < count; i++) {
  1173. auto& p = particles[i];
  1174. if (!p.alive) {
  1175. continue;
  1176. }
  1177. glm::vec3 toParticle = p.position - center;
  1178. // infiniteAxis: project onto the plane perpendicular to axis (cylinder shape);
  1179. // otherwise use full 3D distance (sphere shape)
  1180. float axialDistance = 0.0f;
  1181. glm::vec3 radialVector = toParticle;
  1182. if (infiniteAxis) {
  1183. axialDistance = glm::dot (toParticle, axis);
  1184. radialVector = toParticle - axis * axialDistance;
  1185. }
  1186. float distance = glm::length (radialVector);
  1187. glm::vec3 tangent = glm::cross (axis, radialVector);
  1188. if (glm::length (tangent) > 0.001f) {
  1189. tangent = glm::normalize (tangent);
  1190. } else {
  1191. continue; // particle is on the axis
  1192. }
  1193. float speed = 0.0f;
  1194. glm::vec3 radialForce = glm::vec3 (0.0f);
  1195. if (ringShape) {
  1196. // Ring mode: hollow center with ring-shaped influence zone
  1197. float ringInner = ringRadius - ringWidth * 0.5f;
  1198. float ringOuter = ringRadius + ringWidth * 0.5f;
  1199. if (distance < ringInner) {
  1200. // Inside the ring's hollow center - no spin, but may be pulled outward
  1201. speed = 0.0f;
  1202. } else if (distance <= ringOuter) {
  1203. // Inside the ring - full effect
  1204. float t = (distance - ringInner) / ringWidth;
  1205. speed = glm::mix (speedInner, speedOuter, t);
  1206. } else if (distance <= ringOuter + ringPullDistance) {
  1207. // Outside ring but within pull distance - attract toward ring
  1208. float pullT = (distance - ringOuter) / ringPullDistance;
  1209. speed = speedOuter * (1.0f - pullT);
  1210. if (distance > 0.001f) {
  1211. glm::vec3 towardRing = -glm::normalize (radialVector);
  1212. radialForce = towardRing * ringPullForce * pullT;
  1213. }
  1214. } else {
  1215. // Too far from ring - no effect
  1216. speed = 0.0f;
  1217. }
  1218. } else {
  1219. // Standard vortex mode
  1220. float disMid = distanceOuter - distanceInner + 0.1f;
  1221. if (disMid < 0 || distance < distanceInner) {
  1222. speed = speedInner;
  1223. } else if (distance > distanceOuter) {
  1224. speed = speedOuter;
  1225. } else {
  1226. float t = (distance - distanceInner) / disMid;
  1227. speed = glm::mix (speedInner, speedOuter, t);
  1228. }
  1229. }
  1230. p.velocity += tangent * speed * dt * speedOverride->getFloat ();
  1231. p.velocity += radialForce * dt * speedOverride->getFloat ();
  1232. if (maintainDistance && distance > 0.001f) {
  1233. glm::vec3 towardCenter = -glm::normalize (radialVector);
  1234. p.velocity += towardCenter * centerForce * dt * speedOverride->getFloat ();
  1235. }
  1236. }
  1237. };
  1238. }
  1239. OperatorFunc CParticle::createControlPointAttractOperator (const ControlPointAttractOperator& op) {
  1240. int controlPoint = op.controlPoint;
  1241. DynamicValue* originValue = op.origin->value.get ();
  1242. DynamicValue* scaleValue = op.scale->value.get ();
  1243. DynamicValue* thresholdValue = op.threshold->value.get ();
  1244. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1245. return [controlPoint, originValue, scaleValue, thresholdValue, speedOverride] (
  1246. std::vector<ParticleInstance>& particles, uint32_t count,
  1247. const std::vector<ControlPointData>& controlPoints, float currentTime, float dt
  1248. ) {
  1249. glm::vec3 origin = originValue->getVec3 ();
  1250. float scale = scaleValue->getFloat ();
  1251. float threshold = thresholdValue->getFloat () / 2.0f;
  1252. if (controlPoint < 0 || controlPoint >= static_cast<int> (controlPoints.size ())) {
  1253. return;
  1254. }
  1255. glm::vec3 center = controlPoints[controlPoint].position + origin;
  1256. for (uint32_t i = 0; i < count; i++) {
  1257. auto& p = particles[i];
  1258. if (!p.alive) {
  1259. continue;
  1260. }
  1261. glm::vec3 toCenter = center - p.position;
  1262. float distance = glm::length (toCenter);
  1263. if (distance > 0.001f && distance < threshold) {
  1264. glm::vec3 direction = toCenter / distance;
  1265. glm::vec3 forceVec = direction * scale * dt;
  1266. p.velocity += forceVec * speedOverride->getFloat ();
  1267. }
  1268. }
  1269. };
  1270. }
  1271. OperatorFunc CParticle::createOscillateAlphaOperator (const OscillateAlphaOperator& op) {
  1272. DynamicValue* freqMinValue = op.frequencyMin->value.get ();
  1273. DynamicValue* freqMaxValue = op.frequencyMax->value.get ();
  1274. DynamicValue* scaleMinValue = op.scaleMin->value.get ();
  1275. DynamicValue* scaleMaxValue = op.scaleMax->value.get ();
  1276. DynamicValue* phaseMinValue = op.phaseMin->value.get ();
  1277. DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
  1278. return
  1279. [this, freqMinValue, freqMaxValue, scaleMinValue, scaleMaxValue, phaseMinValue, phaseMaxValue] (
  1280. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  1281. ) {
  1282. float freqMin = freqMinValue->getFloat ();
  1283. float freqMax = freqMaxValue->getFloat ();
  1284. float scaleMin = scaleMinValue->getFloat ();
  1285. float scaleMax = scaleMaxValue->getFloat ();
  1286. float phaseMin = phaseMinValue->getFloat ();
  1287. float phaseMax = phaseMaxValue->getFloat ();
  1288. for (uint32_t i = 0; i < count; i++) {
  1289. auto& p = particles[i];
  1290. // Initialize per-particle oscillator values on first use
  1291. if (!p.oscillateAlpha.initialized) {
  1292. p.oscillateAlpha.frequency = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
  1293. p.oscillateAlpha.scale = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
  1294. p.oscillateAlpha.phase
  1295. = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
  1296. p.oscillateAlpha.base = p.alpha;
  1297. p.oscillateAlpha.initialized = true;
  1298. }
  1299. // Cosine wave interpolating between scaleMin and scaleMax
  1300. float w = p.oscillateAlpha.frequency;
  1301. float t = p.age;
  1302. float cosVal = (std::cos (w * t + p.oscillateAlpha.phase) + 1.0f) * 0.5f;
  1303. float multiplier = glm::mix (scaleMin, scaleMax, cosVal);
  1304. // Apply to base value (alphafade updates base each frame if present)
  1305. p.alpha = p.oscillateAlpha.base * multiplier;
  1306. }
  1307. };
  1308. }
  1309. OperatorFunc CParticle::createOscillateSizeOperator (const OscillateSizeOperator& op) {
  1310. DynamicValue* freqMinValue = op.frequencyMin->value.get ();
  1311. DynamicValue* freqMaxValue = op.frequencyMax->value.get ();
  1312. DynamicValue* scaleMinValue = op.scaleMin->value.get ();
  1313. DynamicValue* scaleMaxValue = op.scaleMax->value.get ();
  1314. DynamicValue* phaseMinValue = op.phaseMin->value.get ();
  1315. DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
  1316. return
  1317. [this, freqMinValue, freqMaxValue, scaleMinValue, scaleMaxValue, phaseMinValue, phaseMaxValue] (
  1318. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  1319. ) {
  1320. float freqMin = freqMinValue->getFloat ();
  1321. float freqMax = freqMaxValue->getFloat ();
  1322. float scaleMin = scaleMinValue->getFloat ();
  1323. float scaleMax = scaleMaxValue->getFloat ();
  1324. float phaseMin = phaseMinValue->getFloat ();
  1325. float phaseMax = phaseMaxValue->getFloat ();
  1326. for (uint32_t i = 0; i < count; i++) {
  1327. auto& p = particles[i];
  1328. // Initialize per-particle oscillator values on first use
  1329. if (!p.oscillateSize.initialized) {
  1330. p.oscillateSize.frequency = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
  1331. p.oscillateSize.scale = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
  1332. p.oscillateSize.phase
  1333. = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
  1334. p.oscillateSize.base = p.size;
  1335. p.oscillateSize.initialized = true;
  1336. }
  1337. // Cosine wave interpolating between scaleMin and scaleMax
  1338. float w = p.oscillateSize.frequency;
  1339. float t = p.age;
  1340. float cosVal = (std::cos (w * t + p.oscillateSize.phase) + 1.0f) * 0.5f;
  1341. float multiplier = glm::mix (scaleMin, scaleMax, cosVal);
  1342. // Apply to base value (sizeChange updates base each frame if present)
  1343. p.size = p.oscillateSize.base * multiplier;
  1344. }
  1345. };
  1346. }
  1347. OperatorFunc CParticle::createOscillatePositionOperator (const OscillatePositionOperator& op) {
  1348. DynamicValue* freqMinValue = op.frequencyMin->value.get ();
  1349. DynamicValue* freqMaxValue = op.frequencyMax->value.get ();
  1350. DynamicValue* scaleMinValue = op.scaleMin->value.get ();
  1351. DynamicValue* scaleMaxValue = op.scaleMax->value.get ();
  1352. DynamicValue* phaseMinValue = op.phaseMin->value.get ();
  1353. DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
  1354. DynamicValue* maskValue = op.mask->value.get ();
  1355. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1356. return [this, freqMinValue, freqMaxValue, scaleMinValue, scaleMaxValue, phaseMinValue, phaseMaxValue, maskValue,
  1357. speedOverride] (
  1358. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  1359. float dt
  1360. ) {
  1361. float freqMin = freqMinValue->getFloat ();
  1362. float freqMax = freqMaxValue->getFloat ();
  1363. float scaleMin = scaleMinValue->getFloat ();
  1364. float scaleMax = scaleMaxValue->getFloat ();
  1365. float phaseMin = phaseMinValue->getFloat ();
  1366. float phaseMax = phaseMaxValue->getFloat ();
  1367. glm::vec3 mask = maskValue->getVec3 ();
  1368. for (uint32_t i = 0; i < count; i++) {
  1369. auto& p = particles[i];
  1370. // Initialize per-particle oscillator values on first use (per axis)
  1371. if (!p.oscillatePosition.initialized) {
  1372. for (int axis = 0; axis < 3; axis++) {
  1373. p.oscillatePosition.frequency[axis] = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
  1374. p.oscillatePosition.scale[axis] = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
  1375. p.oscillatePosition.phase[axis]
  1376. = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
  1377. }
  1378. p.oscillatePosition.initialized = true;
  1379. }
  1380. float t = p.age;
  1381. glm::vec3 delta (0.0f);
  1382. for (int axis = 0; axis < 3; axis++) {
  1383. float w = 2.0f * glm::pi<float> () * p.oscillatePosition.frequency[axis] / (2.0f * glm::pi<float> ());
  1384. // Derivative of cos is -sin; multiplied by dt for position change
  1385. float move
  1386. = -p.oscillatePosition.scale[axis] * w * std::sin (w * t + p.oscillatePosition.phase[axis]) * dt;
  1387. // Apply mask as bias multiplier for this axis
  1388. delta[axis] = move * mask[axis] * speedOverride->getFloat ();
  1389. }
  1390. p.position += delta;
  1391. }
  1392. };
  1393. }
  1394. // ========== RENDERING ==========
  1395. void CParticle::setupPass () {
  1396. if (!m_particle.material || !m_particle.material->material || m_particle.material->material->passes.empty ()) {
  1397. sLog.error ("No valid material for particle ", m_particle.name);
  1398. return;
  1399. }
  1400. const auto& firstPass = **m_particle.material->material->passes.begin ();
  1401. m_passOverride = std::make_unique<ImageEffectPassOverride> ();
  1402. m_passOverride->combos["THICKFORMAT"] = 1;
  1403. if (m_useRopeRenderer) {
  1404. m_passOverride->shaderOverride = "genericropeparticle";
  1405. }
  1406. if (m_spritesheetFrames > 0) {
  1407. m_passOverride->combos["SPRITESHEET"] = 1;
  1408. }
  1409. if (m_useTrailRenderer) {
  1410. m_passOverride->combos["TRAILRENDERER"] = 1;
  1411. }
  1412. // Force texture 0 to use the input (particle texture) rather than the shader's
  1413. // default "util/white" annotation, which would override it in setupRenderTexture()
  1414. m_passBinds = { { 0, "previous" } };
  1415. auto refractIt = firstPass.combos.find ("REFRACT");
  1416. m_hasRefract = refractIt != firstPass.combos.end () && refractIt->second != 0;
  1417. m_passFBOProvider = std::make_shared<FBOProvider> (this);
  1418. // REFRACT: create a copy FBO shadowing _rt_FullFrameBuffer. The shader reads g_Texture3
  1419. // (= _rt_FullFrameBuffer) while we render TO the scene FBO; reading and writing the same FBO
  1420. // is undefined behavior in OpenGL and causes black reads on NVIDIA. Placing a copy FBO under
  1421. // the same name in our FBOProvider makes CPass resolve g_Texture3 to the copy instead - we
  1422. // blit the scene content into it before each render.
  1423. if (m_hasRefract) {
  1424. auto sceneFBO = getScene ().getFBO ();
  1425. float w = static_cast<float> (sceneFBO->getRealWidth ());
  1426. float h = static_cast<float> (sceneFBO->getRealHeight ());
  1427. m_refractFBO = m_passFBOProvider->create (
  1428. "_rt_FullFrameBuffer", TextureFormat_ARGB8888, TextureFlags_ClampUVs, 1.0f, { w, h }, { w, h }
  1429. );
  1430. }
  1431. m_pass = new Effects::CPass (*this, m_passFBOProvider, firstPass, *m_passOverride, m_passBinds, std::nullopt);
  1432. m_pass->setDestination (getScene ().getFBO ());
  1433. m_pass->setInput (getTexture ());
  1434. // Set matrix pointers - CPass will dereference these each frame
  1435. m_pass->setModelViewProjectionMatrix (&m_mvpMatrix);
  1436. m_pass->setModelViewProjectionMatrixInverse (&m_mvpMatrixInverse);
  1437. m_pass->setModelMatrix (&m_modelMatrix);
  1438. m_pass->setViewProjectionMatrix (&m_viewProjectionMatrix);
  1439. GLint prevVAO = 0;
  1440. glGetIntegerv (GL_VERTEX_ARRAY_BINDING, &prevVAO);
  1441. glGenVertexArrays (1, &m_vao);
  1442. glGenBuffers (1, &m_vbo);
  1443. glGenBuffers (1, &m_ebo);
  1444. glBindVertexArray (m_vao);
  1445. glBindBuffer (GL_ARRAY_BUFFER, m_vbo);
  1446. glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, m_ebo);
  1447. const GLuint program = m_pass->getProgramID ();
  1448. if (m_useRopeRenderer) {
  1449. // Rope vertex layout: 7 attributes, 26 floats/vertex, stride=104 bytes
  1450. // a_PositionVec4(4) + a_TexCoordVec4(4) + a_TexCoordVec4C1(4) + a_TexCoordVec4C2(4)
  1451. // + a_TexCoordVec4C3(4) + a_TexCoordC4(2) + a_Color(4) = 26
  1452. const GLsizei stride = sizeof (float) * ROPE_FLOATS_PER_VERTEX;
  1453. const GLint loc0 = glGetAttribLocation (program, "a_PositionVec4");
  1454. const GLint loc1 = glGetAttribLocation (program, "a_TexCoordVec4");
  1455. const GLint loc2 = glGetAttribLocation (program, "a_TexCoordVec4C1");
  1456. const GLint loc3 = glGetAttribLocation (program, "a_TexCoordVec4C2");
  1457. const GLint loc4 = glGetAttribLocation (program, "a_TexCoordVec4C3");
  1458. const GLint loc5 = glGetAttribLocation (program, "a_TexCoordC4");
  1459. const GLint loc6 = glGetAttribLocation (program, "a_Color");
  1460. if (loc0 >= 0) {
  1461. glEnableVertexAttribArray (loc0);
  1462. glVertexAttribPointer (loc0, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 0));
  1463. }
  1464. if (loc1 >= 0) {
  1465. glEnableVertexAttribArray (loc1);
  1466. glVertexAttribPointer (loc1, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 4));
  1467. }
  1468. if (loc2 >= 0) {
  1469. glEnableVertexAttribArray (loc2);
  1470. glVertexAttribPointer (loc2, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 8));
  1471. }
  1472. if (loc3 >= 0) {
  1473. glEnableVertexAttribArray (loc3);
  1474. glVertexAttribPointer (loc3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 12));
  1475. }
  1476. if (loc4 >= 0) {
  1477. glEnableVertexAttribArray (loc4);
  1478. glVertexAttribPointer (loc4, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 16));
  1479. }
  1480. if (loc5 >= 0) {
  1481. glEnableVertexAttribArray (loc5);
  1482. glVertexAttribPointer (loc5, 2, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 20));
  1483. }
  1484. if (loc6 >= 0) {
  1485. glEnableVertexAttribArray (loc6);
  1486. glVertexAttribPointer (loc6, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 22));
  1487. }
  1488. } else {
  1489. // Sprite vertex layout: 5 attributes, 17 floats/vertex, stride=68 bytes
  1490. // a_Position(3) + a_TexCoordVec4(4) + a_Color(4) + a_TexCoordVec4C1(4) + a_TexCoordC2(2) = 17
  1491. const GLsizei stride = sizeof (float) * SPRITE_FLOATS_PER_VERTEX;
  1492. const GLint loc0 = glGetAttribLocation (program, "a_Position");
  1493. const GLint loc1 = glGetAttribLocation (program, "a_TexCoordVec4");
  1494. const GLint loc2 = glGetAttribLocation (program, "a_Color");
  1495. const GLint loc3 = glGetAttribLocation (program, "a_TexCoordVec4C1");
  1496. const GLint loc4 = glGetAttribLocation (program, "a_TexCoordC2");
  1497. if (loc0 >= 0) {
  1498. glEnableVertexAttribArray (loc0);
  1499. glVertexAttribPointer (loc0, 3, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 0));
  1500. }
  1501. if (loc1 >= 0) {
  1502. glEnableVertexAttribArray (loc1);
  1503. glVertexAttribPointer (loc1, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 3));
  1504. }
  1505. if (loc2 >= 0) {
  1506. glEnableVertexAttribArray (loc2);
  1507. glVertexAttribPointer (loc2, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 7));
  1508. }
  1509. if (loc3 >= 0) {
  1510. glEnableVertexAttribArray (loc3);
  1511. glVertexAttribPointer (loc3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 11));
  1512. }
  1513. if (loc4 >= 0) {
  1514. glEnableVertexAttribArray (loc4);
  1515. glVertexAttribPointer (loc4, 2, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 15));
  1516. }
  1517. }
  1518. glBindVertexArray (prevVAO);
  1519. setupGeometryCallbacks ();
  1520. setupParticleUniforms ();
  1521. }
  1522. void CParticle::setupGeometryCallbacks () {
  1523. m_pass->setGeometryCallback (
  1524. // Setup attribs: save current VAO, bind particle VAO
  1525. [this] () {
  1526. glGetIntegerv (GL_VERTEX_ARRAY_BINDING, &m_prevVAO);
  1527. glBindVertexArray (m_vao);
  1528. },
  1529. // Draw geometry: indexed rendering
  1530. [this] () { glDrawElements (GL_TRIANGLES, m_activeIndexCount, GL_UNSIGNED_INT, nullptr); },
  1531. // Cleanup: restore previous VAO
  1532. [this] () { glBindVertexArray (m_prevVAO); }
  1533. );
  1534. }
  1535. void CParticle::setupParticleUniforms () {
  1536. // Add particle-specific uniforms from common_particles.h that CPass doesn't provide
  1537. // These are pointer-based: CPass reads the current value each frame
  1538. m_pass->addUniform ("g_ModelMatrixInverse", &m_modelMatrixInverse);
  1539. m_pass->addUniform ("g_OrientationUp", &m_orientationUp);
  1540. m_pass->addUniform ("g_OrientationRight", &m_orientationRight);
  1541. m_pass->addUniform ("g_OrientationForward", &m_orientationForward);
  1542. m_pass->addUniform ("g_ViewUp", &m_viewUp);
  1543. m_pass->addUniform ("g_ViewRight", &m_viewRight);
  1544. m_pass->addUniform ("g_EyePosition", &m_eyePosition);
  1545. m_pass->addUniform ("g_RenderVar0", &m_renderVar0);
  1546. m_pass->addUniform ("g_RenderVar1", &m_renderVar1);
  1547. // REFRACT: set g_RefractAmount (shader default 0.05, may not be applied by CPass's parameter system)
  1548. if (m_hasRefract) {
  1549. m_pass->addUniform ("g_RefractAmount", &m_refractAmount);
  1550. }
  1551. }
  1552. void CParticle::updateMatrices () {
  1553. glm::vec3 scale = m_particle.scale->value->getVec3 ();
  1554. glm::vec3 angles = m_particle.angles->value->getVec3 ();
  1555. m_modelMatrix = glm::mat4 (1.0f);
  1556. m_modelMatrix = glm::translate (m_modelMatrix, m_transformedOrigin);
  1557. this->applyParallaxToModelMatrix ();
  1558. // Negate X and Z rotations to account for Y-flipped coordinate system
  1559. m_modelMatrix = glm::rotate (m_modelMatrix, -angles.z, glm::vec3 (0, 0, 1));
  1560. m_modelMatrix = glm::rotate (m_modelMatrix, angles.y, glm::vec3 (0, 1, 0));
  1561. m_modelMatrix = glm::rotate (m_modelMatrix, -angles.x, glm::vec3 (1, 0, 0));
  1562. m_modelMatrix = glm::scale (m_modelMatrix, scale);
  1563. m_modelMatrixInverse = glm::inverse (m_modelMatrix);
  1564. this->updateParticleViewProjection ();
  1565. m_mvpMatrix = m_viewProjectionMatrix * m_modelMatrix;
  1566. m_mvpMatrixInverse = glm::inverse (m_mvpMatrix);
  1567. m_orientationUp = glm::vec3 (0.0f, 1.0f, 0.0f);
  1568. m_orientationRight = glm::vec3 (1.0f, 0.0f, 0.0f);
  1569. m_orientationForward = glm::vec3 (0.0f, 0.0f, 1.0f);
  1570. m_viewUp = glm::vec3 (0.0f, 1.0f, 0.0f);
  1571. m_viewRight = glm::vec3 (1.0f, 0.0f, 0.0f);
  1572. this->updateParticleRenderVars ();
  1573. }
  1574. void CParticle::applyParallaxToModelMatrix () {
  1575. // CScene::renderFrame() already folds disableparallax into getParallaxDisplacement()
  1576. if (!getScene ().getScene ().camera.parallax.enabled->value->getBool ()) {
  1577. return;
  1578. }
  1579. const glm::vec2 offset = getScene ().getParallaxOffset (m_particle);
  1580. const glm::vec3 parallaxOffset { offset.x, offset.y, 0.0f };
  1581. m_modelMatrix = glm::translate (m_modelMatrix, parallaxOffset);
  1582. }
  1583. void CParticle::updateParticleViewProjection () {
  1584. if ((m_particle.flags & 4) != 0) {
  1585. // Perspective particles use a dedicated perspective projection
  1586. float width = getScene ().getCamera ().getWidth ();
  1587. float height = getScene ().getCamera ().getHeight ();
  1588. float aspect = width / height;
  1589. float fov = glm::radians (getScene ().getCamera ().getFov ());
  1590. float nearz = getScene ().getCamera ().getNearZ ();
  1591. float farz = getScene ().getCamera ().getFarZ ();
  1592. glm::mat4 perspectiveProj = glm::perspective (fov, aspect, nearz, farz);
  1593. glm::mat4 perspectiveView
  1594. = glm::lookAt (glm::vec3 (0.0f, 0.0f, 1000.0f), glm::vec3 (0.0f, 0.0f, 0.0f), glm::vec3 (0.0f, 1.0f, 0.0f));
  1595. m_viewProjectionMatrix = perspectiveProj * perspectiveView;
  1596. m_eyePosition = glm::vec3 (0.0f, 0.0f, 1000.0f);
  1597. } else {
  1598. // Orthographic projection from scene camera
  1599. m_viewProjectionMatrix = getScene ().getCamera ().getProjection () * getScene ().getCamera ().getLookAt ();
  1600. // The shader's ComputeParticleTrailTangents computes trail ribbon width via
  1601. // cross(eyeDirection, velocity). With the ortho eye at (0,0,0) and particles at z=0,
  1602. // eyeDirection is purely XY, so the cross product is Z-only and invisible under
  1603. // orthographic projection. Placing the eye at z=1000 gives it a visible XY component.
  1604. m_eyePosition = glm::vec3 (0.0f, 0.0f, 1000.0f);
  1605. }
  1606. }
  1607. void CParticle::updateParticleRenderVars () {
  1608. m_renderVar0 = glm::vec4 (m_trailLength, m_trailMaxLength, m_trailMinLength, 0.0f);
  1609. if (m_spritesheetFrames > 0 && m_spritesheetCols > 0 && m_spritesheetRows > 0) {
  1610. float frameWidth = 1.0f / static_cast<float> (m_spritesheetCols);
  1611. float frameHeight = 1.0f / static_cast<float> (m_spritesheetRows);
  1612. float textureRatio = 1.0f;
  1613. if (const auto texture = getTexture ()) {
  1614. // Use atlas dimensions (resolution vec4) rather than getRealWidth/Height, which
  1615. // returns per-frame dimensions for animated textures - the shader needs the
  1616. // per-frame pixel aspect ratio: (atlasH * frameHeight) / (atlasW * frameWidth).
  1617. const glm::vec4* res = texture->getResolution ();
  1618. float w = res->x;
  1619. float h = res->y;
  1620. if (w > 0.0f) {
  1621. textureRatio = (h * frameHeight) / (w * frameWidth);
  1622. }
  1623. }
  1624. m_renderVar1 = glm::vec4 (frameWidth, frameHeight, static_cast<float> (m_spritesheetFrames), textureRatio);
  1625. } else {
  1626. float textureRatio = 1.0f;
  1627. if (const auto texture = getTexture ()) {
  1628. float w = static_cast<float> (texture->getRealWidth ());
  1629. float h = static_cast<float> (texture->getRealHeight ());
  1630. if (w > 0.0f) {
  1631. textureRatio = h / w;
  1632. }
  1633. }
  1634. m_renderVar1 = glm::vec4 (0.0f, 0.0f, 0.0f, textureRatio);
  1635. }
  1636. }
  1637. void CParticle::renderSprites () {
  1638. if (m_particleCount == 0 || m_pass == nullptr) {
  1639. return;
  1640. }
  1641. uint32_t aliveCount = 0;
  1642. for (uint32_t i = 0; i < m_particleCount; i++) {
  1643. if (m_particles[i].alive) {
  1644. aliveCount++;
  1645. }
  1646. }
  1647. if (aliveCount == 0) {
  1648. return;
  1649. }
  1650. // Build vertex data in WP shader layout:
  1651. // a_Position(3) + a_TexCoordVec4(uv.x, uv.y, rotZ, size)(4) + a_Color(4)
  1652. // + a_TexCoordVec4C1(vel.x, vel.y, vel.z, lifetime)(4) + a_TexCoordC2(rotX, rotY)(2) = 17 floats
  1653. uint32_t vertexIndex = 0;
  1654. uint32_t indexOffset = 0;
  1655. for (uint32_t i = 0; i < m_particleCount; i++) {
  1656. const auto& p = m_particles[i];
  1657. if (!p.alive) {
  1658. continue;
  1659. }
  1660. // Skip particles with invalid values
  1661. if (!std::isfinite (p.position.x) || !std::isfinite (p.position.y) || !std::isfinite (p.position.z)
  1662. || !std::isfinite (p.size) || p.size <= 0.0f || p.size > 10000.0f) {
  1663. continue;
  1664. }
  1665. // Encode the CPU-computed frame (accounts for sequenceMultiplier and animation mode)
  1666. // into the lifetime value the WP shader's ComputeSpriteFrame expects: it derives the
  1667. // current frame via floor(frac(lifetime) * numFrames) and the inter-frame blend via
  1668. // frac(lifetime * numFrames).
  1669. float lifetime = p.getLifetimePos ();
  1670. if (m_spritesheetFrames > 0 && p.frame >= 0.0f) {
  1671. if (m_particle.animationMode == "randomframe") {
  1672. // Center within the frame to avoid floating-point edge cases
  1673. lifetime = (p.frame + 0.5f) / static_cast<float> (m_spritesheetFrames);
  1674. } else {
  1675. lifetime = p.frame / static_cast<float> (m_spritesheetFrames);
  1676. }
  1677. }
  1678. auto addVertex = [&] (float u, float v) {
  1679. const uint32_t base = vertexIndex * SPRITE_FLOATS_PER_VERTEX;
  1680. // a_Position (vec3)
  1681. m_vertices[base + 0] = p.position.x;
  1682. m_vertices[base + 1] = p.position.y;
  1683. m_vertices[base + 2] = p.position.z;
  1684. // a_TexCoordVec4 (vec4: uv.x, uv.y, rotZ, size)
  1685. m_vertices[base + 3] = u;
  1686. m_vertices[base + 4] = v;
  1687. m_vertices[base + 5] = p.rotation.z;
  1688. m_vertices[base + 6] = p.size;
  1689. // a_Color (vec4: r, g, b, a)
  1690. m_vertices[base + 7] = p.color.r;
  1691. m_vertices[base + 8] = p.color.g;
  1692. m_vertices[base + 9] = p.color.b;
  1693. m_vertices[base + 10] = p.alpha;
  1694. // a_TexCoordVec4C1 (vec4: vel.x, vel.y, vel.z, lifetime)
  1695. m_vertices[base + 11] = p.velocity.x;
  1696. m_vertices[base + 12] = p.velocity.y;
  1697. m_vertices[base + 13] = p.velocity.z;
  1698. m_vertices[base + 14] = lifetime;
  1699. // a_TexCoordC2 (vec2: rotX, rotY)
  1700. m_vertices[base + 15] = p.rotation.x;
  1701. m_vertices[base + 16] = p.rotation.y;
  1702. vertexIndex++;
  1703. };
  1704. uint32_t baseVertex = vertexIndex;
  1705. addVertex (0.0f, 1.0f); // 0: Bottom-left
  1706. addVertex (1.0f, 1.0f); // 1: Bottom-right
  1707. addVertex (1.0f, 0.0f); // 2: Top-right
  1708. addVertex (0.0f, 0.0f); // 3: Top-left
  1709. m_indices[indexOffset++] = baseVertex + 0;
  1710. m_indices[indexOffset++] = baseVertex + 1;
  1711. m_indices[indexOffset++] = baseVertex + 2;
  1712. m_indices[indexOffset++] = baseVertex + 2;
  1713. m_indices[indexOffset++] = baseVertex + 3;
  1714. m_indices[indexOffset++] = baseVertex + 0;
  1715. }
  1716. m_activeIndexCount = static_cast<GLsizei> (indexOffset);
  1717. if (m_activeIndexCount == 0) {
  1718. return;
  1719. }
  1720. #if !NDEBUG
  1721. std::string str = "Particles ";
  1722. str += this->getParticle ().name + " (" + std::to_string (this->getId ()) + ", " + this->getParticle ().particleFile
  1723. + ")";
  1724. glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());
  1725. #endif
  1726. glBindBuffer (GL_ARRAY_BUFFER, m_vbo);
  1727. glBufferData (
  1728. GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (vertexIndex * SPRITE_FLOATS_PER_VERTEX * sizeof (float)),
  1729. m_vertices.data (), GL_DYNAMIC_DRAW
  1730. );
  1731. glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, m_ebo);
  1732. glBufferData (
  1733. GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr> (indexOffset * sizeof (uint32_t)), m_indices.data (),
  1734. GL_DYNAMIC_DRAW
  1735. );
  1736. updateMatrices ();
  1737. // REFRACT: blit current scene content into the copy FBO first, giving the shader a
  1738. // snapshot of what's behind the particles without a read/write feedback loop
  1739. if (m_hasRefract && m_refractFBO) {
  1740. auto sceneFBO = getScene ().getFBO ();
  1741. GLint w = static_cast<GLint> (sceneFBO->getRealWidth ());
  1742. GLint h = static_cast<GLint> (sceneFBO->getRealHeight ());
  1743. glBindFramebuffer (GL_READ_FRAMEBUFFER, sceneFBO->getFramebuffer ());
  1744. glBindFramebuffer (GL_DRAW_FRAMEBUFFER, m_refractFBO->getFramebuffer ());
  1745. glBlitFramebuffer (0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
  1746. }
  1747. // ComputeParticleTrailTangents produces a right vector with a Z component (from
  1748. // cross(eyeDirection, velocity), where eyeDirection has an XY offset from the model
  1749. // transform). For 2D/ortho particles at z=0, the ortho near plane sits at ndc.z=-1, so any
  1750. // Z offset pushes vertices past it and clips half the quad. GL_DEPTH_CLAMP avoids that by
  1751. // clamping depth instead of clipping.
  1752. glEnable (GL_DEPTH_CLAMP);
  1753. // CPass::render() handles: FBO binding, texture setup, uniforms, blending, draw call, cleanup
  1754. m_pass->render ();
  1755. glDisable (GL_DEPTH_CLAMP);
  1756. #if !NDEBUG
  1757. glPopDebugGroup ();
  1758. #endif
  1759. }
  1760. void CParticle::renderRope () {
  1761. if (m_particleCount < 2 || m_pass == nullptr) {
  1762. return;
  1763. }
  1764. // Already in spawn order (oldest at index 0) thanks to compaction in update();
  1765. // all particles in [0, m_particleCount) are alive.
  1766. const uint32_t aliveCount = m_particleCount;
  1767. // Each segment between consecutive particles is subdivided into m_ropeSubdivision
  1768. // sub-segments via Catmull-Rom spline, for smooth curves instead of harsh corners.
  1769. //
  1770. // Rope vertex layout (26 floats per vertex, THICKFORMAT):
  1771. // [0-3] a_PositionVec4: startPos.xyz, sizeStart
  1772. // [4-7] a_TexCoordVec4: endPos.xyz, trailLength
  1773. // [8-11] a_TexCoordVec4C1: CP0.xyz, trailPosition
  1774. // [12-15] a_TexCoordVec4C2: CP1.xyz, sizeEnd
  1775. // [16-19] a_TexCoordVec4C3: colorEnd.rgba
  1776. // [20-21] a_TexCoordC4: uvs.xy
  1777. // [22-25] a_Color: colorStart.rgba
  1778. const uint32_t numSegments = aliveCount - 1;
  1779. const int subdivision = std::max (1, m_ropeSubdivision);
  1780. auto catmullRom = [] (const glm::vec3& p0, const glm::vec3& p1, const glm::vec3& p2, const glm::vec3& p3,
  1781. float t) -> glm::vec3 {
  1782. float t2 = t * t, t3 = t2 * t;
  1783. return 0.5f
  1784. * ((2.0f * p1) + (-p0 + p2) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2
  1785. + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3);
  1786. };
  1787. // First pass: evaluate the spline to get all interpolated points (position, size, color)
  1788. const uint32_t totalPoints = numSegments * subdivision + 1;
  1789. this->m_splinePositions.resize (totalPoints);
  1790. this->m_splineSizes.resize (totalPoints);
  1791. this->m_splineColors.resize (totalPoints);
  1792. auto& splinePositions = this->m_splinePositions;
  1793. auto& splineSizes = this->m_splineSizes;
  1794. auto& splineColors = this->m_splineColors;
  1795. for (uint32_t i = 0; i < numSegments; i++) {
  1796. const auto& p1 = m_particles[i];
  1797. const auto& p2 = m_particles[i + 1];
  1798. const auto& p0 = (i > 0) ? m_particles[i - 1] : p1;
  1799. const auto& p3 = (i + 2 < aliveCount) ? m_particles[i + 2] : p2;
  1800. for (int k = 0; k < subdivision; k++) {
  1801. float t = static_cast<float> (k) / static_cast<float> (subdivision);
  1802. uint32_t idx = i * subdivision + k;
  1803. splinePositions[idx] = catmullRom (p0.position, p1.position, p2.position, p3.position, t);
  1804. splineSizes[idx] = glm::mix (p1.size, p2.size, t);
  1805. splineColors[idx] = glm::mix (glm::vec4 (p1.color, p1.alpha), glm::vec4 (p2.color, p2.alpha), t);
  1806. }
  1807. }
  1808. // Last point is the final particle
  1809. {
  1810. const auto& pLast = m_particles[aliveCount - 1];
  1811. splinePositions[totalPoints - 1] = pLast.position;
  1812. splineSizes[totalPoints - 1] = pLast.size;
  1813. splineColors[totalPoints - 1] = glm::vec4 (pLast.color, pLast.alpha);
  1814. }
  1815. // Second pass: build quads from consecutive spline points. The shader computes UV.v as
  1816. // trailPosition / (trailLength - 1), so trailLength/trailPosition are expressed in
  1817. // sub-segment units for the correct UV slice per quad. UV scale divides the effective
  1818. // length, pushing UVs past [0,1] so the texture repeats.
  1819. uint32_t vertexIndex = 0;
  1820. uint32_t indexOffset = 0;
  1821. const uint32_t totalSubSegments = totalPoints - 1;
  1822. const float uvScale = (m_ropeUVScale > 0.0f) ? m_ropeUVScale : 1.0f;
  1823. const float trailLength = static_cast<float> (totalSubSegments) / uvScale + 1.0f;
  1824. const float usableLength = trailLength - 1.0f;
  1825. // UV smoothing: distribute UV proportional to arc length instead of uniform index.
  1826. // Per wiki: only when all particle lifetimes match and scrolling is disabled.
  1827. const bool useSmoothing = m_ropeUVSmoothing && m_uniformLifetimes && !m_ropeUVScrolling;
  1828. auto& cumulativeArcLength = this->m_cumulativeArcLength;
  1829. float totalArcLength = 0.0f;
  1830. if (useSmoothing) {
  1831. cumulativeArcLength.resize (totalPoints, 0.0f);
  1832. for (uint32_t i = 1; i < totalPoints; i++) {
  1833. totalArcLength += glm::distance (splinePositions[i], splinePositions[i - 1]);
  1834. cumulativeArcLength[i] = totalArcLength;
  1835. }
  1836. }
  1837. // UV scrolling: shift UV along the rope over time (1 UV cycle per second)
  1838. float scrollOffset = 0.0f;
  1839. if (m_ropeUVScrolling && usableLength > 0.0f) {
  1840. scrollOffset = std::fmod (static_cast<float> (g_Time), 10000.0f) * usableLength;
  1841. }
  1842. for (uint32_t s = 0; s < totalSubSegments; s++) {
  1843. const glm::vec3& posStart = splinePositions[s];
  1844. const glm::vec3& posEnd = splinePositions[s + 1];
  1845. float sizeStart = splineSizes[s];
  1846. float sizeEnd = splineSizes[s + 1];
  1847. const glm::vec4& colorStart = splineColors[s];
  1848. const glm::vec4& colorEnd = splineColors[s + 1];
  1849. // Neighboring points for shader tangent computation (CP0/CP1)
  1850. const glm::vec3& posPrev = (s > 0) ? splinePositions[s - 1] : posStart;
  1851. const glm::vec3& posAfter = (s + 2 < totalPoints) ? splinePositions[s + 2] : posEnd;
  1852. // Compute trailPosition for UV mapping
  1853. float trailPosition;
  1854. if (useSmoothing && totalArcLength > 0.0f) {
  1855. // Arc-length parameterization: map cumulative distance to sub-segment space
  1856. trailPosition = cumulativeArcLength[s] / totalArcLength * static_cast<float> (totalSubSegments);
  1857. } else {
  1858. trailPosition = static_cast<float> (s);
  1859. }
  1860. trailPosition += scrollOffset;
  1861. auto addRopeVertex = [&] (float uvX, float uvY) {
  1862. const uint32_t base = vertexIndex * ROPE_FLOATS_PER_VERTEX;
  1863. // a_PositionVec4: startPos.xyz, sizeStart
  1864. m_vertices[base + 0] = posStart.x;
  1865. m_vertices[base + 1] = posStart.y;
  1866. m_vertices[base + 2] = posStart.z;
  1867. m_vertices[base + 3] = sizeStart;
  1868. // a_TexCoordVec4: endPos.xyz, trailLength
  1869. m_vertices[base + 4] = posEnd.x;
  1870. m_vertices[base + 5] = posEnd.y;
  1871. m_vertices[base + 6] = posEnd.z;
  1872. m_vertices[base + 7] = trailLength;
  1873. // a_TexCoordVec4C1: CP0.xyz (neighbor before start), trailPosition
  1874. m_vertices[base + 8] = posPrev.x;
  1875. m_vertices[base + 9] = posPrev.y;
  1876. m_vertices[base + 10] = posPrev.z;
  1877. m_vertices[base + 11] = trailPosition;
  1878. // a_TexCoordVec4C2: CP1.xyz (neighbor after end), sizeEnd
  1879. m_vertices[base + 12] = posAfter.x;
  1880. m_vertices[base + 13] = posAfter.y;
  1881. m_vertices[base + 14] = posAfter.z;
  1882. m_vertices[base + 15] = sizeEnd;
  1883. // a_TexCoordVec4C3: colorEnd.rgba
  1884. m_vertices[base + 16] = colorEnd.r;
  1885. m_vertices[base + 17] = colorEnd.g;
  1886. m_vertices[base + 18] = colorEnd.b;
  1887. m_vertices[base + 19] = colorEnd.a;
  1888. // a_TexCoordC4: uvs.xy
  1889. m_vertices[base + 20] = uvX;
  1890. m_vertices[base + 21] = uvY;
  1891. // a_Color: colorStart.rgba
  1892. m_vertices[base + 22] = colorStart.r;
  1893. m_vertices[base + 23] = colorStart.g;
  1894. m_vertices[base + 24] = colorStart.b;
  1895. m_vertices[base + 25] = colorStart.a;
  1896. vertexIndex++;
  1897. };
  1898. // Quad: 4 vertices (left/right at start/end of segment)
  1899. uint32_t baseVertex = vertexIndex;
  1900. addRopeVertex (0.0f, 0.0f); // left at start
  1901. addRopeVertex (1.0f, 0.0f); // right at start
  1902. addRopeVertex (1.0f, 1.0f); // right at end
  1903. addRopeVertex (0.0f, 1.0f); // left at end
  1904. m_indices[indexOffset++] = baseVertex + 0;
  1905. m_indices[indexOffset++] = baseVertex + 1;
  1906. m_indices[indexOffset++] = baseVertex + 2;
  1907. m_indices[indexOffset++] = baseVertex + 2;
  1908. m_indices[indexOffset++] = baseVertex + 3;
  1909. m_indices[indexOffset++] = baseVertex + 0;
  1910. }
  1911. m_activeIndexCount = static_cast<GLsizei> (indexOffset);
  1912. if (m_activeIndexCount == 0) {
  1913. return;
  1914. }
  1915. #if !NDEBUG
  1916. std::string str = "Rope particles ";
  1917. str += this->getParticle ().name + " (" + std::to_string (this->getId ()) + ", " + this->getParticle ().particleFile
  1918. + ")";
  1919. glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());
  1920. #endif
  1921. glBindBuffer (GL_ARRAY_BUFFER, m_vbo);
  1922. glBufferData (
  1923. GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (vertexIndex * ROPE_FLOATS_PER_VERTEX * sizeof (float)),
  1924. m_vertices.data (), GL_DYNAMIC_DRAW
  1925. );
  1926. glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, m_ebo);
  1927. glBufferData (
  1928. GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr> (indexOffset * sizeof (uint32_t)), m_indices.data (),
  1929. GL_DYNAMIC_DRAW
  1930. );
  1931. updateMatrices ();
  1932. // REFRACT: blit current scene content into the copy FBO before rendering
  1933. if (m_hasRefract && m_refractFBO) {
  1934. auto sceneFBO = getScene ().getFBO ();
  1935. GLint w = static_cast<GLint> (sceneFBO->getRealWidth ());
  1936. GLint h = static_cast<GLint> (sceneFBO->getRealHeight ());
  1937. glBindFramebuffer (GL_READ_FRAMEBUFFER, sceneFBO->getFramebuffer ());
  1938. glBindFramebuffer (GL_DRAW_FRAMEBUFFER, m_refractFBO->getFramebuffer ());
  1939. glBlitFramebuffer (0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
  1940. }
  1941. glEnable (GL_DEPTH_CLAMP);
  1942. m_pass->render ();
  1943. glDisable (GL_DEPTH_CLAMP);
  1944. #if !NDEBUG
  1945. glPopDebugGroup ();
  1946. #endif
  1947. }