CParticle.cpp 175 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372
  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 <chrono>
  9. #include <cmath>
  10. #include <cstdlib>
  11. #include <cstring>
  12. #include <ctime>
  13. #include <numeric>
  14. #include <glm/gtc/constants.hpp>
  15. #include <glm/gtc/matrix_transform.hpp>
  16. extern float g_Time;
  17. extern float g_RealTime;
  18. using namespace WallpaperEngine::Render::Objects;
  19. using namespace WallpaperEngine::Render::Utils;
  20. using namespace WallpaperEngine::Data::Model;
  21. namespace {
  22. /** wallpaper64.exe works in a y-up particle space, the particles here live in the same space mirrored on y */
  23. glm::vec3 flipY (glm::vec3 value) {
  24. value.y = -value.y;
  25. return value;
  26. }
  27. /** A blend window as sub_1401C2A40 stores it, active only when it changes anything (the loader's blended tags) */
  28. struct BlendWindow {
  29. float inStart;
  30. float inScale;
  31. float outEnd;
  32. float outScale;
  33. bool active;
  34. };
  35. BlendWindow makeBlendWindow (const ParticleBlendWindow& window) {
  36. const float inStart = std::min (window.inStart, window.inEnd - 0.0001f);
  37. const float outEnd = window.outEnd <= window.outStart + 0.0001f ? window.outStart + 0.0001f : window.outEnd;
  38. const float inLength = window.inEnd - inStart;
  39. const float outLength = outEnd - window.outStart;
  40. const bool active = (window.inEnd > 0.01f || window.outStart < 0.99f)
  41. && (window.outStart - window.inEnd > 0.01f || inLength > 0.01f || outLength > 0.01f);
  42. return { inStart, 1.0f / inLength, outEnd, 1.0f / outLength, active };
  43. }
  44. /** sub_14022A530 */
  45. float blendWeight (const BlendWindow& window, const ParticleInstance& p) {
  46. const float life = p.age / p.lifetime;
  47. return std::clamp ((window.outEnd - life) * window.outScale, 0.0f, 1.0f)
  48. * std::clamp ((life - window.inStart) * window.inScale, 0.0f, 1.0f);
  49. }
  50. /** sub_1401D15A0 cases 0xB/0xC: a color moved in HSV by the override color's distance to the reference */
  51. glm::vec3 shiftColor (const glm::vec3& rgb, const glm::vec3& shift) {
  52. const glm::vec3 hsv = WallpaperEngine::Maths::rgbToHsv (rgb);
  53. const float hue = shift.x + hsv.x;
  54. return WallpaperEngine::Maths::hsvToRgb (glm::vec3 (
  55. hue - std::floor (hue), std::clamp (shift.y + hsv.y, 0.0f, 1.0f), std::clamp (shift.z + hsv.z, 0.0f, 1.0f)
  56. ));
  57. }
  58. } // namespace
  59. CParticle::CParticle (Wallpapers::CScene& scene, const Particle& particle) :
  60. CObject (scene, particle), CRenderable (scene, particle, *particle.material->material),
  61. ScriptableObject (scene, particle), m_particle (particle) {
  62. this->registerProperty ("scale", *particle.scale->value);
  63. this->registerProperty ("angles", *particle.angles->value);
  64. this->registerProperty ("visible", *particle.visible->value);
  65. this->registerProperty ("parallaxDepth", *particle.parallaxDepth->value);
  66. this->detectTexture ();
  67. m_worldSpace = (particle.flags & 1) != 0;
  68. if (std::getenv ("LWE_FIXED_TIMESTEP") != nullptr) {
  69. m_rng.seed (static_cast<std::mt19937::result_type> (this->getId ()));
  70. } else {
  71. std::random_device rd;
  72. m_rng.seed (rd ());
  73. }
  74. // Read renderer config early - buffer sizing below depends on it
  75. if (!m_particle.renderers.empty ()) {
  76. const auto& renderer = m_particle.renderers[0];
  77. if (renderer.name == "rope" || renderer.name == "ropetrail") {
  78. // Both rope and ropetrail use genericropeparticle shader
  79. m_useRopeRenderer = true;
  80. m_ropeSubdivision = std::max (0, static_cast<int> (renderer.subdivision));
  81. m_ropeUVScale = renderer.uvScale;
  82. m_ropeUVScrolling = renderer.uvScrolling;
  83. m_ropeUVSmoothing = renderer.uvSmoothing;
  84. if (renderer.name == "ropetrail") {
  85. // clamps from wallpaper64.exe sub_1401C5490
  86. m_useTrailRenderer = true;
  87. m_trailLength = std::max (renderer.length, 0.001f);
  88. m_ropeSegments = std::clamp (static_cast<int> (renderer.segments), 2, 32);
  89. m_ropeSubdivision = std::clamp (static_cast<int> (renderer.subdivision), 0, 32);
  90. m_trailFadeAlpha = renderer.fadeAlpha;
  91. m_trailFadeSize = renderer.fadeSize;
  92. }
  93. } else if (renderer.name == "spritetrail") {
  94. // spritetrail uses genericparticle with TRAILRENDERER combo
  95. m_useTrailRenderer = true;
  96. m_trailLength = renderer.length;
  97. m_trailMaxLength = renderer.maxLength;
  98. m_trailMinLength = renderer.minLength;
  99. }
  100. }
  101. float countMultiplier = particle.instanceOverride.count->value->getFloat ();
  102. uint32_t adjustedMaxCount = static_cast<uint32_t> (particle.maxCount * countMultiplier);
  103. // Use wallpaper's specified count, or default if maxCount is 0
  104. m_maxParticles = (adjustedMaxCount > 0) ? adjustedMaxCount : DEFAULT_MAX_PARTICLES;
  105. m_particles.resize (m_maxParticles);
  106. m_slotUsed.assign (m_maxParticles, 0);
  107. if (m_useRopeRenderer && m_useTrailRenderer) {
  108. // ropetrail: a quad per history segment of every particle
  109. const size_t quads = static_cast<size_t> (m_maxParticles) * m_ropeSegments;
  110. m_vertices.resize (quads * 4 * ROPE_FLOATS_PER_VERTEX);
  111. m_indices.resize (quads * 6);
  112. m_trailHistory.resize (static_cast<size_t> (m_maxParticles) * m_ropeSegments);
  113. m_trailCount.resize (m_maxParticles);
  114. m_trailScroll.resize (m_maxParticles);
  115. m_trailInterval = m_trailLength / static_cast<float> (m_ropeSegments);
  116. } else if (m_useRopeRenderer) {
  117. // Rope: N particles connect via (N-1) segments, each subdivided into sub-segments
  118. const int subdivision = std::max (1, m_ropeSubdivision);
  119. const int maxSubSegments = std::max (1, static_cast<int> (m_maxParticles - 1)) * subdivision;
  120. m_vertices.resize (maxSubSegments * 4 * ROPE_FLOATS_PER_VERTEX);
  121. m_indices.resize (maxSubSegments * 6);
  122. } else {
  123. // 4 vertices, 6 indices per particle
  124. const int verticesPerParticle = 4;
  125. const int indicesPerParticle = 6;
  126. m_vertices.resize (m_maxParticles * verticesPerParticle * SPRITE_FLOATS_PER_VERTEX);
  127. m_indices.resize (m_maxParticles * indicesPerParticle);
  128. }
  129. }
  130. CParticle::CParticle (CParticle& parent, const ParticleChild& child) :
  131. CParticle (parent.getScene (), *child.particle) {
  132. m_parent = &parent;
  133. m_childDefinition = &child;
  134. m_placement = child.transform;
  135. }
  136. CParticle::~CParticle () {
  137. delete m_pass;
  138. if (m_vao != 0) {
  139. glDeleteVertexArrays (1, &m_vao);
  140. }
  141. if (m_vbo != 0) {
  142. glDeleteBuffers (1, &m_vbo);
  143. }
  144. if (m_ebo != 0) {
  145. glDeleteBuffers (1, &m_ebo);
  146. }
  147. m_vertices.clear ();
  148. m_indices.clear ();
  149. }
  150. void CParticle::setup () {
  151. if (m_initialized) {
  152. return;
  153. }
  154. if (m_particle.material && m_particle.material->material && !m_particle.material->material->passes.empty ()) {
  155. auto& firstPass = *m_particle.material->material->passes.begin ();
  156. // Overbright: brightness multiplier for additive particles
  157. auto overbrightIt = firstPass->constants.find ("ui_editor_properties_overbright");
  158. if (overbrightIt != firstPass->constants.end ()) {
  159. m_overbright = overbrightIt->second->value->getFloat ();
  160. }
  161. }
  162. // TextureParser computes the spritesheet grid from TEXS frame data (animated textures) or
  163. // .tex-json metadata (static textures). GIF-style animated textures (separate GL texture per
  164. // frame) get 0 cols/rows since a 1x1 grid can't hold all frames - no SPRITESHEET mode needed,
  165. // frame switching happens via texture ID instead.
  166. if (const auto texture = getTexture ()) {
  167. m_spritesheetCols = static_cast<int> (texture->getSpritesheetCols ());
  168. m_spritesheetRows = static_cast<int> (texture->getSpritesheetRows ());
  169. m_spritesheetFrames = static_cast<int> (texture->getSpritesheetFrames ());
  170. m_spritesheetDuration = texture->getSpritesheetDuration ();
  171. }
  172. // wallpaper64.exe system flag 2: only angularvelocityrandom and angularmovement make angular speed a thing, the
  173. // remap components ignore it otherwise
  174. for (const auto& initializer : m_particle.initializers) {
  175. if (initializer && initializer->is<AngularVelocityRandomInitializer> ()) {
  176. m_hasAngularVelocity = true;
  177. }
  178. }
  179. for (const auto& op : m_particle.operators) {
  180. if (op && op->is<AngularMovementOperator> ()) {
  181. m_hasAngularVelocity = true;
  182. }
  183. }
  184. setupEmitters ();
  185. setupInitializers ();
  186. setupOperators ();
  187. setupPass ();
  188. m_controlPoints.resize (8);
  189. for (const auto& cp : m_particle.controlPoints) {
  190. if (cp.id >= 0 && cp.id < 8) {
  191. auto& point = m_controlPoints[cp.id];
  192. // offsets are in WE's y-up particle space like emitter origins, particles here are mirrored on y
  193. point.offset = glm::vec3 (cp.offset.x, -cp.offset.y, cp.offset.z);
  194. point.linkMouse = (cp.flags & 1) != 0;
  195. point.worldSpace = (cp.flags & 2) != 0;
  196. point.followParent = (cp.flags & 4) != 0;
  197. point.copyUntransformed = (cp.flags & 8) != 0;
  198. point.parentIndex = cp.parentControlPoint;
  199. // sub_14022C3C0 starts every control point as a plain translation to its offset
  200. point.position = point.offset;
  201. if (point.linkMouse) {
  202. m_hasMouseControlPoint = true;
  203. }
  204. }
  205. }
  206. for (size_t i = 0; i < m_controlPoints.size (); i++) {
  207. m_controlPoints[i].remapOutput = (m_particle.remapOutputControlPoints & (1u << i)) != 0;
  208. }
  209. this->updateFrame ();
  210. this->updateControlPoints ();
  211. m_initialized = true;
  212. this->refreshColorOverride ();
  213. this->setupChildren ();
  214. }
  215. void CParticle::render () {
  216. if (!m_initialized) {
  217. return;
  218. }
  219. const auto& appContext = this->getScene ().getContext ().getApp ().getContext ();
  220. const auto visibility = appContext.resolveObjectVisibility (this->getId (), this->getObject ().name);
  221. if (!visibility.value_or (m_particle.visible->value->getBool ())) {
  222. // sub_140230650 starts the layer's time over while it is hidden
  223. m_layerTime = 0.0f;
  224. return;
  225. }
  226. // stop() drops every particle, and a later play() starts emitting from scratch
  227. const auto playback = this->getPlayback ();
  228. if (playback == Playback::Stopped) {
  229. m_particleCount = 0;
  230. std::fill (m_slotUsed.begin (), m_slotUsed.end (), 0);
  231. m_slotExtent = 0;
  232. std::fill (m_ghostUsed.begin (), m_ghostUsed.end (), 0);
  233. if (m_lastPlayback != Playback::Stopped) {
  234. this->clearEventChildren ();
  235. for (const auto& child : m_staticChildren) {
  236. child->restart ();
  237. }
  238. }
  239. } else if (m_lastPlayback == Playback::Stopped) {
  240. m_emitters.clear ();
  241. setupEmitters ();
  242. }
  243. m_lastPlayback = playback;
  244. const float currentTime = m_hasMouseControlPoint ? g_RealTime : g_Time;
  245. // Initialize time on first render to avoid a huge dt spike, and skip the update
  246. // that frame to avoid an initial burst
  247. if (m_time == 0.0) {
  248. // "starttime" prewarms the system so it starts already populated instead of every
  249. // particle visibly leaving the emitter at once
  250. if (playback == Playback::Playing) {
  251. this->prewarm (currentTime);
  252. }
  253. m_time = currentTime;
  254. this->draw (glm::mat4 (1.0f));
  255. return;
  256. }
  257. float dt = currentTime - static_cast<float> (m_time);
  258. m_time = currentTime;
  259. if (dt > 0.0f && playback != Playback::Stopped) {
  260. // Cap dt to prevent simulation instability across different FPS
  261. dt = std::min (dt, 0.1f);
  262. update (dt);
  263. }
  264. this->draw (glm::mat4 (1.0f));
  265. }
  266. void CParticle::draw (const glm::mat4& base) {
  267. // sub_140236600 / sub_1402366F0: world space systems draw with the stack's base, everything else multiplies
  268. // its +928 matrix onto what the parent left
  269. const glm::mat4 placement = m_parent == nullptr ? this->objectMatrix () : m_placement;
  270. const glm::mat4 top = m_worldSpace ? base : base * placement;
  271. m_modelMatrix = m_worldSpace ? glm::mat4 (1.0f) : top;
  272. m_drawFlipY = !getScene ().getCamera ().isPerspective ();
  273. if (m_drawFlipY) {
  274. m_modelMatrix = m_modelMatrix * glm::scale (glm::mat4 (1.0f), glm::vec3 (1.0f, -1.0f, 1.0f));
  275. }
  276. if (m_particleCount > 0 && m_particle.material) {
  277. if (m_useRopeRenderer) {
  278. renderRope ();
  279. } else {
  280. renderSprites ();
  281. }
  282. }
  283. if (m_staticChildren.empty () && m_eventChildren.empty ()) {
  284. return;
  285. }
  286. // drawn right away, depth first: static children, then event children slot by slot. A world space system
  287. // hands down its raw +928 matrix, for a static child that is only the child transform, not its full frame
  288. const glm::mat4 childBase = m_worldSpace ? placement : top;
  289. for (const auto& child : m_staticChildren) {
  290. child->draw (childBase);
  291. }
  292. for (const auto& slot : m_eventChildren) {
  293. for (const auto& child : slot.active) {
  294. child->draw (childBase);
  295. }
  296. }
  297. }
  298. void CParticle::prewarm (double now) {
  299. if (m_prewarmed || m_particle.startTime <= 0.0f) {
  300. return;
  301. }
  302. m_prewarmed = true;
  303. // wallpaper64.exe sub_14022EBE0: whole fixed steps, coarser ones for big systems, and no child events meanwhile
  304. const float step = m_particle.maxCount < 500 ? 0.05f : 0.2f;
  305. m_prewarming = true;
  306. m_time = now - m_particle.startTime;
  307. for (float done = 0.0f; done < m_particle.startTime; done += step) {
  308. m_time += step;
  309. update (step);
  310. }
  311. m_time = now;
  312. m_prewarming = false;
  313. }
  314. bool CParticle::isPlaying () const {
  315. const auto playback = this->getPlayback ();
  316. return playback == Playback::Playing || (playback == Playback::Paused && m_particleCount > 0);
  317. }
  318. void CParticle::update (float dt) {
  319. // children share the instance override and scale their own time
  320. const float childDt = dt;
  321. // instanceoverride "rate" scales the whole simulation's time, not only emission (wallpaper64.exe sub_1401B7FF0)
  322. dt *= std::max (0.01f, m_particle.instanceOverride.rate->value->getFloat ());
  323. // prewarming runs the simulation directly, the layer and system clocks only move in real updates
  324. if (!m_prewarming) {
  325. m_systemTime += dt;
  326. if (m_parent == nullptr) {
  327. m_layerTime += dt;
  328. }
  329. }
  330. if (g_RealTime != m_lastRealTime) {
  331. if (m_lastRealTime > 0.0f) {
  332. m_frameDelta = g_RealTime - m_lastRealTime;
  333. }
  334. m_lastRealTime = g_RealTime;
  335. m_frameCounter++;
  336. }
  337. this->refreshColorOverride ();
  338. // sub_140236CD0: ages first, so a new particle is drawn at age 0 on its first frame, then control points,
  339. // emission and operators
  340. for (uint32_t i = 0; i < m_particleCount; i++) {
  341. m_particles[i].age += dt;
  342. }
  343. // Order-preserving compaction: particles only die from lifetime expiry (never from
  344. // size, since size can oscillate), and index 0 must stay the oldest particle
  345. uint32_t writeIdx = 0;
  346. for (uint32_t readIdx = 0; readIdx < m_particleCount; readIdx++) {
  347. if (m_particles[readIdx].isAlive ()) {
  348. if (writeIdx != readIdx) {
  349. m_particles[writeIdx] = m_particles[readIdx];
  350. if (!m_trailHistory.empty ()) {
  351. std::copy_n (
  352. m_trailHistory.begin () + static_cast<size_t> (readIdx) * m_ropeSegments, m_ropeSegments,
  353. m_trailHistory.begin () + static_cast<size_t> (writeIdx) * m_ropeSegments
  354. );
  355. m_trailCount[writeIdx] = m_trailCount[readIdx];
  356. m_trailScroll[writeIdx] = m_trailScroll[readIdx];
  357. }
  358. }
  359. writeIdx++;
  360. } else {
  361. const uint32_t slot = m_particles[readIdx].slot;
  362. if (slot < m_slotUsed.size ()) {
  363. m_slotUsed[slot] = 0;
  364. }
  365. if (slot < m_ghostUsed.size ()) {
  366. m_ghosts[slot] = m_particles[readIdx];
  367. m_ghostUsed[slot] = 1;
  368. }
  369. if (m_hasDeathEvents && !m_prewarming) {
  370. m_deaths.push_back (m_particles[readIdx]);
  371. }
  372. }
  373. }
  374. m_particleCount = writeIdx;
  375. this->updateFrame ();
  376. this->updateControlPoints ();
  377. for (auto& cp : m_controlPoints) {
  378. cp.movement = cp.hasPreviousPosition ? cp.position - cp.previousPosition : glm::vec3 (0.0f);
  379. cp.velocity = cp.hasPreviousPosition && dt > 0.0f ? cp.movement / dt : glm::vec3 (0.0f);
  380. cp.previousPosition = cp.position;
  381. cp.hasPreviousPosition = true;
  382. }
  383. // pause() stops emission but keeps simulating what is already alive
  384. if (this->getPlayback () == Playback::Playing && !m_emissionStopped) {
  385. const uint32_t firstNew = m_particleCount;
  386. for (auto& emitter : m_emitters) {
  387. emitter (m_particles, m_particleCount, dt);
  388. }
  389. m_emitterTime += dt;
  390. for (uint32_t i = firstNew; i < m_particleCount; i++) {
  391. auto& p = m_particles[i];
  392. p.id = m_nextParticleId++;
  393. // sub_1402378A0 puts a new particle in the lowest free pool slot
  394. const auto freeSlot = std::find (m_slotUsed.begin (), m_slotUsed.end (), 0);
  395. p.slot = static_cast<uint32_t> (freeSlot - m_slotUsed.begin ());
  396. if (freeSlot != m_slotUsed.end ()) {
  397. *freeSlot = 1;
  398. }
  399. m_slotExtent = std::max (m_slotExtent, p.slot + 1);
  400. if (p.slot < m_ghostUsed.size ()) {
  401. m_ghostUsed[p.slot] = 0;
  402. }
  403. // sub_14023B340 end: a new particle's whole trail history starts where it spawned
  404. if (!m_trailHistory.empty ()) {
  405. std::fill_n (m_trailHistory.begin () + static_cast<size_t> (i) * m_ropeSegments, m_ropeSegments, p.position);
  406. m_trailCount[i] = 1;
  407. m_trailScroll[i] = 0;
  408. }
  409. if (m_hasBirthEvents && !m_prewarming) {
  410. m_births.push_back (p.id);
  411. }
  412. }
  413. }
  414. // sub_14023FBC0 starts from the spawn values of what a remapvalue writes, and remembers where particles are
  415. // for collisionquad
  416. if (m_resetSizeFromBase || m_resetAlphaFromBase || m_resetColorFromBase || m_tracksPreviousPosition) {
  417. for (uint32_t i = 0; i < m_particleCount; i++) {
  418. auto& p = m_particles[i];
  419. if (m_resetSizeFromBase) {
  420. p.size = p.initial.size;
  421. }
  422. if (m_resetAlphaFromBase) {
  423. p.alpha = p.initial.alpha;
  424. }
  425. if (m_resetColorFromBase) {
  426. p.color = p.initial.color;
  427. }
  428. if (m_tracksPreviousPosition) {
  429. p.previousPosition = p.position;
  430. }
  431. }
  432. }
  433. for (auto& op : m_operators) {
  434. op (m_particles, m_particleCount, m_controlPoints, static_cast<float> (m_time), dt);
  435. }
  436. for (uint32_t i = 0; i < m_particleCount; i++) {
  437. auto& p = m_particles[i];
  438. if (m_spritesheetFrames > 0) {
  439. float lifetimePos = p.getLifetimePos ();
  440. float animSpeed = m_particle.sequenceMultiplier > 0.0f ? m_particle.sequenceMultiplier : 1.0f;
  441. if (m_particle.animationMode == "randomframe") {
  442. if (p.frame < 0.0f) {
  443. // per slot rather than per address, the address changes between runs
  444. std::mt19937 particleRng (
  445. static_cast<std::mt19937::result_type> (i + this->getId () * 2654435761u)
  446. );
  447. std::uniform_int_distribution<int> dist (0, m_spritesheetFrames - 1);
  448. p.frame = static_cast<float> (dist (particleRng));
  449. }
  450. } else if (m_particle.animationMode == "once") {
  451. p.frame = std::min (
  452. lifetimePos * m_spritesheetFrames * animSpeed, static_cast<float> (m_spritesheetFrames - 1)
  453. );
  454. } else {
  455. if (m_spritesheetDuration > 0.0f) {
  456. float timeInCycle = std::fmod (p.age * animSpeed, m_spritesheetDuration);
  457. float cyclePos = timeInCycle / m_spritesheetDuration;
  458. p.frame = std::fmod (cyclePos * m_spritesheetFrames, static_cast<float> (m_spritesheetFrames));
  459. } else {
  460. p.frame = std::fmod (
  461. lifetimePos * m_spritesheetFrames * animSpeed, static_cast<float> (m_spritesheetFrames)
  462. );
  463. }
  464. }
  465. }
  466. }
  467. // sub_1402308A0: every interval each particle pushes its position to the front of its history. It runs with
  468. // the vertex build, which prewarming skips
  469. if (!m_trailHistory.empty () && !m_prewarming) {
  470. m_trailTimer -= dt;
  471. if (m_trailTimer <= 0.0f) {
  472. m_trailTimer = m_trailInterval;
  473. for (uint32_t i = 0; i < m_particleCount; i++) {
  474. const auto history = m_trailHistory.begin () + static_cast<size_t> (i) * m_ropeSegments;
  475. std::copy_backward (history, history + m_ropeSegments - 1, history + m_ropeSegments);
  476. *history = m_particles[i].position;
  477. m_trailCount[i] = static_cast<uint16_t> (std::min<int> (m_trailCount[i] + 1, m_ropeSegments));
  478. m_trailScroll[i]++;
  479. }
  480. }
  481. }
  482. // prewarming leaves children alone, static ones prewarm on their own when created
  483. if (!m_prewarming) {
  484. this->updateChildren (childDt);
  485. }
  486. }
  487. void CParticle::refreshColorOverride () {
  488. const auto& instanceOverride = m_particle.instanceOverride;
  489. const glm::vec3 color = instanceOverride.colorn->value->getVec3 ();
  490. const glm::vec3 distance = glm::abs (m_particle.colorReference - color);
  491. // children only follow it while their parent does
  492. m_colorOverride.active = instanceOverride.hasColor && color.r >= 0.0f && (m_particle.flags & 8) == 0
  493. && (distance.x >= 0.0035294117f || distance.y >= 0.0035294117f || distance.z >= 0.0035294117f)
  494. && (m_parent == nullptr || m_parent->m_colorOverride.active);
  495. // sub_1401D15A0: the spawn color starts at the brightness override (HDR scene rendering only, not with particle
  496. // flag 8). A file without a color initializer, or any scene before version 5, also multiplies by the color
  497. const float brightness = getScene ().isHDR () && (m_particle.flags & 8) == 0
  498. ? instanceOverride.brightness->value->getFloat ()
  499. : 1.0f;
  500. m_colorOverride.tint
  501. = (m_colorOverride.active && m_particle.overrideColorTints ? color : glm::vec3 (1.0f)) * brightness;
  502. if (m_colorOverride.active) {
  503. m_colorOverride.hsv = WallpaperEngine::Maths::rgbToHsv (color);
  504. m_colorOverride.shift = m_colorOverride.hsv - WallpaperEngine::Maths::rgbToHsv (m_particle.colorReference);
  505. }
  506. }
  507. void CParticle::setupChildren () {
  508. for (const auto& child : m_particle.children) {
  509. if (child.particle == nullptr) {
  510. continue;
  511. }
  512. if (child.type == ParticleChildType::Static) {
  513. std::unique_ptr<CParticle> instance (new CParticle (*this, child));
  514. instance->setup ();
  515. instance->prewarm (m_time);
  516. m_staticChildren.push_back (std::move (instance));
  517. continue;
  518. }
  519. m_eventChildren.push_back (EventChildSlot { .child = &child, .active = {}, .pool = {} });
  520. m_hasBirthEvents |= child.type != ParticleChildType::EventDeath;
  521. m_hasDeathEvents |= child.type == ParticleChildType::EventDeath;
  522. }
  523. }
  524. const ParticleInstance* CParticle::findParticle (uint32_t id) const {
  525. // ids grow in spawn order and compaction keeps that order
  526. const auto end = m_particles.begin () + m_particleCount;
  527. const auto it = std::lower_bound (
  528. m_particles.begin (), end, id, [] (const ParticleInstance& p, uint32_t value) { return p.id < value; }
  529. );
  530. return it != end && it->id == id ? &*it : nullptr;
  531. }
  532. void CParticle::placeChild (const glm::mat4& placement) {
  533. m_placement = placement;
  534. }
  535. glm::mat4 CParticle::eventPlacement (const ParticleChild& child, const glm::vec3& position) const {
  536. // particle positions are in this system's frame, or the world when it is world space; the child's +928
  537. // matrix is relative to this frame, or the world when the child is world space
  538. const glm::mat4 placement = glm::translate (glm::mat4 (1.0f), position) * child.transform;
  539. const bool childWorldSpace = (child.particle->flags & 1) != 0;
  540. if (childWorldSpace == m_worldSpace) {
  541. return placement;
  542. }
  543. return childWorldSpace ? m_frame * placement : glm::inverse (m_frame) * placement;
  544. }
  545. glm::mat4 CParticle::objectMatrix () const {
  546. // WE's world matrix, parent chain included: particles parented to a layer or group move and scale with it
  547. glm::mat4 matrix = getScene ().objectWorldMatrix (m_particle);
  548. // 2D scenes: WE's space (y up from the bottom left) to the centered y down space of the ortho projection here
  549. if (!getScene ().getCamera ().isPerspective ()) {
  550. const glm::mat4 flipY = glm::scale (glm::mat4 (1.0f), glm::vec3 (1.0f, -1.0f, 1.0f));
  551. const glm::vec3 center (getScene ().getWidth () / 2.0f, getScene ().getHeight () / 2.0f, 0.0f);
  552. matrix = flipY * glm::translate (glm::mat4 (1.0f), -center) * matrix * flipY;
  553. }
  554. // CScene::renderFrame() already folds disableparallax into getParallaxDisplacement()
  555. if (getScene ().getScene ().camera.parallax.enabled->value->getBool ()) {
  556. const glm::vec2 offset = getScene ().getParallaxOffset (m_particle);
  557. matrix = glm::translate (glm::mat4 (1.0f), glm::vec3 (offset.x, offset.y, 0.0f)) * matrix;
  558. }
  559. return matrix;
  560. }
  561. void CParticle::updateFrame () {
  562. if (m_parent == nullptr) {
  563. m_placement = this->objectMatrix ();
  564. m_frame = m_placement;
  565. } else if (m_worldSpace && m_childDefinition->type != ParticleChildType::Static) {
  566. // sub_140229760 replaces the stack top for world space systems
  567. m_frame = m_placement;
  568. } else {
  569. // static children go through sub_140229810, which always multiplies
  570. m_frame = m_parent->m_frame * m_placement;
  571. }
  572. }
  573. void CParticle::updateControlPoints () {
  574. const glm::mat4 toLocal = glm::inverse (m_frame);
  575. const glm::vec2* mousePos = getScene ().getMousePositionNormalized ();
  576. const bool takesParentParticles = m_childDefinition != nullptr && (m_childDefinition->flags & 1) != 0;
  577. const int firstParticlePoint = m_childDefinition != nullptr ? m_childDefinition->controlPointStartIndex : 0;
  578. for (size_t i = 0; i < m_controlPoints.size (); i++) {
  579. auto& cp = m_controlPoints[i];
  580. if (cp.remapOutput) {
  581. continue;
  582. }
  583. if (cp.linkMouse) {
  584. if (mousePos == nullptr) {
  585. continue;
  586. }
  587. // the cursor unprojected at NDC depth 0 replaces the point's translation, its offset plays no part
  588. const glm::vec4 ndc { mousePos->x * 2.0f - 1.0f, (1.0f - mousePos->y) * 2.0f - 1.0f, 0.0f, 1.0f };
  589. glm::vec3 position;
  590. const auto& camera = getScene ().getCamera ();
  591. if (camera.isPerspective ()) {
  592. const glm::vec4 world = glm::inverse (camera.getPerspective () * camera.getView ()) * ndc;
  593. position = glm::vec3 (world) / world.w;
  594. } else {
  595. // the inverse of the centered orthographic projection
  596. const float screenWidth = static_cast<float> (getScene ().getWidth ());
  597. const float screenHeight = static_cast<float> (getScene ().getHeight ());
  598. position = glm::vec3 (ndc.x * screenWidth / 2.0f, ndc.y * screenHeight / 2.0f, 0.0f);
  599. }
  600. // world space systems keep x and y only
  601. cp.position = m_worldSpace ? glm::vec3 (position.x, position.y, 0.0f)
  602. : glm::vec3 (toLocal * glm::vec4 (position, 1.0f));
  603. continue;
  604. }
  605. if (cp.followParent && m_parent != nullptr && cp.parentIndex >= 0
  606. && cp.parentIndex < static_cast<int> (m_parent->m_controlPoints.size ())) {
  607. const auto& source = m_parent->m_controlPoints[cp.parentIndex];
  608. glm::mat4 matrix (source.orientation);
  609. matrix[3] = glm::vec4 (source.position, 1.0f);
  610. if (!cp.copyUntransformed && !(m_worldSpace && m_parent->m_worldSpace)) {
  611. if (m_worldSpace) {
  612. matrix = m_parent->m_frame * matrix;
  613. } else if (m_parent->m_worldSpace) {
  614. matrix = toLocal * matrix;
  615. } else {
  616. matrix = toLocal * m_parent->m_frame * matrix;
  617. }
  618. }
  619. cp.orientation = glm::mat3 (matrix);
  620. cp.position = glm::vec3 (matrix[3]);
  621. continue;
  622. }
  623. // the parent's particles own these (sub_14022A580)
  624. if (!cp.followParent && takesParentParticles && static_cast<int> (i) >= firstParticlePoint) {
  625. continue;
  626. }
  627. // sub_14022A070: the point's local matrix (its offset, or what the instance override made of it) is local
  628. // unless flag 2 says world, and the point lives in the system's space. Control point 0 of a world space system
  629. // always counts as local
  630. const glm::mat4 local = localControlPointMatrix (i);
  631. glm::mat4 matrix;
  632. if (m_worldSpace) {
  633. matrix = cp.worldSpace && i != 0 ? local : m_frame * local;
  634. } else {
  635. matrix = cp.worldSpace ? toLocal * local : local;
  636. }
  637. cp.orientation = glm::mat3 (matrix);
  638. cp.position = glm::vec3 (matrix[3]);
  639. }
  640. }
  641. void CParticle::spawnEventChildren (ParticleChildType type, const ParticleInstance& particle, bool alive) {
  642. for (auto& slot : m_eventChildren) {
  643. const auto& child = *slot.child;
  644. if (child.type != type || slot.active.size () >= static_cast<size_t> (std::max (0, child.maxCount))) {
  645. continue;
  646. }
  647. if (WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) > child.probability) {
  648. continue;
  649. }
  650. const glm::mat4 placement = this->eventPlacement (child, particle.position);
  651. // finished instances are kept around and started over instead of building a new pass every time
  652. std::unique_ptr<CParticle> instance;
  653. if (!slot.pool.empty ()) {
  654. instance = std::move (slot.pool.back ());
  655. slot.pool.pop_back ();
  656. instance->placeChild (placement);
  657. instance->restart ();
  658. } else {
  659. instance.reset (new CParticle (*this, child));
  660. instance->m_placement = placement;
  661. instance->setup ();
  662. }
  663. // the inherit components read the event's particle from the first emission on, prewarm included
  664. instance->m_eventId = alive ? std::optional<uint32_t> (particle.id) : std::nullopt;
  665. instance->m_eventParticle = particle;
  666. instance->m_hasEventParticle = true;
  667. instance->m_following = type == ParticleChildType::EventFollow;
  668. instance->m_time = m_time;
  669. instance->prewarm (m_time);
  670. slot.active.push_back (std::move (instance));
  671. }
  672. }
  673. void CParticle::passControlPoints (CParticle& child, const ParticleChild& definition) const {
  674. // sub_14022A580: child flags bit 0 hands the parent's particles to the control points from the start index on,
  675. // one each in order. WE never gets past a control point that follows the cursor or the parent or that a remap
  676. // component owns (flags & 0x10005)
  677. if ((definition.flags & 1) == 0) {
  678. return;
  679. }
  680. // only the space switch through this system's frame, WE doesn't undo the child's own placement
  681. glm::mat4 transform (1.0f);
  682. if (m_worldSpace && !child.m_worldSpace) {
  683. transform = glm::inverse (m_frame);
  684. } else if (!m_worldSpace && child.m_worldSpace) {
  685. transform = m_frame;
  686. }
  687. auto& points = child.m_controlPoints;
  688. int index = std::max (0, definition.controlPointStartIndex);
  689. if (index >= static_cast<int> (points.size ()) || m_particleCount == 0) {
  690. return;
  691. }
  692. // WE walks its particle pool slot by slot, so the particles go out in slot order, not by age
  693. std::vector<uint32_t> order (m_particleCount);
  694. std::iota (order.begin (), order.end (), 0u);
  695. const size_t wanted = std::min<size_t> (order.size (), points.size () - index);
  696. std::partial_sort (order.begin (), order.begin () + wanted, order.end (), [this] (uint32_t a, uint32_t b) {
  697. return m_particles[a].slot < m_particles[b].slot;
  698. });
  699. for (size_t n = 0; n < wanted; n++) {
  700. auto& cp = points[index];
  701. if (cp.linkMouse || cp.followParent || cp.remapOutput) {
  702. break;
  703. }
  704. cp.position = glm::vec3 (transform * glm::vec4 (m_particles[order[n]].position, 1.0f));
  705. index++;
  706. }
  707. }
  708. void CParticle::updateChildren (float dt) {
  709. for (const auto& child : m_staticChildren) {
  710. this->passControlPoints (*child, *child->m_childDefinition);
  711. child->m_time += dt;
  712. child->update (dt);
  713. }
  714. if (m_eventChildren.empty ()) {
  715. return;
  716. }
  717. // wallpaper64.exe sub_140236CD0 (deaths) and the end of sub_1402378A0 (births)
  718. for (const auto& particle : m_deaths) {
  719. this->spawnEventChildren (ParticleChildType::EventDeath, particle, false);
  720. }
  721. m_deaths.clear ();
  722. for (const uint32_t id : m_births) {
  723. if (const auto* p = this->findParticle (id)) {
  724. const ParticleInstance particle = *p;
  725. this->spawnEventChildren (ParticleChildType::EventFollow, particle, true);
  726. this->spawnEventChildren (ParticleChildType::EventSpawn, particle, true);
  727. }
  728. }
  729. m_births.clear ();
  730. // sub_1402308A0: followers move with their particle, once it dies they stop emitting and fade out,
  731. // and finished instances go back to the pool
  732. for (auto& slot : m_eventChildren) {
  733. for (auto it = slot.active.begin (); it != slot.active.end ();) {
  734. CParticle& child = **it;
  735. if (child.m_eventId.has_value ()) {
  736. if (const auto* p = this->findParticle (*child.m_eventId)) {
  737. child.m_eventParticle = *p;
  738. if (child.m_following) {
  739. child.placeChild (this->eventPlacement (*slot.child, p->position));
  740. }
  741. } else {
  742. child.m_eventId.reset ();
  743. if (child.m_following) {
  744. child.m_emissionStopped = true;
  745. }
  746. }
  747. }
  748. this->passControlPoints (child, *slot.child);
  749. child.m_time += dt;
  750. child.update (dt);
  751. if (child.isFinished ()) {
  752. slot.pool.push_back (std::move (*it));
  753. it = slot.active.erase (it);
  754. } else {
  755. ++it;
  756. }
  757. }
  758. }
  759. }
  760. void CParticle::clearEventChildren () {
  761. for (auto& slot : m_eventChildren) {
  762. for (auto& child : slot.active) {
  763. slot.pool.push_back (std::move (child));
  764. }
  765. slot.active.clear ();
  766. }
  767. m_births.clear ();
  768. m_deaths.clear ();
  769. }
  770. void CParticle::restart () {
  771. m_particleCount = 0;
  772. std::fill (m_slotUsed.begin (), m_slotUsed.end (), 0);
  773. std::fill (m_ghostUsed.begin (), m_ghostUsed.end (), 0);
  774. m_slotExtent = 0;
  775. m_systemTime = 0.0f;
  776. m_emitters.clear ();
  777. setupEmitters ();
  778. m_emitterTime = 0.0f;
  779. m_emissionStopped = false;
  780. m_prewarmed = false;
  781. m_eventId.reset ();
  782. m_hasEventParticle = false;
  783. m_following = false;
  784. for (auto& cp : m_controlPoints) {
  785. cp.hasPreviousPosition = false;
  786. }
  787. for (const auto& child : m_staticChildren) {
  788. child->restart ();
  789. }
  790. this->clearEventChildren ();
  791. }
  792. bool CParticle::isFinished () const {
  793. if (m_particleCount > 0 || (!m_emissionStopped && !this->emittersExhausted ())) {
  794. return false;
  795. }
  796. for (const auto& child : m_staticChildren) {
  797. if (!child->isFinished ()) {
  798. return false;
  799. }
  800. }
  801. for (const auto& slot : m_eventChildren) {
  802. if (!slot.active.empty ()) {
  803. return false;
  804. }
  805. }
  806. return true;
  807. }
  808. DynamicValue* CParticle::emitterCountOverride () const {
  809. // wallpaper64.exe sub_1401C5490 binds the instanceoverride count to every emitter's rate, speed to its
  810. // speedmin/speedmax, unless particle flag 0x20 / 0x10
  811. return (m_particle.flags & 0x20) == 0 ? m_particle.instanceOverride.count->value.get () : nullptr;
  812. }
  813. bool CParticle::emittersExhausted () const {
  814. // sub_1402378A0: past its delay an emitter without a rate switches off after its burst, one with a duration
  815. // once that runs out, and a rate without a duration keeps going forever
  816. for (const auto& emitter : m_particle.emitters) {
  817. if (emitter.rate <= 0.0f) {
  818. if (m_emitterTime <= emitter.delay) {
  819. return false;
  820. }
  821. } else if (emitter.duration <= 0.0f || m_emitterTime < emitter.delay + emitter.duration) {
  822. return false;
  823. }
  824. }
  825. return true;
  826. }
  827. const Particle& CParticle::getParticle () const { return m_particle; }
  828. const float& CParticle::getBrightness () const { return m_overbright; }
  829. const float& CParticle::getUserAlpha () const { return m_particle.instanceOverride.alpha->value->getFloat (); }
  830. const float& CParticle::getAlpha () const { return m_particle.instanceOverride.alpha->value->getFloat (); }
  831. const glm::vec3& CParticle::getColor () const {
  832. static const glm::vec3 defaultColor (1.0f);
  833. if (m_particle.instanceOverride.color && m_particle.instanceOverride.color->value) {
  834. return m_particle.instanceOverride.color->value->getVec3 ();
  835. }
  836. return defaultColor;
  837. }
  838. const glm::vec4& CParticle::getColor4 () const {
  839. static const glm::vec4 defaultColor (1.0f);
  840. if (m_particle.instanceOverride.color && m_particle.instanceOverride.color->value) {
  841. return m_particle.instanceOverride.color->value->getVec4 ();
  842. }
  843. return defaultColor;
  844. }
  845. const glm::vec3& CParticle::getCompositeColor () const { return getColor (); }
  846. // ========== EMITTERS ==========
  847. void CParticle::setupEmitters () {
  848. for (const auto& emitter : m_particle.emitters) {
  849. EmitterFunc func;
  850. if (emitter.name == "boxrandom") {
  851. func = createBoxEmitter (emitter);
  852. } else if (emitter.name == "sphererandom") {
  853. func = createSphereEmitter (emitter);
  854. } else {
  855. sLog.out ("Unknown emitter type: ", emitter.name);
  856. continue;
  857. }
  858. if (func) {
  859. m_emitters.push_back (std::move (func));
  860. }
  861. }
  862. }
  863. float CParticle::sampleAudio (
  864. int mode, const glm::vec2& bounds, float exponent, int frequencyStart, int frequencyEnd
  865. ) const {
  866. // same curve as wallpaper64.exe: modes 1/2/3 read left, right or (left + right) / 2 of the 16 band buffer
  867. if (mode == 0) {
  868. return 1.0f;
  869. }
  870. int first = std::clamp (frequencyStart, 0, 15);
  871. int last = std::clamp (frequencyEnd, 0, 15);
  872. if (last < first) {
  873. std::swap (first, last);
  874. }
  875. const auto& recorder = this->getScene ().getAudioContext ().getRecorder ();
  876. float peak = 0.0f;
  877. const float* left = recorder.audio16;
  878. const float* right = recorder.audio16 + 16;
  879. for (int i = first; i <= last; i++) {
  880. if (mode == 1) {
  881. peak = std::max (peak, left[i]);
  882. } else if (mode == 2) {
  883. peak = std::max (peak, right[i]);
  884. } else if (mode == 3) {
  885. peak = std::max (peak, (left[i] + right[i]) * 0.5f);
  886. }
  887. }
  888. float t = (peak - bounds.x) / (bounds.y - bounds.x);
  889. // NaN from equal bounds ends up as 0 like the original
  890. t = t >= 1.0f ? 1.0f : (t >= 0.0f ? t : 0.0f);
  891. const float response = std::pow (t * t * (3.0f - 2.0f * t), exponent);
  892. return response >= 1.0f ? 1.0f : (response >= 0.0f ? response : 0.0f);
  893. }
  894. EmitterFunc CParticle::createBoxEmitter (const ParticleEmitter& emitter) {
  895. DynamicValue* countOverride = this->emitterCountOverride ();
  896. glm::vec3 transformedEmitterOrigin = emitter.origin;
  897. transformedEmitterOrigin.y = -transformedEmitterOrigin.y;
  898. int controlPointIndex = emitter.controlPoint;
  899. if (controlPointIndex == -1 && !m_particle.controlPoints.empty ()) {
  900. const auto& cp0 = m_particle.controlPoints[0];
  901. if ((cp0.flags & 1) != 0) {
  902. controlPointIndex = 0;
  903. }
  904. }
  905. glm::vec3 flippedDirections = emitter.directions;
  906. flippedDirections.y = -flippedDirections.y;
  907. bool limitOnePerFrame = (emitter.flags & 2) != 0;
  908. bool randomPeriodicEmission = (emitter.flags & 4) != 0;
  909. return
  910. [this, emitter, transformedEmitterOrigin, controlPointIndex, countOverride, flippedDirections, limitOnePerFrame,
  911. randomPeriodicEmission, emissionTimer = 0.0f, delayTimer = emitter.delay, durationTimer = 0.0f,
  912. periodicTimer = 0.0f, periodicDuration = 0.0f, periodicDelay = 0.0f, emitting = false,
  913. instantaneousEmitted = false] (std::vector<ParticleInstance>& particles, uint32_t& count, float dt) mutable {
  914. if (count >= particles.size ()) {
  915. return;
  916. }
  917. if (delayTimer > 0.0f) {
  918. delayTimer -= dt;
  919. return;
  920. }
  921. if (emitter.duration > 0.0f) {
  922. durationTimer += dt;
  923. if (durationTimer >= emitter.duration) {
  924. return;
  925. }
  926. }
  927. if (randomPeriodicEmission) {
  928. periodicTimer += dt;
  929. if (!emitting) {
  930. if (periodicTimer >= periodicDelay) {
  931. emitting = true;
  932. periodicTimer = 0.0f;
  933. periodicDuration = WallpaperEngine::Maths::randomFloat (
  934. m_rng, emitter.minPeriodicDuration, emitter.maxPeriodicDuration
  935. );
  936. } else {
  937. return;
  938. }
  939. } else {
  940. if (periodicTimer >= periodicDuration) {
  941. emitting = false;
  942. periodicTimer = 0.0f;
  943. periodicDelay = WallpaperEngine::Maths::randomFloat (
  944. m_rng, emitter.minPeriodicDelay, emitter.maxPeriodicDelay
  945. );
  946. return;
  947. }
  948. }
  949. }
  950. uint32_t toEmit = 0;
  951. if (emitter.instantaneous > 0 && !instantaneousEmitted) {
  952. toEmit = emitter.instantaneous;
  953. instantaneousEmitted = true;
  954. }
  955. if (emitter.rate > 0.0f) {
  956. const float audio = sampleAudio (
  957. emitter.audioProcessingMode, emitter.audioProcessingBounds, emitter.audioProcessingExponent,
  958. emitter.audioProcessingFrequencyStart, emitter.audioProcessingFrequencyEnd
  959. );
  960. const float rate = emitter.rate * (countOverride != nullptr ? countOverride->getFloat () : 1.0f);
  961. emissionTimer += dt * rate * audio;
  962. uint32_t rateEmit = static_cast<uint32_t> (emissionTimer);
  963. emissionTimer -= static_cast<float> (rateEmit);
  964. // limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
  965. if (limitOnePerFrame && rateEmit > 1) {
  966. rateEmit = 1;
  967. }
  968. toEmit += rateEmit;
  969. }
  970. for (uint32_t i = 0; i < toEmit && count < particles.size (); i++) {
  971. auto& p = particles[count];
  972. const ControlPointData* cp
  973. = controlPointIndex >= 0 && controlPointIndex < static_cast<int> (m_controlPoints.size ())
  974. ? &m_controlPoints[controlPointIndex]
  975. : nullptr;
  976. // Random position within the box volume (hollow box if distanceMin > 0)
  977. glm::vec3 randomPos;
  978. for (int axis = 0; axis < 3; axis++) {
  979. float minDist = emitter.distanceMin[axis];
  980. float maxDist = emitter.distanceMax[axis];
  981. float dist = WallpaperEngine::Maths::randomFloat (m_rng, minDist, maxDist);
  982. // Randomly flip sign to center the distribution
  983. if (WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) < 0.5f) {
  984. dist = -dist;
  985. }
  986. randomPos[axis] = dist;
  987. }
  988. randomPos *= flippedDirections;
  989. this->placeSpawn (p, cp, controlPointIndex, transformedEmitterOrigin, randomPos);
  990. // Emitter does not set velocity - initializers handle that
  991. p.velocity = glm::vec3 (0.0f);
  992. p.acceleration = glm::vec3 (0.0f);
  993. p.rotation = glm::vec3 (0.0f);
  994. p.angularVelocity = glm::vec3 (0.0f);
  995. p.angularAcceleration = glm::vec3 (0.0f);
  996. p.color = m_colorOverride.tint;
  997. p.alpha = 1.0f * m_particle.instanceOverride.alpha->value->getFloat ();
  998. p.size = 20.0f * m_particle.instanceOverride.size->value->getFloat ();
  999. p.lifetime = 1.0f * m_particle.instanceOverride.lifetime->value->getFloat ();
  1000. p.age = 0.0f;
  1001. p.alive = true;
  1002. p.frame = -1.0f;
  1003. p.seed = m_usesParticleSeed ? WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) : 0.0f;
  1004. p.initial.color = p.color;
  1005. p.initial.alpha = p.alpha;
  1006. p.initial.size = p.size;
  1007. p.initial.lifetime = p.lifetime;
  1008. // Reset oscillator state for reused particles
  1009. p.oscillateAlpha = {};
  1010. p.oscillateSize = {};
  1011. p.oscillatePosition = {};
  1012. // sub_14023B340 starts the step collisionquad looks at where the emitter put the particle
  1013. p.previousPosition = p.position;
  1014. for (auto& init : m_initializers) {
  1015. init (p);
  1016. }
  1017. count++;
  1018. }
  1019. };
  1020. }
  1021. EmitterFunc CParticle::createSphereEmitter (const ParticleEmitter& emitter) {
  1022. DynamicValue* countOverride = this->emitterCountOverride ();
  1023. DynamicValue* speedOverride
  1024. = (m_particle.flags & 0x10) == 0 ? m_particle.instanceOverride.speed->value.get () : nullptr;
  1025. float lifetime = 1.0f * m_particle.instanceOverride.lifetime->value->getFloat ();
  1026. // Convert emitter origin from screen space (Y down) to centered space (Y up)
  1027. glm::vec3 transformedEmitterOrigin = emitter.origin;
  1028. transformedEmitterOrigin.y = -transformedEmitterOrigin.y;
  1029. int controlPointIndex = emitter.controlPoint;
  1030. // Auto-detect control point 0 if not specified and CP0 has linkMouse
  1031. if (controlPointIndex == -1 && !m_particle.controlPoints.empty ()) {
  1032. const auto& cp0 = m_particle.controlPoints[0];
  1033. if ((cp0.flags & 1) != 0) { // bit 0 = linkMouse
  1034. controlPointIndex = 0;
  1035. }
  1036. }
  1037. bool limitOnePerFrame = (emitter.flags & 2) != 0;
  1038. return [this, emitter, transformedEmitterOrigin, controlPointIndex, countOverride, speedOverride, lifetime,
  1039. limitOnePerFrame,
  1040. emissionTimer = 0.0f,
  1041. remaining
  1042. = emitter.instantaneous] (std::vector<ParticleInstance>& particles, uint32_t& count, float dt) mutable {
  1043. if (count >= particles.size ()) {
  1044. return;
  1045. }
  1046. const float audio = sampleAudio (
  1047. emitter.audioProcessingMode, emitter.audioProcessingBounds, emitter.audioProcessingExponent,
  1048. emitter.audioProcessingFrequencyStart, emitter.audioProcessingFrequencyEnd
  1049. );
  1050. const float rate = emitter.rate * (countOverride != nullptr ? countOverride->getFloat () : 1.0f);
  1051. emissionTimer += dt * rate * audio;
  1052. uint32_t toEmit = static_cast<uint32_t> (emissionTimer);
  1053. emissionTimer -= static_cast<float> (toEmit);
  1054. // limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
  1055. if (limitOnePerFrame && toEmit > 1) {
  1056. toEmit = 1;
  1057. }
  1058. if (remaining > 0) {
  1059. toEmit = remaining;
  1060. remaining = 0;
  1061. }
  1062. for (uint32_t i = 0; i < toEmit && count < particles.size (); i++) {
  1063. auto& p = particles[count];
  1064. const ControlPointData* cp
  1065. = controlPointIndex >= 0 && controlPointIndex < static_cast<int> (m_controlPoints.size ())
  1066. ? &m_controlPoints[controlPointIndex]
  1067. : nullptr;
  1068. // sub_1402378A0 sphererandom, in WE's y up space: a point in the unit ball (cone around x) scaled by
  1069. // directions, whose length also picks the distance between distancemin and distancemax
  1070. const float phi = glm::two_pi<float> () * WallpaperEngine::Maths::randomFloat (m_rng, 0.00001f, 1.0f);
  1071. const float coneMin = -std::cos (emitter.cone * glm::pi<float> ());
  1072. const float c = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) * (1.0f - coneMin) + coneMin;
  1073. const float r = std::cbrt (WallpaperEngine::Maths::randomFloat (m_rng, 0.00001f, 1.0f));
  1074. const float s = std::sqrt (std::max (0.0f, 1.0f - c * c));
  1075. const glm::vec3 point = glm::vec3 (r * c, r * s * std::sin (phi), r * s * std::cos (phi)) * emitter.directions;
  1076. const float pointLength = glm::length (point);
  1077. const float distance
  1078. = emitter.distanceMin.x + (emitter.distanceMax.x - emitter.distanceMin.x) * pointLength;
  1079. glm::vec3 randomPos = pointLength > 0.0f ? point / pointLength : glm::vec3 (0.0f);
  1080. // a nonzero sign component forces that axis to its sign, zero leaves it alone
  1081. for (int i = 0; i < 3; i++) {
  1082. if (emitter.sign[i] > 0.0f) {
  1083. randomPos[i] = std::abs (randomPos[i]);
  1084. } else if (emitter.sign[i] < 0.0f) {
  1085. randomPos[i] = -std::abs (randomPos[i]);
  1086. }
  1087. }
  1088. randomPos *= distance;
  1089. // particles live in y down
  1090. randomPos.y = -randomPos.y;
  1091. this->placeSpawn (p, cp, controlPointIndex, transformedEmitterOrigin, randomPos);
  1092. // the velocity points along the (control point transformed) offset, a zero offset gets a random direction
  1093. // inside directions instead
  1094. glm::vec3 direction = randomPos;
  1095. if (glm::dot (direction, direction) < 0.0001f) {
  1096. direction = emitter.directions
  1097. * glm::vec3 (
  1098. WallpaperEngine::Maths::randomFloat (m_rng, 0.00001f, 1.0f) * 2.0f - 1.0f,
  1099. WallpaperEngine::Maths::randomFloat (m_rng, 0.00001f, 1.0f) * 2.0f - 1.0f,
  1100. WallpaperEngine::Maths::randomFloat (m_rng, 0.00001f, 1.0f) * 2.0f - 1.0f
  1101. );
  1102. direction.y = -direction.y;
  1103. if (cp != nullptr && (controlPointIndex != 0 || m_worldSpace)) {
  1104. direction = cp->orientation * direction;
  1105. }
  1106. }
  1107. const float directionLength = glm::length (direction);
  1108. const float speedScale = speedOverride != nullptr ? speedOverride->getFloat () : 1.0f;
  1109. const float speed = (emitter.speedMin
  1110. + (emitter.speedMax - emitter.speedMin) * WallpaperEngine::Maths::randomFloat (m_rng, 0.00001f, 1.0f))
  1111. * speedScale;
  1112. p.velocity = directionLength > 0.0f ? direction * (speed / directionLength) : glm::vec3 (0.0f);
  1113. p.acceleration = glm::vec3 (0.0f);
  1114. p.rotation = glm::vec3 (0.0f);
  1115. p.angularVelocity = glm::vec3 (0.0f);
  1116. p.angularAcceleration = glm::vec3 (0.0f);
  1117. p.color = m_colorOverride.tint;
  1118. p.alpha = 1.0f * m_particle.instanceOverride.alpha->value->getFloat ();
  1119. p.size = 20.0f * m_particle.instanceOverride.size->value->getFloat ();
  1120. p.lifetime = lifetime;
  1121. p.age = 0.0f;
  1122. p.alive = true;
  1123. p.frame = -1.0f;
  1124. p.seed = m_usesParticleSeed ? WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) : 0.0f;
  1125. p.initial.color = p.color;
  1126. p.initial.alpha = p.alpha;
  1127. p.initial.size = p.size;
  1128. p.initial.lifetime = p.lifetime;
  1129. // Reset oscillator state for reused particles
  1130. p.oscillateAlpha = {};
  1131. p.oscillateSize = {};
  1132. p.oscillatePosition = {};
  1133. p.previousPosition = p.position;
  1134. for (auto& init : m_initializers) {
  1135. init (p);
  1136. }
  1137. count++;
  1138. }
  1139. };
  1140. }
  1141. void CParticle::placeSpawn (
  1142. ParticleInstance& p, const ControlPointData* cp, int controlPointIndex, const glm::vec3& origin, glm::vec3& offset
  1143. ) {
  1144. // sub_1402378A0 box/sphere emitters: the shape turns with the control point's matrix unless it is control
  1145. // point 0 of a local system, the emitter origin is added untransformed, and the velocity initializers get
  1146. // the control point's rotation and scale either way
  1147. if (cp != nullptr && (controlPointIndex != 0 || m_worldSpace)) {
  1148. offset = cp->orientation * offset;
  1149. }
  1150. p.position = origin + offset + (cp != nullptr ? cp->position : glm::vec3 (0.0f));
  1151. m_emitOrientation = cp != nullptr ? cp->orientation : glm::mat3 (1.0f);
  1152. }
  1153. // ========== INITIALIZERS ==========
  1154. void CParticle::setupInitializers () {
  1155. for (const auto& initializer : m_particle.initializers) {
  1156. if (!initializer) {
  1157. continue;
  1158. }
  1159. InitializerFunc func;
  1160. if (initializer->is<ColorRandomInitializer> ()) {
  1161. func = createColorRandomInitializer (*initializer->as<ColorRandomInitializer> ());
  1162. } else if (initializer->is<SizeRandomInitializer> ()) {
  1163. func = createSizeRandomInitializer (*initializer->as<SizeRandomInitializer> ());
  1164. } else if (initializer->is<AlphaRandomInitializer> ()) {
  1165. func = createAlphaRandomInitializer (*initializer->as<AlphaRandomInitializer> ());
  1166. } else if (initializer->is<LifetimeRandomInitializer> ()) {
  1167. const auto& lifeInit = *initializer->as<LifetimeRandomInitializer> ();
  1168. m_uniformLifetimes = (lifeInit.min->value->getFloat () == lifeInit.max->value->getFloat ());
  1169. func = createLifetimeRandomInitializer (lifeInit);
  1170. } else if (initializer->is<VelocityRandomInitializer> ()) {
  1171. func = createVelocityRandomInitializer (*initializer->as<VelocityRandomInitializer> ());
  1172. } else if (initializer->is<RotationRandomInitializer> ()) {
  1173. func = createRotationRandomInitializer (*initializer->as<RotationRandomInitializer> ());
  1174. } else if (initializer->is<AngularVelocityRandomInitializer> ()) {
  1175. func = createAngularVelocityRandomInitializer (*initializer->as<AngularVelocityRandomInitializer> ());
  1176. } else if (initializer->is<TurbulentVelocityRandomInitializer> ()) {
  1177. func = createTurbulentVelocityRandomInitializer (*initializer->as<TurbulentVelocityRandomInitializer> ());
  1178. } else if (initializer->is<MapSequenceAroundControlPointInitializer> ()) {
  1179. func = createMapSequenceAroundControlPointInitializer (
  1180. *initializer->as<MapSequenceAroundControlPointInitializer> ()
  1181. );
  1182. } else if (initializer->is<InheritInitialValueFromEventInitializer> ()) {
  1183. func = createInheritInitialValueFromEventInitializer (
  1184. *initializer->as<InheritInitialValueFromEventInitializer> ()
  1185. );
  1186. } else if (initializer->is<InheritControlPointVelocityInitializer> ()) {
  1187. func = createInheritControlPointVelocityInitializer (
  1188. *initializer->as<InheritControlPointVelocityInitializer> ()
  1189. );
  1190. } else if (initializer->is<HsvColorRandomInitializer> ()) {
  1191. func = createHsvColorRandomInitializer (*initializer->as<HsvColorRandomInitializer> ());
  1192. } else if (initializer->is<ColorListInitializer> ()) {
  1193. func = createColorListInitializer (*initializer->as<ColorListInitializer> ());
  1194. } else if (initializer->is<PositionOffsetRandomInitializer> ()) {
  1195. func = createPositionOffsetRandomInitializer (*initializer->as<PositionOffsetRandomInitializer> ());
  1196. } else if (initializer->is<MapSequenceBetweenControlPointsInitializer> ()) {
  1197. func = createMapSequenceBetweenControlPointsInitializer (
  1198. *initializer->as<MapSequenceBetweenControlPointsInitializer> ()
  1199. );
  1200. } else if (initializer->is<RemapInitialValueInitializer> ()) {
  1201. func = createRemapInitialValueInitializer (*initializer->as<RemapInitialValueInitializer> ());
  1202. } else {
  1203. sLog.out ("Unknown initializer type");
  1204. }
  1205. if (func) {
  1206. m_initializers.push_back (std::move (func));
  1207. }
  1208. }
  1209. }
  1210. InitializerFunc CParticle::createColorRandomInitializer (const ColorRandomInitializer& init) {
  1211. DynamicValue* minValue = init.min->value.get ();
  1212. DynamicValue* maxValue = init.max->value.get ();
  1213. DynamicValue* exponentValue = init.exponent->value.get ();
  1214. // wallpaper64.exe sub_14023B340 case 3, one random for all three channels. An active override color moves
  1215. // both ends in HSV first (sub_1401D15A0 case 0xB)
  1216. return [this, minValue, maxValue, exponentValue] (ParticleInstance& p) {
  1217. glm::vec3 min = minValue->getVec3 () / 255.0f;
  1218. glm::vec3 max = maxValue->getVec3 () / 255.0f;
  1219. if (m_colorOverride.active) {
  1220. min = shiftColor (min, m_colorOverride.shift);
  1221. max = shiftColor (max, m_colorOverride.shift);
  1222. }
  1223. float t = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f);
  1224. if (const float exponent = exponentValue->getFloat (); exponent != 1.0f) {
  1225. t = std::pow (t, exponent);
  1226. }
  1227. p.color *= (max - min) * t + min;
  1228. p.initial.color = p.color;
  1229. };
  1230. }
  1231. InitializerFunc CParticle::createSizeRandomInitializer (const SizeRandomInitializer& init) {
  1232. DynamicValue* minValue = init.min->value.get ();
  1233. DynamicValue* maxValue = init.max->value.get ();
  1234. DynamicValue* exponentValue = init.exponent->value.get ();
  1235. DynamicValue* sizeOverride = (m_particle.flags & 0x80) == 0 ? m_particle.instanceOverride.size->value.get () : nullptr;
  1236. return [this, minValue, maxValue, exponentValue, sizeOverride] (ParticleInstance& p) {
  1237. float t = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f);
  1238. float exponent = exponentValue->getFloat ();
  1239. float min = minValue->getFloat ();
  1240. float max = maxValue->getFloat ();
  1241. // Apply exponent for non-linear distribution
  1242. float adjustedT = std::pow (t, exponent);
  1243. p.size = (min + adjustedT * (max - min)) * (sizeOverride != nullptr ? sizeOverride->getFloat () : 1.0f) / 2.0f;
  1244. p.initial.size = p.size;
  1245. };
  1246. }
  1247. InitializerFunc CParticle::createAlphaRandomInitializer (const AlphaRandomInitializer& init) {
  1248. DynamicValue* minValue = init.min->value.get ();
  1249. DynamicValue* maxValue = init.max->value.get ();
  1250. DynamicValue* alphaOverride = m_particle.instanceOverride.alpha->value.get ();
  1251. return [this, minValue, maxValue, alphaOverride] (ParticleInstance& p) {
  1252. p.alpha = WallpaperEngine::Maths::randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ())
  1253. * alphaOverride->getFloat ();
  1254. p.initial.alpha = p.alpha;
  1255. };
  1256. }
  1257. InitializerFunc CParticle::createLifetimeRandomInitializer (const LifetimeRandomInitializer& init) {
  1258. DynamicValue* minValue = init.min->value.get ();
  1259. DynamicValue* maxValue = init.max->value.get ();
  1260. DynamicValue* lifetimeOverride = m_particle.instanceOverride.lifetime->value.get ();
  1261. return [this, minValue, maxValue, lifetimeOverride] (ParticleInstance& p) {
  1262. p.lifetime = WallpaperEngine::Maths::randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ())
  1263. * lifetimeOverride->getFloat ();
  1264. p.initial.lifetime = p.lifetime;
  1265. };
  1266. }
  1267. InitializerFunc CParticle::createVelocityRandomInitializer (const VelocityRandomInitializer& init) {
  1268. DynamicValue* minValue = init.min->value.get ();
  1269. DynamicValue* maxValue = init.max->value.get ();
  1270. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1271. return [this, minValue, maxValue, speedOverride] (ParticleInstance& p) {
  1272. glm::vec3 vel = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
  1273. * speedOverride->getFloat ();
  1274. vel.y = -vel.y;
  1275. p.velocity += m_emitOrientation * vel;
  1276. };
  1277. }
  1278. InitializerFunc CParticle::createRotationRandomInitializer (const RotationRandomInitializer& init) {
  1279. DynamicValue* minValue = init.min->value.get ();
  1280. DynamicValue* maxValue = init.max->value.get ();
  1281. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1282. return [this, minValue, maxValue, speedOverride] (ParticleInstance& p) {
  1283. p.rotation = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
  1284. * speedOverride->getFloat ();
  1285. };
  1286. }
  1287. InitializerFunc CParticle::createAngularVelocityRandomInitializer (const AngularVelocityRandomInitializer& init) {
  1288. DynamicValue* minValue = init.min->value.get ();
  1289. DynamicValue* maxValue = init.max->value.get ();
  1290. DynamicValue* exponentValue = init.exponent->value.get ();
  1291. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1292. return [this, minValue, maxValue, exponentValue, speedOverride] (ParticleInstance& p) {
  1293. glm::vec3 minVec = minValue->getVec3 ();
  1294. glm::vec3 maxVec = maxValue->getVec3 ();
  1295. float exponent = exponentValue->getFloat ();
  1296. // exponent = 1: uniform; exponent -> 0: bias towards max; exponent >= 2: bias towards min
  1297. glm::vec3 result;
  1298. for (int i = 0; i < 3; i++) {
  1299. float t = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f);
  1300. t = std::pow (t, exponent);
  1301. result[i] = minVec[i] + t * (maxVec[i] - minVec[i]);
  1302. }
  1303. p.angularVelocity = result * speedOverride->getFloat ();
  1304. };
  1305. }
  1306. InitializerFunc CParticle::createTurbulentVelocityRandomInitializer (const TurbulentVelocityRandomInitializer& init) {
  1307. DynamicValue* speedMin = init.speedMin->value.get ();
  1308. DynamicValue* speedMax = init.speedMax->value.get ();
  1309. DynamicValue* offsetVal = init.offset->value.get ();
  1310. DynamicValue* scaleVal = init.scale->value.get ();
  1311. DynamicValue* forwardVal = init.forward->value.get ();
  1312. DynamicValue* timeScaleVal = init.timeScale->value.get ();
  1313. DynamicValue* phaseMinVal = init.phaseMin->value.get ();
  1314. DynamicValue* phaseMaxVal = init.phaseMax->value.get ();
  1315. DynamicValue* rightVal = init.right->value.get ();
  1316. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1317. DynamicValue* audioModeValue = init.audioProcessingMode->value.get ();
  1318. DynamicValue* audioBoundsValue = init.audioProcessingBounds->value.get ();
  1319. DynamicValue* audioExponentValue = init.audioProcessingExponent->value.get ();
  1320. DynamicValue* audioStartValue = init.audioProcessingFrequencyStart->value.get ();
  1321. DynamicValue* audioEndValue = init.audioProcessingFrequencyEnd->value.get ();
  1322. // same formula as wallpaper64.exe (sub_1401C8AF0)
  1323. return [this, speedMin, speedMax, offsetVal, scaleVal, forwardVal, timeScaleVal, phaseMinVal, phaseMaxVal, rightVal,
  1324. speedOverride, audioModeValue, audioBoundsValue, audioExponentValue, audioStartValue,
  1325. audioEndValue] (ParticleInstance& p) {
  1326. const float audio = sampleAudio (
  1327. audioModeValue->getInt (), audioBoundsValue->getVec2 (), audioExponentValue->getFloat (),
  1328. audioStartValue->getInt (), audioEndValue->getInt ()
  1329. );
  1330. const float phaseMin = phaseMinVal->getFloat ();
  1331. const float phaseRange = (phaseMaxVal->getFloat () - phaseMin) * audio;
  1332. // sub_14023B340 case 9: the renderer's scene clock is added to the phase
  1333. const float phase = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) * phaseRange + phaseMin
  1334. + getScene ().getSceneClock ();
  1335. const float timeScale = timeScaleVal->getFloat () * m_particle.instanceOverride.rate->value->getFloat ();
  1336. const float angle = simplexNoise1D (phase * timeScale) * glm::pi<float> () * scaleVal->getFloat ()
  1337. + offsetVal->getFloat ();
  1338. const float speed = WallpaperEngine::Maths::randomFloat (m_rng, speedMin->getFloat (), speedMax->getFloat ());
  1339. glm::vec3 right = rightVal->getVec3 ();
  1340. if (glm::length (right) < 0.0001f) {
  1341. right = glm::vec3 (0.0f, 0.0f, 1.0f);
  1342. }
  1343. glm::vec3 direction = glm::mat3 (glm::rotate (glm::mat4 (1.0f), angle, right)) * forwardVal->getVec3 ();
  1344. direction.y = -direction.y;
  1345. // z moves nothing on screen in 2D systems but pulls rope segments apart in depth
  1346. if ((m_particle.flags & 4) == 0) {
  1347. direction.z = 0.0f;
  1348. }
  1349. p.velocity += m_emitOrientation * (direction * speed * speedOverride->getFloat ());
  1350. };
  1351. }
  1352. InitializerFunc
  1353. CParticle::createMapSequenceAroundControlPointInitializer (const MapSequenceAroundControlPointInitializer& init) {
  1354. DynamicValue* controlPointValue = init.controlPoint->value.get ();
  1355. DynamicValue* countValue = init.count->value.get ();
  1356. DynamicValue* speedMinValue = init.speedMin->value.get ();
  1357. DynamicValue* speedMaxValue = init.speedMax->value.get ();
  1358. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1359. // Sequence counter is shared (closure state) across all particles spawned by this
  1360. // initializer, giving each one a distinct angle around the circle
  1361. int sequenceIndex = 0;
  1362. return [this, controlPointValue, countValue, speedMinValue, speedMaxValue, sequenceIndex,
  1363. speedOverride] (ParticleInstance& p) mutable {
  1364. int controlPoint = static_cast<int> (controlPointValue->getFloat ());
  1365. int count = static_cast<int> (countValue->getFloat ());
  1366. if (count < 1) {
  1367. count = 1;
  1368. }
  1369. float angle = (static_cast<float> (sequenceIndex) / static_cast<float> (count)) * glm::two_pi<float> ();
  1370. sequenceIndex = (sequenceIndex + 1) % count;
  1371. glm::vec3 centerPos = glm::vec3 (0.0f);
  1372. if (controlPoint >= 0 && controlPoint < static_cast<int> (m_controlPoints.size ())) {
  1373. centerPos = m_controlPoints[controlPoint].position;
  1374. }
  1375. p.position = centerPos;
  1376. glm::vec3 speedMin = speedMinValue->getVec3 ();
  1377. glm::vec3 speedMax = speedMaxValue->getVec3 ();
  1378. glm::vec3 speed = WallpaperEngine::Maths::randomVec3 (m_rng, speedMin, speedMax);
  1379. // Flip Y before rotation to convert to centered space
  1380. speed.y = -speed.y;
  1381. // Rotating by the sequence angle gives the outward radial/circular pattern
  1382. glm::mat3 rotationMatrix = glm::mat3 (
  1383. std::cos (angle), -std::sin (angle), 0.0f, std::sin (angle), std::cos (angle), 0.0f, 0.0f, 0.0f, 1.0f
  1384. );
  1385. glm::vec3 rotatedSpeed = rotationMatrix * speed * speedOverride->getFloat ();
  1386. p.velocity = rotatedSpeed;
  1387. };
  1388. }
  1389. // ========== OPERATORS ==========
  1390. namespace {
  1391. /** Applies an inherit input from the event's particle, the initializer also moves the base values operators start from */
  1392. void applyEventInput (ParticleInstance& p, const ParticleInstance& source, ParticleEventInput input, bool initial) {
  1393. switch (input) {
  1394. case ParticleEventInput::SetColor: p.color = source.color; break;
  1395. case ParticleEventInput::MultiplyColor: p.color *= source.color; break;
  1396. case ParticleEventInput::SetOpacity: p.alpha = source.alpha; break;
  1397. case ParticleEventInput::MultiplyOpacity: p.alpha *= source.alpha; break;
  1398. case ParticleEventInput::SetColorOpacity:
  1399. p.color = source.color;
  1400. p.alpha = source.alpha;
  1401. break;
  1402. case ParticleEventInput::MultiplyColorOpacity:
  1403. p.color *= source.color;
  1404. p.alpha *= source.alpha;
  1405. break;
  1406. case ParticleEventInput::SetVelocity: p.velocity = source.velocity; break;
  1407. case ParticleEventInput::AddVelocity: p.velocity += source.velocity; break;
  1408. case ParticleEventInput::SetSize: p.size = source.size; break;
  1409. case ParticleEventInput::MultiplySize: p.size *= source.size; break;
  1410. case ParticleEventInput::SetRotation: p.rotation = source.rotation; break;
  1411. case ParticleEventInput::AddRotation: p.rotation += source.rotation; break;
  1412. case ParticleEventInput::SetAngularVelocity: p.angularVelocity = source.angularVelocity; break;
  1413. case ParticleEventInput::AddAngularVelocity: p.angularVelocity += source.angularVelocity; break;
  1414. }
  1415. if (initial) {
  1416. p.initial.color = p.color;
  1417. p.initial.alpha = p.alpha;
  1418. p.initial.size = p.size;
  1419. }
  1420. }
  1421. } // namespace
  1422. InitializerFunc
  1423. CParticle::createInheritInitialValueFromEventInitializer (const InheritInitialValueFromEventInitializer& init) {
  1424. const ParticleEventInput input = init.input;
  1425. // wallpaper64.exe sub_14023B340 case 16: the event's particle as it was, a dead one included
  1426. return [this, input] (ParticleInstance& p) {
  1427. if (m_hasEventParticle) {
  1428. applyEventInput (p, m_eventParticle, input, true);
  1429. }
  1430. };
  1431. }
  1432. InitializerFunc
  1433. CParticle::createInheritControlPointVelocityInitializer (const InheritControlPointVelocityInitializer& init) {
  1434. const int controlPoint = init.controlPoint;
  1435. DynamicValue* minValue = init.min->value.get ();
  1436. DynamicValue* maxValue = init.max->value.get ();
  1437. // sub_14023B340 case 8: a random share of how fast the control point moved over the last frame
  1438. return [this, controlPoint, minValue, maxValue] (ParticleInstance& p) {
  1439. if (controlPoint < 0 || controlPoint >= static_cast<int> (m_controlPoints.size ())) {
  1440. return;
  1441. }
  1442. const auto& cp = m_controlPoints[controlPoint];
  1443. glm::vec3 velocity = cp.velocity;
  1444. // a world space system's control points move in the world, back into its own space unless flag 2 made
  1445. // the point a world one, then through the emitter's orientation like every velocity initializer
  1446. if (m_worldSpace && !cp.worldSpace) {
  1447. velocity = glm::mat3 (glm::inverse (m_frame)) * velocity;
  1448. }
  1449. const float share = WallpaperEngine::Maths::randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ());
  1450. p.velocity += m_emitOrientation * (velocity * share);
  1451. };
  1452. }
  1453. OperatorFunc CParticle::createInheritValueFromEventOperator (const InheritValueFromEventOperator& op) {
  1454. const ParticleEventInput input = op.input;
  1455. // same clamps as sub_1401C2A40 so the blend windows never have zero length
  1456. const float inStart = std::min (op.blend.x, op.blend.y - 0.0001f);
  1457. const float inEnd = op.blend.y;
  1458. const float outStart = op.blend.z;
  1459. const float outEnd = std::max (op.blend.w, op.blend.z + 0.0001f);
  1460. // sub_14023FBC0 case 20: every frame while the event's particle lives, nothing for eventdeath children
  1461. return [this, input, inStart, inEnd, outStart, outEnd] (
  1462. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  1463. float
  1464. ) {
  1465. if (!m_eventId.has_value ()) {
  1466. return;
  1467. }
  1468. for (uint32_t i = 0; i < count; i++) {
  1469. auto& p = particles[i];
  1470. const float life = p.getLifetimePos ();
  1471. const float weight = std::clamp ((life - inStart) / (inEnd - inStart), 0.0f, 1.0f)
  1472. * std::clamp ((outEnd - life) / (outEnd - outStart), 0.0f, 1.0f);
  1473. if (weight >= 1.0f) {
  1474. applyEventInput (p, m_eventParticle, input, false);
  1475. continue;
  1476. }
  1477. ParticleInstance target = p;
  1478. applyEventInput (target, m_eventParticle, input, false);
  1479. p.color = glm::mix (p.color, target.color, weight);
  1480. p.alpha = glm::mix (p.alpha, target.alpha, weight);
  1481. p.velocity = glm::mix (p.velocity, target.velocity, weight);
  1482. p.size = glm::mix (p.size, target.size, weight);
  1483. p.rotation = glm::mix (p.rotation, target.rotation, weight);
  1484. p.angularVelocity = glm::mix (p.angularVelocity, target.angularVelocity, weight);
  1485. }
  1486. };
  1487. }
  1488. void CParticle::setupOperators () {
  1489. for (const auto& op : m_particle.operators) {
  1490. if (!op) {
  1491. continue;
  1492. }
  1493. OperatorFunc func;
  1494. if (op->is<MovementOperator> ()) {
  1495. func = createMovementOperator (*op->as<MovementOperator> ());
  1496. } else if (op->is<AngularMovementOperator> ()) {
  1497. func = createAngularMovementOperator (*op->as<AngularMovementOperator> ());
  1498. } else if (op->is<AlphaFadeOperator> ()) {
  1499. func = createAlphaFadeOperator (*op->as<AlphaFadeOperator> ());
  1500. } else if (op->is<SizeChangeOperator> ()) {
  1501. func = createSizeChangeOperator (*op->as<SizeChangeOperator> ());
  1502. } else if (op->is<AlphaChangeOperator> ()) {
  1503. func = createAlphaChangeOperator (*op->as<AlphaChangeOperator> ());
  1504. } else if (op->is<ColorChangeOperator> ()) {
  1505. func = createColorChangeOperator (*op->as<ColorChangeOperator> ());
  1506. } else if (op->is<TurbulenceOperator> ()) {
  1507. func = createTurbulenceOperator (*op->as<TurbulenceOperator> ());
  1508. } else if (op->is<VortexOperator> ()) {
  1509. func = createVortexOperator (*op->as<VortexOperator> ());
  1510. } else if (op->is<ControlPointAttractOperator> ()) {
  1511. func = createControlPointAttractOperator (*op->as<ControlPointAttractOperator> ());
  1512. } else if (op->is<OscillateAlphaOperator> ()) {
  1513. func = createOscillateAlphaOperator (*op->as<OscillateAlphaOperator> ());
  1514. } else if (op->is<OscillateSizeOperator> ()) {
  1515. func = createOscillateSizeOperator (*op->as<OscillateSizeOperator> ());
  1516. } else if (op->is<OscillatePositionOperator> ()) {
  1517. func = createOscillatePositionOperator (*op->as<OscillatePositionOperator> ());
  1518. } else if (op->is<InheritValueFromEventOperator> ()) {
  1519. func = createInheritValueFromEventOperator (*op->as<InheritValueFromEventOperator> ());
  1520. } else if (op->is<CapVelocityOperator> ()) {
  1521. func = createCapVelocityOperator (*op->as<CapVelocityOperator> ());
  1522. } else if (op->is<BoidsOperator> ()) {
  1523. func = createBoidsOperator (*op->as<BoidsOperator> ());
  1524. if (!m_hasBoids) {
  1525. m_hasBoids = true;
  1526. m_ghosts.resize (m_maxParticles);
  1527. m_ghostUsed.assign (m_maxParticles, 0);
  1528. }
  1529. } else if (op->is<RemapValueOperator> ()) {
  1530. func = createRemapValueOperator (*op->as<RemapValueOperator> ());
  1531. } else if (op->is<MaintainDistanceToControlPointOperator> ()) {
  1532. func = createMaintainDistanceToControlPointOperator (*op->as<MaintainDistanceToControlPointOperator> ());
  1533. } else if (op->is<MaintainDistanceBetweenControlPointsOperator> ()) {
  1534. func = createMaintainDistanceBetweenControlPointsOperator (
  1535. *op->as<MaintainDistanceBetweenControlPointsOperator> ()
  1536. );
  1537. } else if (op->is<ReduceMovementNearControlPointOperator> ()) {
  1538. func = createReduceMovementNearControlPointOperator (*op->as<ReduceMovementNearControlPointOperator> ());
  1539. } else if (op->is<CollisionOperator> ()) {
  1540. func = createCollisionOperator (*op->as<CollisionOperator> ());
  1541. } else {
  1542. sLog.out ("Unknown operator type");
  1543. }
  1544. if (func) {
  1545. m_operators.push_back (std::move (func));
  1546. }
  1547. }
  1548. }
  1549. OperatorFunc CParticle::createMovementOperator (const MovementOperator& op) {
  1550. DynamicValue* dragValue = op.drag->value.get ();
  1551. DynamicValue* gravityValue = op.gravity->value.get ();
  1552. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1553. // sub_14023FBC0 case 1: velocity first, then position with the new velocity, then drag over the frame scaled dt.
  1554. // It runs over every pool slot, dead ones included
  1555. const bool turnGravity = (op.flags & 1) != 0;
  1556. return [this, dragValue, gravityValue, speedOverride, turnGravity] (
  1557. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  1558. float dt
  1559. ) {
  1560. glm::vec3 gravity = gravityValue->getVec3 () * speedOverride->getFloat ();
  1561. gravity.y = -gravity.y;
  1562. // sub_1401F87E0 multiplies the gravity by the frame's 3x3 from the other side (its transpose), so drawn
  1563. // through the frame a rotation cancels out and the scale applies twice
  1564. if (turnGravity && !m_worldSpace) {
  1565. gravity = glm::transpose (glm::mat3 (m_frame)) * gravity;
  1566. }
  1567. const float drag = std::min (dragValue->getFloat () * frameScaledDelta (dt), 0.99999988f);
  1568. const glm::vec3 step = gravity * dt;
  1569. const auto move = [&] (ParticleInstance& p) {
  1570. const glm::vec3 velocity = p.velocity + step;
  1571. p.position += velocity * dt;
  1572. p.velocity = velocity * (1.0f - drag);
  1573. };
  1574. for (uint32_t i = 0; i < count; i++) {
  1575. if (particles[i].alive) {
  1576. move (particles[i]);
  1577. }
  1578. }
  1579. for (uint32_t slot = 0; slot < m_ghostUsed.size (); slot++) {
  1580. if (m_ghostUsed[slot]) {
  1581. move (m_ghosts[slot]);
  1582. }
  1583. }
  1584. };
  1585. }
  1586. OperatorFunc CParticle::createAngularMovementOperator (const AngularMovementOperator& op) {
  1587. DynamicValue* dragValue = op.drag->value.get ();
  1588. DynamicValue* forceValue = op.force->value.get ();
  1589. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  1590. return [dragValue, forceValue, speedOverride] (
  1591. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  1592. float dt
  1593. ) {
  1594. float drag = dragValue->getFloat ();
  1595. float speed = speedOverride->getFloat ();
  1596. glm::vec3 force = forceValue->getVec3 ();
  1597. for (uint32_t i = 0; i < count; i++) {
  1598. auto& p = particles[i];
  1599. if (!p.alive) {
  1600. continue;
  1601. }
  1602. p.rotation += p.angularVelocity * dt * speed;
  1603. p.angularVelocity += force * dt * speed;
  1604. // Positive drag slows down, negative speeds up; clamped so drag*dt > 1.0 can't reverse it
  1605. float dragFactor = 1.0f - (drag * dt);
  1606. if (dragFactor < 0.0f) {
  1607. dragFactor = 0.0f;
  1608. }
  1609. p.angularVelocity *= dragFactor;
  1610. // Wrap rotation to prevent floating-point precision issues
  1611. const float pi = glm::pi<float> ();
  1612. const float two_pi = glm::two_pi<float> ();
  1613. for (int j = 0; j < 3; j++) {
  1614. while (p.rotation[j] > pi) {
  1615. p.rotation[j] -= two_pi;
  1616. }
  1617. while (p.rotation[j] < -pi) {
  1618. p.rotation[j] += two_pi;
  1619. }
  1620. }
  1621. }
  1622. };
  1623. }
  1624. OperatorFunc CParticle::createAlphaFadeOperator (const AlphaFadeOperator& op) {
  1625. DynamicValue* fadeInTimeValue = op.fadeInTime->value.get ();
  1626. DynamicValue* fadeOutTimeValue = op.fadeOutTime->value.get ();
  1627. return
  1628. [fadeInTimeValue, fadeOutTimeValue] (
  1629. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  1630. ) {
  1631. float fadeInTime = fadeInTimeValue->getFloat ();
  1632. float fadeOutTime = fadeOutTimeValue->getFloat ();
  1633. for (uint32_t i = 0; i < count; i++) {
  1634. auto& p = particles[i];
  1635. if (!p.alive) {
  1636. continue;
  1637. }
  1638. float life = p.getLifetimePos ();
  1639. if (life <= fadeInTime) {
  1640. float fade = WallpaperEngine::Maths::fadeValue (life, 0.0f, fadeInTime, 0.0f, 1.0f);
  1641. p.alpha = p.initial.alpha * fade;
  1642. } else if (life > fadeOutTime) {
  1643. float fade = 1.0f - WallpaperEngine::Maths::fadeValue (life, fadeOutTime, 1.0f, 0.0f, 1.0f);
  1644. p.alpha = p.initial.alpha * fade;
  1645. } else {
  1646. p.alpha = p.initial.alpha;
  1647. }
  1648. // Update oscillator base so oscillateAlpha combines properly
  1649. p.oscillateAlpha.base = p.alpha;
  1650. }
  1651. };
  1652. }
  1653. OperatorFunc CParticle::createSizeChangeOperator (const SizeChangeOperator& op) {
  1654. DynamicValue* startTimeValue = op.startTime->value.get ();
  1655. DynamicValue* endTimeValue = op.endTime->value.get ();
  1656. DynamicValue* startValueValue = op.startValue->value.get ();
  1657. DynamicValue* endValueValue = op.endValue->value.get ();
  1658. // wallpaper64.exe binds the instanceoverride size to both values (sub_1401C5490, sub_1401D15A0 case 7), on top of
  1659. // sizerandom's own binding. Particle flag 0x80 turns the size bindings off
  1660. DynamicValue* sizeOverride = (m_particle.flags & 0x80) == 0 ? m_particle.instanceOverride.size->value.get () : nullptr;
  1661. return
  1662. [startTimeValue, endTimeValue, startValueValue, endValueValue, sizeOverride] (
  1663. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  1664. ) {
  1665. const float scale = sizeOverride != nullptr ? sizeOverride->getFloat () : 1.0f;
  1666. float startTime = startTimeValue->getFloat ();
  1667. float endTime = endTimeValue->getFloat ();
  1668. float startValue = startValueValue->getFloat () * scale;
  1669. float endValue = endValueValue->getFloat () * scale;
  1670. for (uint32_t i = 0; i < count; i++) {
  1671. auto& p = particles[i];
  1672. if (!p.alive) {
  1673. continue;
  1674. }
  1675. float life = p.getLifetimePos ();
  1676. float multiplier = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue, endValue);
  1677. p.size = p.initial.size * multiplier;
  1678. // Update oscillator base so oscillateSize combines properly
  1679. p.oscillateSize.base = p.size;
  1680. }
  1681. };
  1682. }
  1683. OperatorFunc CParticle::createAlphaChangeOperator (const AlphaChangeOperator& op) {
  1684. DynamicValue* startTimeValue = op.startTime->value.get ();
  1685. DynamicValue* endTimeValue = op.endTime->value.get ();
  1686. DynamicValue* startValueValue = op.startValue->value.get ();
  1687. DynamicValue* endValueValue = op.endValue->value.get ();
  1688. return
  1689. [startTimeValue, endTimeValue, startValueValue, endValueValue] (
  1690. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  1691. ) {
  1692. float startTime = startTimeValue->getFloat ();
  1693. float endTime = endTimeValue->getFloat ();
  1694. float startValue = startValueValue->getFloat ();
  1695. float endValue = endValueValue->getFloat ();
  1696. for (uint32_t i = 0; i < count; i++) {
  1697. auto& p = particles[i];
  1698. if (!p.alive) {
  1699. continue;
  1700. }
  1701. float life = p.getLifetimePos ();
  1702. float multiplier = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue, endValue);
  1703. p.alpha = p.initial.alpha * multiplier;
  1704. // Update oscillator base so oscillateAlpha combines properly
  1705. p.oscillateAlpha.base = p.alpha;
  1706. }
  1707. };
  1708. }
  1709. OperatorFunc CParticle::createColorChangeOperator (const ColorChangeOperator& op) {
  1710. DynamicValue* startTimeValue = op.startTime->value.get ();
  1711. DynamicValue* endTimeValue = op.endTime->value.get ();
  1712. DynamicValue* startValueValue = op.startValue->value.get ();
  1713. DynamicValue* endValueValue = op.endValue->value.get ();
  1714. return
  1715. [this, startTimeValue, endTimeValue, startValueValue, endValueValue] (
  1716. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  1717. ) {
  1718. float startTime = startTimeValue->getFloat ();
  1719. float endTime = endTimeValue->getFloat ();
  1720. glm::vec3 startValue = startValueValue->getVec3 ();
  1721. glm::vec3 endValue = endValueValue->getVec3 ();
  1722. // sub_1401D15A0 case 0xC, same shift as colorrandom
  1723. if (m_colorOverride.active) {
  1724. startValue = shiftColor (startValue, m_colorOverride.shift);
  1725. endValue = shiftColor (endValue, m_colorOverride.shift);
  1726. }
  1727. for (uint32_t i = 0; i < count; i++) {
  1728. auto& p = particles[i];
  1729. if (!p.alive) {
  1730. continue;
  1731. }
  1732. float life = p.getLifetimePos ();
  1733. glm::vec3 color;
  1734. color.r = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.r, endValue.r);
  1735. color.g = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.g, endValue.g);
  1736. color.b = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.b, endValue.b);
  1737. p.color = p.initial.color * color;
  1738. }
  1739. };
  1740. }
  1741. OperatorFunc CParticle::createTurbulenceOperator (const TurbulenceOperator& op) {
  1742. DynamicValue* scaleValue = op.scale ? op.scale->value.get () : nullptr;
  1743. DynamicValue* speedMinValue = op.speedMin ? op.speedMin->value.get () : nullptr;
  1744. DynamicValue* speedMaxValue = op.speedMax ? op.speedMax->value.get () : nullptr;
  1745. DynamicValue* timeScaleValue = op.timeScale ? op.timeScale->value.get () : nullptr;
  1746. DynamicValue* maskValue = op.mask ? op.mask->value.get () : nullptr;
  1747. DynamicValue* phaseMinValue = op.phaseMin->value.get ();
  1748. DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
  1749. // the loader binds speedmin/speedmax to the speed override unless particle flag 0x10, timescale to the rate
  1750. DynamicValue* speedOverride
  1751. = (m_particle.flags & 0x10) == 0 ? m_particle.instanceOverride.speed->value.get () : nullptr;
  1752. DynamicValue* rateOverride = m_particle.instanceOverride.rate->value.get ();
  1753. DynamicValue* audioModeValue = op.audioProcessingMode->value.get ();
  1754. DynamicValue* audioBoundsValue = op.audioProcessingBounds->value.get ();
  1755. DynamicValue* audioExponentValue = op.audioProcessingExponent->value.get ();
  1756. DynamicValue* audioStartValue = op.audioProcessingFrequencyStart->value.get ();
  1757. DynamicValue* audioEndValue = op.audioProcessingFrequencyEnd->value.get ();
  1758. this->m_usesParticleSeed = true;
  1759. // sub_14023FBC0 case 14, defaults from sub_1401BEB80. phasemin is loaded but never read
  1760. return [this, scaleValue, speedMinValue, speedMaxValue, timeScaleValue, maskValue, phaseMinValue, phaseMaxValue,
  1761. speedOverride, rateOverride, audioModeValue, audioBoundsValue, audioExponentValue, audioStartValue,
  1762. audioEndValue] (
  1763. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  1764. float dt
  1765. ) {
  1766. const bool flat = !getScene ().getCamera ().isPerspective ();
  1767. const glm::vec3 mask
  1768. = maskValue != nullptr ? maskValue->getVec3 () : (flat ? glm::vec3 (1.0f, 1.0f, 0.0f) : glm::vec3 (1.0f));
  1769. if (mask.x == 0.0f && mask.y == 0.0f && mask.z == 0.0f) {
  1770. return;
  1771. }
  1772. const float audio = audioModeValue->getInt () != 0
  1773. ? sampleAudio (
  1774. audioModeValue->getInt (), audioBoundsValue->getVec2 (), audioExponentValue->getFloat (),
  1775. audioStartValue->getInt (), audioEndValue->getInt ()
  1776. )
  1777. : 1.0f;
  1778. const float speed = speedOverride != nullptr ? speedOverride->getFloat () : 1.0f;
  1779. const float speedMin
  1780. = (speedMinValue != nullptr ? speedMinValue->getFloat () : (flat ? 500.0f : 1.0f)) * speed;
  1781. const float speedMax
  1782. = (speedMaxValue != nullptr ? speedMaxValue->getFloat () : (flat ? 1000.0f : 5.0f)) * speed;
  1783. const float speedRange = (speedMax - speedMin) * audio;
  1784. const float scaledMin = speedMin * audio;
  1785. const float phaseRange = phaseMaxValue->getFloat () - phaseMinValue->getFloat ();
  1786. const float scale = scaleValue != nullptr ? scaleValue->getFloat () : (flat ? 0.01f : 0.5f);
  1787. const float timeScale
  1788. = (timeScaleValue != nullptr ? timeScaleValue->getFloat () : (flat ? 20.0f : 1.0f)) * rateOverride->getFloat ();
  1789. // the renderer's scene clock moves the noise field
  1790. const float time = getScene ().getSceneClock () * timeScale;
  1791. const glm::vec3 step = mask * frameScaledDelta (dt);
  1792. const auto push = [&] (ParticleInstance& p) {
  1793. const float phase = p.seed * phaseRange + time;
  1794. // WE's particle space is y-up
  1795. const float x = scale * (phase + p.position.x);
  1796. const float y = scale * (phase - p.position.y);
  1797. const float z = scale * (phase + p.position.z);
  1798. const float strength = p.seed * speedRange + scaledMin;
  1799. glm::vec3 delta (0.0f);
  1800. if (mask.x != 0.0f) {
  1801. delta.x = simplexNoise3D (x, y, z);
  1802. }
  1803. if (mask.y != 0.0f) {
  1804. delta.y = simplexNoise3D (z, x, y);
  1805. }
  1806. if (mask.z != 0.0f) {
  1807. delta.z = simplexNoise3D (y, z, x);
  1808. }
  1809. delta *= step * strength;
  1810. p.velocity += glm::vec3 (delta.x, -delta.y, delta.z);
  1811. };
  1812. // every pool slot, dead ones included
  1813. for (uint32_t i = 0; i < count; i++) {
  1814. if (particles[i].alive) {
  1815. push (particles[i]);
  1816. }
  1817. }
  1818. for (uint32_t slot = 0; slot < m_ghostUsed.size (); slot++) {
  1819. if (m_ghostUsed[slot]) {
  1820. push (m_ghosts[slot]);
  1821. }
  1822. }
  1823. };
  1824. }
  1825. OperatorFunc CParticle::createVortexOperator (const VortexOperator& op) {
  1826. const int controlPoint = op.controlPoint;
  1827. const bool v2 = op.v2;
  1828. const bool infiniteAxis = (op.flags & 1) != 0;
  1829. // vortex (v1) reads neither centerforce nor the ring
  1830. const bool useCenterForce = v2 && (op.flags & 2) != 0;
  1831. const bool ringShape = v2 && (op.flags & 4) != 0;
  1832. const BlendWindow blend = makeBlendWindow (op.blend);
  1833. DynamicValue* axisValue = op.axis->value.get ();
  1834. DynamicValue* offsetValue = op.offset->value.get ();
  1835. DynamicValue* distanceInnerValue = op.distanceInner ? op.distanceInner->value.get () : nullptr;
  1836. DynamicValue* distanceOuterValue = op.distanceOuter ? op.distanceOuter->value.get () : nullptr;
  1837. DynamicValue* speedInnerValue = op.speedInner ? op.speedInner->value.get () : nullptr;
  1838. DynamicValue* speedOuterValue = op.speedOuter->value.get ();
  1839. DynamicValue* centerForceValue = op.centerForce->value.get ();
  1840. DynamicValue* ringRadiusValue = op.ringRadius ? op.ringRadius->value.get () : nullptr;
  1841. DynamicValue* ringWidthValue = op.ringWidth ? op.ringWidth->value.get () : nullptr;
  1842. DynamicValue* ringPullDistanceValue = op.ringPullDistance ? op.ringPullDistance->value.get () : nullptr;
  1843. DynamicValue* ringPullForceValue = op.ringPullForce ? op.ringPullForce->value.get () : nullptr;
  1844. DynamicValue* audioModeValue = op.audioProcessingMode->value.get ();
  1845. DynamicValue* audioBoundsValue = op.audioProcessingBounds->value.get ();
  1846. DynamicValue* audioExponentValue = op.audioProcessingExponent->value.get ();
  1847. DynamicValue* audioStartValue = op.audioProcessingFrequencyStart->value.get ();
  1848. DynamicValue* audioEndValue = op.audioProcessingFrequencyEnd->value.get ();
  1849. DynamicValue* speedOverride
  1850. = (m_particle.flags & 0x10) == 0 ? m_particle.instanceOverride.speed->value.get () : nullptr;
  1851. // sub_14023FBC0 case 15 (vortex), 16 and its blended variant 37 (vortex_v2). Loaders in sub_1401C5490, defaults
  1852. // from sub_1401BEF00 / sub_1401BF2D0. Worked out in WE's y-up space
  1853. return [this, controlPoint, v2, infiniteAxis, useCenterForce, ringShape, blend, axisValue, offsetValue,
  1854. distanceInnerValue, distanceOuterValue, speedInnerValue, speedOuterValue, centerForceValue, ringRadiusValue,
  1855. ringWidthValue, ringPullDistanceValue, ringPullForceValue, audioModeValue, audioBoundsValue,
  1856. audioExponentValue, audioStartValue, audioEndValue, speedOverride] (
  1857. std::vector<ParticleInstance>& particles, uint32_t count,
  1858. const std::vector<ControlPointData>& controlPoints, float, float dt
  1859. ) {
  1860. const bool flat = !getScene ().getCamera ().isPerspective ();
  1861. const float audio = audioModeValue->getInt () != 0
  1862. ? sampleAudio (
  1863. audioModeValue->getInt (), audioBoundsValue->getVec2 (), audioExponentValue->getFloat (),
  1864. audioStartValue->getInt (), audioEndValue->getInt ()
  1865. )
  1866. : 1.0f;
  1867. const float speedScale = speedOverride != nullptr ? speedOverride->getFloat () : 1.0f;
  1868. const float speedInner
  1869. = (speedInnerValue != nullptr ? speedInnerValue->getFloat () : (flat ? 2500.0f : 1.0f)) * speedScale;
  1870. const float speedOuter = speedOuterValue->getFloat () * speedScale;
  1871. const float scaledDt = frameScaledDelta (dt);
  1872. const float innerSpeed = speedInner * audio * scaledDt;
  1873. const float speedRange = (speedOuter - speedInner) * audio * scaledDt;
  1874. // the loader normalizes the axis, (0, 0, 1) when it is too short
  1875. glm::vec3 axis = axisValue->getVec3 ();
  1876. axis = glm::length (axis) >= 0.001f ? glm::normalize (axis) : glm::vec3 (0.0f, 0.0f, 1.0f);
  1877. const ControlPointData& point = controlPoints[controlPoint];
  1878. glm::vec3 center = controlPointWE (controlPoint);
  1879. if (v2) {
  1880. // turned (and scaled) by the control point's matrix, not normalized again
  1881. axis = flipY (point.orientation * flipY (axis));
  1882. } else {
  1883. center += offsetValue->getVec3 ();
  1884. }
  1885. float start;
  1886. float rangeScale;
  1887. if (ringShape) {
  1888. const float pullDistance
  1889. = ringPullDistanceValue != nullptr ? ringPullDistanceValue->getFloat () : (flat ? 50.0f : 0.25f);
  1890. start = ringWidthValue != nullptr ? ringWidthValue->getFloat () : (flat ? 50.0f : 0.2f);
  1891. rangeScale = pullDistance != 0.0f ? 1.0f / pullDistance : 1.0f;
  1892. } else {
  1893. const float distanceOuter
  1894. = distanceOuterValue != nullptr ? distanceOuterValue->getFloat () : (flat ? 650.0f : 2.0f);
  1895. start = distanceInnerValue != nullptr ? distanceInnerValue->getFloat () : (flat ? 500.0f : 1.0f);
  1896. rangeScale = start != distanceOuter ? 1.0f / (distanceOuter - start) : 1.0f;
  1897. }
  1898. const float ringRadius = ringRadiusValue != nullptr ? ringRadiusValue->getFloat () : (flat ? 300.0f : 1.0f);
  1899. const float ringPull
  1900. = (ringPullForceValue != nullptr ? ringPullForceValue->getFloat () : (flat ? 10.0f : 0.05f)) * dt;
  1901. const float centerPull = useCenterForce ? centerForceValue->getFloat () / dt : 0.0f;
  1902. const auto spin = [&] (ParticleInstance& p) {
  1903. const glm::vec3 position = flipY (p.position);
  1904. const glm::vec3 velocity = flipY (p.velocity);
  1905. const glm::vec3 toParticle = position - center;
  1906. const glm::vec3 along = infiniteAxis ? axis * glm::dot (toParticle, axis) : glm::vec3 (0.0f);
  1907. const glm::vec3 radial = toParticle - along;
  1908. const float distance = glm::length (radial);
  1909. // WE's rsqrt turns a particle on the axis into NaN, leave it alone instead
  1910. if (distance == 0.0f) {
  1911. return;
  1912. }
  1913. const glm::vec3 direction = radial / distance;
  1914. const float weight = v2 && blend.active ? blendWeight (blend, p) : 1.0f;
  1915. float t;
  1916. if (ringShape) {
  1917. t = std::clamp ((std::abs (ringRadius - distance) - start) * rangeScale, 0.0f, 1.0f);
  1918. } else {
  1919. t = std::clamp ((distance - start) * rangeScale, 0.0f, 1.0f);
  1920. }
  1921. glm::vec3 change = glm::cross (direction, axis) * ((t * speedRange + innerSpeed) * weight);
  1922. if (v2) {
  1923. // keeps the particle at its distance from the axis over the coming move, and pulls it onto the ring
  1924. const glm::vec3 next = velocity * dt + position - center - along;
  1925. const float nextDistance = glm::length (next);
  1926. float pull = nextDistance > 0.0f ? (distance / nextDistance - 1.0f) * centerPull : 0.0f;
  1927. if (ringShape) {
  1928. const float falloff = 1.0f - t;
  1929. pull += (falloff != 1.0f ? std::copysign (falloff, ringRadius - distance) : 0.0f) * ringPull;
  1930. }
  1931. change += next * (pull * weight);
  1932. }
  1933. p.velocity += flipY (change);
  1934. };
  1935. for (uint32_t i = 0; i < count; i++) {
  1936. if (particles[i].alive) {
  1937. spin (particles[i]);
  1938. }
  1939. }
  1940. for (uint32_t slot = 0; slot < m_ghostUsed.size (); slot++) {
  1941. if (m_ghostUsed[slot]) {
  1942. spin (m_ghosts[slot]);
  1943. }
  1944. }
  1945. };
  1946. }
  1947. OperatorFunc CParticle::createControlPointAttractOperator (const ControlPointAttractOperator& op) {
  1948. const int controlPoint = op.controlPoint;
  1949. const bool deleteNear = (op.flags & 1) != 0;
  1950. const bool clampToPoint = (op.flags & 2) != 0;
  1951. DynamicValue* scaleValue = op.scale ? op.scale->value.get () : nullptr;
  1952. DynamicValue* thresholdValue = op.threshold ? op.threshold->value.get () : nullptr;
  1953. DynamicValue* deleteThresholdValue = op.deleteThreshold ? op.deleteThreshold->value.get () : nullptr;
  1954. DynamicValue* speedOverride
  1955. = (m_particle.flags & 0x10) == 0 ? m_particle.instanceOverride.speed->value.get () : nullptr;
  1956. if (deleteNear) {
  1957. m_tracksPreviousPosition = true;
  1958. }
  1959. // sub_14023FBC0 case 10, defaults from sub_1401BDEE0
  1960. return [this, controlPoint, deleteNear, clampToPoint, scaleValue, thresholdValue, deleteThresholdValue,
  1961. speedOverride] (
  1962. std::vector<ParticleInstance>& particles, uint32_t count,
  1963. const std::vector<ControlPointData>& controlPoints, float, float dt
  1964. ) {
  1965. const bool flat = !getScene ().getCamera ().isPerspective ();
  1966. const float scale = (scaleValue != nullptr ? scaleValue->getFloat () : (flat ? 512.0f : 20.0f))
  1967. * (speedOverride != nullptr ? speedOverride->getFloat () : 1.0f);
  1968. const float threshold = thresholdValue != nullptr ? thresholdValue->getFloat () : (flat ? 512.0f : 5.0f);
  1969. const float force = scale * frameScaledDelta (dt);
  1970. const glm::vec3 center = controlPoints[controlPoint].position;
  1971. const auto attract = [&] (ParticleInstance& p) {
  1972. const glm::vec3 toCenter = center - p.position;
  1973. const float distance = glm::length (toCenter);
  1974. if (!(distance < threshold) || !(distance > std::numeric_limits<float>::min ())) {
  1975. return;
  1976. }
  1977. float pull = (1.0f - distance / threshold) * force;
  1978. if (clampToPoint && distance < pull) {
  1979. pull = distance;
  1980. }
  1981. p.velocity += toCenter * (pull / distance);
  1982. };
  1983. for (uint32_t i = 0; i < count; i++) {
  1984. if (particles[i].alive) {
  1985. attract (particles[i]);
  1986. }
  1987. }
  1988. for (uint32_t slot = 0; slot < m_ghostUsed.size (); slot++) {
  1989. if (m_ghostUsed[slot]) {
  1990. attract (m_ghosts[slot]);
  1991. }
  1992. }
  1993. if (!deleteNear) {
  1994. return;
  1995. }
  1996. // sub_14022A150: a particle whose step since the operators started came within deletethreshold dies
  1997. const float deleteThreshold
  1998. = deleteThresholdValue != nullptr ? deleteThresholdValue->getFloat () : (flat ? 15.0f : 0.5f);
  1999. for (uint32_t i = 0; i < count; i++) {
  2000. auto& p = particles[i];
  2001. if (!p.alive) {
  2002. continue;
  2003. }
  2004. const glm::vec3 step = p.position - p.previousPosition;
  2005. const float length = glm::length (step);
  2006. const glm::vec3 direction = length > 0.0f ? step / length : step;
  2007. const float along = std::clamp (glm::dot (center - p.previousPosition, direction), 0.0f, length);
  2008. const glm::vec3 closest = center - (p.previousPosition + direction * along);
  2009. if (!(deleteThreshold * deleteThreshold < glm::dot (closest, closest))) {
  2010. p.age = p.lifetime;
  2011. }
  2012. }
  2013. };
  2014. }
  2015. OperatorFunc CParticle::createOscillateAlphaOperator (const OscillateAlphaOperator& op) {
  2016. DynamicValue* freqMinValue = op.frequencyMin->value.get ();
  2017. DynamicValue* freqMaxValue = op.frequencyMax->value.get ();
  2018. DynamicValue* scaleMinValue = op.scaleMin->value.get ();
  2019. DynamicValue* scaleMaxValue = op.scaleMax->value.get ();
  2020. DynamicValue* phaseMinValue = op.phaseMin->value.get ();
  2021. DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
  2022. return
  2023. [this, freqMinValue, freqMaxValue, scaleMinValue, scaleMaxValue, phaseMinValue, phaseMaxValue] (
  2024. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  2025. ) {
  2026. float freqMin = freqMinValue->getFloat ();
  2027. float freqMax = freqMaxValue->getFloat ();
  2028. float scaleMin = scaleMinValue->getFloat ();
  2029. float scaleMax = scaleMaxValue->getFloat ();
  2030. float phaseMin = phaseMinValue->getFloat ();
  2031. float phaseMax = phaseMaxValue->getFloat ();
  2032. for (uint32_t i = 0; i < count; i++) {
  2033. auto& p = particles[i];
  2034. // Initialize per-particle oscillator values on first use
  2035. if (!p.oscillateAlpha.initialized) {
  2036. p.oscillateAlpha.frequency = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
  2037. p.oscillateAlpha.scale = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
  2038. p.oscillateAlpha.phase
  2039. = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
  2040. p.oscillateAlpha.base = p.alpha;
  2041. p.oscillateAlpha.initialized = true;
  2042. }
  2043. // Cosine wave interpolating between scaleMin and scaleMax
  2044. float w = p.oscillateAlpha.frequency;
  2045. float t = p.age;
  2046. float cosVal = (std::cos (w * t + p.oscillateAlpha.phase) + 1.0f) * 0.5f;
  2047. float multiplier = glm::mix (scaleMin, scaleMax, cosVal);
  2048. // Apply to base value (alphafade updates base each frame if present)
  2049. p.alpha = p.oscillateAlpha.base * multiplier;
  2050. }
  2051. };
  2052. }
  2053. OperatorFunc CParticle::createOscillateSizeOperator (const OscillateSizeOperator& op) {
  2054. DynamicValue* freqMinValue = op.frequencyMin->value.get ();
  2055. DynamicValue* freqMaxValue = op.frequencyMax->value.get ();
  2056. DynamicValue* scaleMinValue = op.scaleMin->value.get ();
  2057. DynamicValue* scaleMaxValue = op.scaleMax->value.get ();
  2058. DynamicValue* phaseMinValue = op.phaseMin->value.get ();
  2059. DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
  2060. return
  2061. [this, freqMinValue, freqMaxValue, scaleMinValue, scaleMaxValue, phaseMinValue, phaseMaxValue] (
  2062. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
  2063. ) {
  2064. float freqMin = freqMinValue->getFloat ();
  2065. float freqMax = freqMaxValue->getFloat ();
  2066. float scaleMin = scaleMinValue->getFloat ();
  2067. float scaleMax = scaleMaxValue->getFloat ();
  2068. float phaseMin = phaseMinValue->getFloat ();
  2069. float phaseMax = phaseMaxValue->getFloat ();
  2070. for (uint32_t i = 0; i < count; i++) {
  2071. auto& p = particles[i];
  2072. // Initialize per-particle oscillator values on first use
  2073. if (!p.oscillateSize.initialized) {
  2074. p.oscillateSize.frequency = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
  2075. p.oscillateSize.scale = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
  2076. p.oscillateSize.phase
  2077. = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
  2078. p.oscillateSize.base = p.size;
  2079. p.oscillateSize.initialized = true;
  2080. }
  2081. // Cosine wave interpolating between scaleMin and scaleMax
  2082. float w = p.oscillateSize.frequency;
  2083. float t = p.age;
  2084. float cosVal = (std::cos (w * t + p.oscillateSize.phase) + 1.0f) * 0.5f;
  2085. float multiplier = glm::mix (scaleMin, scaleMax, cosVal);
  2086. // Apply to base value (sizeChange updates base each frame if present)
  2087. p.size = p.oscillateSize.base * multiplier;
  2088. }
  2089. };
  2090. }
  2091. OperatorFunc CParticle::createOscillatePositionOperator (const OscillatePositionOperator& op) {
  2092. DynamicValue* freqMinValue = op.frequencyMin->value.get ();
  2093. DynamicValue* freqMaxValue = op.frequencyMax->value.get ();
  2094. DynamicValue* scaleMinValue = op.scaleMin->value.get ();
  2095. DynamicValue* scaleMaxValue = op.scaleMax->value.get ();
  2096. DynamicValue* phaseMinValue = op.phaseMin->value.get ();
  2097. DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
  2098. DynamicValue* maskValue = op.mask->value.get ();
  2099. DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
  2100. return [this, freqMinValue, freqMaxValue, scaleMinValue, scaleMaxValue, phaseMinValue, phaseMaxValue, maskValue,
  2101. speedOverride] (
  2102. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  2103. float dt
  2104. ) {
  2105. float freqMin = freqMinValue->getFloat ();
  2106. float freqMax = freqMaxValue->getFloat ();
  2107. float scaleMin = scaleMinValue->getFloat ();
  2108. float scaleMax = scaleMaxValue->getFloat ();
  2109. float phaseMin = phaseMinValue->getFloat ();
  2110. float phaseMax = phaseMaxValue->getFloat ();
  2111. glm::vec3 mask = maskValue->getVec3 ();
  2112. for (uint32_t i = 0; i < count; i++) {
  2113. auto& p = particles[i];
  2114. // Initialize per-particle oscillator values on first use (per axis)
  2115. if (!p.oscillatePosition.initialized) {
  2116. for (int axis = 0; axis < 3; axis++) {
  2117. p.oscillatePosition.frequency[axis] = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
  2118. p.oscillatePosition.scale[axis] = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
  2119. p.oscillatePosition.phase[axis]
  2120. = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
  2121. }
  2122. p.oscillatePosition.initialized = true;
  2123. }
  2124. float t = p.age;
  2125. glm::vec3 delta (0.0f);
  2126. for (int axis = 0; axis < 3; axis++) {
  2127. float w = 2.0f * glm::pi<float> () * p.oscillatePosition.frequency[axis] / (2.0f * glm::pi<float> ());
  2128. // Derivative of cos is -sin; multiplied by dt for position change
  2129. float move
  2130. = -p.oscillatePosition.scale[axis] * w * std::sin (w * t + p.oscillatePosition.phase[axis]) * dt;
  2131. // Apply mask as bias multiplier for this axis
  2132. delta[axis] = move * mask[axis] * speedOverride->getFloat ();
  2133. }
  2134. p.position += delta;
  2135. }
  2136. };
  2137. }
  2138. // ========== 2.7 COMPONENTS ==========
  2139. namespace {
  2140. /** 0.0 up to the zero range wallpaper64.exe replaces with FLT_EPSILON */
  2141. glm::vec3 remapRange (const glm::vec3& min, const glm::vec3& max) {
  2142. glm::vec3 range = max - min;
  2143. for (int i = 0; i < 3; i++) {
  2144. if (range[i] == 0.0f) {
  2145. range[i] = 1.1920929e-7f;
  2146. }
  2147. }
  2148. return range;
  2149. }
  2150. bool remapIsVector (ParticleRemapValue value) { return static_cast<int> (value) > 12; }
  2151. glm::vec3 remapSelectComponent (const glm::vec3& value, ParticleRemapComponent component) {
  2152. switch (component) {
  2153. case ParticleRemapComponent::X: return glm::vec3 (value.x);
  2154. case ParticleRemapComponent::Y: return glm::vec3 (value.y);
  2155. case ParticleRemapComponent::Z: return glm::vec3 (value.z);
  2156. case ParticleRemapComponent::Sum: return glm::vec3 ((value.y + value.x) + value.z);
  2157. case ParticleRemapComponent::Average: return glm::vec3 (((value.y + value.x) + value.z) * 0.33333334f);
  2158. case ParticleRemapComponent::Max: return glm::vec3 (std::fmax (std::fmax (value.x, value.y), value.z));
  2159. case ParticleRemapComponent::Min: return glm::vec3 (std::fmin (std::fmin (value.x, value.y), value.z));
  2160. default: return value;
  2161. }
  2162. }
  2163. float remapApply (ParticleRemapOperation operation, float current, float value) {
  2164. switch (operation) {
  2165. case ParticleRemapOperation::Remap: return value;
  2166. case ParticleRemapOperation::Multiply: return value * current;
  2167. case ParticleRemapOperation::Add: return value + current;
  2168. case ParticleRemapOperation::Subtract: return current - value;
  2169. default: return current;
  2170. }
  2171. }
  2172. /** The transform functions of sub_14023FBC0 case 19, seed is the particle's random as integer bits */
  2173. float remapTransformOperator (
  2174. ParticleRemapTransform transform, float value, float scale, int octaves, float fbmAmplitude, int32_t seed
  2175. ) {
  2176. switch (transform) {
  2177. case ParticleRemapTransform::Sine:
  2178. return std::sin (value * (scale * glm::pi<float> ()) - glm::half_pi<float> ()) * 0.5f + 0.5f;
  2179. case ParticleRemapTransform::Square: {
  2180. const float scaled = value * scale;
  2181. return std::nearbyint (scaled - std::trunc (scaled)) + (scaled < 0.0f ? 1.0f : 0.0f);
  2182. }
  2183. case ParticleRemapTransform::Saw: {
  2184. const float scaled = value * scale;
  2185. return (scaled - std::trunc (scaled)) + (value < 0.0f ? 1.0f : 0.0f);
  2186. }
  2187. case ParticleRemapTransform::Triangle: {
  2188. const float scaled = std::fabs (value * scale);
  2189. return 1.0f - std::fabs ((scaled - std::trunc (scaled)) * 2.0f - 1.0f);
  2190. }
  2191. case ParticleRemapTransform::SimplexNoise: return hashedNoise2D (seed, value * scale, 0.0f) * 0.5f + 0.5f;
  2192. case ParticleRemapTransform::FbmNoise:
  2193. return hashedNoiseFbm (octaves, fbmAmplitude, seed, value * scale, 0.0f) * 0.5f + 0.5f;
  2194. default: return value;
  2195. }
  2196. }
  2197. /** The transform functions of sub_14023B340 case 15: floor instead of trunc, 1D noise without a seed */
  2198. float remapTransformInitial (ParticleRemapTransform transform, float value, float scale, int octaves) {
  2199. switch (transform) {
  2200. case ParticleRemapTransform::Sine:
  2201. return std::sin ((value * glm::pi<float> ()) * scale - glm::half_pi<float> ()) * 0.5f + 0.5f;
  2202. case ParticleRemapTransform::Square: {
  2203. const float scaled = value * scale;
  2204. return std::round (scaled - std::floor (scaled)) + (scaled < 0.0f ? 1.0f : 0.0f);
  2205. }
  2206. case ParticleRemapTransform::Saw: {
  2207. const float scaled = value * scale;
  2208. return (scaled - std::floor (scaled)) + (value < 0.0f ? 1.0f : 0.0f);
  2209. }
  2210. case ParticleRemapTransform::Triangle: {
  2211. const float scaled = std::fabs (value * scale);
  2212. return 1.0f - std::fabs ((scaled - std::floor (scaled)) * 2.0f - 1.0f);
  2213. }
  2214. case ParticleRemapTransform::SimplexNoise: return simplexNoise1D (value * scale) * 0.5f + 0.5f;
  2215. case ParticleRemapTransform::FbmNoise: return simplexFbm1D (value, scale, octaves) * 0.5f + 0.5f;
  2216. default: return value;
  2217. }
  2218. }
  2219. int32_t particleSeedBits (const ParticleInstance& p) {
  2220. int32_t bits;
  2221. std::memcpy (&bits, &p.seed, sizeof (bits));
  2222. return bits;
  2223. }
  2224. /** The engine's g_Daytime as sub_140110630 fills it: local time as a fraction of the day, milliseconds included */
  2225. float dayFraction () {
  2226. const auto now = std::chrono::system_clock::now ();
  2227. const std::time_t seconds = std::chrono::system_clock::to_time_t (now);
  2228. const auto milliseconds
  2229. = std::chrono::duration_cast<std::chrono::milliseconds> (now.time_since_epoch ()).count () % 1000;
  2230. std::tm local {};
  2231. localtime_r (&seconds, &local);
  2232. const double fraction = local.tm_min * (1.0 / 1440.0) + local.tm_hour * (1.0 / 24.0)
  2233. + local.tm_sec * (1.0 / 86400.0) + static_cast<double> (milliseconds) * (1.0 / 86400000.0);
  2234. return static_cast<float> (fraction);
  2235. }
  2236. } // namespace
  2237. glm::vec3 CParticle::controlPointWE (int index) const { return flipY (m_controlPoints[index].position); }
  2238. glm::vec3 CParticle::drawVector (const glm::vec3& value) const { return m_drawFlipY ? flipY (value) : value; }
  2239. glm::mat4 CParticle::localControlPointMatrix (size_t index) const {
  2240. const auto& cp = m_controlPoints[index];
  2241. glm::mat4 local = glm::translate (glm::mat4 (1.0f), cp.offset);
  2242. // sub_14022BD40, skipped for points with flags 0x10005 like WE
  2243. if (cp.followParent || index >= m_particle.instanceOverride.controlPoints.size ()) {
  2244. return local;
  2245. }
  2246. if (const auto& angles = m_particle.instanceOverride.controlPointAngles[index]; angles) {
  2247. // Rz * Ry * Rx in WE's y-up space, radians
  2248. const glm::vec3 angle = angles->value->getVec3 ();
  2249. const float cx = std::cos (angle.x), sx = std::sin (angle.x);
  2250. const float cy = std::cos (angle.y), sy = std::sin (angle.y);
  2251. const float cz = std::cos (angle.z), sz = std::sin (angle.z);
  2252. const glm::mat3 rotation (
  2253. cy * cz, cy * sz, -sy,
  2254. sy * cz * sx - cx * sz, sy * sz * sx + cx * cz, sx * cy,
  2255. cx * cz * sy + sx * sz, cx * sz * sy - sx * cz, cx * cy
  2256. );
  2257. // the same rotation in the y mirrored space used here
  2258. const glm::mat3 mirror (1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
  2259. const glm::mat3 mirrored = mirror * rotation * mirror;
  2260. local[0] = glm::vec4 (mirrored[0], 0.0f);
  2261. local[1] = glm::vec4 (mirrored[1], 0.0f);
  2262. local[2] = glm::vec4 (mirrored[2], 0.0f);
  2263. }
  2264. if (const auto& position = m_particle.instanceOverride.controlPoints[index]; position) {
  2265. local[3] = glm::vec4 (flipY (position->value->getVec3 ()), 1.0f);
  2266. }
  2267. return local;
  2268. }
  2269. float CParticle::frameScaledDelta (float dt) const {
  2270. const float ratio = m_frameDelta > 0.0f ? std::min (1.0f, 0.025f / m_frameDelta) : 1.0f;
  2271. return std::pow (ratio, 0.7f) * dt;
  2272. }
  2273. float CParticle::layerTime () const {
  2274. const CParticle* root = this;
  2275. while (root->m_parent != nullptr) {
  2276. root = root->m_parent;
  2277. }
  2278. return root->m_layerTime;
  2279. }
  2280. glm::vec3 CParticle::remapLayerOrigin () const {
  2281. const CParticle* root = this;
  2282. while (root->m_parent != nullptr) {
  2283. root = root->m_parent;
  2284. }
  2285. // the translation of the layer's matrix, in WE's scene space (y up, from the bottom left) for 2D scenes
  2286. const glm::vec3 translation (root->objectMatrix ()[3]);
  2287. if (getScene ().getCamera ().isPerspective ()) {
  2288. return translation;
  2289. }
  2290. const float width = static_cast<float> (getScene ().getWidth ());
  2291. const float height = static_cast<float> (getScene ().getHeight ());
  2292. return { translation.x + width / 2.0f, height / 2.0f - translation.y, translation.z };
  2293. }
  2294. InitializerFunc CParticle::createHsvColorRandomInitializer (const HsvColorRandomInitializer& init) {
  2295. DynamicValue* hueMinValue = init.hueMin->value.get ();
  2296. DynamicValue* hueMaxValue = init.hueMax->value.get ();
  2297. DynamicValue* saturationMinValue = init.saturationMin->value.get ();
  2298. DynamicValue* saturationMaxValue = init.saturationMax->value.get ();
  2299. DynamicValue* valueMinValue = init.valueMin->value.get ();
  2300. DynamicValue* valueMaxValue = init.valueMax->value.get ();
  2301. const int steps = init.hueSteps;
  2302. // wallpaper64.exe sub_14023B340 case 4, the hue step from sub_1401C5490
  2303. return [this, hueMinValue, hueMaxValue, saturationMinValue, saturationMaxValue, valueMinValue, valueMaxValue,
  2304. steps] (ParticleInstance& p) {
  2305. float hueMin = hueMinValue->getFloat ();
  2306. float hueSpan = 0.0f;
  2307. float step = 0.0f;
  2308. if (static_cast<float> (steps) > 1.0f) {
  2309. const float span = hueMaxValue->getFloat () - hueMin;
  2310. // a full circle has as many steps as hues, anything shorter ends on huemax
  2311. float divisions = static_cast<float> (steps) - 1.0f;
  2312. if (std::fabs (std::fmod (span, 1.0f)) < 0.0027777778f) {
  2313. divisions += 1.0f;
  2314. }
  2315. hueSpan = span != 0.0f ? span : 1.0f;
  2316. step = hueSpan / divisions;
  2317. }
  2318. float saturationMin = saturationMinValue->getFloat ();
  2319. float saturationSpan = saturationMaxValue->getFloat () - saturationMin;
  2320. float valueMin = valueMinValue->getFloat ();
  2321. float valueSpan = valueMaxValue->getFloat () - valueMin;
  2322. // sub_1401D15A0 case 0xD: the ranges get centered on the override color
  2323. if (m_colorOverride.active) {
  2324. const glm::vec3& target = m_colorOverride.hsv;
  2325. const auto center = [] (float goal, float& min, float& span) {
  2326. min = std::clamp (goal - (span * 0.5f + min) + min, 0.0f, 1.0f);
  2327. if (min + span > 1.0f) {
  2328. span = 1.0f - std::fmin (min, 1.0f);
  2329. }
  2330. };
  2331. center (target.y, saturationMin, saturationSpan);
  2332. center (target.z, valueMin, valueSpan);
  2333. hueMin = target.x - (hueSpan * 0.5f + hueMin) + hueMin;
  2334. }
  2335. // rand () / 32767 can reach steps + 1, clamped back
  2336. const int draw = std::uniform_int_distribution<int> (0, 32767) (m_rng);
  2337. int index = static_cast<int> ((static_cast<float> (draw) / 32767.0f) * static_cast<float> (steps + 1) + 0.0f);
  2338. index = std::max (0, std::min (steps, index));
  2339. const float hue = static_cast<float> (index) * step + hueMin;
  2340. const float saturation = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) * saturationSpan + saturationMin;
  2341. const float value = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) * valueSpan + valueMin;
  2342. p.color *= WallpaperEngine::Maths::hsvToRgb ({ hue, saturation, value });
  2343. p.initial.color = p.color;
  2344. };
  2345. }
  2346. InitializerFunc CParticle::createColorListInitializer (const ColorListInitializer& init) {
  2347. // sub_1401C5490 keeps the colors as HSV, an empty list is one pure red
  2348. std::vector<glm::vec3> colors;
  2349. for (const auto& color : init.colors) {
  2350. colors.push_back (WallpaperEngine::Maths::rgbToHsv (color));
  2351. }
  2352. if (colors.empty ()) {
  2353. colors.emplace_back (0.0f, 1.0f, 1.0f);
  2354. }
  2355. DynamicValue* hueNoiseValue = init.hueNoise->value.get ();
  2356. DynamicValue* saturationNoiseValue = init.saturationNoise->value.get ();
  2357. DynamicValue* valueNoiseValue = init.valueNoise->value.get ();
  2358. // sub_14023B340 case 5: a random entry, each channel randomized within its noise
  2359. return [this, colors, hueNoiseValue, saturationNoiseValue, valueNoiseValue] (ParticleInstance& p) {
  2360. const auto& picked
  2361. = colors[std::uniform_int_distribution<size_t> (0, colors.size () - 1) (m_rng)];
  2362. const glm::vec3 noise (
  2363. hueNoiseValue->getFloat (), saturationNoiseValue->getFloat (), valueNoiseValue->getFloat ()
  2364. );
  2365. // sub_1401D15A0 case 0xE: an active override color moves every entry by its distance to the first one
  2366. const glm::vec3 offset = m_colorOverride.active ? m_colorOverride.hsv - colors.front () : glm::vec3 (0.0f);
  2367. glm::vec3 hsv;
  2368. for (int i = 0; i < 3; i++) {
  2369. const float low = std::max (picked[i] - noise[i], 0.0f);
  2370. const float high = std::min (noise[i] + picked[i], 1.0f);
  2371. hsv[i] = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) * (high - low) + low + offset[i];
  2372. }
  2373. hsv.x -= std::floor (hsv.x);
  2374. hsv.y = std::clamp (hsv.y, 0.0f, 1.0f);
  2375. hsv.z = std::clamp (hsv.z, 0.0f, 1.0f);
  2376. p.color *= WallpaperEngine::Maths::hsvToRgb (hsv);
  2377. p.initial.color = p.color;
  2378. };
  2379. }
  2380. InitializerFunc CParticle::createPositionOffsetRandomInitializer (const PositionOffsetRandomInitializer& init) {
  2381. DynamicValue* directionsValue = init.directions ? init.directions->value.get () : nullptr;
  2382. DynamicValue* signValue = init.sign->value.get ();
  2383. DynamicValue* scaleValue = init.scale ? init.scale->value.get () : nullptr;
  2384. DynamicValue* distanceValue = init.distance ? init.distance->value.get () : nullptr;
  2385. DynamicValue* timeScaleValue = init.timeScale->value.get ();
  2386. const int octaves = init.octaves;
  2387. // sub_14023B340 case 11, defaults from sub_1401BB660
  2388. return [this, directionsValue, signValue, scaleValue, distanceValue, timeScaleValue,
  2389. octaves] (ParticleInstance& p) {
  2390. const bool flat = !getScene ().getCamera ().isPerspective ();
  2391. const glm::vec3 directions
  2392. = directionsValue != nullptr ? directionsValue->getVec3 () : (flat ? glm::vec3 (1, 1, 0) : glm::vec3 (1));
  2393. const float scale = scaleValue != nullptr ? scaleValue->getFloat () : (flat ? 0.001f : 1.0f);
  2394. const float distance = distanceValue != nullptr ? distanceValue->getFloat () : (flat ? 100.0f : 0.1f);
  2395. const glm::vec3 sign = signValue->getVec3 ();
  2396. // the renderer's scene clock is the time axis of the noise
  2397. const float time = timeScaleValue->getFloat () * getScene ().getSceneClock ();
  2398. const glm::vec3 position = flipY (p.position);
  2399. const auto fbm = [octaves] (float x, float y) {
  2400. float sum = 0.0f;
  2401. float total = 0.0f;
  2402. float amplitude = 1.0f;
  2403. float frequency = 1.0f;
  2404. for (int octave = 0; octave < octaves; octave++) {
  2405. const float noise = simplexNoise2D (frequency * x, frequency * y) * amplitude;
  2406. total += amplitude;
  2407. amplitude *= 0.5f;
  2408. frequency += frequency;
  2409. sum += noise;
  2410. }
  2411. return sum / total;
  2412. };
  2413. glm::vec3 offset (
  2414. fbm (position.x * scale, time) * directions.x, fbm (time, position.y * scale) * directions.y,
  2415. fbm (position.z * scale, -time) * directions.z
  2416. );
  2417. // sign pushes the offset to one side per axis
  2418. if (glm::length (sign) > 1.1920929e-7f) {
  2419. const glm::vec3 absoluteSign = glm::abs (sign);
  2420. offset = glm::abs (offset) * sign + offset * (1.0f - absoluteSign);
  2421. }
  2422. p.position = flipY (offset * distance + position);
  2423. };
  2424. }
  2425. InitializerFunc
  2426. CParticle::createMapSequenceBetweenControlPointsInitializer (const MapSequenceBetweenControlPointsInitializer& init) {
  2427. const int start = init.controlPointStart;
  2428. const int end = init.controlPointEnd;
  2429. const uint32_t flags = init.flags;
  2430. const bool mirror = init.mirror;
  2431. const float boundsMin = init.bounds.x;
  2432. const float boundsRange = init.bounds.y - init.bounds.x;
  2433. DynamicValue* arcAmountValue = init.arcAmount->value.get ();
  2434. DynamicValue* arcDirectionValue = init.arcDirection->value.get ();
  2435. DynamicValue* sizeReductionValue = init.sizeReductionAmount->value.get ();
  2436. // sub_1401C5490, with flag 0x10 the instance override's count takes part (sub_1401D15A0 case 4)
  2437. float step;
  2438. if ((flags & 0x10) != 0 && (m_particle.flags & 0x20) == 0) {
  2439. const float countOverride = m_particle.instanceOverride.count->value->getFloat ();
  2440. step = 1.0f / std::fmax (init.count * countOverride - 1.0f, 0.000099999997f);
  2441. } else {
  2442. step = 1.0f / (init.count - 1.0f <= 0.000099999997f ? 0.000099999997f : init.count - 1.0f);
  2443. }
  2444. float sequence = 0.0f;
  2445. // sub_14023B340 case 14: spread along the line between two control points, one step per particle
  2446. return [this, start, end, flags, mirror, boundsMin, boundsRange, arcAmountValue, arcDirectionValue,
  2447. sizeReductionValue, step, sequence] (ParticleInstance& p) mutable {
  2448. const glm::vec3 first = m_controlPoints[start].position;
  2449. const glm::vec3 line = m_controlPoints[end].position - first;
  2450. const float length = std::fmax (glm::length (line), 1.1754944e-38f);
  2451. const glm::vec3 direction = line / length;
  2452. glm::vec3 position = p.position;
  2453. if (m_worldSpace) {
  2454. position -= first;
  2455. }
  2456. const float along = glm::dot (position, direction);
  2457. glm::vec3 offset = position - along * direction;
  2458. const float at = sequence * boundsRange + boundsMin;
  2459. const float middle = 1.0f - std::pow (std::fabs ((sequence + sequence) - 1.0f), 2.0f);
  2460. if ((flags & 1) != 0) {
  2461. offset *= middle;
  2462. }
  2463. position = offset + ((at * direction) * length + first);
  2464. if ((flags & 8) != 0) {
  2465. position += flipY (arcDirectionValue->getVec3 ()) * (middle * length * arcAmountValue->getFloat ());
  2466. }
  2467. p.position = position;
  2468. if ((flags & 2) != 0) {
  2469. p.velocity *= middle;
  2470. }
  2471. if ((flags & 4) != 0) {
  2472. const float reduction = sizeReductionValue->getFloat ();
  2473. p.size *= (1.0f - reduction) + reduction * middle;
  2474. p.initial.size = p.size;
  2475. }
  2476. sequence += step;
  2477. if (sequence > 1.0f) {
  2478. if (mirror) {
  2479. step = -step;
  2480. sequence = 1.0f - (sequence - 1.0f);
  2481. } else {
  2482. sequence = 0.0f;
  2483. }
  2484. } else if (sequence < 0.0f) {
  2485. sequence = -sequence;
  2486. step = -step;
  2487. }
  2488. };
  2489. }
  2490. glm::vec3 CParticle::remapInitialInput (const ParticleRemap& remap, ParticleInstance& p) {
  2491. // sub_14023B340 case 15: scalars fill every component, the base values are read where the engine keeps them
  2492. const auto controlPoint = [this] (int index) { return this->controlPointWE (index); };
  2493. switch (remap.input) {
  2494. case ParticleRemapValue::LifetimeFraction:
  2495. // the sprite frame array, only filled at spawn for randomframe (with the particle's random)
  2496. return glm::vec3 (m_particle.animationMode == "randomframe" ? p.seed : 0.0f);
  2497. case ParticleRemapValue::MaxLifetime: return glm::vec3 (p.lifetime);
  2498. case ParticleRemapValue::Size: return glm::vec3 (p.initial.size);
  2499. case ParticleRemapValue::Opacity: return glm::vec3 (p.initial.alpha);
  2500. case ParticleRemapValue::Speed: return glm::vec3 (glm::length (p.velocity));
  2501. case ParticleRemapValue::Rotation: return glm::vec3 (p.rotation.z);
  2502. case ParticleRemapValue::AngularSpeed: return glm::vec3 (m_hasAngularVelocity ? p.angularVelocity.z : 0.0f);
  2503. case ParticleRemapValue::DistanceToControlPoint:
  2504. return glm::vec3 (glm::length (flipY (p.position) - controlPoint (remap.inputControlPoint0)));
  2505. case ParticleRemapValue::PositionBetweenTwoControlPoints: {
  2506. // the engine reads the output control points here, same as remapvalue
  2507. const glm::vec3 first = controlPoint (remap.outputControlPoint0);
  2508. glm::vec3 line = controlPoint (remap.outputControlPoint1) - first;
  2509. const float length = glm::length (line);
  2510. if (length <= 1.1920929e-7f) {
  2511. return glm::vec3 (0.0f);
  2512. }
  2513. line /= length;
  2514. return glm::vec3 (glm::dot (flipY (p.position) - first, line) / length);
  2515. }
  2516. case ParticleRemapValue::Runtime: return glm::vec3 (getScene ().getSceneClock ());
  2517. case ParticleRemapValue::TimeOfDay: return glm::vec3 (dayFraction ());
  2518. case ParticleRemapValue::ParticleSystemTime: return glm::vec3 (m_systemTime);
  2519. case ParticleRemapValue::LayerTime: return glm::vec3 (layerTime ());
  2520. case ParticleRemapValue::Color: return p.initial.color;
  2521. case ParticleRemapValue::Position: return flipY (p.position);
  2522. case ParticleRemapValue::Velocity: return flipY (p.velocity);
  2523. case ParticleRemapValue::ControlPoint:
  2524. case ParticleRemapValue::DeltaToControlPoint:
  2525. case ParticleRemapValue::DirectionToControlPoint: {
  2526. // the engine writes zeros into the control point's translation here instead of reading it
  2527. m_controlPoints[remap.inputControlPoint0].position = glm::vec3 (0.0f);
  2528. if (remap.input == ParticleRemapValue::ControlPoint) {
  2529. return glm::vec3 (0.0f);
  2530. }
  2531. const glm::vec3 delta = -flipY (p.position);
  2532. if (remap.input == ParticleRemapValue::DeltaToControlPoint) {
  2533. return delta;
  2534. }
  2535. const float length = glm::length (delta);
  2536. return length != 0.0f ? delta / length : glm::vec3 (0.0f);
  2537. }
  2538. case ParticleRemapValue::LayerOrigin: return remapLayerOrigin ();
  2539. default: return glm::vec3 (0.0f);
  2540. }
  2541. }
  2542. InitializerFunc CParticle::createRemapInitialValueInitializer (const RemapInitialValueInitializer& init) {
  2543. const ParticleRemap remap = init.remap;
  2544. const glm::vec3 inputRange = remapRange (remap.inputRangeMin, remap.inputRangeMax);
  2545. const glm::vec3 outputRange = remap.outputRangeMax - remap.outputRangeMin;
  2546. if (remap.input == ParticleRemapValue::LifetimeFraction && m_particle.animationMode == "randomframe") {
  2547. m_usesParticleSeed = true;
  2548. }
  2549. return [this, remap, inputRange, outputRange] (ParticleInstance& p) {
  2550. if (remap.output == ParticleRemapValue::Unknown) {
  2551. return;
  2552. }
  2553. glm::vec3 value = remapInitialInput (remap, p);
  2554. if (remapIsVector (remap.input)) {
  2555. value = remapSelectComponent (value, remap.inputComponent);
  2556. }
  2557. value = (value - remap.inputRangeMin) / inputRange;
  2558. if ((remap.flags & 1) != 0) {
  2559. value = glm::clamp (value, 0.0f, 1.0f);
  2560. }
  2561. value.x = remapTransformInitial (remap.transform, value.x, remap.transformInputScale, remap.transformOctaves);
  2562. if (remapIsVector (remap.output)) {
  2563. for (int i = 1; i < 3; i++) {
  2564. value[i] = remapTransformInitial (
  2565. remap.transform, value[i], remap.transformInputScale, remap.transformOctaves
  2566. );
  2567. }
  2568. }
  2569. value = value * outputRange + remap.outputRangeMin;
  2570. if ((remap.flags & 2) != 0) {
  2571. value = glm::clamp (value, 0.0f, 1.0f);
  2572. }
  2573. const auto apply = [&remap] (float current, float target) { return remapApply (remap.operation, current, target); };
  2574. // all three components, or the one outputcomponent names
  2575. const auto applyVector = [&remap, &apply] (glm::vec3 current, const glm::vec3& target) {
  2576. switch (remap.outputComponent) {
  2577. case ParticleRemapComponent::All:
  2578. for (int i = 0; i < 3; i++) {
  2579. current[i] = apply (current[i], target[i]);
  2580. }
  2581. break;
  2582. case ParticleRemapComponent::X: current.x = apply (current.x, target.x); break;
  2583. case ParticleRemapComponent::Y: current.y = apply (current.y, target.y); break;
  2584. case ParticleRemapComponent::Z: current.z = apply (current.z, target.z); break;
  2585. default: break;
  2586. }
  2587. return current;
  2588. };
  2589. switch (remap.output) {
  2590. case ParticleRemapValue::MaxLifetime:
  2591. p.lifetime = apply (p.lifetime, value.x);
  2592. p.initial.lifetime = p.lifetime;
  2593. break;
  2594. case ParticleRemapValue::Size:
  2595. p.initial.size = apply (p.initial.size, value.x);
  2596. p.size = p.initial.size;
  2597. break;
  2598. case ParticleRemapValue::Opacity:
  2599. p.initial.alpha = apply (p.initial.alpha, value.x);
  2600. p.alpha = p.initial.alpha;
  2601. break;
  2602. case ParticleRemapValue::Speed: {
  2603. const float speed = glm::length (p.velocity);
  2604. float scale = apply (speed, value.x);
  2605. if (speed != 0.0f) {
  2606. scale /= speed;
  2607. }
  2608. p.velocity *= scale;
  2609. break;
  2610. }
  2611. case ParticleRemapValue::Rotation: p.rotation.z = apply (p.rotation.z, value.x); break;
  2612. case ParticleRemapValue::AngularSpeed:
  2613. if (m_hasAngularVelocity) {
  2614. p.angularVelocity.z = apply (p.angularVelocity.z, value.x);
  2615. }
  2616. break;
  2617. case ParticleRemapValue::DistanceToControlPoint: {
  2618. const glm::vec3 point = controlPointWE (remap.outputControlPoint0);
  2619. glm::vec3 offset = flipY (p.position) - point;
  2620. const float distance = glm::length (offset);
  2621. if (distance != 0.0f) {
  2622. offset /= distance;
  2623. }
  2624. p.position = flipY (point + offset * apply (distance, value.x));
  2625. break;
  2626. }
  2627. case ParticleRemapValue::PositionBetweenTwoControlPoints: {
  2628. const glm::vec3 first = controlPointWE (remap.outputControlPoint0);
  2629. glm::vec3 line = controlPointWE (remap.outputControlPoint1) - first;
  2630. const float length = glm::length (line);
  2631. if (length > 1.1920929e-7f) {
  2632. line /= length;
  2633. }
  2634. const glm::vec3 relative = flipY (p.position) - first;
  2635. const float along = glm::dot (relative, line);
  2636. const glm::vec3 offset = relative - along * line;
  2637. const float fraction = apply (length > 1.1920929e-7f ? along / length : along, value.x);
  2638. p.position = flipY ((first + offset) + (line * fraction) * length);
  2639. break;
  2640. }
  2641. case ParticleRemapValue::Color:
  2642. p.initial.color = applyVector (p.initial.color, value);
  2643. p.color = p.initial.color;
  2644. break;
  2645. case ParticleRemapValue::Position: p.position = flipY (applyVector (flipY (p.position), value)); break;
  2646. case ParticleRemapValue::Velocity: p.velocity = flipY (applyVector (flipY (p.velocity), value)); break;
  2647. case ParticleRemapValue::ControlPoint: {
  2648. auto& point = m_controlPoints[remap.outputControlPoint0];
  2649. point.position = flipY (applyVector (flipY (point.position), value));
  2650. break;
  2651. }
  2652. case ParticleRemapValue::DeltaToControlPoint: {
  2653. const glm::vec3 point = controlPointWE (remap.outputControlPoint0);
  2654. p.position = flipY (point - applyVector (point - flipY (p.position), value));
  2655. break;
  2656. }
  2657. case ParticleRemapValue::DirectionToControlPoint: {
  2658. const glm::vec3 point = controlPointWE (remap.outputControlPoint0);
  2659. glm::vec3 direction = point - flipY (p.position);
  2660. const float distance = glm::length (direction);
  2661. if (distance != 0.0f) {
  2662. direction /= distance;
  2663. }
  2664. direction = applyVector (direction, value);
  2665. const float length = glm::length (direction);
  2666. p.position = flipY (point - (length != 0.0f ? direction / length : glm::vec3 (0.0f)) * distance);
  2667. break;
  2668. }
  2669. default: break;
  2670. }
  2671. };
  2672. }
  2673. glm::vec3 CParticle::remapOperatorInput (const ParticleRemap& remap, const ParticleInstance& p) const {
  2674. // sub_14023FBC0 case 19, scalars in x
  2675. switch (remap.input) {
  2676. case ParticleRemapValue::LifetimeFraction: return glm::vec3 (p.age / p.lifetime, 0.0f, 0.0f);
  2677. case ParticleRemapValue::MaxLifetime: return glm::vec3 (p.lifetime, 0.0f, 0.0f);
  2678. case ParticleRemapValue::Size: return glm::vec3 (p.size, 0.0f, 0.0f);
  2679. case ParticleRemapValue::Opacity: return glm::vec3 (p.alpha, 0.0f, 0.0f);
  2680. case ParticleRemapValue::Speed: return glm::vec3 (glm::length (p.velocity), 0.0f, 0.0f);
  2681. case ParticleRemapValue::Rotation: return glm::vec3 (p.rotation.z, 0.0f, 0.0f);
  2682. case ParticleRemapValue::AngularSpeed:
  2683. return glm::vec3 (m_hasAngularVelocity ? p.angularVelocity.z : 0.0f, 0.0f, 0.0f);
  2684. case ParticleRemapValue::DistanceToControlPoint:
  2685. return glm::vec3 (glm::length (flipY (p.position) - controlPointWE (remap.inputControlPoint0)), 0.0f, 0.0f);
  2686. case ParticleRemapValue::PositionBetweenTwoControlPoints: {
  2687. // the engine reads the output control points here, not the input ones
  2688. const glm::vec3 first = controlPointWE (remap.outputControlPoint0);
  2689. const glm::vec3 line = controlPointWE (remap.outputControlPoint1) - first;
  2690. const float lengthSquared = glm::dot (line, line);
  2691. return glm::vec3 (
  2692. lengthSquared > 0.0f ? glm::dot (flipY (p.position) - first, line) / lengthSquared : 0.0f, 0.0f, 0.0f
  2693. );
  2694. }
  2695. // runtime and particlesystemtime both read the renderer's scene clock here (+304)
  2696. case ParticleRemapValue::Runtime:
  2697. case ParticleRemapValue::ParticleSystemTime: return glm::vec3 (getScene ().getSceneClock (), 0.0f, 0.0f);
  2698. case ParticleRemapValue::TimeOfDay: return glm::vec3 (dayFraction (), 0.0f, 0.0f);
  2699. case ParticleRemapValue::LayerTime: return glm::vec3 (layerTime (), 0.0f, 0.0f);
  2700. case ParticleRemapValue::Color: return p.color;
  2701. case ParticleRemapValue::Position: return flipY (p.position);
  2702. case ParticleRemapValue::Velocity: return flipY (p.velocity);
  2703. case ParticleRemapValue::ControlPoint: return controlPointWE (remap.inputControlPoint0);
  2704. case ParticleRemapValue::DeltaToControlPoint:
  2705. return controlPointWE (remap.inputControlPoint0) - flipY (p.position);
  2706. case ParticleRemapValue::DirectionToControlPoint: {
  2707. const glm::vec3 delta = controlPointWE (remap.inputControlPoint0) - flipY (p.position);
  2708. const float length = glm::length (delta);
  2709. return length > 0.0f ? delta / length : glm::vec3 (0.0f);
  2710. }
  2711. case ParticleRemapValue::LayerOrigin: return remapLayerOrigin ();
  2712. default: return glm::vec3 (0.0f);
  2713. }
  2714. }
  2715. OperatorFunc CParticle::createRemapValueOperator (const RemapValueOperator& op) {
  2716. const ParticleRemap remap = op.remap;
  2717. const BlendWindow blend = makeBlendWindow (op.blend);
  2718. const glm::vec3 inputRange = remapRange (remap.inputRangeMin, remap.inputRangeMax);
  2719. const glm::vec3 outputRange = remap.outputRangeMax - remap.outputRangeMin;
  2720. const float fbmAmplitude = hashedNoiseFbmNormalizer (remap.transformOctaves);
  2721. // sub_14023FBC0 restores size every frame, and alpha or color when a remap writes them (system flags 0x10/8)
  2722. m_resetSizeFromBase = true;
  2723. if (remap.output == ParticleRemapValue::Opacity) {
  2724. m_resetAlphaFromBase = true;
  2725. } else if (remap.output == ParticleRemapValue::Color) {
  2726. m_resetColorFromBase = true;
  2727. }
  2728. if (remap.transform == ParticleRemapTransform::SimplexNoise || remap.transform == ParticleRemapTransform::FbmNoise) {
  2729. m_usesParticleSeed = true;
  2730. }
  2731. // sub_14023FBC0 case 19 and its blended variant 39, which moves every value only part of the way
  2732. return [this, remap, blend, inputRange, outputRange, fbmAmplitude] (
  2733. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  2734. float
  2735. ) {
  2736. if (remap.input == ParticleRemapValue::Unknown || remap.output == ParticleRemapValue::Unknown) {
  2737. return;
  2738. }
  2739. const bool vectorInput = remapIsVector (remap.input);
  2740. const bool vectorOutput = remapIsVector (remap.output);
  2741. for (uint32_t i = 0; i < count; i++) {
  2742. auto& p = particles[i];
  2743. const glm::vec3 raw = remapOperatorInput (remap, p);
  2744. glm::vec3 value;
  2745. if (vectorInput) {
  2746. value = (remapSelectComponent (raw, remap.inputComponent) - remap.inputRangeMin) / inputRange;
  2747. } else {
  2748. value = glm::vec3 ((raw.x - remap.inputRangeMin.x) / inputRange.x);
  2749. }
  2750. if ((remap.flags & 1) != 0) {
  2751. value = glm::clamp (value, 0.0f, 1.0f);
  2752. }
  2753. // the y and z noise use the particle's random with a few bits flipped
  2754. const int32_t seed = particleSeedBits (p);
  2755. const int32_t seeds[3] = { seed, seed ^ 0x0B3924AD, seed ^ 0x493A8E83 };
  2756. const int transformed = vectorOutput ? 3 : 1;
  2757. for (int c = 0; c < transformed; c++) {
  2758. value[c] = remapTransformOperator (
  2759. remap.transform, value[c], remap.transformInputScale, remap.transformOctaves, fbmAmplitude, seeds[c]
  2760. );
  2761. value[c] = outputRange[c] * value[c] + remap.outputRangeMin[c];
  2762. if ((remap.flags & 2) != 0) {
  2763. value[c] = std::clamp (value[c], 0.0f, 1.0f);
  2764. }
  2765. }
  2766. const float weight = blend.active ? blendWeight (blend, p) : 1.0f;
  2767. const auto apply = [&remap, &blend, weight] (float current, float target) {
  2768. const float result = remapApply (remap.operation, current, target);
  2769. return blend.active ? (result - current) * weight + current : result;
  2770. };
  2771. // blended "remap" on all components gives every component the x value, like wallpaper64.exe's variant 39
  2772. const auto applyVector = [&remap, &blend, &apply] (glm::vec3 current, const glm::vec3& target, bool setQuirk) {
  2773. switch (remap.outputComponent) {
  2774. case ParticleRemapComponent::All: {
  2775. const bool useX
  2776. = setQuirk && blend.active && remap.operation == ParticleRemapOperation::Remap;
  2777. for (int c = 0; c < 3; c++) {
  2778. current[c] = apply (current[c], useX ? target.x : target[c]);
  2779. }
  2780. break;
  2781. }
  2782. case ParticleRemapComponent::X: current.x = apply (current.x, target.x); break;
  2783. case ParticleRemapComponent::Y: current.y = apply (current.y, target.y); break;
  2784. case ParticleRemapComponent::Z: current.z = apply (current.z, target.z); break;
  2785. default: break;
  2786. }
  2787. return current;
  2788. };
  2789. switch (remap.output) {
  2790. case ParticleRemapValue::MaxLifetime: p.lifetime = apply (p.lifetime, value.x); break;
  2791. case ParticleRemapValue::Size: p.size = apply (p.size, value.x); break;
  2792. case ParticleRemapValue::Opacity: p.alpha = apply (p.alpha, value.x); break;
  2793. case ParticleRemapValue::Speed: {
  2794. const float speed = glm::length (p.velocity);
  2795. const float target = apply (speed, value.x);
  2796. p.velocity *= speed > 0.0f ? target / speed : target;
  2797. break;
  2798. }
  2799. case ParticleRemapValue::Rotation: p.rotation.z = apply (p.rotation.z, value.x); break;
  2800. case ParticleRemapValue::AngularSpeed:
  2801. if (m_hasAngularVelocity) {
  2802. p.angularVelocity.z = apply (p.angularVelocity.z, value.x);
  2803. }
  2804. break;
  2805. case ParticleRemapValue::DistanceToControlPoint: {
  2806. const glm::vec3 point = controlPointWE (remap.outputControlPoint0);
  2807. const glm::vec3 offset = flipY (p.position) - point;
  2808. const float distance = glm::length (offset);
  2809. const glm::vec3 direction = distance != 0.0f ? offset / distance : glm::vec3 (0.0f);
  2810. p.position = flipY (point + direction * apply (distance, value.x));
  2811. break;
  2812. }
  2813. case ParticleRemapValue::PositionBetweenTwoControlPoints: {
  2814. const glm::vec3 first = controlPointWE (remap.outputControlPoint0);
  2815. const glm::vec3 line = controlPointWE (remap.outputControlPoint1) - first;
  2816. const float length = glm::length (line);
  2817. const glm::vec3 direction = length != 0.0f ? line / length : glm::vec3 (0.0f);
  2818. const glm::vec3 relative = flipY (p.position) - first;
  2819. const float along = glm::dot (relative, direction);
  2820. const glm::vec3 offset = relative - along * direction;
  2821. const float fraction = apply (length != 0.0f ? along / length : 0.0f, value.x);
  2822. p.position = flipY ((fraction * length) * direction + offset + first);
  2823. break;
  2824. }
  2825. case ParticleRemapValue::Color: p.color = applyVector (p.color, value, true); break;
  2826. case ParticleRemapValue::Position: p.position = flipY (applyVector (flipY (p.position), value, true)); break;
  2827. case ParticleRemapValue::Velocity: p.velocity = flipY (applyVector (flipY (p.velocity), value, true)); break;
  2828. case ParticleRemapValue::ControlPoint: {
  2829. // written per particle, the last one wins
  2830. auto& point = m_controlPoints[remap.outputControlPoint0];
  2831. point.position = flipY (applyVector (flipY (point.position), value, false));
  2832. break;
  2833. }
  2834. case ParticleRemapValue::DeltaToControlPoint: {
  2835. const glm::vec3 point = controlPointWE (remap.outputControlPoint0);
  2836. p.position = flipY (point - applyVector (point - flipY (p.position), value, false));
  2837. break;
  2838. }
  2839. case ParticleRemapValue::DirectionToControlPoint: {
  2840. const glm::vec3 point = controlPointWE (remap.outputControlPoint0);
  2841. const glm::vec3 delta = point - flipY (p.position);
  2842. const float distance = glm::length (delta);
  2843. const glm::vec3 direction
  2844. = applyVector (distance > 0.0f ? delta / distance : glm::vec3 (0.0f), value, false);
  2845. const float length = glm::length (direction);
  2846. p.position = flipY (point - (length > 0.0f ? direction / length : glm::vec3 (0.0f)) * distance);
  2847. break;
  2848. }
  2849. default: break;
  2850. }
  2851. }
  2852. };
  2853. }
  2854. OperatorFunc CParticle::createCapVelocityOperator (const CapVelocityOperator& op) {
  2855. DynamicValue* maxSpeedValue = op.maxSpeed ? op.maxSpeed->value.get () : nullptr;
  2856. const BlendWindow blend = makeBlendWindow (op.blend);
  2857. // sub_14023FBC0 case 18 and its blended variant 38, maxspeed defaults from sub_1401BFAB0
  2858. return [this, maxSpeedValue, blend] (
  2859. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  2860. float
  2861. ) {
  2862. const float maxSpeed = maxSpeedValue != nullptr ? maxSpeedValue->getFloat ()
  2863. : (getScene ().getCamera ().isPerspective () ? 1.0f : 100.0f);
  2864. for (uint32_t i = 0; i < count; i++) {
  2865. auto& p = particles[i];
  2866. const float speed = glm::length (p.velocity);
  2867. if (speed == 0.0f) {
  2868. continue;
  2869. }
  2870. const float ratio = maxSpeed / speed;
  2871. p.velocity *= blend.active ? std::min (0.0f, ratio - 1.0f) * blendWeight (blend, p) + 1.0f
  2872. : std::min (1.0f, ratio);
  2873. }
  2874. };
  2875. }
  2876. OperatorFunc CParticle::createBoidsOperator (const BoidsOperator& op) {
  2877. DynamicValue* separationThresholdValue = op.separationThreshold ? op.separationThreshold->value.get () : nullptr;
  2878. DynamicValue* neighborThresholdValue = op.neighborThreshold ? op.neighborThreshold->value.get () : nullptr;
  2879. DynamicValue* maxSpeedValue = op.maxSpeed ? op.maxSpeed->value.get () : nullptr;
  2880. DynamicValue* separationFactorValue = op.separationFactor->value.get ();
  2881. DynamicValue* alignmentFactorValue = op.alignmentFactor->value.get ();
  2882. DynamicValue* cohesionFactorValue = op.cohesionFactor->value.get ();
  2883. const uint32_t flags = op.flags;
  2884. // sub_14023FBC0 case 17, defaults from sub_1401BF700. The engine walks its pool four slots at a time and only
  2885. // every stride-th block of them (and of their neighbors) per frame, rotating with the frame count, the forces
  2886. // grow by the stride to make up for it
  2887. return [this, separationThresholdValue, neighborThresholdValue, maxSpeedValue, separationFactorValue,
  2888. alignmentFactorValue, cohesionFactorValue, flags] (
  2889. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
  2890. float dt
  2891. ) {
  2892. const bool flat = !getScene ().getCamera ().isPerspective ();
  2893. const float separationThreshold
  2894. = separationThresholdValue != nullptr ? separationThresholdValue->getFloat () : (flat ? 20.0f : 0.02f);
  2895. const float neighborThreshold
  2896. = neighborThresholdValue != nullptr ? neighborThresholdValue->getFloat () : (flat ? 50.0f : 0.2f);
  2897. const float maxSpeed = maxSpeedValue != nullptr ? maxSpeedValue->getFloat () : (flat ? 500.0f : 1.0f);
  2898. // every pool slot below the high water mark takes part, a dead one with its last state (m_ghosts)
  2899. const uint32_t blocks = (m_slotExtent + 3) / 4;
  2900. std::vector<ParticleInstance*> slots (static_cast<size_t> (blocks) * 4, nullptr);
  2901. std::vector<uint8_t> alive (slots.size (), 0);
  2902. for (uint32_t i = 0; i < count; i++) {
  2903. if (particles[i].slot < slots.size ()) {
  2904. slots[particles[i].slot] = &particles[i];
  2905. alive[particles[i].slot] = 1;
  2906. }
  2907. }
  2908. for (uint32_t slot = 0; slot < slots.size () && slot < m_ghostUsed.size (); slot++) {
  2909. if (slots[slot] == nullptr && m_ghostUsed[slot]) {
  2910. slots[slot] = &m_ghosts[slot];
  2911. }
  2912. }
  2913. const uint32_t stride = m_slotExtent / 200 + 1;
  2914. const float scaledDt = frameScaledDelta (dt);
  2915. const float separationFactor = static_cast<float> (stride) * separationFactorValue->getFloat () * scaledDt;
  2916. const float alignmentFactor = static_cast<float> (stride) * alignmentFactorValue->getFloat () * scaledDt;
  2917. const float cohesionFactor = static_cast<float> (stride) * cohesionFactorValue->getFloat () * scaledDt;
  2918. // the lane a neighbor block is compared on in each of the four passes (_mm_shuffle_ps 0, 147, 78, 57)
  2919. static constexpr int lanes[4][4] = { { 0, 1, 2, 3 }, { 3, 0, 1, 2 }, { 2, 3, 0, 1 }, { 1, 2, 3, 0 } };
  2920. for (uint32_t block = m_frameCounter % stride; block < blocks; block += stride) {
  2921. glm::vec3 results[4];
  2922. for (int lane = 0; lane < 4; lane++) {
  2923. const ParticleInstance* self = slots[block * 4 + lane];
  2924. if (self == nullptr) {
  2925. continue;
  2926. }
  2927. float separationCount = 0.0f;
  2928. float neighborCount = 0.0f;
  2929. glm::vec3 separation (0.0f);
  2930. glm::vec3 velocitySum (0.0f);
  2931. glm::vec3 positionSum (0.0f);
  2932. for (uint32_t other = (block * 4 + m_frameCounter) % stride; other < blocks; other += stride) {
  2933. // WE's alive mask (lifetime != 0) is taken from the neighbor block unshuffled, so it belongs to this
  2934. // lane's slot there, not to the shuffled neighbor it gets applied to
  2935. if (!alive[other * 4 + lane]) {
  2936. continue;
  2937. }
  2938. for (const auto& pass : lanes) {
  2939. const ParticleInstance* neighbor = slots[other * 4 + pass[lane]];
  2940. if (neighbor == nullptr) {
  2941. continue;
  2942. }
  2943. const glm::vec3 delta = self->position - neighbor->position;
  2944. const float distanceSquared = glm::dot (delta, delta);
  2945. const float distance = std::sqrt (distanceSquared);
  2946. if (distanceSquared != 0.0f && distance < separationThreshold) {
  2947. separationCount += 1.0f;
  2948. separation += (separationThreshold / distance - 1.0f) * delta;
  2949. }
  2950. if (distance < neighborThreshold) {
  2951. neighborCount += 1.0f;
  2952. velocitySum += neighbor->velocity;
  2953. positionSum += neighbor->position;
  2954. }
  2955. }
  2956. }
  2957. const float separationWeight = separationCount != 0.0f ? separationFactor / separationCount : 0.0f;
  2958. const float average = neighborCount != 0.0f ? 1.0f / neighborCount : 0.0f;
  2959. const float alignment = neighborCount != 0.0f ? alignmentFactor : 0.0f;
  2960. const float cohesion = neighborCount != 0.0f ? cohesionFactor : 0.0f;
  2961. const glm::vec3 change
  2962. = ((average * velocitySum - self->velocity) * alignment + separationWeight * separation)
  2963. + (average * positionSum - self->position) * cohesion;
  2964. glm::vec3 velocity = self->velocity + change;
  2965. if ((flags & 1) != 0) {
  2966. const float speedSquared = glm::dot (velocity, velocity);
  2967. if (std::max (glm::dot (self->velocity, self->velocity), maxSpeed * maxSpeed) < speedSquared) {
  2968. velocity *= maxSpeed / std::sqrt (speedSquared);
  2969. }
  2970. }
  2971. results[lane] = velocity;
  2972. }
  2973. // the four lanes are written together, after all of them were computed
  2974. for (int lane = 0; lane < 4; lane++) {
  2975. if (ParticleInstance* target = slots[block * 4 + lane]) {
  2976. target->velocity = results[lane];
  2977. }
  2978. }
  2979. }
  2980. };
  2981. }
  2982. OperatorFunc CParticle::createMaintainDistanceToControlPointOperator (const MaintainDistanceToControlPointOperator& op) {
  2983. const int controlPoint = op.controlPoint;
  2984. DynamicValue* distanceValue = op.distance ? op.distance->value.get () : nullptr;
  2985. DynamicValue* strengthValue = op.variableStrength->value.get ();
  2986. const BlendWindow blend = makeBlendWindow (op.blend);
  2987. // sub_14023FBC0 case 11 and its blended variant 33, distance defaults from sub_1401BE2A0
  2988. return [this, controlPoint, distanceValue, strengthValue, blend] (
  2989. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>& points,
  2990. float, float dt
  2991. ) {
  2992. const auto& point = points[controlPoint];
  2993. const float distance = distanceValue != nullptr ? distanceValue->getFloat ()
  2994. : (getScene ().getCamera ().isPerspective () ? 1.0f : 200.0f);
  2995. const float variableStrength = strengthValue->getFloat ();
  2996. const float strength = variableStrength == 0.0f ? 1.0f : std::clamp (variableStrength * dt, 0.0f, 1.0f);
  2997. // the distance is measured in the control point's own frame
  2998. const glm::mat3 toPoint = glm::inverse (point.orientation);
  2999. for (uint32_t i = 0; i < count; i++) {
  3000. auto& p = particles[i];
  3001. // particles move along with the control point, then get pulled onto the sphere around it
  3002. const glm::vec3 moved = p.position + point.movement;
  3003. const glm::vec3 offset = moved - point.position;
  3004. const float length = glm::length (toPoint * offset);
  3005. if (length == 0.0f) {
  3006. p.position = moved;
  3007. continue;
  3008. }
  3009. float pull = (distance / length - 1.0f) * strength;
  3010. if (blend.active) {
  3011. pull *= blendWeight (blend, p);
  3012. }
  3013. p.position = pull * offset + moved;
  3014. }
  3015. };
  3016. }
  3017. OperatorFunc
  3018. CParticle::createMaintainDistanceBetweenControlPointsOperator (const MaintainDistanceBetweenControlPointsOperator& op) {
  3019. const int start = op.controlPointStart;
  3020. const int end = op.controlPointEnd;
  3021. const BlendWindow blend = makeBlendWindow (op.blend);
  3022. // sub_14023FBC0 case 12 and its blended variant 34: whatever lay along the line between the two control points
  3023. // last frame is moved to the same share of the line now
  3024. return [start, end, blend] (
  3025. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>& points,
  3026. float, float
  3027. ) {
  3028. const glm::vec3 first = points[start].position;
  3029. const glm::vec3 previousFirst = first - points[start].movement;
  3030. const glm::vec3 previousLast = points[end].position - points[end].movement;
  3031. const glm::vec3 line = points[end].position - first;
  3032. const glm::vec3 previousLine = previousLast - previousFirst;
  3033. const float lengthSquared = glm::dot (line, line);
  3034. const float previousLengthSquared = glm::dot (previousLine, previousLine);
  3035. if (lengthSquared <= 1.4210855e-14f || previousLengthSquared <= 1.4210855e-14f) {
  3036. return;
  3037. }
  3038. const float length = std::sqrt (lengthSquared);
  3039. const float previousLength = std::sqrt (previousLengthSquared);
  3040. const glm::vec3 direction = line / length;
  3041. const glm::vec3 previousDirection = previousLine / previousLength;
  3042. const glm::vec3 shift = first - previousFirst;
  3043. for (uint32_t i = 0; i < count; i++) {
  3044. auto& p = particles[i];
  3045. const float along = glm::dot (p.position - previousFirst, previousDirection);
  3046. const float scaled = std::clamp (along / previousLength, 0.0f, 1.0f) * length;
  3047. glm::vec3 change = (scaled * direction - along * previousDirection) + shift;
  3048. if (blend.active) {
  3049. change *= blendWeight (blend, p);
  3050. }
  3051. p.position += change;
  3052. }
  3053. };
  3054. }
  3055. OperatorFunc CParticle::createReduceMovementNearControlPointOperator (const ReduceMovementNearControlPointOperator& op) {
  3056. const int controlPoint = op.controlPoint;
  3057. DynamicValue* innerValue = op.distanceInner ? op.distanceInner->value.get () : nullptr;
  3058. DynamicValue* outerValue = op.distanceOuter ? op.distanceOuter->value.get () : nullptr;
  3059. DynamicValue* reductionInnerValue = op.reductionInner->value.get ();
  3060. DynamicValue* reductionOuterValue = op.reductionOuter->value.get ();
  3061. const BlendWindow blend = makeBlendWindow (op.blend);
  3062. // sub_14023FBC0 case 13 and its blended variant 35, distance defaults from sub_1401BE810
  3063. return [this, controlPoint, innerValue, outerValue, reductionInnerValue, reductionOuterValue, blend] (
  3064. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>& points,
  3065. float, float dt
  3066. ) {
  3067. const bool flat = !getScene ().getCamera ().isPerspective ();
  3068. const float inner = innerValue != nullptr ? innerValue->getFloat () : (flat ? 100.0f : 0.5f);
  3069. const float outer = outerValue != nullptr ? outerValue->getFloat () : (flat ? 350.0f : 1.0f);
  3070. const float reductionInner = reductionInnerValue->getFloat ();
  3071. const float reductionOuter = reductionOuterValue->getFloat ();
  3072. const float distanceScale = inner == outer ? 1.0f : 1.0f / (outer - inner);
  3073. const float reductionRange = reductionInner == reductionOuter ? 1.0f : reductionOuter - reductionInner;
  3074. const glm::vec3 center = points[controlPoint].position;
  3075. for (uint32_t i = 0; i < count; i++) {
  3076. auto& p = particles[i];
  3077. const float distance = glm::length (p.position - center);
  3078. const float share = std::clamp ((distance - inner) * distanceScale, 0.0f, 1.0f);
  3079. float reduction = std::clamp ((share * reductionRange + reductionInner) * dt, 0.0f, 1.0f);
  3080. if (blend.active) {
  3081. reduction *= blendWeight (blend, p);
  3082. }
  3083. p.velocity *= 1.0f - reduction;
  3084. }
  3085. };
  3086. }
  3087. OperatorFunc CParticle::createCollisionOperator (const CollisionOperator& op) {
  3088. if (op.shape == ParticleCollisionShape::Model) {
  3089. sLog.error ("Particle operator collisionmodel is not supported, it is ignored");
  3090. return nullptr;
  3091. }
  3092. // sub_14023FBC0 case 23 does nothing, collisionbox only exists in the file format
  3093. if (op.shape == ParticleCollisionShape::Box) {
  3094. return nullptr;
  3095. }
  3096. if (op.shape == ParticleCollisionShape::Quad) {
  3097. m_tracksPreviousPosition = true;
  3098. }
  3099. const ParticleCollisionShape shape = op.shape;
  3100. const ParticleCollisionBehavior behavior = op.behavior;
  3101. const uint32_t flags = op.flags;
  3102. const int controlPoint = op.controlPoint;
  3103. DynamicValue* bounceValue = op.bounceFactor->value.get ();
  3104. DynamicValue* planeValue = op.plane->value.get ();
  3105. DynamicValue* distanceValue = op.distance ? op.distance->value.get () : nullptr;
  3106. DynamicValue* originValue = op.origin ? op.origin->value.get () : nullptr;
  3107. DynamicValue* radiusValue = op.radius ? op.radius->value.get () : nullptr;
  3108. DynamicValue* forwardValue = op.forward->value.get ();
  3109. DynamicValue* sizeValue = op.size ? op.size->value.get () : nullptr;
  3110. // sub_14023FBC0 cases 21, 22, 24 and 25 with the per behavior workers (sub_14024F5E0 and siblings), defaults
  3111. // from sub_1401C00A0, sub_1401C0540, sub_1401C0740 and sub_1401C0870
  3112. return [this, shape, behavior, flags, controlPoint, bounceValue, planeValue, distanceValue, originValue,
  3113. radiusValue, forwardValue, sizeValue] (
  3114. std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>& points,
  3115. float, float
  3116. ) {
  3117. const bool flat = !getScene ().getCamera ().isPerspective ();
  3118. const float bounce = -1.0f - bounceValue->getFloat ();
  3119. const bool followPoint = (flags & 1) != 0;
  3120. const auto& point = points[controlPoint];
  3121. // pushes the particle back by depth along the normal, then the velocity rule, flag 2 also stops the spin
  3122. const auto collide = [behavior, bounce, flags] (ParticleInstance& p, const glm::vec3& normal, float depth) {
  3123. if (behavior == ParticleCollisionBehavior::Delete) {
  3124. p.age = p.lifetime;
  3125. } else {
  3126. p.position -= depth * normal;
  3127. const float along = glm::dot (p.velocity, normal);
  3128. switch (behavior) {
  3129. case ParticleCollisionBehavior::Bounce: p.velocity += (along * bounce) * normal; break;
  3130. case ParticleCollisionBehavior::Slide: p.velocity -= along * normal; break;
  3131. default: p.velocity = glm::vec3 (0.0f); break;
  3132. }
  3133. }
  3134. if ((flags & 2) != 0) {
  3135. p.angularVelocity = glm::vec3 (0.0f);
  3136. }
  3137. };
  3138. switch (shape) {
  3139. case ParticleCollisionShape::Plane: {
  3140. glm::vec3 normal = flipY (glm::normalize (planeValue->getVec3 ()));
  3141. float distance = distanceValue != nullptr ? distanceValue->getFloat () : (flat ? -150.0f : 0.0f);
  3142. if (followPoint) {
  3143. normal = point.orientation * normal;
  3144. distance = glm::dot (normal, point.position);
  3145. }
  3146. for (uint32_t i = 0; i < count; i++) {
  3147. const float along = glm::dot (particles[i].position, normal);
  3148. if (along < distance) {
  3149. collide (particles[i], normal, along - distance);
  3150. }
  3151. }
  3152. break;
  3153. }
  3154. case ParticleCollisionShape::Sphere: {
  3155. glm::vec3 center = originValue != nullptr ? flipY (originValue->getVec3 ())
  3156. : (flat ? glm::vec3 (0.0f, 200.0f, 0.0f) : glm::vec3 (0.0f));
  3157. const float radius = radiusValue != nullptr ? radiusValue->getFloat () : (flat ? 50.0f : 1.0f);
  3158. if (followPoint) {
  3159. center = point.position;
  3160. }
  3161. for (uint32_t i = 0; i < count; i++) {
  3162. const glm::vec3 offset = particles[i].position - center;
  3163. const float distanceSquared = glm::dot (offset, offset);
  3164. // at the very center the engine's normal would be NaN
  3165. if (distanceSquared < radius * radius && distanceSquared > 0.0f) {
  3166. const float distance = std::sqrt (distanceSquared);
  3167. collide (particles[i], offset / distance, distance - radius);
  3168. }
  3169. }
  3170. break;
  3171. }
  3172. case ParticleCollisionShape::Quad: {
  3173. glm::vec3 origin = originValue != nullptr ? flipY (originValue->getVec3 ())
  3174. : (flat ? glm::vec3 (0.0f, 150.0f, 0.0f) : glm::vec3 (0.0f));
  3175. const glm::vec2 halfSize
  3176. = (sizeValue != nullptr ? sizeValue->getVec2 () : (flat ? glm::vec2 (200.0f) : glm::vec2 (1.0f)))
  3177. * 0.5f;
  3178. glm::vec3 normal = glm::normalize (flipY (planeValue->getVec3 ()));
  3179. const glm::vec3 forward = glm::normalize (flipY (forwardValue->getVec3 ()));
  3180. glm::vec3 right = glm::normalize (glm::cross (normal, forward));
  3181. glm::vec3 up = glm::normalize (glm::cross (right, normal));
  3182. if (followPoint) {
  3183. origin = point.position;
  3184. normal = point.orientation * normal;
  3185. up = point.orientation * up;
  3186. right = point.orientation * right;
  3187. }
  3188. // only particles crossing it from the front during this frame hit it
  3189. for (uint32_t i = 0; i < count; i++) {
  3190. auto& p = particles[i];
  3191. const glm::vec3 offset = p.position - origin;
  3192. const float along = glm::dot (offset, normal);
  3193. if (glm::dot (p.previousPosition - origin, normal) > 0.0f && along <= 0.0f
  3194. && std::fabs (glm::dot (offset, up)) < halfSize.y
  3195. && std::fabs (glm::dot (offset, right)) < halfSize.x) {
  3196. collide (p, normal, along * 1.05f);
  3197. }
  3198. }
  3199. break;
  3200. }
  3201. case ParticleCollisionShape::Bounds: {
  3202. // the scene's own orthographic size, 0 for automatic and perspective ones, as a box from (0, 0) up in
  3203. // WE's scene space, turned into this scene's space and then the system's
  3204. const auto& projection = getScene ().getScene ().camera.projection;
  3205. const float width = static_cast<float> (projection.width);
  3206. const float height = static_cast<float> (projection.height);
  3207. glm::vec3 low (0.0f);
  3208. glm::vec3 high (width, -height, 0.0f);
  3209. if (flat) {
  3210. const float sceneWidth = static_cast<float> (getScene ().getWidth ());
  3211. const float sceneHeight = static_cast<float> (getScene ().getHeight ());
  3212. low = glm::vec3 (-sceneWidth / 2.0f, sceneHeight / 2.0f, 0.0f);
  3213. high = glm::vec3 (width - sceneWidth / 2.0f, sceneHeight / 2.0f - height, 0.0f);
  3214. }
  3215. glm::vec3 normals[4] = { { 1, 0, 0 }, { 0, -1, 0 }, { -1, 0, 0 }, { 0, 1, 0 } };
  3216. if (!m_worldSpace) {
  3217. const glm::mat4 toLocal = glm::inverse (m_frame);
  3218. for (auto& normal : normals) {
  3219. normal = glm::mat3 (toLocal) * normal;
  3220. }
  3221. low = glm::vec3 (toLocal * glm::vec4 (low, 1.0f));
  3222. high = glm::vec3 (toLocal * glm::vec4 (high, 1.0f));
  3223. }
  3224. const float distances[4] = { glm::dot (low, normals[0]), glm::dot (low, normals[1]),
  3225. glm::dot (high, normals[2]), glm::dot (high, normals[3]) };
  3226. // the last side the particle is outside of is the one it hits
  3227. for (uint32_t i = 0; i < count; i++) {
  3228. auto& p = particles[i];
  3229. int side = -1;
  3230. for (int s = 0; s < 4; s++) {
  3231. if (glm::dot (p.position, normals[s]) < distances[s]) {
  3232. side = s;
  3233. }
  3234. }
  3235. if (side >= 0) {
  3236. collide (p, normals[side], glm::dot (p.position, normals[side]) - distances[side]);
  3237. }
  3238. }
  3239. break;
  3240. }
  3241. default: break;
  3242. }
  3243. };
  3244. }
  3245. // ========== RENDERING ==========
  3246. void CParticle::setupPass () {
  3247. if (!m_particle.material || !m_particle.material->material || m_particle.material->material->passes.empty ()) {
  3248. sLog.error ("No valid material for particle ", m_particle.name);
  3249. return;
  3250. }
  3251. const auto& firstPass = **m_particle.material->material->passes.begin ();
  3252. m_passOverride = std::make_unique<ImageEffectPassOverride> ();
  3253. m_passOverride->combos["THICKFORMAT"] = 1;
  3254. if (m_useRopeRenderer) {
  3255. m_passOverride->shaderOverride = "genericropeparticle";
  3256. }
  3257. if (m_spritesheetFrames > 0) {
  3258. m_passOverride->combos["SPRITESHEET"] = 1;
  3259. }
  3260. if (m_useTrailRenderer) {
  3261. m_passOverride->combos["TRAILRENDERER"] = 1;
  3262. }
  3263. if (m_useRopeRenderer && m_useTrailRenderer) {
  3264. // sub_1401D2340 case 4. WE leaves THICKFORMAT off here and packs a single size and color per point, the
  3265. // THICKFORMAT layout below repeats them as the end values, which the shader treats the same way
  3266. m_passOverride->combos["TRAILSUBDIVISION"] = m_ropeSubdivision;
  3267. if (m_ropeUVScrolling) {
  3268. m_passOverride->combos["TRAILSCROLLALPHA"] = 1;
  3269. }
  3270. if (m_trailFadeAlpha) {
  3271. m_passOverride->combos["TRAILFADEALPHA"] = 1;
  3272. }
  3273. if (m_trailFadeSize) {
  3274. m_passOverride->combos["TRAILFADESIZE"] = 1;
  3275. }
  3276. }
  3277. // Force texture 0 to use the input (particle texture) rather than the shader's
  3278. // default "util/white" annotation, which would override it in setupRenderTexture()
  3279. m_passBinds = { { 0, "previous" } };
  3280. auto refractIt = firstPass.combos.find ("REFRACT");
  3281. m_hasRefract = refractIt != firstPass.combos.end () && refractIt->second != 0;
  3282. m_passFBOProvider = std::make_shared<FBOProvider> (this);
  3283. // REFRACT: create a copy FBO shadowing _rt_FullFrameBuffer. The shader reads g_Texture3
  3284. // (= _rt_FullFrameBuffer) while we render TO the scene FBO; reading and writing the same FBO
  3285. // is undefined behavior in OpenGL and causes black reads on NVIDIA. Placing a copy FBO under
  3286. // the same name in our FBOProvider makes CPass resolve g_Texture3 to the copy instead - we
  3287. // blit the scene content into it before each render.
  3288. if (m_hasRefract) {
  3289. auto sceneFBO = getScene ().getFBO ();
  3290. float w = static_cast<float> (sceneFBO->getRealWidth ());
  3291. float h = static_cast<float> (sceneFBO->getRealHeight ());
  3292. m_refractFBO = m_passFBOProvider->create (
  3293. "_rt_FullFrameBuffer", TextureFormat_ARGB8888, TextureFlags_ClampUVs, 1.0f, { w, h }, { w, h }
  3294. );
  3295. }
  3296. m_pass = new Effects::CPass (*this, m_passFBOProvider, firstPass, *m_passOverride, m_passBinds, std::nullopt);
  3297. m_pass->setDestination (getScene ().getFBO ());
  3298. m_pass->setInput (getTexture ());
  3299. // Set matrix pointers - CPass will dereference these each frame
  3300. m_pass->setModelViewProjectionMatrix (&m_mvpMatrix);
  3301. m_pass->setModelViewProjectionMatrixInverse (&m_mvpMatrixInverse);
  3302. m_pass->setModelMatrix (&m_modelMatrix);
  3303. m_pass->setViewProjectionMatrix (&m_viewProjectionMatrix);
  3304. GLint prevVAO = 0;
  3305. glGetIntegerv (GL_VERTEX_ARRAY_BINDING, &prevVAO);
  3306. glGenVertexArrays (1, &m_vao);
  3307. glGenBuffers (1, &m_vbo);
  3308. glGenBuffers (1, &m_ebo);
  3309. glBindVertexArray (m_vao);
  3310. glBindBuffer (GL_ARRAY_BUFFER, m_vbo);
  3311. glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, m_ebo);
  3312. const GLuint program = m_pass->getProgramID ();
  3313. if (m_useRopeRenderer) {
  3314. // Rope vertex layout: 7 attributes, 26 floats/vertex, stride=104 bytes
  3315. // a_PositionVec4(4) + a_TexCoordVec4(4) + a_TexCoordVec4C1(4) + a_TexCoordVec4C2(4)
  3316. // + a_TexCoordVec4C3(4) + a_TexCoordC4(2) + a_Color(4) = 26
  3317. const GLsizei stride = sizeof (float) * ROPE_FLOATS_PER_VERTEX;
  3318. const GLint loc0 = glGetAttribLocation (program, "a_PositionVec4");
  3319. const GLint loc1 = glGetAttribLocation (program, "a_TexCoordVec4");
  3320. const GLint loc2 = glGetAttribLocation (program, "a_TexCoordVec4C1");
  3321. const GLint loc3 = glGetAttribLocation (program, "a_TexCoordVec4C2");
  3322. const GLint loc4 = glGetAttribLocation (program, "a_TexCoordVec4C3");
  3323. const GLint loc5 = glGetAttribLocation (program, "a_TexCoordC4");
  3324. const GLint loc6 = glGetAttribLocation (program, "a_Color");
  3325. if (loc0 >= 0) {
  3326. glEnableVertexAttribArray (loc0);
  3327. glVertexAttribPointer (loc0, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 0));
  3328. }
  3329. if (loc1 >= 0) {
  3330. glEnableVertexAttribArray (loc1);
  3331. glVertexAttribPointer (loc1, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 4));
  3332. }
  3333. if (loc2 >= 0) {
  3334. glEnableVertexAttribArray (loc2);
  3335. glVertexAttribPointer (loc2, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 8));
  3336. }
  3337. if (loc3 >= 0) {
  3338. glEnableVertexAttribArray (loc3);
  3339. glVertexAttribPointer (loc3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 12));
  3340. }
  3341. if (loc4 >= 0) {
  3342. glEnableVertexAttribArray (loc4);
  3343. glVertexAttribPointer (loc4, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 16));
  3344. }
  3345. if (loc5 >= 0) {
  3346. glEnableVertexAttribArray (loc5);
  3347. glVertexAttribPointer (loc5, 2, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 20));
  3348. }
  3349. if (loc6 >= 0) {
  3350. glEnableVertexAttribArray (loc6);
  3351. glVertexAttribPointer (loc6, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 22));
  3352. }
  3353. } else {
  3354. // Sprite vertex layout: 5 attributes, 17 floats/vertex, stride=68 bytes
  3355. // a_Position(3) + a_TexCoordVec4(4) + a_Color(4) + a_TexCoordVec4C1(4) + a_TexCoordC2(2) = 17
  3356. const GLsizei stride = sizeof (float) * SPRITE_FLOATS_PER_VERTEX;
  3357. const GLint loc0 = glGetAttribLocation (program, "a_Position");
  3358. const GLint loc1 = glGetAttribLocation (program, "a_TexCoordVec4");
  3359. const GLint loc2 = glGetAttribLocation (program, "a_Color");
  3360. const GLint loc3 = glGetAttribLocation (program, "a_TexCoordVec4C1");
  3361. const GLint loc4 = glGetAttribLocation (program, "a_TexCoordC2");
  3362. if (loc0 >= 0) {
  3363. glEnableVertexAttribArray (loc0);
  3364. glVertexAttribPointer (loc0, 3, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 0));
  3365. }
  3366. if (loc1 >= 0) {
  3367. glEnableVertexAttribArray (loc1);
  3368. glVertexAttribPointer (loc1, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 3));
  3369. }
  3370. if (loc2 >= 0) {
  3371. glEnableVertexAttribArray (loc2);
  3372. glVertexAttribPointer (loc2, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 7));
  3373. }
  3374. if (loc3 >= 0) {
  3375. glEnableVertexAttribArray (loc3);
  3376. glVertexAttribPointer (loc3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 11));
  3377. }
  3378. if (loc4 >= 0) {
  3379. glEnableVertexAttribArray (loc4);
  3380. glVertexAttribPointer (loc4, 2, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 15));
  3381. }
  3382. }
  3383. glBindVertexArray (prevVAO);
  3384. setupGeometryCallbacks ();
  3385. setupParticleUniforms ();
  3386. }
  3387. void CParticle::setupGeometryCallbacks () {
  3388. m_pass->setGeometryCallback (
  3389. // Setup attribs: save current VAO, bind particle VAO
  3390. [this] () {
  3391. glGetIntegerv (GL_VERTEX_ARRAY_BINDING, &m_prevVAO);
  3392. glBindVertexArray (m_vao);
  3393. },
  3394. // Draw geometry: indexed rendering
  3395. [this] () { glDrawElements (GL_TRIANGLES, m_activeIndexCount, GL_UNSIGNED_INT, nullptr); },
  3396. // Cleanup: restore previous VAO
  3397. [this] () { glBindVertexArray (m_prevVAO); }
  3398. );
  3399. }
  3400. void CParticle::setupParticleUniforms () {
  3401. // Add particle-specific uniforms from common_particles.h that CPass doesn't provide
  3402. // These are pointer-based: CPass reads the current value each frame
  3403. m_pass->addUniform ("g_ModelMatrixInverse", &m_modelMatrixInverse);
  3404. m_pass->addUniform ("g_OrientationUp", &m_orientationUp);
  3405. m_pass->addUniform ("g_OrientationRight", &m_orientationRight);
  3406. m_pass->addUniform ("g_OrientationForward", &m_orientationForward);
  3407. m_pass->addUniform ("g_ViewUp", &m_viewUp);
  3408. m_pass->addUniform ("g_ViewRight", &m_viewRight);
  3409. m_pass->addUniform ("g_EyePosition", &m_eyePosition);
  3410. m_pass->addUniform ("g_RenderVar0", &m_renderVar0);
  3411. m_pass->addUniform ("g_RenderVar1", &m_renderVar1);
  3412. // REFRACT: set g_RefractAmount (shader default 0.05, may not be applied by CPass's parameter system)
  3413. if (m_hasRefract) {
  3414. m_pass->addUniform ("g_RefractAmount", &m_refractAmount);
  3415. }
  3416. }
  3417. void CParticle::updateMatrices () {
  3418. // m_modelMatrix comes from draw ()
  3419. m_modelMatrixInverse = glm::inverse (m_modelMatrix);
  3420. this->updateParticleViewProjection ();
  3421. m_mvpMatrix = m_viewProjectionMatrix * m_modelMatrix;
  3422. m_mvpMatrixInverse = glm::inverse (m_mvpMatrix);
  3423. m_orientationUp = glm::vec3 (0.0f, 1.0f, 0.0f);
  3424. m_orientationRight = glm::vec3 (1.0f, 0.0f, 0.0f);
  3425. m_orientationForward = glm::vec3 (0.0f, 0.0f, 1.0f);
  3426. m_viewUp = glm::vec3 (0.0f, 1.0f, 0.0f);
  3427. m_viewRight = glm::vec3 (1.0f, 0.0f, 0.0f);
  3428. this->updateParticleRenderVars ();
  3429. }
  3430. void CParticle::updateParticleViewProjection () {
  3431. const auto& camera = getScene ().getCamera ();
  3432. if (camera.isPerspective ()) {
  3433. m_viewProjectionMatrix = camera.getPerspective () * camera.getView ();
  3434. m_eyePosition = camera.getEye ();
  3435. } else {
  3436. // particle file flags 4 (sub_1402366F0) and the object's "perspective" (sub_1402222A0) both switch to the
  3437. // perspective layer camera (sub_1401E5B60)
  3438. const bool perspective = (m_particle.flags & 4) != 0 || m_particle.perspective->value->getBool ();
  3439. m_viewProjectionMatrix = perspective ? camera.getPerspectiveLayerViewProjection ()
  3440. : camera.getProjection () * camera.getLookAt ();
  3441. // g_EyePosition in 2D scenes is the camera position 2000 units out (end of sub_1401891A0), the trail
  3442. // shader's ComputeParticleTrailTangents crosses the eye direction with the velocity
  3443. const glm::vec2 eye = getScene ().getCameraEye ();
  3444. m_eyePosition = glm::vec3 (eye.x, eye.y, 2000.0f);
  3445. }
  3446. }
  3447. void CParticle::updateParticleRenderVars () {
  3448. if (m_useRopeRenderer && m_useTrailRenderer) {
  3449. // sub_1402366F0 renderer type 4: z is how far the history timer got, w the segment count the UVs span
  3450. const float segments = static_cast<float> (m_ropeSegments);
  3451. const float timeOffset = 1.0f - std::max (m_trailTimer, 0.0f) / m_trailInterval;
  3452. m_renderVar0 = m_ropeUVScrolling
  3453. ? glm::vec4 (segments - 1.0f, 0.0f, timeOffset, (segments - 1.0f) / this->ropeUVScale ())
  3454. : glm::vec4 (0.0f, 0.0f, timeOffset, segments - 0.5f);
  3455. } else {
  3456. m_renderVar0 = glm::vec4 (m_trailLength, m_trailMaxLength, m_trailMinLength, 0.0f);
  3457. }
  3458. if (m_spritesheetFrames > 0 && m_spritesheetCols > 0 && m_spritesheetRows > 0) {
  3459. float frameWidth = 1.0f / static_cast<float> (m_spritesheetCols);
  3460. float frameHeight = 1.0f / static_cast<float> (m_spritesheetRows);
  3461. float textureRatio = 1.0f;
  3462. if (const auto texture = getTexture ()) {
  3463. // Use atlas dimensions (resolution vec4) rather than getRealWidth/Height, which
  3464. // returns per-frame dimensions for animated textures - the shader needs the
  3465. // per-frame pixel aspect ratio: (atlasH * frameHeight) / (atlasW * frameWidth).
  3466. const glm::vec4* res = texture->getResolution ();
  3467. float w = res->x;
  3468. float h = res->y;
  3469. if (w > 0.0f) {
  3470. textureRatio = (h * frameHeight) / (w * frameWidth);
  3471. }
  3472. }
  3473. m_renderVar1 = glm::vec4 (frameWidth, frameHeight, static_cast<float> (m_spritesheetFrames), textureRatio);
  3474. } else {
  3475. float textureRatio = 1.0f;
  3476. if (const auto texture = getTexture ()) {
  3477. float w = static_cast<float> (texture->getRealWidth ());
  3478. float h = static_cast<float> (texture->getRealHeight ());
  3479. if (w > 0.0f) {
  3480. textureRatio = h / w;
  3481. }
  3482. }
  3483. m_renderVar1 = glm::vec4 (0.0f, 0.0f, 0.0f, textureRatio);
  3484. }
  3485. }
  3486. void CParticle::renderSprites () {
  3487. if (m_particleCount == 0 || m_pass == nullptr) {
  3488. return;
  3489. }
  3490. uint32_t aliveCount = 0;
  3491. for (uint32_t i = 0; i < m_particleCount; i++) {
  3492. if (m_particles[i].alive) {
  3493. aliveCount++;
  3494. }
  3495. }
  3496. if (aliveCount == 0) {
  3497. return;
  3498. }
  3499. // Build vertex data in WP shader layout:
  3500. // a_Position(3) + a_TexCoordVec4(uv.x, uv.y, rotZ, size)(4) + a_Color(4)
  3501. // + a_TexCoordVec4C1(vel.x, vel.y, vel.z, lifetime)(4) + a_TexCoordC2(rotX, rotY)(2) = 17 floats
  3502. uint32_t vertexIndex = 0;
  3503. uint32_t indexOffset = 0;
  3504. for (uint32_t i = 0; i < m_particleCount; i++) {
  3505. const auto& p = m_particles[i];
  3506. if (!p.alive) {
  3507. continue;
  3508. }
  3509. // Skip particles with invalid values
  3510. if (!std::isfinite (p.position.x) || !std::isfinite (p.position.y) || !std::isfinite (p.position.z)
  3511. || !std::isfinite (p.size) || p.size <= 0.0f || p.size > 10000.0f) {
  3512. continue;
  3513. }
  3514. // Encode the CPU-computed frame (accounts for sequenceMultiplier and animation mode)
  3515. // into the lifetime value the WP shader's ComputeSpriteFrame expects: it derives the
  3516. // current frame via floor(frac(lifetime) * numFrames) and the inter-frame blend via
  3517. // frac(lifetime * numFrames).
  3518. float lifetime = p.getLifetimePos ();
  3519. if (m_spritesheetFrames > 0 && p.frame >= 0.0f) {
  3520. if (m_particle.animationMode == "randomframe") {
  3521. // Center within the frame to avoid floating-point edge cases
  3522. lifetime = (p.frame + 0.5f) / static_cast<float> (m_spritesheetFrames);
  3523. } else {
  3524. lifetime = p.frame / static_cast<float> (m_spritesheetFrames);
  3525. }
  3526. }
  3527. const glm::vec3 position = this->drawVector (p.position);
  3528. const glm::vec3 velocity = this->drawVector (p.velocity);
  3529. auto addVertex = [&] (float u, float v) {
  3530. const uint32_t base = vertexIndex * SPRITE_FLOATS_PER_VERTEX;
  3531. // a_Position (vec3)
  3532. m_vertices[base + 0] = position.x;
  3533. m_vertices[base + 1] = position.y;
  3534. m_vertices[base + 2] = position.z;
  3535. // a_TexCoordVec4 (vec4: uv.x, uv.y, rotZ, size)
  3536. m_vertices[base + 3] = u;
  3537. m_vertices[base + 4] = v;
  3538. m_vertices[base + 5] = p.rotation.z;
  3539. m_vertices[base + 6] = p.size;
  3540. // a_Color (vec4: r, g, b, a)
  3541. m_vertices[base + 7] = p.color.r;
  3542. m_vertices[base + 8] = p.color.g;
  3543. m_vertices[base + 9] = p.color.b;
  3544. m_vertices[base + 10] = p.alpha;
  3545. // a_TexCoordVec4C1 (vec4: vel.x, vel.y, vel.z, lifetime)
  3546. m_vertices[base + 11] = velocity.x;
  3547. m_vertices[base + 12] = velocity.y;
  3548. m_vertices[base + 13] = velocity.z;
  3549. m_vertices[base + 14] = lifetime;
  3550. // a_TexCoordC2 (vec2: rotX, rotY)
  3551. m_vertices[base + 15] = p.rotation.x;
  3552. m_vertices[base + 16] = p.rotation.y;
  3553. vertexIndex++;
  3554. };
  3555. uint32_t baseVertex = vertexIndex;
  3556. addVertex (0.0f, 1.0f); // 0: Bottom-left
  3557. addVertex (1.0f, 1.0f); // 1: Bottom-right
  3558. addVertex (1.0f, 0.0f); // 2: Top-right
  3559. addVertex (0.0f, 0.0f); // 3: Top-left
  3560. m_indices[indexOffset++] = baseVertex + 0;
  3561. m_indices[indexOffset++] = baseVertex + 1;
  3562. m_indices[indexOffset++] = baseVertex + 2;
  3563. m_indices[indexOffset++] = baseVertex + 2;
  3564. m_indices[indexOffset++] = baseVertex + 3;
  3565. m_indices[indexOffset++] = baseVertex + 0;
  3566. }
  3567. m_activeIndexCount = static_cast<GLsizei> (indexOffset);
  3568. if (m_activeIndexCount == 0) {
  3569. return;
  3570. }
  3571. #if !NDEBUG
  3572. std::string str = "Particles ";
  3573. str += this->getParticle ().name + " (" + std::to_string (this->getId ()) + ", " + this->getParticle ().particleFile
  3574. + ")";
  3575. glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());
  3576. #endif
  3577. glBindBuffer (GL_ARRAY_BUFFER, m_vbo);
  3578. glBufferData (
  3579. GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (vertexIndex * SPRITE_FLOATS_PER_VERTEX * sizeof (float)),
  3580. m_vertices.data (), GL_DYNAMIC_DRAW
  3581. );
  3582. glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, m_ebo);
  3583. glBufferData (
  3584. GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr> (indexOffset * sizeof (uint32_t)), m_indices.data (),
  3585. GL_DYNAMIC_DRAW
  3586. );
  3587. updateMatrices ();
  3588. // REFRACT: blit current scene content into the copy FBO first, giving the shader a
  3589. // snapshot of what's behind the particles without a read/write feedback loop
  3590. if (m_hasRefract && m_refractFBO) {
  3591. auto sceneFBO = getScene ().getFBO ();
  3592. GLint w = static_cast<GLint> (sceneFBO->getRealWidth ());
  3593. GLint h = static_cast<GLint> (sceneFBO->getRealHeight ());
  3594. glBindFramebuffer (GL_READ_FRAMEBUFFER, sceneFBO->getFramebuffer ());
  3595. glBindFramebuffer (GL_DRAW_FRAMEBUFFER, m_refractFBO->getFramebuffer ());
  3596. glBlitFramebuffer (0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
  3597. }
  3598. // ComputeParticleTrailTangents produces a right vector with a Z component (from
  3599. // cross(eyeDirection, velocity), where eyeDirection has an XY offset from the model
  3600. // transform). For 2D/ortho particles at z=0, the ortho near plane sits at ndc.z=-1, so any
  3601. // Z offset pushes vertices past it and clips half the quad. GL_DEPTH_CLAMP avoids that by
  3602. // clamping depth instead of clipping.
  3603. glEnable (GL_DEPTH_CLAMP);
  3604. // CPass::render() handles: FBO binding, texture setup, uniforms, blending, draw call, cleanup
  3605. m_pass->render ();
  3606. glDisable (GL_DEPTH_CLAMP);
  3607. #if !NDEBUG
  3608. glPopDebugGroup ();
  3609. #endif
  3610. }
  3611. float CParticle::ropeUVScale () const {
  3612. return m_ropeUVScale != 0.0f ? m_ropeUVScale : 1.0f;
  3613. }
  3614. void CParticle::buildRopeTrail (uint32_t& vertexIndex, uint32_t& indexOffset) {
  3615. // sub_1402308A0, the ropetrail vertex build without a geometry shader: every particle gets one strip through
  3616. // its position and its history, a quad per segment whether that history is filled yet or not
  3617. const int segments = m_ropeSegments;
  3618. const float uvScaleInverse = 1.0f / this->ropeUVScale ();
  3619. for (uint32_t i = 0; i < m_particleCount; i++) {
  3620. const auto& p = m_particles[i];
  3621. const glm::vec3* history = m_trailHistory.data () + static_cast<size_t> (i) * segments;
  3622. const auto point = [&] (int index) -> const glm::vec3& { return index == 0 ? p.position : history[index - 1]; };
  3623. const glm::vec4 color (p.color, p.alpha);
  3624. const float trailLength = static_cast<float> (m_trailCount[i]) * uvScaleInverse;
  3625. const float scroll = static_cast<float> (m_trailScroll[i]);
  3626. for (int k = 0; k < segments; k++) {
  3627. const glm::vec3 start = this->drawVector (point (k));
  3628. const glm::vec3 end = this->drawVector (point (k + 1));
  3629. const glm::vec3 before = this->drawVector (point (std::max (k - 1, 0)));
  3630. const glm::vec3 after = this->drawVector (point (std::min (k + 2, segments)));
  3631. // with uvscrolling the length slot carries the segment index and positions move back with every push
  3632. const float lengthSlot = m_ropeUVScrolling ? static_cast<float> (k) : trailLength;
  3633. const float position = m_ropeUVScrolling ? static_cast<float> (k) - scroll : static_cast<float> (k);
  3634. const uint32_t baseVertex = vertexIndex;
  3635. for (const glm::vec2 uv : { glm::vec2 (0.0f, 0.0f), glm::vec2 (1.0f, 0.0f), glm::vec2 (1.0f, 1.0f),
  3636. glm::vec2 (0.0f, 1.0f) }) {
  3637. float* v = &m_vertices[static_cast<size_t> (vertexIndex++) * ROPE_FLOATS_PER_VERTEX];
  3638. const float values[ROPE_FLOATS_PER_VERTEX] = {
  3639. start.x, start.y, start.z, p.size, end.x, end.y, end.z, lengthSlot, before.x,
  3640. before.y, before.z, position, after.x, after.y, after.z, p.size, color.r, color.g,
  3641. color.b, color.a, uv.x, uv.y, color.r, color.g, color.b, color.a,
  3642. };
  3643. std::copy (std::begin (values), std::end (values), v);
  3644. }
  3645. for (const uint32_t corner : { 0u, 1u, 2u, 2u, 3u, 0u }) {
  3646. m_indices[indexOffset++] = baseVertex + corner;
  3647. }
  3648. }
  3649. }
  3650. }
  3651. void CParticle::renderRope () {
  3652. if (m_pass == nullptr || m_particleCount < (m_useTrailRenderer ? 1u : 2u)) {
  3653. return;
  3654. }
  3655. uint32_t vertexIndex = 0;
  3656. uint32_t indexOffset = 0;
  3657. if (m_useTrailRenderer) {
  3658. this->buildRopeTrail (vertexIndex, indexOffset);
  3659. } else {
  3660. // Already in spawn order (oldest at index 0) thanks to compaction in update();
  3661. // all particles in [0, m_particleCount) are alive.
  3662. const uint32_t aliveCount = m_particleCount;
  3663. // Each segment between consecutive particles is subdivided into m_ropeSubdivision
  3664. // sub-segments via Catmull-Rom spline, for smooth curves instead of harsh corners.
  3665. //
  3666. // Rope vertex layout (26 floats per vertex, THICKFORMAT):
  3667. // [0-3] a_PositionVec4: startPos.xyz, sizeStart
  3668. // [4-7] a_TexCoordVec4: endPos.xyz, trailLength
  3669. // [8-11] a_TexCoordVec4C1: CP0.xyz, trailPosition
  3670. // [12-15] a_TexCoordVec4C2: CP1.xyz, sizeEnd
  3671. // [16-19] a_TexCoordVec4C3: colorEnd.rgba
  3672. // [20-21] a_TexCoordC4: uvs.xy
  3673. // [22-25] a_Color: colorStart.rgba
  3674. const uint32_t numSegments = aliveCount - 1;
  3675. const int subdivision = std::max (1, m_ropeSubdivision);
  3676. auto catmullRom = [] (const glm::vec3& p0, const glm::vec3& p1, const glm::vec3& p2, const glm::vec3& p3,
  3677. float t) -> glm::vec3 {
  3678. float t2 = t * t, t3 = t2 * t;
  3679. return 0.5f
  3680. * ((2.0f * p1) + (-p0 + p2) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2
  3681. + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3);
  3682. };
  3683. // First pass: evaluate the spline to get all interpolated points (position, size, color)
  3684. const uint32_t totalPoints = numSegments * subdivision + 1;
  3685. this->m_splinePositions.resize (totalPoints);
  3686. this->m_splineSizes.resize (totalPoints);
  3687. this->m_splineColors.resize (totalPoints);
  3688. auto& splinePositions = this->m_splinePositions;
  3689. auto& splineSizes = this->m_splineSizes;
  3690. auto& splineColors = this->m_splineColors;
  3691. for (uint32_t i = 0; i < numSegments; i++) {
  3692. const auto& p1 = m_particles[i];
  3693. const auto& p2 = m_particles[i + 1];
  3694. const auto& p0 = (i > 0) ? m_particles[i - 1] : p1;
  3695. const auto& p3 = (i + 2 < aliveCount) ? m_particles[i + 2] : p2;
  3696. for (int k = 0; k < subdivision; k++) {
  3697. float t = static_cast<float> (k) / static_cast<float> (subdivision);
  3698. uint32_t idx = i * subdivision + k;
  3699. splinePositions[idx] = catmullRom (p0.position, p1.position, p2.position, p3.position, t);
  3700. splineSizes[idx] = glm::mix (p1.size, p2.size, t);
  3701. splineColors[idx] = glm::mix (glm::vec4 (p1.color, p1.alpha), glm::vec4 (p2.color, p2.alpha), t);
  3702. }
  3703. }
  3704. // Last point is the final particle
  3705. {
  3706. const auto& pLast = m_particles[aliveCount - 1];
  3707. splinePositions[totalPoints - 1] = pLast.position;
  3708. splineSizes[totalPoints - 1] = pLast.size;
  3709. splineColors[totalPoints - 1] = glm::vec4 (pLast.color, pLast.alpha);
  3710. }
  3711. // Second pass: build quads from consecutive spline points. The shader computes UV.v as
  3712. // trailPosition / (trailLength - 1), so trailLength/trailPosition are expressed in
  3713. // sub-segment units for the correct UV slice per quad. UV scale divides the effective
  3714. // length, pushing UVs past [0,1] so the texture repeats.
  3715. const uint32_t totalSubSegments = totalPoints - 1;
  3716. const float uvScale = (m_ropeUVScale > 0.0f) ? m_ropeUVScale : 1.0f;
  3717. const float trailLength = static_cast<float> (totalSubSegments) / uvScale + 1.0f;
  3718. const float usableLength = trailLength - 1.0f;
  3719. // UV smoothing: distribute UV proportional to arc length instead of uniform index.
  3720. // Per wiki: only when all particle lifetimes match and scrolling is disabled.
  3721. const bool useSmoothing = m_ropeUVSmoothing && m_uniformLifetimes && !m_ropeUVScrolling;
  3722. auto& cumulativeArcLength = this->m_cumulativeArcLength;
  3723. float totalArcLength = 0.0f;
  3724. if (useSmoothing) {
  3725. cumulativeArcLength.resize (totalPoints, 0.0f);
  3726. for (uint32_t i = 1; i < totalPoints; i++) {
  3727. totalArcLength += glm::distance (splinePositions[i], splinePositions[i - 1]);
  3728. cumulativeArcLength[i] = totalArcLength;
  3729. }
  3730. }
  3731. // UV scrolling: shift UV along the rope over time (1 UV cycle per second)
  3732. float scrollOffset = 0.0f;
  3733. if (m_ropeUVScrolling && usableLength > 0.0f) {
  3734. scrollOffset = std::fmod (static_cast<float> (g_Time), 10000.0f) * usableLength;
  3735. }
  3736. for (uint32_t s = 0; s < totalSubSegments; s++) {
  3737. const glm::vec3 posStart = this->drawVector (splinePositions[s]);
  3738. const glm::vec3 posEnd = this->drawVector (splinePositions[s + 1]);
  3739. float sizeStart = splineSizes[s];
  3740. float sizeEnd = splineSizes[s + 1];
  3741. const glm::vec4& colorStart = splineColors[s];
  3742. const glm::vec4& colorEnd = splineColors[s + 1];
  3743. // Neighboring points for shader tangent computation (CP0/CP1)
  3744. const glm::vec3 posPrev = (s > 0) ? this->drawVector (splinePositions[s - 1]) : posStart;
  3745. const glm::vec3 posAfter = (s + 2 < totalPoints) ? this->drawVector (splinePositions[s + 2]) : posEnd;
  3746. // Compute trailPosition for UV mapping
  3747. float trailPosition;
  3748. if (useSmoothing && totalArcLength > 0.0f) {
  3749. // Arc-length parameterization: map cumulative distance to sub-segment space
  3750. trailPosition = cumulativeArcLength[s] / totalArcLength * static_cast<float> (totalSubSegments);
  3751. } else {
  3752. trailPosition = static_cast<float> (s);
  3753. }
  3754. trailPosition += scrollOffset;
  3755. auto addRopeVertex = [&] (float uvX, float uvY) {
  3756. const uint32_t base = vertexIndex * ROPE_FLOATS_PER_VERTEX;
  3757. // a_PositionVec4: startPos.xyz, sizeStart
  3758. m_vertices[base + 0] = posStart.x;
  3759. m_vertices[base + 1] = posStart.y;
  3760. m_vertices[base + 2] = posStart.z;
  3761. m_vertices[base + 3] = sizeStart;
  3762. // a_TexCoordVec4: endPos.xyz, trailLength
  3763. m_vertices[base + 4] = posEnd.x;
  3764. m_vertices[base + 5] = posEnd.y;
  3765. m_vertices[base + 6] = posEnd.z;
  3766. m_vertices[base + 7] = trailLength;
  3767. // a_TexCoordVec4C1: CP0.xyz (neighbor before start), trailPosition
  3768. m_vertices[base + 8] = posPrev.x;
  3769. m_vertices[base + 9] = posPrev.y;
  3770. m_vertices[base + 10] = posPrev.z;
  3771. m_vertices[base + 11] = trailPosition;
  3772. // a_TexCoordVec4C2: CP1.xyz (neighbor after end), sizeEnd
  3773. m_vertices[base + 12] = posAfter.x;
  3774. m_vertices[base + 13] = posAfter.y;
  3775. m_vertices[base + 14] = posAfter.z;
  3776. m_vertices[base + 15] = sizeEnd;
  3777. // a_TexCoordVec4C3: colorEnd.rgba
  3778. m_vertices[base + 16] = colorEnd.r;
  3779. m_vertices[base + 17] = colorEnd.g;
  3780. m_vertices[base + 18] = colorEnd.b;
  3781. m_vertices[base + 19] = colorEnd.a;
  3782. // a_TexCoordC4: uvs.xy
  3783. m_vertices[base + 20] = uvX;
  3784. m_vertices[base + 21] = uvY;
  3785. // a_Color: colorStart.rgba
  3786. m_vertices[base + 22] = colorStart.r;
  3787. m_vertices[base + 23] = colorStart.g;
  3788. m_vertices[base + 24] = colorStart.b;
  3789. m_vertices[base + 25] = colorStart.a;
  3790. vertexIndex++;
  3791. };
  3792. // Quad: 4 vertices (left/right at start/end of segment)
  3793. uint32_t baseVertex = vertexIndex;
  3794. addRopeVertex (0.0f, 0.0f); // left at start
  3795. addRopeVertex (1.0f, 0.0f); // right at start
  3796. addRopeVertex (1.0f, 1.0f); // right at end
  3797. addRopeVertex (0.0f, 1.0f); // left at end
  3798. m_indices[indexOffset++] = baseVertex + 0;
  3799. m_indices[indexOffset++] = baseVertex + 1;
  3800. m_indices[indexOffset++] = baseVertex + 2;
  3801. m_indices[indexOffset++] = baseVertex + 2;
  3802. m_indices[indexOffset++] = baseVertex + 3;
  3803. m_indices[indexOffset++] = baseVertex + 0;
  3804. }
  3805. }
  3806. m_activeIndexCount = static_cast<GLsizei> (indexOffset);
  3807. if (m_activeIndexCount == 0) {
  3808. return;
  3809. }
  3810. #if !NDEBUG
  3811. std::string str = "Rope particles ";
  3812. str += this->getParticle ().name + " (" + std::to_string (this->getId ()) + ", " + this->getParticle ().particleFile
  3813. + ")";
  3814. glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());
  3815. #endif
  3816. glBindBuffer (GL_ARRAY_BUFFER, m_vbo);
  3817. glBufferData (
  3818. GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (vertexIndex * ROPE_FLOATS_PER_VERTEX * sizeof (float)),
  3819. m_vertices.data (), GL_DYNAMIC_DRAW
  3820. );
  3821. glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, m_ebo);
  3822. glBufferData (
  3823. GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr> (indexOffset * sizeof (uint32_t)), m_indices.data (),
  3824. GL_DYNAMIC_DRAW
  3825. );
  3826. updateMatrices ();
  3827. // REFRACT: blit current scene content into the copy FBO before rendering
  3828. if (m_hasRefract && m_refractFBO) {
  3829. auto sceneFBO = getScene ().getFBO ();
  3830. GLint w = static_cast<GLint> (sceneFBO->getRealWidth ());
  3831. GLint h = static_cast<GLint> (sceneFBO->getRealHeight ());
  3832. glBindFramebuffer (GL_READ_FRAMEBUFFER, sceneFBO->getFramebuffer ());
  3833. glBindFramebuffer (GL_DRAW_FRAMEBUFFER, m_refractFBO->getFramebuffer ());
  3834. glBlitFramebuffer (0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
  3835. }
  3836. glEnable (GL_DEPTH_CLAMP);
  3837. m_pass->render ();
  3838. glDisable (GL_DEPTH_CLAMP);
  3839. #if !NDEBUG
  3840. glPopDebugGroup ();
  3841. #endif
  3842. }