CParticle.cpp 83 KB

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