| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251 |
- #include "CParticle.h"
- #include "WallpaperEngine/Data/Model/Property.h"
- #include "WallpaperEngine/Logging/Log.h"
- #include "WallpaperEngine/Maths.h"
- #include "WallpaperEngine/Render/Utils/NoiseUtils.h"
- #include <GL/glew.h>
- #include <algorithm>
- #include <cmath>
- #include <glm/gtc/constants.hpp>
- #include <glm/gtc/matrix_transform.hpp>
- extern float g_Time;
- extern float g_RealTime;
- using namespace WallpaperEngine::Render::Objects;
- using namespace WallpaperEngine::Render::Utils;
- using namespace WallpaperEngine::Data::Model;
- CParticle::CParticle (Wallpapers::CScene& scene, const Particle& particle) :
- CObject (scene, particle), CRenderable (scene, particle, *particle.material->material),
- ScriptableObject (scene, particle), m_particle (particle) {
- this->registerProperty ("scale", *particle.scale->value);
- this->registerProperty ("angles", *particle.angles->value);
- this->registerProperty ("visible", *particle.visible->value);
- this->registerProperty ("parallaxDepth", *particle.parallaxDepth->value);
- this->detectTexture ();
- std::random_device rd;
- m_rng.seed (rd ());
- // Read renderer config early - buffer sizing below depends on it
- if (!m_particle.renderers.empty ()) {
- const auto& renderer = m_particle.renderers[0];
- if (renderer.name == "rope" || renderer.name == "ropetrail") {
- // Both rope and ropetrail use genericropeparticle shader
- m_useRopeRenderer = true;
- m_ropeSubdivision = std::max (0, static_cast<int> (renderer.subdivision));
- m_ropeUVScale = renderer.uvScale;
- m_ropeUVScrolling = renderer.uvScrolling;
- m_ropeUVSmoothing = renderer.uvSmoothing;
- if (renderer.name == "ropetrail") {
- m_useTrailRenderer = true;
- m_trailLength = renderer.length;
- m_ropeSegments = std::max (2, static_cast<int> (renderer.segments));
- }
- } else if (renderer.name == "spritetrail") {
- // spritetrail uses genericparticle with TRAILRENDERER combo
- m_useTrailRenderer = true;
- m_trailLength = renderer.length;
- m_trailMaxLength = renderer.maxLength;
- m_trailMinLength = renderer.minLength;
- }
- }
- float countMultiplier = particle.instanceOverride.count->value->getFloat ();
- uint32_t adjustedMaxCount = static_cast<uint32_t> (particle.maxCount * countMultiplier);
- // Use wallpaper's specified count, or default if maxCount is 0
- m_maxParticles = (adjustedMaxCount > 0) ? adjustedMaxCount : DEFAULT_MAX_PARTICLES;
- m_particles.resize (m_maxParticles);
- if (m_useRopeRenderer) {
- // Rope: N particles connect via (N-1) segments, each subdivided into sub-segments
- const int subdivision = std::max (1, m_ropeSubdivision);
- const int maxSubSegments = std::max (1, static_cast<int> (m_maxParticles - 1)) * subdivision;
- m_vertices.resize (maxSubSegments * 4 * ROPE_FLOATS_PER_VERTEX);
- m_indices.resize (maxSubSegments * 6);
- } else {
- // 4 vertices, 6 indices per particle
- const int verticesPerParticle = 4;
- const int indicesPerParticle = 6;
- m_vertices.resize (m_maxParticles * verticesPerParticle * SPRITE_FLOATS_PER_VERTEX);
- m_indices.resize (m_maxParticles * indicesPerParticle);
- }
- }
- CParticle::~CParticle () {
- delete m_pass;
- if (m_vao != 0) {
- glDeleteVertexArrays (1, &m_vao);
- }
- if (m_vbo != 0) {
- glDeleteBuffers (1, &m_vbo);
- }
- if (m_ebo != 0) {
- glDeleteBuffers (1, &m_ebo);
- }
- m_vertices.clear ();
- m_indices.clear ();
- }
- void CParticle::setup () {
- if (m_initialized) {
- return;
- }
- // Convert origin from screen space (0,0 top-left) to centered space, matching the
- // ortho(-width/2, width/2, -height/2, height/2) projection
- m_lastScreenWidth = getScene ().getCamera ().getWidth ();
- m_lastScreenHeight = getScene ().getCamera ().getHeight ();
- glm::vec3 origin = m_particle.origin->value->getVec3 ();
- origin.x -= m_lastScreenWidth / 2.0f;
- origin.y = m_lastScreenHeight / 2.0f - origin.y;
- m_transformedOrigin = origin;
- if (m_particle.material && m_particle.material->material && !m_particle.material->material->passes.empty ()) {
- auto& firstPass = *m_particle.material->material->passes.begin ();
- // Overbright: brightness multiplier for additive particles
- auto overbrightIt = firstPass->constants.find ("ui_editor_properties_overbright");
- if (overbrightIt != firstPass->constants.end ()) {
- m_overbright = overbrightIt->second->value->getFloat ();
- }
- }
- // TextureParser computes the spritesheet grid from TEXS frame data (animated textures) or
- // .tex-json metadata (static textures). GIF-style animated textures (separate GL texture per
- // frame) get 0 cols/rows since a 1x1 grid can't hold all frames - no SPRITESHEET mode needed,
- // frame switching happens via texture ID instead.
- if (const auto texture = getTexture ()) {
- m_spritesheetCols = static_cast<int> (texture->getSpritesheetCols ());
- m_spritesheetRows = static_cast<int> (texture->getSpritesheetRows ());
- m_spritesheetFrames = static_cast<int> (texture->getSpritesheetFrames ());
- m_spritesheetDuration = texture->getSpritesheetDuration ();
- }
- setupEmitters ();
- setupInitializers ();
- setupOperators ();
- setupPass ();
- m_controlPoints.resize (8);
- for (const auto& cp : m_particle.controlPoints) {
- if (cp.id >= 0 && cp.id < 8) {
- m_controlPoints[cp.id].offset = cp.offset;
- // flags bit 0 = linkMouse
- m_controlPoints[cp.id].linkMouse = (cp.flags & 1) != 0;
- m_controlPoints[cp.id].worldSpace = (cp.flags & 2) != 0;
- if (m_controlPoints[cp.id].linkMouse) {
- m_hasMouseControlPoint = true;
- }
- // Mouse-linked CPs get their position from update() instead
- if (!m_controlPoints[cp.id].linkMouse) {
- if (m_controlPoints[cp.id].worldSpace) {
- // World space: offset is in screen-centered coords, convert to particle local space
- m_controlPoints[cp.id].position = cp.offset - m_transformedOrigin;
- } else {
- // Local space: offset is already relative to particle system center
- m_controlPoints[cp.id].position = cp.offset;
- }
- }
- }
- }
- m_initialized = true;
- }
- void CParticle::render () {
- if (!m_initialized) {
- return;
- }
- const auto& appContext = this->getScene ().getContext ().getApp ().getContext ();
- const auto visibility = appContext.resolveObjectVisibility (this->getId (), this->getObject ().name);
- if (!visibility.value_or (m_particle.visible->value->getBool ())) {
- return;
- }
- syncTransformedOrigin ();
- // stop() drops every particle, and a later play() starts emitting from scratch
- const auto playback = this->getPlayback ();
- if (playback == Playback::Stopped) {
- m_particleCount = 0;
- } else if (m_lastPlayback == Playback::Stopped) {
- m_emitters.clear ();
- setupEmitters ();
- }
- m_lastPlayback = playback;
- const float currentTime = m_hasMouseControlPoint ? g_RealTime : g_Time;
- // Initialize time on first render to avoid a huge dt spike, and skip the update
- // that frame to avoid an initial burst
- if (m_time == 0.0) {
- m_time = currentTime;
- // "starttime" prewarms the system so it starts already populated instead of every
- // particle visibly leaving the emitter at once
- if (!m_prewarmed && m_particle.startTime > 0.0f && playback == Playback::Playing) {
- m_prewarmed = true;
- constexpr float step = 1.0f / 30.0f;
- m_time = currentTime - m_particle.startTime;
- for (float left = m_particle.startTime; left > 0.0f; left -= step) {
- const float dt = std::min (step, left);
- m_time += dt;
- update (dt);
- }
- m_time = currentTime;
- }
- if (m_useRopeRenderer) {
- renderRope ();
- } else {
- renderSprites ();
- }
- return;
- }
- float dt = currentTime - static_cast<float> (m_time);
- m_time = currentTime;
- if (dt > 0.0f && playback != Playback::Stopped) {
- // Cap dt to prevent simulation instability across different FPS
- dt = std::min (dt, 0.1f);
- update (dt);
- }
- if (m_particleCount > 0 && m_particle.material) {
- if (m_useRopeRenderer) {
- renderRope ();
- } else {
- renderSprites ();
- }
- }
- }
- bool CParticle::isPlaying () const {
- const auto playback = this->getPlayback ();
- return playback == Playback::Playing || (playback == Playback::Paused && m_particleCount > 0);
- }
- // scripts can move the system every frame (e.g. an origin that follows the cursor)
- void CParticle::syncTransformedOrigin () {
- const float screenWidth = static_cast<float> (getScene ().getWidth ());
- const float screenHeight = static_cast<float> (getScene ().getHeight ());
- glm::vec3 origin = m_particle.origin->value->getVec3 ();
- origin.x -= screenWidth / 2.0f;
- origin.y = screenHeight / 2.0f - origin.y;
- if (origin == m_transformedOrigin && screenWidth == m_lastScreenWidth && screenHeight == m_lastScreenHeight) {
- return;
- }
- m_transformedOrigin = origin;
- m_lastScreenWidth = screenWidth;
- m_lastScreenHeight = screenHeight;
- for (auto& cp : m_controlPoints) {
- if (!cp.linkMouse && cp.worldSpace) {
- cp.position = cp.offset - m_transformedOrigin;
- }
- }
- }
- void CParticle::update (float dt) {
- float screenWidth = static_cast<float> (getScene ().getWidth ());
- float screenHeight = static_cast<float> (getScene ().getHeight ());
- const glm::vec2* mousePos = getScene ().getMousePositionNormalized ();
- if (mousePos) {
- for (auto& cp : m_controlPoints) {
- if (cp.linkMouse) {
- // Convert mouse position from normalized [0,1] to centered screen space
- glm::vec3 position;
- position.x = (mousePos->x * screenWidth) - (screenWidth / 2.0f);
- position.y = (screenHeight / 2.0f) - (mousePos->y * screenHeight);
- position.z = 0.0f;
- position += cp.offset;
- // Subtract transformed origin to keep in particle local space (avoids
- // double transformation by the model matrix)
- cp.position = position - m_transformedOrigin;
- }
- }
- }
- // pause() stops emission but keeps simulating what is already alive
- if (this->getPlayback () == Playback::Playing) {
- for (auto& emitter : m_emitters) {
- emitter (m_particles, m_particleCount, dt);
- }
- }
- for (uint32_t i = 0; i < m_particleCount; i++) {
- m_particles[i].age += dt;
- }
- for (auto& op : m_operators) {
- op (m_particles, m_particleCount, m_controlPoints, static_cast<float> (m_time), dt);
- }
- for (uint32_t i = 0; i < m_particleCount; i++) {
- auto& p = m_particles[i];
- if (m_spritesheetFrames > 0) {
- float lifetimePos = p.getLifetimePos ();
- float animSpeed = m_particle.sequenceMultiplier > 0.0f ? m_particle.sequenceMultiplier : 1.0f;
- if (m_particle.animationMode == "randomframe") {
- if (p.frame < 0.0f) {
- std::mt19937 particleRng (
- static_cast<std::mt19937::result_type> (reinterpret_cast<uintptr_t> (&p))
- );
- std::uniform_int_distribution<int> dist (0, m_spritesheetFrames - 1);
- p.frame = static_cast<float> (dist (particleRng));
- }
- } else if (m_particle.animationMode == "once") {
- p.frame = std::min (
- lifetimePos * m_spritesheetFrames * animSpeed, static_cast<float> (m_spritesheetFrames - 1)
- );
- } else {
- if (m_spritesheetDuration > 0.0f) {
- float timeInCycle = std::fmod (p.age * animSpeed, m_spritesheetDuration);
- float cyclePos = timeInCycle / m_spritesheetDuration;
- p.frame = std::fmod (cyclePos * m_spritesheetFrames, static_cast<float> (m_spritesheetFrames));
- } else {
- p.frame = std::fmod (
- lifetimePos * m_spritesheetFrames * animSpeed, static_cast<float> (m_spritesheetFrames)
- );
- }
- }
- }
- }
- // Order-preserving compaction: particles only die from lifetime expiry (never from
- // size, since size can oscillate), and index 0 must stay the oldest particle
- uint32_t writeIdx = 0;
- for (uint32_t readIdx = 0; readIdx < m_particleCount; readIdx++) {
- if (m_particles[readIdx].isAlive ()) {
- if (writeIdx != readIdx) {
- m_particles[writeIdx] = m_particles[readIdx];
- }
- writeIdx++;
- }
- }
- m_particleCount = writeIdx;
- }
- const Particle& CParticle::getParticle () const { return m_particle; }
- const float& CParticle::getBrightness () const { return m_overbright; }
- const float& CParticle::getUserAlpha () const { return m_particle.instanceOverride.alpha->value->getFloat (); }
- const float& CParticle::getAlpha () const { return m_particle.instanceOverride.alpha->value->getFloat (); }
- const glm::vec3& CParticle::getColor () const {
- static const glm::vec3 defaultColor (1.0f);
- if (m_particle.instanceOverride.color && m_particle.instanceOverride.color->value) {
- return m_particle.instanceOverride.color->value->getVec3 ();
- }
- return defaultColor;
- }
- const glm::vec4& CParticle::getColor4 () const {
- static const glm::vec4 defaultColor (1.0f);
- if (m_particle.instanceOverride.color && m_particle.instanceOverride.color->value) {
- return m_particle.instanceOverride.color->value->getVec4 ();
- }
- return defaultColor;
- }
- const glm::vec3& CParticle::getCompositeColor () const { return getColor (); }
- // ========== EMITTERS ==========
- void CParticle::setupEmitters () {
- for (const auto& emitter : m_particle.emitters) {
- EmitterFunc func;
- if (emitter.name == "boxrandom") {
- func = createBoxEmitter (emitter);
- } else if (emitter.name == "sphererandom") {
- func = createSphereEmitter (emitter);
- } else {
- sLog.out ("Unknown emitter type: ", emitter.name);
- continue;
- }
- if (func) {
- m_emitters.push_back (std::move (func));
- }
- }
- }
- EmitterFunc CParticle::createBoxEmitter (const ParticleEmitter& emitter) {
- float rate = emitter.rate * m_particle.instanceOverride.rate->value->getFloat ();
- glm::vec3 transformedEmitterOrigin = emitter.origin;
- transformedEmitterOrigin.y = -transformedEmitterOrigin.y;
- int controlPointIndex = emitter.controlPoint;
- if (controlPointIndex == -1 && !m_particle.controlPoints.empty ()) {
- const auto& cp0 = m_particle.controlPoints[0];
- if ((cp0.flags & 1) != 0) {
- controlPointIndex = 0;
- }
- }
- glm::vec3 flippedDirections = emitter.directions;
- flippedDirections.y = -flippedDirections.y;
- bool limitOnePerFrame = (emitter.flags & 2) != 0;
- bool randomPeriodicEmission = (emitter.flags & 4) != 0;
- return
- [this, emitter, transformedEmitterOrigin, controlPointIndex, rate, flippedDirections, limitOnePerFrame,
- randomPeriodicEmission, emissionTimer = 0.0f, delayTimer = emitter.delay, durationTimer = 0.0f,
- periodicTimer = 0.0f, periodicDuration = 0.0f, periodicDelay = 0.0f, emitting = false,
- instantaneousEmitted = false] (std::vector<ParticleInstance>& particles, uint32_t& count, float dt) mutable {
- if (count >= particles.size ()) {
- return;
- }
- if (delayTimer > 0.0f) {
- delayTimer -= dt;
- return;
- }
- if (emitter.duration > 0.0f) {
- durationTimer += dt;
- if (durationTimer >= emitter.duration) {
- return;
- }
- }
- if (randomPeriodicEmission) {
- periodicTimer += dt;
- if (!emitting) {
- if (periodicTimer >= periodicDelay) {
- emitting = true;
- periodicTimer = 0.0f;
- periodicDuration = WallpaperEngine::Maths::randomFloat (
- m_rng, emitter.minPeriodicDuration, emitter.maxPeriodicDuration
- );
- } else {
- return;
- }
- } else {
- if (periodicTimer >= periodicDuration) {
- emitting = false;
- periodicTimer = 0.0f;
- periodicDelay = WallpaperEngine::Maths::randomFloat (
- m_rng, emitter.minPeriodicDelay, emitter.maxPeriodicDelay
- );
- return;
- }
- }
- }
- // TODO: audio processing (audioProcessingMode, audioProcessingBounds, etc.)
- uint32_t toEmit = 0;
- if (emitter.instantaneous > 0 && !instantaneousEmitted) {
- toEmit = emitter.instantaneous;
- instantaneousEmitted = true;
- }
- if (emitter.rate > 0.0f) {
- emissionTimer += dt * rate;
- uint32_t rateEmit = static_cast<uint32_t> (emissionTimer);
- emissionTimer -= static_cast<float> (rateEmit);
- // limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
- if (limitOnePerFrame && rateEmit > 1) {
- rateEmit = 1;
- }
- toEmit += rateEmit;
- }
- for (uint32_t i = 0; i < toEmit && count < particles.size (); i++) {
- auto& p = particles[count];
- glm::vec3 spawnOrigin = transformedEmitterOrigin;
- if (controlPointIndex >= 0 && controlPointIndex < static_cast<int> (m_controlPoints.size ())) {
- spawnOrigin += m_controlPoints[controlPointIndex].position;
- }
- // Random position within the box volume (hollow box if distanceMin > 0)
- glm::vec3 randomPos;
- for (int axis = 0; axis < 3; axis++) {
- float minDist = emitter.distanceMin[axis];
- float maxDist = emitter.distanceMax[axis];
- float dist = WallpaperEngine::Maths::randomFloat (m_rng, minDist, maxDist);
- // Randomly flip sign to center the distribution
- if (WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f) < 0.5f) {
- dist = -dist;
- }
- randomPos[axis] = dist;
- }
- randomPos *= flippedDirections;
- p.position = spawnOrigin + randomPos;
- // Emitter does not set velocity - initializers handle that
- p.velocity = glm::vec3 (0.0f);
- p.acceleration = glm::vec3 (0.0f);
- p.rotation = glm::vec3 (0.0f);
- p.angularVelocity = glm::vec3 (0.0f);
- p.angularAcceleration = glm::vec3 (0.0f);
- p.color = glm::vec3 (1.0f) * m_particle.instanceOverride.colorn->value->getVec3 ();
- p.alpha = 1.0f * m_particle.instanceOverride.alpha->value->getFloat ();
- p.size = 20.0f * m_particle.instanceOverride.size->value->getFloat ();
- p.lifetime = 1.0f * m_particle.instanceOverride.lifetime->value->getFloat ();
- p.age = 0.0f;
- p.alive = true;
- p.frame = -1.0f;
- p.initial.color = p.color;
- p.initial.alpha = p.alpha;
- p.initial.size = p.size;
- p.initial.lifetime = p.lifetime;
- // Reset oscillator state for reused particles
- p.oscillateAlpha = {};
- p.oscillateSize = {};
- p.oscillatePosition = {};
- for (auto& init : m_initializers) {
- init (p);
- }
- count++;
- }
- };
- }
- EmitterFunc CParticle::createSphereEmitter (const ParticleEmitter& emitter) {
- float rate = emitter.rate * m_particle.instanceOverride.rate->value->getFloat ();
- float lifetime = 1.0f * m_particle.instanceOverride.lifetime->value->getFloat ();
- // Convert emitter origin from screen space (Y down) to centered space (Y up)
- glm::vec3 transformedEmitterOrigin = emitter.origin;
- transformedEmitterOrigin.y = -transformedEmitterOrigin.y;
- int controlPointIndex = emitter.controlPoint;
- // Auto-detect control point 0 if not specified and CP0 has linkMouse
- if (controlPointIndex == -1 && !m_particle.controlPoints.empty ()) {
- const auto& cp0 = m_particle.controlPoints[0];
- if ((cp0.flags & 1) != 0) { // bit 0 = linkMouse
- controlPointIndex = 0;
- }
- }
- bool limitOnePerFrame = (emitter.flags & 2) != 0;
- return [this, emitter, transformedEmitterOrigin, controlPointIndex, rate, lifetime, limitOnePerFrame,
- emissionTimer = 0.0f,
- remaining
- = emitter.instantaneous] (std::vector<ParticleInstance>& particles, uint32_t& count, float dt) mutable {
- if (count >= particles.size ()) {
- return;
- }
- emissionTimer += dt * rate;
- uint32_t toEmit = static_cast<uint32_t> (emissionTimer);
- emissionTimer -= static_cast<float> (toEmit);
- // limitOnePerFrame (flags bit 1): cap at 1 to prevent rope artifacts
- if (limitOnePerFrame && toEmit > 1) {
- toEmit = 1;
- }
- if (remaining > 0) {
- toEmit = remaining;
- remaining = 0;
- }
- for (uint32_t i = 0; i < toEmit && count < particles.size (); i++) {
- auto& p = particles[count];
- glm::vec3 spawnOrigin = transformedEmitterOrigin;
- if (controlPointIndex >= 0 && controlPointIndex < static_cast<int> (m_controlPoints.size ())) {
- spawnOrigin += m_controlPoints[controlPointIndex].position;
- }
- glm::vec3 randomPos;
- // flags & 4 == 0: orthographic particles use a 2D disk distribution in X/Y
- // flags & 4 != 0: perspective particles use a 3D spherical shell distribution
- if ((m_particle.flags & 4) == 0) {
- float angle = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, glm::two_pi<float> ());
- float minRadius = emitter.distanceMin.x;
- float maxRadius = emitter.distanceMax.x;
- // Use sqrt for uniform area distribution in annulus
- float minRadiusSq = minRadius * minRadius;
- float maxRadiusSq = maxRadius * maxRadius;
- float radiusXY = std::sqrt (WallpaperEngine::Maths::randomFloat (m_rng, minRadiusSq, maxRadiusSq));
- randomPos = glm::vec3 (
- radiusXY * std::cos (angle), radiusXY * std::sin (angle),
- WallpaperEngine::Maths::randomFloat (m_rng, -maxRadius, maxRadius)
- );
- randomPos *= emitter.directions;
- } else {
- float theta = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, glm::two_pi<float> ());
- float cosTheta = WallpaperEngine::Maths::randomFloat (m_rng, -1.0f, 1.0f);
- float sinTheta = std::sqrt (1.0f - cosTheta * cosTheta);
- randomPos = glm::vec3 (sinTheta * std::cos (theta), sinTheta * std::sin (theta), cosTheta);
- // Use cubic root for uniform volume distribution
- float minRadius = emitter.distanceMin.x;
- float maxRadius = emitter.distanceMax.x;
- float minRadiusCubed = minRadius * minRadius * minRadius;
- float maxRadiusCubed = maxRadius * maxRadius * maxRadius;
- float radius = std::cbrt (WallpaperEngine::Maths::randomFloat (m_rng, minRadiusCubed, maxRadiusCubed));
- randomPos *= radius;
- randomPos *= emitter.directions;
- }
- // sign property forces per-axis polarity: 0 = both, 1 = positive only, -1 = negative only
- for (int i = 0; i < 3; i++) {
- if (emitter.sign[i] == 1) {
- randomPos[i] = std::abs (randomPos[i]);
- } else if (emitter.sign[i] == -1) {
- randomPos[i] = -std::abs (randomPos[i]);
- }
- }
- p.position = spawnOrigin + randomPos;
- // Set velocity only if emitter specifies speed (otherwise use initializers)
- if (emitter.speedMax > 0.0f || emitter.speedMin != 0.0f) {
- // Velocity pointing outward from ellipsoid (randomPos already includes directions scaling)
- glm::vec3 direction
- = glm::length (randomPos) > 0.0f ? glm::normalize (randomPos) : glm::vec3 (0.0f, 1.0f, 0.0f);
- float speed = WallpaperEngine::Maths::randomFloat (m_rng, emitter.speedMin, emitter.speedMax);
- p.velocity = direction * speed;
- } else {
- p.velocity = glm::vec3 (0.0f);
- }
- p.acceleration = glm::vec3 (0.0f);
- p.rotation = glm::vec3 (0.0f);
- p.angularVelocity = glm::vec3 (0.0f);
- p.angularAcceleration = glm::vec3 (0.0f);
- p.color = glm::vec3 (1.0f) * m_particle.instanceOverride.colorn->value->getVec3 ();
- p.alpha = 1.0f * m_particle.instanceOverride.alpha->value->getFloat ();
- p.size = 20.0f * m_particle.instanceOverride.size->value->getFloat ();
- p.lifetime = lifetime;
- p.age = 0.0f;
- p.alive = true;
- p.frame = -1.0f;
- p.initial.color = p.color;
- p.initial.alpha = p.alpha;
- p.initial.size = p.size;
- p.initial.lifetime = p.lifetime;
- // Reset oscillator state for reused particles
- p.oscillateAlpha = {};
- p.oscillateSize = {};
- p.oscillatePosition = {};
- for (auto& init : m_initializers) {
- init (p);
- }
- count++;
- }
- };
- }
- // ========== INITIALIZERS ==========
- void CParticle::setupInitializers () {
- for (const auto& initializer : m_particle.initializers) {
- if (!initializer) {
- continue;
- }
- InitializerFunc func;
- if (initializer->is<ColorRandomInitializer> ()) {
- func = createColorRandomInitializer (*initializer->as<ColorRandomInitializer> ());
- } else if (initializer->is<SizeRandomInitializer> ()) {
- func = createSizeRandomInitializer (*initializer->as<SizeRandomInitializer> ());
- } else if (initializer->is<AlphaRandomInitializer> ()) {
- func = createAlphaRandomInitializer (*initializer->as<AlphaRandomInitializer> ());
- } else if (initializer->is<LifetimeRandomInitializer> ()) {
- const auto& lifeInit = *initializer->as<LifetimeRandomInitializer> ();
- m_uniformLifetimes = (lifeInit.min->value->getFloat () == lifeInit.max->value->getFloat ());
- func = createLifetimeRandomInitializer (lifeInit);
- } else if (initializer->is<VelocityRandomInitializer> ()) {
- func = createVelocityRandomInitializer (*initializer->as<VelocityRandomInitializer> ());
- } else if (initializer->is<RotationRandomInitializer> ()) {
- func = createRotationRandomInitializer (*initializer->as<RotationRandomInitializer> ());
- } else if (initializer->is<AngularVelocityRandomInitializer> ()) {
- func = createAngularVelocityRandomInitializer (*initializer->as<AngularVelocityRandomInitializer> ());
- } else if (initializer->is<TurbulentVelocityRandomInitializer> ()) {
- func = createTurbulentVelocityRandomInitializer (*initializer->as<TurbulentVelocityRandomInitializer> ());
- } else if (initializer->is<MapSequenceAroundControlPointInitializer> ()) {
- func = createMapSequenceAroundControlPointInitializer (
- *initializer->as<MapSequenceAroundControlPointInitializer> ()
- );
- } else {
- sLog.out ("Unknown initializer type");
- }
- if (func) {
- m_initializers.push_back (std::move (func));
- }
- }
- }
- InitializerFunc CParticle::createColorRandomInitializer (const ColorRandomInitializer& init) {
- DynamicValue* minValue = init.min->value.get ();
- DynamicValue* maxValue = init.max->value.get ();
- DynamicValue* colorOverride = m_particle.instanceOverride.colorn->value.get ();
- return [this, minValue, maxValue, colorOverride] (ParticleInstance& p) {
- p.color = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
- * colorOverride->getVec3 ();
- p.initial.color = p.color;
- };
- }
- InitializerFunc CParticle::createSizeRandomInitializer (const SizeRandomInitializer& init) {
- DynamicValue* minValue = init.min->value.get ();
- DynamicValue* maxValue = init.max->value.get ();
- DynamicValue* exponentValue = init.exponent->value.get ();
- DynamicValue* sizeOverride = m_particle.instanceOverride.size->value.get ();
- return [this, minValue, maxValue, exponentValue, sizeOverride] (ParticleInstance& p) {
- float t = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f);
- float exponent = exponentValue->getFloat ();
- float min = minValue->getFloat ();
- float max = maxValue->getFloat ();
- // Apply exponent for non-linear distribution
- float adjustedT = std::pow (t, exponent);
- p.size = (min + adjustedT * (max - min)) * sizeOverride->getFloat () / 2.0f;
- p.initial.size = p.size;
- };
- }
- InitializerFunc CParticle::createAlphaRandomInitializer (const AlphaRandomInitializer& init) {
- DynamicValue* minValue = init.min->value.get ();
- DynamicValue* maxValue = init.max->value.get ();
- DynamicValue* alphaOverride = m_particle.instanceOverride.alpha->value.get ();
- return [this, minValue, maxValue, alphaOverride] (ParticleInstance& p) {
- p.alpha = WallpaperEngine::Maths::randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ())
- * alphaOverride->getFloat ();
- p.initial.alpha = p.alpha;
- };
- }
- InitializerFunc CParticle::createLifetimeRandomInitializer (const LifetimeRandomInitializer& init) {
- DynamicValue* minValue = init.min->value.get ();
- DynamicValue* maxValue = init.max->value.get ();
- DynamicValue* lifetimeOverride = m_particle.instanceOverride.lifetime->value.get ();
- return [this, minValue, maxValue, lifetimeOverride] (ParticleInstance& p) {
- p.lifetime = WallpaperEngine::Maths::randomFloat (m_rng, minValue->getFloat (), maxValue->getFloat ())
- * lifetimeOverride->getFloat ();
- p.initial.lifetime = p.lifetime;
- };
- }
- InitializerFunc CParticle::createVelocityRandomInitializer (const VelocityRandomInitializer& init) {
- DynamicValue* minValue = init.min->value.get ();
- DynamicValue* maxValue = init.max->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- return [this, minValue, maxValue, speedOverride] (ParticleInstance& p) {
- glm::vec3 vel = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
- * speedOverride->getFloat ();
- vel.y = -vel.y;
- p.velocity += vel;
- };
- }
- InitializerFunc CParticle::createRotationRandomInitializer (const RotationRandomInitializer& init) {
- DynamicValue* minValue = init.min->value.get ();
- DynamicValue* maxValue = init.max->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- return [this, minValue, maxValue, speedOverride] (ParticleInstance& p) {
- p.rotation = WallpaperEngine::Maths::randomVec3 (m_rng, minValue->getVec3 (), maxValue->getVec3 ())
- * speedOverride->getFloat ();
- };
- }
- InitializerFunc CParticle::createAngularVelocityRandomInitializer (const AngularVelocityRandomInitializer& init) {
- DynamicValue* minValue = init.min->value.get ();
- DynamicValue* maxValue = init.max->value.get ();
- DynamicValue* exponentValue = init.exponent->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- return [this, minValue, maxValue, exponentValue, speedOverride] (ParticleInstance& p) {
- glm::vec3 minVec = minValue->getVec3 ();
- glm::vec3 maxVec = maxValue->getVec3 ();
- float exponent = exponentValue->getFloat ();
- // exponent = 1: uniform; exponent -> 0: bias towards max; exponent >= 2: bias towards min
- glm::vec3 result;
- for (int i = 0; i < 3; i++) {
- float t = WallpaperEngine::Maths::randomFloat (m_rng, 0.0f, 1.0f);
- t = std::pow (t, exponent);
- result[i] = minVec[i] + t * (maxVec[i] - minVec[i]);
- }
- p.angularVelocity = result * speedOverride->getFloat ();
- };
- }
- InitializerFunc CParticle::createTurbulentVelocityRandomInitializer (const TurbulentVelocityRandomInitializer& init) {
- DynamicValue* speedMin = init.speedMin->value.get ();
- DynamicValue* speedMax = init.speedMax->value.get ();
- DynamicValue* offsetVal = init.offset->value.get ();
- DynamicValue* scaleVal = init.scale->value.get ();
- DynamicValue* forwardVal = init.forward->value.get ();
- DynamicValue* timeScaleVal = init.timeScale->value.get ();
- DynamicValue* phaseMinVal = init.phaseMin->value.get ();
- DynamicValue* phaseMaxVal = init.phaseMax->value.get ();
- DynamicValue* rightVal = init.right->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- return [this, speedMin, speedMax, offsetVal, scaleVal, forwardVal, timeScaleVal, phaseMinVal, phaseMaxVal, rightVal,
- speedOverride] (ParticleInstance& p) {
- glm::vec3 forward = forwardVal->getVec3 ();
- glm::vec3 right = rightVal->getVec3 ();
- // Y-flip for coordinate system conversion
- forward.y = -forward.y;
- right.y = -right.y;
- if (glm::length (forward) > 0.0001f) {
- forward = glm::normalize (forward);
- } else {
- // Default forward direction when not specified (up in centered space)
- forward = glm::vec3 (0.0f, 1.0f, 0.0f);
- }
- if (glm::length (right) > 0.0001f) {
- right = glm::normalize (right);
- } else {
- right = glm::vec3 (1.0f, 0.0f, 0.0f);
- }
- float speed = WallpaperEngine::Maths::randomFloat (m_rng, speedMin->getFloat (), speedMax->getFloat ());
- float scale = scaleVal->getFloat ();
- float offset = offsetVal->getFloat ();
- float timeScale = timeScaleVal->getFloat ();
- float phaseMin = phaseMinVal->getFloat ();
- float phaseMax = phaseMaxVal->getFloat ();
- // Sample noise at position + time offset: timescale shifts the field over time so
- // particles spawned at different times drift differently (evolving vapor stream);
- // the position term gives spatial coherence between nearby particles.
- glm::vec3 noisePos = p.position * 0.1f;
- noisePos += glm::vec3 (static_cast<float> (m_time) * timeScale);
- // Phase adds per-particle randomization to noise position
- float phase = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax);
- glm::vec3 samplePos = noisePos + glm::vec3 (phase, phase * 0.7f, phase * 1.3f);
- glm::vec3 result = curlNoise (samplePos);
- float len = glm::length (result);
- if (len < 0.0001f) {
- result = forward;
- } else {
- result = result / len;
- }
- // Scale limits how far direction can deviate from forward
- if (scale < 2.0f) {
- float cosAngle = glm::dot (result, forward);
- float angle = std::acos (glm::clamp (cosAngle, -1.0f, 1.0f)) / glm::pi<float> ();
- float maxAngle = scale / 2.0f;
- if (angle > maxAngle && maxAngle > 0.0001f) {
- glm::vec3 axis = glm::cross (result, forward);
- float axisLen = glm::length (axis);
- if (axisLen > 0.0001f) {
- axis = axis / axisLen;
- float rotAngle = (angle - maxAngle) * glm::pi<float> ();
- glm::mat3 rot = glm::mat3 (glm::rotate (glm::mat4 (1.0f), rotAngle, axis));
- result = rot * result;
- }
- }
- }
- // Offset rotates result around right axis (tilts up/down)
- if (std::abs (offset) > 0.0001f) {
- glm::mat3 rot = glm::mat3 (glm::rotate (glm::mat4 (1.0f), -offset, right));
- result = rot * result;
- }
- // 2D/orthographic particles (flags & 4 == 0): project onto XY. curlNoise is 3D but
- // z-drift is meaningless here and makes rope segments diverge in depth.
- if ((m_particle.flags & 4) == 0) {
- result.z = 0.0f;
- float len2d = glm::length (result);
- if (len2d > 0.0001f) {
- result /= len2d;
- }
- }
- glm::vec3 finalVel = result * speed * speedOverride->getFloat ();
- p.velocity += finalVel;
- };
- }
- InitializerFunc
- CParticle::createMapSequenceAroundControlPointInitializer (const MapSequenceAroundControlPointInitializer& init) {
- DynamicValue* controlPointValue = init.controlPoint->value.get ();
- DynamicValue* countValue = init.count->value.get ();
- DynamicValue* speedMinValue = init.speedMin->value.get ();
- DynamicValue* speedMaxValue = init.speedMax->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- // Sequence counter is shared (closure state) across all particles spawned by this
- // initializer, giving each one a distinct angle around the circle
- int sequenceIndex = 0;
- return [this, controlPointValue, countValue, speedMinValue, speedMaxValue, sequenceIndex,
- speedOverride] (ParticleInstance& p) mutable {
- int controlPoint = static_cast<int> (controlPointValue->getFloat ());
- int count = static_cast<int> (countValue->getFloat ());
- if (count < 1) {
- count = 1;
- }
- float angle = (static_cast<float> (sequenceIndex) / static_cast<float> (count)) * glm::two_pi<float> ();
- sequenceIndex = (sequenceIndex + 1) % count;
- glm::vec3 centerPos = glm::vec3 (0.0f);
- if (controlPoint >= 0 && controlPoint < static_cast<int> (m_controlPoints.size ())) {
- centerPos = m_controlPoints[controlPoint].position;
- }
- p.position = centerPos;
- glm::vec3 speedMin = speedMinValue->getVec3 ();
- glm::vec3 speedMax = speedMaxValue->getVec3 ();
- glm::vec3 speed = WallpaperEngine::Maths::randomVec3 (m_rng, speedMin, speedMax);
- // Flip Y before rotation to convert to centered space
- speed.y = -speed.y;
- // Rotating by the sequence angle gives the outward radial/circular pattern
- glm::mat3 rotationMatrix = glm::mat3 (
- std::cos (angle), -std::sin (angle), 0.0f, std::sin (angle), std::cos (angle), 0.0f, 0.0f, 0.0f, 1.0f
- );
- glm::vec3 rotatedSpeed = rotationMatrix * speed * speedOverride->getFloat ();
- p.velocity = rotatedSpeed;
- };
- }
- // ========== OPERATORS ==========
- void CParticle::setupOperators () {
- for (const auto& op : m_particle.operators) {
- if (!op) {
- continue;
- }
- OperatorFunc func;
- if (op->is<MovementOperator> ()) {
- func = createMovementOperator (*op->as<MovementOperator> ());
- } else if (op->is<AngularMovementOperator> ()) {
- func = createAngularMovementOperator (*op->as<AngularMovementOperator> ());
- } else if (op->is<AlphaFadeOperator> ()) {
- func = createAlphaFadeOperator (*op->as<AlphaFadeOperator> ());
- } else if (op->is<SizeChangeOperator> ()) {
- func = createSizeChangeOperator (*op->as<SizeChangeOperator> ());
- } else if (op->is<AlphaChangeOperator> ()) {
- func = createAlphaChangeOperator (*op->as<AlphaChangeOperator> ());
- } else if (op->is<ColorChangeOperator> ()) {
- func = createColorChangeOperator (*op->as<ColorChangeOperator> ());
- } else if (op->is<TurbulenceOperator> ()) {
- func = createTurbulenceOperator (*op->as<TurbulenceOperator> ());
- } else if (op->is<VortexOperator> ()) {
- func = createVortexOperator (*op->as<VortexOperator> ());
- } else if (op->is<ControlPointAttractOperator> ()) {
- func = createControlPointAttractOperator (*op->as<ControlPointAttractOperator> ());
- } else if (op->is<OscillateAlphaOperator> ()) {
- func = createOscillateAlphaOperator (*op->as<OscillateAlphaOperator> ());
- } else if (op->is<OscillateSizeOperator> ()) {
- func = createOscillateSizeOperator (*op->as<OscillateSizeOperator> ());
- } else if (op->is<OscillatePositionOperator> ()) {
- func = createOscillatePositionOperator (*op->as<OscillatePositionOperator> ());
- } else {
- sLog.out ("Unknown operator type");
- }
- if (func) {
- m_operators.push_back (std::move (func));
- }
- }
- }
- OperatorFunc CParticle::createMovementOperator (const MovementOperator& op) {
- DynamicValue* dragValue = op.drag->value.get ();
- DynamicValue* gravityValue = op.gravity->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- return [dragValue, gravityValue, speedOverride] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
- float dt
- ) {
- float speed = speedOverride->getFloat ();
- float drag = dragValue->getFloat ();
- glm::vec3 gravity = gravityValue->getVec3 ();
- // Flip gravity Y for centered space
- gravity.y = -gravity.y;
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- if (!p.alive) {
- continue;
- }
- // Integrate position from current velocity (already speed-scaled) before
- // updating velocity for next frame
- p.position += p.velocity * dt;
- p.velocity += gravity * dt * speed;
- // Drag decay, clamped so drag*dt > 1.0 can't reverse velocity
- float dragFactor = 1.0f - (drag * dt);
- if (dragFactor < 0.0f) {
- dragFactor = 0.0f;
- }
- p.velocity *= dragFactor;
- }
- };
- }
- OperatorFunc CParticle::createAngularMovementOperator (const AngularMovementOperator& op) {
- DynamicValue* dragValue = op.drag->value.get ();
- DynamicValue* forceValue = op.force->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- return [dragValue, forceValue, speedOverride] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
- float dt
- ) {
- float drag = dragValue->getFloat ();
- float speed = speedOverride->getFloat ();
- glm::vec3 force = forceValue->getVec3 ();
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- if (!p.alive) {
- continue;
- }
- p.rotation += p.angularVelocity * dt * speed;
- p.angularVelocity += force * dt * speed;
- // Positive drag slows down, negative speeds up; clamped so drag*dt > 1.0 can't reverse it
- float dragFactor = 1.0f - (drag * dt);
- if (dragFactor < 0.0f) {
- dragFactor = 0.0f;
- }
- p.angularVelocity *= dragFactor;
- // Wrap rotation to prevent floating-point precision issues
- const float pi = glm::pi<float> ();
- const float two_pi = glm::two_pi<float> ();
- for (int j = 0; j < 3; j++) {
- while (p.rotation[j] > pi) {
- p.rotation[j] -= two_pi;
- }
- while (p.rotation[j] < -pi) {
- p.rotation[j] += two_pi;
- }
- }
- }
- };
- }
- OperatorFunc CParticle::createAlphaFadeOperator (const AlphaFadeOperator& op) {
- DynamicValue* fadeInTimeValue = op.fadeInTime->value.get ();
- DynamicValue* fadeOutTimeValue = op.fadeOutTime->value.get ();
- return
- [fadeInTimeValue, fadeOutTimeValue] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
- ) {
- float fadeInTime = fadeInTimeValue->getFloat ();
- float fadeOutTime = fadeOutTimeValue->getFloat ();
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- if (!p.alive) {
- continue;
- }
- float life = p.getLifetimePos ();
- if (life <= fadeInTime) {
- float fade = WallpaperEngine::Maths::fadeValue (life, 0.0f, fadeInTime, 0.0f, 1.0f);
- p.alpha = p.initial.alpha * fade;
- } else if (life > fadeOutTime) {
- float fade = 1.0f - WallpaperEngine::Maths::fadeValue (life, fadeOutTime, 1.0f, 0.0f, 1.0f);
- p.alpha = p.initial.alpha * fade;
- } else {
- p.alpha = p.initial.alpha;
- }
- // Update oscillator base so oscillateAlpha combines properly
- p.oscillateAlpha.base = p.alpha;
- }
- };
- }
- OperatorFunc CParticle::createSizeChangeOperator (const SizeChangeOperator& op) {
- DynamicValue* startTimeValue = op.startTime->value.get ();
- DynamicValue* endTimeValue = op.endTime->value.get ();
- DynamicValue* startValueValue = op.startValue->value.get ();
- DynamicValue* endValueValue = op.endValue->value.get ();
- return
- [startTimeValue, endTimeValue, startValueValue, endValueValue] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
- ) {
- float startTime = startTimeValue->getFloat ();
- float endTime = endTimeValue->getFloat ();
- float startValue = startValueValue->getFloat ();
- float endValue = endValueValue->getFloat ();
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- if (!p.alive) {
- continue;
- }
- float life = p.getLifetimePos ();
- float multiplier = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue, endValue);
- p.size = p.initial.size * multiplier;
- // Update oscillator base so oscillateSize combines properly
- p.oscillateSize.base = p.size;
- }
- };
- }
- OperatorFunc CParticle::createAlphaChangeOperator (const AlphaChangeOperator& op) {
- DynamicValue* startTimeValue = op.startTime->value.get ();
- DynamicValue* endTimeValue = op.endTime->value.get ();
- DynamicValue* startValueValue = op.startValue->value.get ();
- DynamicValue* endValueValue = op.endValue->value.get ();
- return
- [startTimeValue, endTimeValue, startValueValue, endValueValue] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
- ) {
- float startTime = startTimeValue->getFloat ();
- float endTime = endTimeValue->getFloat ();
- float startValue = startValueValue->getFloat ();
- float endValue = endValueValue->getFloat ();
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- if (!p.alive) {
- continue;
- }
- float life = p.getLifetimePos ();
- float multiplier = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue, endValue);
- p.alpha = p.initial.alpha * multiplier;
- // Update oscillator base so oscillateAlpha combines properly
- p.oscillateAlpha.base = p.alpha;
- }
- };
- }
- OperatorFunc CParticle::createColorChangeOperator (const ColorChangeOperator& op) {
- DynamicValue* startTimeValue = op.startTime->value.get ();
- DynamicValue* endTimeValue = op.endTime->value.get ();
- DynamicValue* startValueValue = op.startValue->value.get ();
- DynamicValue* endValueValue = op.endValue->value.get ();
- return
- [startTimeValue, endTimeValue, startValueValue, endValueValue] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
- ) {
- float startTime = startTimeValue->getFloat ();
- float endTime = endTimeValue->getFloat ();
- glm::vec3 startValue = startValueValue->getVec3 ();
- glm::vec3 endValue = endValueValue->getVec3 ();
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- if (!p.alive) {
- continue;
- }
- float life = p.getLifetimePos ();
- glm::vec3 color;
- color.r = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.r, endValue.r);
- color.g = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.g, endValue.g);
- color.b = WallpaperEngine::Maths::fadeValue (life, startTime, endTime, startValue.b, endValue.b);
- p.color = p.initial.color * color;
- }
- };
- }
- OperatorFunc CParticle::createTurbulenceOperator (const TurbulenceOperator& op) {
- DynamicValue* scaleValue = op.scale->value.get ();
- DynamicValue* speedMinValue = op.speedMin->value.get ();
- DynamicValue* speedMaxValue = op.speedMax->value.get ();
- DynamicValue* timeScaleValue = op.timeScale->value.get ();
- DynamicValue* maskValue = op.mask->value.get ();
- DynamicValue* phaseMinValue = op.phaseMin->value.get ();
- DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- // TODO: audio processing support (audioProcessingMode/Bounds/Exponent/FrequencyStart/FrequencyEnd)
- // Phase and speed are randomized once per operator instance, not per particle
- const float phase
- = WallpaperEngine::Maths::randomFloat (m_rng, phaseMinValue->getFloat (), phaseMaxValue->getFloat ());
- const float turbSpeed
- = WallpaperEngine::Maths::randomFloat (m_rng, speedMinValue->getFloat (), speedMaxValue->getFloat ());
- return [scaleValue, timeScaleValue, maskValue, speedOverride, phase, turbSpeed] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&,
- float currentTime, float dt
- ) {
- const float noiseScale = scaleValue->getFloat () * 2.0f;
- const float timeScale = timeScaleValue->getFloat ();
- const glm::vec3 mask = maskValue->getVec3 ();
- const float speed = speedOverride->getFloat ();
- if (turbSpeed <= 0.0001f) {
- return;
- }
- for (size_t i = 0; i < count; ++i) {
- ParticleInstance& p = particles[i];
- if (!p.alive) {
- continue;
- }
- glm::vec3 noisePos = p.position;
- noisePos.x += phase + timeScale * currentTime;
- noisePos *= noiseScale;
- glm::vec3 curlDir = curlNoise (noisePos);
- const float len = glm::length (curlDir);
- if (len > 0.0001f) {
- curlDir = (curlDir / len) * turbSpeed;
- }
- curlDir *= mask;
- p.velocity += curlDir * dt * speed;
- }
- };
- }
- OperatorFunc CParticle::createVortexOperator (const VortexOperator& op) {
- int controlPoint = op.controlPoint;
- int flags = op.flags;
- DynamicValue* axisValue = op.axis->value.get ();
- DynamicValue* offsetValue = op.offset->value.get ();
- DynamicValue* distanceInnerValue = op.distanceInner->value.get ();
- DynamicValue* distanceOuterValue = op.distanceOuter->value.get ();
- DynamicValue* speedInnerValue = op.speedInner->value.get ();
- DynamicValue* speedOuterValue = op.speedOuter->value.get ();
- DynamicValue* centerForceValue = op.centerForce->value.get ();
- DynamicValue* ringRadiusValue = op.ringRadius->value.get ();
- DynamicValue* ringWidthValue = op.ringWidth->value.get ();
- DynamicValue* ringPullDistanceValue = op.ringPullDistance->value.get ();
- DynamicValue* ringPullForceValue = op.ringPullForce->value.get ();
- DynamicValue* audioModeValue = op.audioProcessingMode->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- int audioMode = static_cast<int> (audioModeValue->getFloat ());
- bool infiniteAxis = (flags & 1) != 0;
- bool maintainDistance = (flags & 2) != 0;
- bool ringShape = (flags & 4) != 0;
- return [controlPoint, axisValue, offsetValue, distanceInnerValue, distanceOuterValue, speedInnerValue,
- speedOuterValue, centerForceValue, ringRadiusValue, ringWidthValue, ringPullDistanceValue,
- ringPullForceValue, audioMode, infiniteAxis, maintainDistance, ringShape, speedOverride] (
- std::vector<ParticleInstance>& particles, uint32_t count,
- const std::vector<ControlPointData>& controlPoints, float, float dt
- ) {
- float audioAmplitude = 0.0f; // TODO: sample from AudioContext once audio processing is implemented
- // Audio mode enabled but no audio available yet - skip vortex entirely
- if (audioMode > 0 && audioAmplitude == 0.0f) {
- return;
- }
- glm::vec3 axis = axisValue->getVec3 ();
- glm::vec3 offset = offsetValue->getVec3 ();
- float distanceInner = distanceInnerValue->getFloat ();
- float distanceOuter = distanceOuterValue->getFloat ();
- float speedInner = speedInnerValue->getFloat ();
- float speedOuter = speedOuterValue->getFloat ();
- float centerForce = centerForceValue->getFloat ();
- float ringRadius = ringRadiusValue->getFloat ();
- float ringWidth = ringWidthValue->getFloat ();
- float ringPullDistance = ringPullDistanceValue->getFloat ();
- float ringPullForce = ringPullForceValue->getFloat ();
- if (audioMode > 0) {
- speedInner *= (1.0f + audioAmplitude);
- speedOuter *= (1.0f + audioAmplitude);
- }
- glm::vec3 center = glm::vec3 (0.0f);
- if (controlPoint >= 0 && controlPoint < static_cast<int> (controlPoints.size ())) {
- center = controlPoints[controlPoint].position + offset;
- } else {
- center = offset;
- }
- if (glm::length (axis) > 0.0f) {
- axis = glm::normalize (axis);
- } else {
- axis = glm::vec3 (0.0f, 0.0f, 1.0f);
- }
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- if (!p.alive) {
- continue;
- }
- glm::vec3 toParticle = p.position - center;
- // infiniteAxis: project onto the plane perpendicular to axis (cylinder shape);
- // otherwise use full 3D distance (sphere shape)
- float axialDistance = 0.0f;
- glm::vec3 radialVector = toParticle;
- if (infiniteAxis) {
- axialDistance = glm::dot (toParticle, axis);
- radialVector = toParticle - axis * axialDistance;
- }
- float distance = glm::length (radialVector);
- glm::vec3 tangent = glm::cross (axis, radialVector);
- if (glm::length (tangent) > 0.001f) {
- tangent = glm::normalize (tangent);
- } else {
- continue; // particle is on the axis
- }
- float speed = 0.0f;
- glm::vec3 radialForce = glm::vec3 (0.0f);
- if (ringShape) {
- // Ring mode: hollow center with ring-shaped influence zone
- float ringInner = ringRadius - ringWidth * 0.5f;
- float ringOuter = ringRadius + ringWidth * 0.5f;
- if (distance < ringInner) {
- // Inside the ring's hollow center - no spin, but may be pulled outward
- speed = 0.0f;
- } else if (distance <= ringOuter) {
- // Inside the ring - full effect
- float t = (distance - ringInner) / ringWidth;
- speed = glm::mix (speedInner, speedOuter, t);
- } else if (distance <= ringOuter + ringPullDistance) {
- // Outside ring but within pull distance - attract toward ring
- float pullT = (distance - ringOuter) / ringPullDistance;
- speed = speedOuter * (1.0f - pullT);
- if (distance > 0.001f) {
- glm::vec3 towardRing = -glm::normalize (radialVector);
- radialForce = towardRing * ringPullForce * pullT;
- }
- } else {
- // Too far from ring - no effect
- speed = 0.0f;
- }
- } else {
- // Standard vortex mode
- float disMid = distanceOuter - distanceInner + 0.1f;
- if (disMid < 0 || distance < distanceInner) {
- speed = speedInner;
- } else if (distance > distanceOuter) {
- speed = speedOuter;
- } else {
- float t = (distance - distanceInner) / disMid;
- speed = glm::mix (speedInner, speedOuter, t);
- }
- }
- p.velocity += tangent * speed * dt * speedOverride->getFloat ();
- p.velocity += radialForce * dt * speedOverride->getFloat ();
- if (maintainDistance && distance > 0.001f) {
- glm::vec3 towardCenter = -glm::normalize (radialVector);
- p.velocity += towardCenter * centerForce * dt * speedOverride->getFloat ();
- }
- }
- };
- }
- OperatorFunc CParticle::createControlPointAttractOperator (const ControlPointAttractOperator& op) {
- int controlPoint = op.controlPoint;
- DynamicValue* originValue = op.origin->value.get ();
- DynamicValue* scaleValue = op.scale->value.get ();
- DynamicValue* thresholdValue = op.threshold->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- return [controlPoint, originValue, scaleValue, thresholdValue, speedOverride] (
- std::vector<ParticleInstance>& particles, uint32_t count,
- const std::vector<ControlPointData>& controlPoints, float currentTime, float dt
- ) {
- glm::vec3 origin = originValue->getVec3 ();
- float scale = scaleValue->getFloat ();
- float threshold = thresholdValue->getFloat () / 2.0f;
- if (controlPoint < 0 || controlPoint >= static_cast<int> (controlPoints.size ())) {
- return;
- }
- glm::vec3 center = controlPoints[controlPoint].position + origin;
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- if (!p.alive) {
- continue;
- }
- glm::vec3 toCenter = center - p.position;
- float distance = glm::length (toCenter);
- if (distance > 0.001f && distance < threshold) {
- glm::vec3 direction = toCenter / distance;
- glm::vec3 forceVec = direction * scale * dt;
- p.velocity += forceVec * speedOverride->getFloat ();
- }
- }
- };
- }
- OperatorFunc CParticle::createOscillateAlphaOperator (const OscillateAlphaOperator& op) {
- DynamicValue* freqMinValue = op.frequencyMin->value.get ();
- DynamicValue* freqMaxValue = op.frequencyMax->value.get ();
- DynamicValue* scaleMinValue = op.scaleMin->value.get ();
- DynamicValue* scaleMaxValue = op.scaleMax->value.get ();
- DynamicValue* phaseMinValue = op.phaseMin->value.get ();
- DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
- return
- [this, freqMinValue, freqMaxValue, scaleMinValue, scaleMaxValue, phaseMinValue, phaseMaxValue] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
- ) {
- float freqMin = freqMinValue->getFloat ();
- float freqMax = freqMaxValue->getFloat ();
- float scaleMin = scaleMinValue->getFloat ();
- float scaleMax = scaleMaxValue->getFloat ();
- float phaseMin = phaseMinValue->getFloat ();
- float phaseMax = phaseMaxValue->getFloat ();
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- // Initialize per-particle oscillator values on first use
- if (!p.oscillateAlpha.initialized) {
- p.oscillateAlpha.frequency = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
- p.oscillateAlpha.scale = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
- p.oscillateAlpha.phase
- = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
- p.oscillateAlpha.base = p.alpha;
- p.oscillateAlpha.initialized = true;
- }
- // Cosine wave interpolating between scaleMin and scaleMax
- float w = p.oscillateAlpha.frequency;
- float t = p.age;
- float cosVal = (std::cos (w * t + p.oscillateAlpha.phase) + 1.0f) * 0.5f;
- float multiplier = glm::mix (scaleMin, scaleMax, cosVal);
- // Apply to base value (alphafade updates base each frame if present)
- p.alpha = p.oscillateAlpha.base * multiplier;
- }
- };
- }
- OperatorFunc CParticle::createOscillateSizeOperator (const OscillateSizeOperator& op) {
- DynamicValue* freqMinValue = op.frequencyMin->value.get ();
- DynamicValue* freqMaxValue = op.frequencyMax->value.get ();
- DynamicValue* scaleMinValue = op.scaleMin->value.get ();
- DynamicValue* scaleMaxValue = op.scaleMax->value.get ();
- DynamicValue* phaseMinValue = op.phaseMin->value.get ();
- DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
- return
- [this, freqMinValue, freqMaxValue, scaleMinValue, scaleMaxValue, phaseMinValue, phaseMaxValue] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float, float
- ) {
- float freqMin = freqMinValue->getFloat ();
- float freqMax = freqMaxValue->getFloat ();
- float scaleMin = scaleMinValue->getFloat ();
- float scaleMax = scaleMaxValue->getFloat ();
- float phaseMin = phaseMinValue->getFloat ();
- float phaseMax = phaseMaxValue->getFloat ();
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- // Initialize per-particle oscillator values on first use
- if (!p.oscillateSize.initialized) {
- p.oscillateSize.frequency = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
- p.oscillateSize.scale = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
- p.oscillateSize.phase
- = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
- p.oscillateSize.base = p.size;
- p.oscillateSize.initialized = true;
- }
- // Cosine wave interpolating between scaleMin and scaleMax
- float w = p.oscillateSize.frequency;
- float t = p.age;
- float cosVal = (std::cos (w * t + p.oscillateSize.phase) + 1.0f) * 0.5f;
- float multiplier = glm::mix (scaleMin, scaleMax, cosVal);
- // Apply to base value (sizeChange updates base each frame if present)
- p.size = p.oscillateSize.base * multiplier;
- }
- };
- }
- OperatorFunc CParticle::createOscillatePositionOperator (const OscillatePositionOperator& op) {
- DynamicValue* freqMinValue = op.frequencyMin->value.get ();
- DynamicValue* freqMaxValue = op.frequencyMax->value.get ();
- DynamicValue* scaleMinValue = op.scaleMin->value.get ();
- DynamicValue* scaleMaxValue = op.scaleMax->value.get ();
- DynamicValue* phaseMinValue = op.phaseMin->value.get ();
- DynamicValue* phaseMaxValue = op.phaseMax->value.get ();
- DynamicValue* maskValue = op.mask->value.get ();
- DynamicValue* speedOverride = m_particle.instanceOverride.speed->value.get ();
- return [this, freqMinValue, freqMaxValue, scaleMinValue, scaleMaxValue, phaseMinValue, phaseMaxValue, maskValue,
- speedOverride] (
- std::vector<ParticleInstance>& particles, uint32_t count, const std::vector<ControlPointData>&, float,
- float dt
- ) {
- float freqMin = freqMinValue->getFloat ();
- float freqMax = freqMaxValue->getFloat ();
- float scaleMin = scaleMinValue->getFloat ();
- float scaleMax = scaleMaxValue->getFloat ();
- float phaseMin = phaseMinValue->getFloat ();
- float phaseMax = phaseMaxValue->getFloat ();
- glm::vec3 mask = maskValue->getVec3 ();
- for (uint32_t i = 0; i < count; i++) {
- auto& p = particles[i];
- // Initialize per-particle oscillator values on first use (per axis)
- if (!p.oscillatePosition.initialized) {
- for (int axis = 0; axis < 3; axis++) {
- p.oscillatePosition.frequency[axis] = WallpaperEngine::Maths::randomFloat (m_rng, freqMin, freqMax);
- p.oscillatePosition.scale[axis] = WallpaperEngine::Maths::randomFloat (m_rng, scaleMin, scaleMax);
- p.oscillatePosition.phase[axis]
- = WallpaperEngine::Maths::randomFloat (m_rng, phaseMin, phaseMax + 2.0f * glm::pi<float> ());
- }
- p.oscillatePosition.initialized = true;
- }
- float t = p.age;
- glm::vec3 delta (0.0f);
- for (int axis = 0; axis < 3; axis++) {
- float w = 2.0f * glm::pi<float> () * p.oscillatePosition.frequency[axis] / (2.0f * glm::pi<float> ());
- // Derivative of cos is -sin; multiplied by dt for position change
- float move
- = -p.oscillatePosition.scale[axis] * w * std::sin (w * t + p.oscillatePosition.phase[axis]) * dt;
- // Apply mask as bias multiplier for this axis
- delta[axis] = move * mask[axis] * speedOverride->getFloat ();
- }
- p.position += delta;
- }
- };
- }
- // ========== RENDERING ==========
- void CParticle::setupPass () {
- if (!m_particle.material || !m_particle.material->material || m_particle.material->material->passes.empty ()) {
- sLog.error ("No valid material for particle ", m_particle.name);
- return;
- }
- const auto& firstPass = **m_particle.material->material->passes.begin ();
- m_passOverride = std::make_unique<ImageEffectPassOverride> ();
- m_passOverride->combos["THICKFORMAT"] = 1;
- if (m_useRopeRenderer) {
- m_passOverride->shaderOverride = "genericropeparticle";
- }
- if (m_spritesheetFrames > 0) {
- m_passOverride->combos["SPRITESHEET"] = 1;
- }
- if (m_useTrailRenderer) {
- m_passOverride->combos["TRAILRENDERER"] = 1;
- }
- // Force texture 0 to use the input (particle texture) rather than the shader's
- // default "util/white" annotation, which would override it in setupRenderTexture()
- m_passBinds = { { 0, "previous" } };
- auto refractIt = firstPass.combos.find ("REFRACT");
- m_hasRefract = refractIt != firstPass.combos.end () && refractIt->second != 0;
- m_passFBOProvider = std::make_shared<FBOProvider> (this);
- // REFRACT: create a copy FBO shadowing _rt_FullFrameBuffer. The shader reads g_Texture3
- // (= _rt_FullFrameBuffer) while we render TO the scene FBO; reading and writing the same FBO
- // is undefined behavior in OpenGL and causes black reads on NVIDIA. Placing a copy FBO under
- // the same name in our FBOProvider makes CPass resolve g_Texture3 to the copy instead - we
- // blit the scene content into it before each render.
- if (m_hasRefract) {
- auto sceneFBO = getScene ().getFBO ();
- float w = static_cast<float> (sceneFBO->getRealWidth ());
- float h = static_cast<float> (sceneFBO->getRealHeight ());
- m_refractFBO = m_passFBOProvider->create (
- "_rt_FullFrameBuffer", TextureFormat_ARGB8888, TextureFlags_ClampUVs, 1.0f, { w, h }, { w, h }
- );
- }
- m_pass = new Effects::CPass (*this, m_passFBOProvider, firstPass, *m_passOverride, m_passBinds, std::nullopt);
- m_pass->setDestination (getScene ().getFBO ());
- m_pass->setInput (getTexture ());
- // Set matrix pointers - CPass will dereference these each frame
- m_pass->setModelViewProjectionMatrix (&m_mvpMatrix);
- m_pass->setModelViewProjectionMatrixInverse (&m_mvpMatrixInverse);
- m_pass->setModelMatrix (&m_modelMatrix);
- m_pass->setViewProjectionMatrix (&m_viewProjectionMatrix);
- GLint prevVAO = 0;
- glGetIntegerv (GL_VERTEX_ARRAY_BINDING, &prevVAO);
- glGenVertexArrays (1, &m_vao);
- glGenBuffers (1, &m_vbo);
- glGenBuffers (1, &m_ebo);
- glBindVertexArray (m_vao);
- glBindBuffer (GL_ARRAY_BUFFER, m_vbo);
- glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, m_ebo);
- const GLuint program = m_pass->getProgramID ();
- if (m_useRopeRenderer) {
- // Rope vertex layout: 7 attributes, 26 floats/vertex, stride=104 bytes
- // a_PositionVec4(4) + a_TexCoordVec4(4) + a_TexCoordVec4C1(4) + a_TexCoordVec4C2(4)
- // + a_TexCoordVec4C3(4) + a_TexCoordC4(2) + a_Color(4) = 26
- const GLsizei stride = sizeof (float) * ROPE_FLOATS_PER_VERTEX;
- const GLint loc0 = glGetAttribLocation (program, "a_PositionVec4");
- const GLint loc1 = glGetAttribLocation (program, "a_TexCoordVec4");
- const GLint loc2 = glGetAttribLocation (program, "a_TexCoordVec4C1");
- const GLint loc3 = glGetAttribLocation (program, "a_TexCoordVec4C2");
- const GLint loc4 = glGetAttribLocation (program, "a_TexCoordVec4C3");
- const GLint loc5 = glGetAttribLocation (program, "a_TexCoordC4");
- const GLint loc6 = glGetAttribLocation (program, "a_Color");
- if (loc0 >= 0) {
- glEnableVertexAttribArray (loc0);
- glVertexAttribPointer (loc0, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 0));
- }
- if (loc1 >= 0) {
- glEnableVertexAttribArray (loc1);
- glVertexAttribPointer (loc1, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 4));
- }
- if (loc2 >= 0) {
- glEnableVertexAttribArray (loc2);
- glVertexAttribPointer (loc2, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 8));
- }
- if (loc3 >= 0) {
- glEnableVertexAttribArray (loc3);
- glVertexAttribPointer (loc3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 12));
- }
- if (loc4 >= 0) {
- glEnableVertexAttribArray (loc4);
- glVertexAttribPointer (loc4, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 16));
- }
- if (loc5 >= 0) {
- glEnableVertexAttribArray (loc5);
- glVertexAttribPointer (loc5, 2, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 20));
- }
- if (loc6 >= 0) {
- glEnableVertexAttribArray (loc6);
- glVertexAttribPointer (loc6, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 22));
- }
- } else {
- // Sprite vertex layout: 5 attributes, 17 floats/vertex, stride=68 bytes
- // a_Position(3) + a_TexCoordVec4(4) + a_Color(4) + a_TexCoordVec4C1(4) + a_TexCoordC2(2) = 17
- const GLsizei stride = sizeof (float) * SPRITE_FLOATS_PER_VERTEX;
- const GLint loc0 = glGetAttribLocation (program, "a_Position");
- const GLint loc1 = glGetAttribLocation (program, "a_TexCoordVec4");
- const GLint loc2 = glGetAttribLocation (program, "a_Color");
- const GLint loc3 = glGetAttribLocation (program, "a_TexCoordVec4C1");
- const GLint loc4 = glGetAttribLocation (program, "a_TexCoordC2");
- if (loc0 >= 0) {
- glEnableVertexAttribArray (loc0);
- glVertexAttribPointer (loc0, 3, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 0));
- }
- if (loc1 >= 0) {
- glEnableVertexAttribArray (loc1);
- glVertexAttribPointer (loc1, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 3));
- }
- if (loc2 >= 0) {
- glEnableVertexAttribArray (loc2);
- glVertexAttribPointer (loc2, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 7));
- }
- if (loc3 >= 0) {
- glEnableVertexAttribArray (loc3);
- glVertexAttribPointer (loc3, 4, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 11));
- }
- if (loc4 >= 0) {
- glEnableVertexAttribArray (loc4);
- glVertexAttribPointer (loc4, 2, GL_FLOAT, GL_FALSE, stride, (void*)(sizeof (float) * 15));
- }
- }
- glBindVertexArray (prevVAO);
- setupGeometryCallbacks ();
- setupParticleUniforms ();
- }
- void CParticle::setupGeometryCallbacks () {
- m_pass->setGeometryCallback (
- // Setup attribs: save current VAO, bind particle VAO
- [this] () {
- glGetIntegerv (GL_VERTEX_ARRAY_BINDING, &m_prevVAO);
- glBindVertexArray (m_vao);
- },
- // Draw geometry: indexed rendering
- [this] () { glDrawElements (GL_TRIANGLES, m_activeIndexCount, GL_UNSIGNED_INT, nullptr); },
- // Cleanup: restore previous VAO
- [this] () { glBindVertexArray (m_prevVAO); }
- );
- }
- void CParticle::setupParticleUniforms () {
- // Add particle-specific uniforms from common_particles.h that CPass doesn't provide
- // These are pointer-based: CPass reads the current value each frame
- m_pass->addUniform ("g_ModelMatrixInverse", &m_modelMatrixInverse);
- m_pass->addUniform ("g_OrientationUp", &m_orientationUp);
- m_pass->addUniform ("g_OrientationRight", &m_orientationRight);
- m_pass->addUniform ("g_OrientationForward", &m_orientationForward);
- m_pass->addUniform ("g_ViewUp", &m_viewUp);
- m_pass->addUniform ("g_ViewRight", &m_viewRight);
- m_pass->addUniform ("g_EyePosition", &m_eyePosition);
- m_pass->addUniform ("g_RenderVar0", &m_renderVar0);
- m_pass->addUniform ("g_RenderVar1", &m_renderVar1);
- // REFRACT: set g_RefractAmount (shader default 0.05, may not be applied by CPass's parameter system)
- if (m_hasRefract) {
- m_pass->addUniform ("g_RefractAmount", &m_refractAmount);
- }
- }
- void CParticle::updateMatrices () {
- glm::vec3 scale = m_particle.scale->value->getVec3 ();
- glm::vec3 angles = m_particle.angles->value->getVec3 ();
- m_modelMatrix = glm::mat4 (1.0f);
- m_modelMatrix = glm::translate (m_modelMatrix, m_transformedOrigin);
- this->applyParallaxToModelMatrix ();
- // Negate X and Z rotations to account for Y-flipped coordinate system
- m_modelMatrix = glm::rotate (m_modelMatrix, -angles.z, glm::vec3 (0, 0, 1));
- m_modelMatrix = glm::rotate (m_modelMatrix, angles.y, glm::vec3 (0, 1, 0));
- m_modelMatrix = glm::rotate (m_modelMatrix, -angles.x, glm::vec3 (1, 0, 0));
- m_modelMatrix = glm::scale (m_modelMatrix, scale);
- m_modelMatrixInverse = glm::inverse (m_modelMatrix);
- this->updateParticleViewProjection ();
- m_mvpMatrix = m_viewProjectionMatrix * m_modelMatrix;
- m_mvpMatrixInverse = glm::inverse (m_mvpMatrix);
- m_orientationUp = glm::vec3 (0.0f, 1.0f, 0.0f);
- m_orientationRight = glm::vec3 (1.0f, 0.0f, 0.0f);
- m_orientationForward = glm::vec3 (0.0f, 0.0f, 1.0f);
- m_viewUp = glm::vec3 (0.0f, 1.0f, 0.0f);
- m_viewRight = glm::vec3 (1.0f, 0.0f, 0.0f);
- this->updateParticleRenderVars ();
- }
- void CParticle::applyParallaxToModelMatrix () {
- // CScene::renderFrame() already folds disableparallax into getParallaxDisplacement()
- if (!getScene ().getScene ().camera.parallax.enabled->value->getBool ()) {
- return;
- }
- const glm::vec2 offset = getScene ().getParallaxOffset (m_particle);
- const glm::vec3 parallaxOffset { offset.x, offset.y, 0.0f };
- m_modelMatrix = glm::translate (m_modelMatrix, parallaxOffset);
- }
- void CParticle::updateParticleViewProjection () {
- if ((m_particle.flags & 4) != 0) {
- // Perspective particles use a dedicated perspective projection
- float width = getScene ().getCamera ().getWidth ();
- float height = getScene ().getCamera ().getHeight ();
- float aspect = width / height;
- float fov = glm::radians (getScene ().getCamera ().getFov ());
- float nearz = getScene ().getCamera ().getNearZ ();
- float farz = getScene ().getCamera ().getFarZ ();
- glm::mat4 perspectiveProj = glm::perspective (fov, aspect, nearz, farz);
- glm::mat4 perspectiveView
- = glm::lookAt (glm::vec3 (0.0f, 0.0f, 1000.0f), glm::vec3 (0.0f, 0.0f, 0.0f), glm::vec3 (0.0f, 1.0f, 0.0f));
- m_viewProjectionMatrix = perspectiveProj * perspectiveView;
- m_eyePosition = glm::vec3 (0.0f, 0.0f, 1000.0f);
- } else {
- // Orthographic projection from scene camera
- m_viewProjectionMatrix = getScene ().getCamera ().getProjection () * getScene ().getCamera ().getLookAt ();
- // The shader's ComputeParticleTrailTangents computes trail ribbon width via
- // cross(eyeDirection, velocity). With the ortho eye at (0,0,0) and particles at z=0,
- // eyeDirection is purely XY, so the cross product is Z-only and invisible under
- // orthographic projection. Placing the eye at z=1000 gives it a visible XY component.
- m_eyePosition = glm::vec3 (0.0f, 0.0f, 1000.0f);
- }
- }
- void CParticle::updateParticleRenderVars () {
- m_renderVar0 = glm::vec4 (m_trailLength, m_trailMaxLength, m_trailMinLength, 0.0f);
- if (m_spritesheetFrames > 0 && m_spritesheetCols > 0 && m_spritesheetRows > 0) {
- float frameWidth = 1.0f / static_cast<float> (m_spritesheetCols);
- float frameHeight = 1.0f / static_cast<float> (m_spritesheetRows);
- float textureRatio = 1.0f;
- if (const auto texture = getTexture ()) {
- // Use atlas dimensions (resolution vec4) rather than getRealWidth/Height, which
- // returns per-frame dimensions for animated textures - the shader needs the
- // per-frame pixel aspect ratio: (atlasH * frameHeight) / (atlasW * frameWidth).
- const glm::vec4* res = texture->getResolution ();
- float w = res->x;
- float h = res->y;
- if (w > 0.0f) {
- textureRatio = (h * frameHeight) / (w * frameWidth);
- }
- }
- m_renderVar1 = glm::vec4 (frameWidth, frameHeight, static_cast<float> (m_spritesheetFrames), textureRatio);
- } else {
- float textureRatio = 1.0f;
- if (const auto texture = getTexture ()) {
- float w = static_cast<float> (texture->getRealWidth ());
- float h = static_cast<float> (texture->getRealHeight ());
- if (w > 0.0f) {
- textureRatio = h / w;
- }
- }
- m_renderVar1 = glm::vec4 (0.0f, 0.0f, 0.0f, textureRatio);
- }
- }
- void CParticle::renderSprites () {
- if (m_particleCount == 0 || m_pass == nullptr) {
- return;
- }
- uint32_t aliveCount = 0;
- for (uint32_t i = 0; i < m_particleCount; i++) {
- if (m_particles[i].alive) {
- aliveCount++;
- }
- }
- if (aliveCount == 0) {
- return;
- }
- // Build vertex data in WP shader layout:
- // a_Position(3) + a_TexCoordVec4(uv.x, uv.y, rotZ, size)(4) + a_Color(4)
- // + a_TexCoordVec4C1(vel.x, vel.y, vel.z, lifetime)(4) + a_TexCoordC2(rotX, rotY)(2) = 17 floats
- uint32_t vertexIndex = 0;
- uint32_t indexOffset = 0;
- for (uint32_t i = 0; i < m_particleCount; i++) {
- const auto& p = m_particles[i];
- if (!p.alive) {
- continue;
- }
- // Skip particles with invalid values
- if (!std::isfinite (p.position.x) || !std::isfinite (p.position.y) || !std::isfinite (p.position.z)
- || !std::isfinite (p.size) || p.size <= 0.0f || p.size > 10000.0f) {
- continue;
- }
- // Encode the CPU-computed frame (accounts for sequenceMultiplier and animation mode)
- // into the lifetime value the WP shader's ComputeSpriteFrame expects: it derives the
- // current frame via floor(frac(lifetime) * numFrames) and the inter-frame blend via
- // frac(lifetime * numFrames).
- float lifetime = p.getLifetimePos ();
- if (m_spritesheetFrames > 0 && p.frame >= 0.0f) {
- if (m_particle.animationMode == "randomframe") {
- // Center within the frame to avoid floating-point edge cases
- lifetime = (p.frame + 0.5f) / static_cast<float> (m_spritesheetFrames);
- } else {
- lifetime = p.frame / static_cast<float> (m_spritesheetFrames);
- }
- }
- auto addVertex = [&] (float u, float v) {
- const uint32_t base = vertexIndex * SPRITE_FLOATS_PER_VERTEX;
- // a_Position (vec3)
- m_vertices[base + 0] = p.position.x;
- m_vertices[base + 1] = p.position.y;
- m_vertices[base + 2] = p.position.z;
- // a_TexCoordVec4 (vec4: uv.x, uv.y, rotZ, size)
- m_vertices[base + 3] = u;
- m_vertices[base + 4] = v;
- m_vertices[base + 5] = p.rotation.z;
- m_vertices[base + 6] = p.size;
- // a_Color (vec4: r, g, b, a)
- m_vertices[base + 7] = p.color.r;
- m_vertices[base + 8] = p.color.g;
- m_vertices[base + 9] = p.color.b;
- m_vertices[base + 10] = p.alpha;
- // a_TexCoordVec4C1 (vec4: vel.x, vel.y, vel.z, lifetime)
- m_vertices[base + 11] = p.velocity.x;
- m_vertices[base + 12] = p.velocity.y;
- m_vertices[base + 13] = p.velocity.z;
- m_vertices[base + 14] = lifetime;
- // a_TexCoordC2 (vec2: rotX, rotY)
- m_vertices[base + 15] = p.rotation.x;
- m_vertices[base + 16] = p.rotation.y;
- vertexIndex++;
- };
- uint32_t baseVertex = vertexIndex;
- addVertex (0.0f, 1.0f); // 0: Bottom-left
- addVertex (1.0f, 1.0f); // 1: Bottom-right
- addVertex (1.0f, 0.0f); // 2: Top-right
- addVertex (0.0f, 0.0f); // 3: Top-left
- m_indices[indexOffset++] = baseVertex + 0;
- m_indices[indexOffset++] = baseVertex + 1;
- m_indices[indexOffset++] = baseVertex + 2;
- m_indices[indexOffset++] = baseVertex + 2;
- m_indices[indexOffset++] = baseVertex + 3;
- m_indices[indexOffset++] = baseVertex + 0;
- }
- m_activeIndexCount = static_cast<GLsizei> (indexOffset);
- if (m_activeIndexCount == 0) {
- return;
- }
- #if !NDEBUG
- std::string str = "Particles ";
- str += this->getParticle ().name + " (" + std::to_string (this->getId ()) + ", " + this->getParticle ().particleFile
- + ")";
- glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());
- #endif
- glBindBuffer (GL_ARRAY_BUFFER, m_vbo);
- glBufferData (
- GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (vertexIndex * SPRITE_FLOATS_PER_VERTEX * sizeof (float)),
- m_vertices.data (), GL_DYNAMIC_DRAW
- );
- glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, m_ebo);
- glBufferData (
- GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr> (indexOffset * sizeof (uint32_t)), m_indices.data (),
- GL_DYNAMIC_DRAW
- );
- updateMatrices ();
- // REFRACT: blit current scene content into the copy FBO first, giving the shader a
- // snapshot of what's behind the particles without a read/write feedback loop
- if (m_hasRefract && m_refractFBO) {
- auto sceneFBO = getScene ().getFBO ();
- GLint w = static_cast<GLint> (sceneFBO->getRealWidth ());
- GLint h = static_cast<GLint> (sceneFBO->getRealHeight ());
- glBindFramebuffer (GL_READ_FRAMEBUFFER, sceneFBO->getFramebuffer ());
- glBindFramebuffer (GL_DRAW_FRAMEBUFFER, m_refractFBO->getFramebuffer ());
- glBlitFramebuffer (0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
- }
- // ComputeParticleTrailTangents produces a right vector with a Z component (from
- // cross(eyeDirection, velocity), where eyeDirection has an XY offset from the model
- // transform). For 2D/ortho particles at z=0, the ortho near plane sits at ndc.z=-1, so any
- // Z offset pushes vertices past it and clips half the quad. GL_DEPTH_CLAMP avoids that by
- // clamping depth instead of clipping.
- glEnable (GL_DEPTH_CLAMP);
- // CPass::render() handles: FBO binding, texture setup, uniforms, blending, draw call, cleanup
- m_pass->render ();
- glDisable (GL_DEPTH_CLAMP);
- #if !NDEBUG
- glPopDebugGroup ();
- #endif
- }
- void CParticle::renderRope () {
- if (m_particleCount < 2 || m_pass == nullptr) {
- return;
- }
- // Already in spawn order (oldest at index 0) thanks to compaction in update();
- // all particles in [0, m_particleCount) are alive.
- const uint32_t aliveCount = m_particleCount;
- // Each segment between consecutive particles is subdivided into m_ropeSubdivision
- // sub-segments via Catmull-Rom spline, for smooth curves instead of harsh corners.
- //
- // Rope vertex layout (26 floats per vertex, THICKFORMAT):
- // [0-3] a_PositionVec4: startPos.xyz, sizeStart
- // [4-7] a_TexCoordVec4: endPos.xyz, trailLength
- // [8-11] a_TexCoordVec4C1: CP0.xyz, trailPosition
- // [12-15] a_TexCoordVec4C2: CP1.xyz, sizeEnd
- // [16-19] a_TexCoordVec4C3: colorEnd.rgba
- // [20-21] a_TexCoordC4: uvs.xy
- // [22-25] a_Color: colorStart.rgba
- const uint32_t numSegments = aliveCount - 1;
- const int subdivision = std::max (1, m_ropeSubdivision);
- auto catmullRom = [] (const glm::vec3& p0, const glm::vec3& p1, const glm::vec3& p2, const glm::vec3& p3,
- float t) -> glm::vec3 {
- float t2 = t * t, t3 = t2 * t;
- return 0.5f
- * ((2.0f * p1) + (-p0 + p2) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2
- + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3);
- };
- // First pass: evaluate the spline to get all interpolated points (position, size, color)
- const uint32_t totalPoints = numSegments * subdivision + 1;
- this->m_splinePositions.resize (totalPoints);
- this->m_splineSizes.resize (totalPoints);
- this->m_splineColors.resize (totalPoints);
- auto& splinePositions = this->m_splinePositions;
- auto& splineSizes = this->m_splineSizes;
- auto& splineColors = this->m_splineColors;
- for (uint32_t i = 0; i < numSegments; i++) {
- const auto& p1 = m_particles[i];
- const auto& p2 = m_particles[i + 1];
- const auto& p0 = (i > 0) ? m_particles[i - 1] : p1;
- const auto& p3 = (i + 2 < aliveCount) ? m_particles[i + 2] : p2;
- for (int k = 0; k < subdivision; k++) {
- float t = static_cast<float> (k) / static_cast<float> (subdivision);
- uint32_t idx = i * subdivision + k;
- splinePositions[idx] = catmullRom (p0.position, p1.position, p2.position, p3.position, t);
- splineSizes[idx] = glm::mix (p1.size, p2.size, t);
- splineColors[idx] = glm::mix (glm::vec4 (p1.color, p1.alpha), glm::vec4 (p2.color, p2.alpha), t);
- }
- }
- // Last point is the final particle
- {
- const auto& pLast = m_particles[aliveCount - 1];
- splinePositions[totalPoints - 1] = pLast.position;
- splineSizes[totalPoints - 1] = pLast.size;
- splineColors[totalPoints - 1] = glm::vec4 (pLast.color, pLast.alpha);
- }
- // Second pass: build quads from consecutive spline points. The shader computes UV.v as
- // trailPosition / (trailLength - 1), so trailLength/trailPosition are expressed in
- // sub-segment units for the correct UV slice per quad. UV scale divides the effective
- // length, pushing UVs past [0,1] so the texture repeats.
- uint32_t vertexIndex = 0;
- uint32_t indexOffset = 0;
- const uint32_t totalSubSegments = totalPoints - 1;
- const float uvScale = (m_ropeUVScale > 0.0f) ? m_ropeUVScale : 1.0f;
- const float trailLength = static_cast<float> (totalSubSegments) / uvScale + 1.0f;
- const float usableLength = trailLength - 1.0f;
- // UV smoothing: distribute UV proportional to arc length instead of uniform index.
- // Per wiki: only when all particle lifetimes match and scrolling is disabled.
- const bool useSmoothing = m_ropeUVSmoothing && m_uniformLifetimes && !m_ropeUVScrolling;
- auto& cumulativeArcLength = this->m_cumulativeArcLength;
- float totalArcLength = 0.0f;
- if (useSmoothing) {
- cumulativeArcLength.resize (totalPoints, 0.0f);
- for (uint32_t i = 1; i < totalPoints; i++) {
- totalArcLength += glm::distance (splinePositions[i], splinePositions[i - 1]);
- cumulativeArcLength[i] = totalArcLength;
- }
- }
- // UV scrolling: shift UV along the rope over time (1 UV cycle per second)
- float scrollOffset = 0.0f;
- if (m_ropeUVScrolling && usableLength > 0.0f) {
- scrollOffset = std::fmod (static_cast<float> (g_Time), 10000.0f) * usableLength;
- }
- for (uint32_t s = 0; s < totalSubSegments; s++) {
- const glm::vec3& posStart = splinePositions[s];
- const glm::vec3& posEnd = splinePositions[s + 1];
- float sizeStart = splineSizes[s];
- float sizeEnd = splineSizes[s + 1];
- const glm::vec4& colorStart = splineColors[s];
- const glm::vec4& colorEnd = splineColors[s + 1];
- // Neighboring points for shader tangent computation (CP0/CP1)
- const glm::vec3& posPrev = (s > 0) ? splinePositions[s - 1] : posStart;
- const glm::vec3& posAfter = (s + 2 < totalPoints) ? splinePositions[s + 2] : posEnd;
- // Compute trailPosition for UV mapping
- float trailPosition;
- if (useSmoothing && totalArcLength > 0.0f) {
- // Arc-length parameterization: map cumulative distance to sub-segment space
- trailPosition = cumulativeArcLength[s] / totalArcLength * static_cast<float> (totalSubSegments);
- } else {
- trailPosition = static_cast<float> (s);
- }
- trailPosition += scrollOffset;
- auto addRopeVertex = [&] (float uvX, float uvY) {
- const uint32_t base = vertexIndex * ROPE_FLOATS_PER_VERTEX;
- // a_PositionVec4: startPos.xyz, sizeStart
- m_vertices[base + 0] = posStart.x;
- m_vertices[base + 1] = posStart.y;
- m_vertices[base + 2] = posStart.z;
- m_vertices[base + 3] = sizeStart;
- // a_TexCoordVec4: endPos.xyz, trailLength
- m_vertices[base + 4] = posEnd.x;
- m_vertices[base + 5] = posEnd.y;
- m_vertices[base + 6] = posEnd.z;
- m_vertices[base + 7] = trailLength;
- // a_TexCoordVec4C1: CP0.xyz (neighbor before start), trailPosition
- m_vertices[base + 8] = posPrev.x;
- m_vertices[base + 9] = posPrev.y;
- m_vertices[base + 10] = posPrev.z;
- m_vertices[base + 11] = trailPosition;
- // a_TexCoordVec4C2: CP1.xyz (neighbor after end), sizeEnd
- m_vertices[base + 12] = posAfter.x;
- m_vertices[base + 13] = posAfter.y;
- m_vertices[base + 14] = posAfter.z;
- m_vertices[base + 15] = sizeEnd;
- // a_TexCoordVec4C3: colorEnd.rgba
- m_vertices[base + 16] = colorEnd.r;
- m_vertices[base + 17] = colorEnd.g;
- m_vertices[base + 18] = colorEnd.b;
- m_vertices[base + 19] = colorEnd.a;
- // a_TexCoordC4: uvs.xy
- m_vertices[base + 20] = uvX;
- m_vertices[base + 21] = uvY;
- // a_Color: colorStart.rgba
- m_vertices[base + 22] = colorStart.r;
- m_vertices[base + 23] = colorStart.g;
- m_vertices[base + 24] = colorStart.b;
- m_vertices[base + 25] = colorStart.a;
- vertexIndex++;
- };
- // Quad: 4 vertices (left/right at start/end of segment)
- uint32_t baseVertex = vertexIndex;
- addRopeVertex (0.0f, 0.0f); // left at start
- addRopeVertex (1.0f, 0.0f); // right at start
- addRopeVertex (1.0f, 1.0f); // right at end
- addRopeVertex (0.0f, 1.0f); // left at end
- m_indices[indexOffset++] = baseVertex + 0;
- m_indices[indexOffset++] = baseVertex + 1;
- m_indices[indexOffset++] = baseVertex + 2;
- m_indices[indexOffset++] = baseVertex + 2;
- m_indices[indexOffset++] = baseVertex + 3;
- m_indices[indexOffset++] = baseVertex + 0;
- }
- m_activeIndexCount = static_cast<GLsizei> (indexOffset);
- if (m_activeIndexCount == 0) {
- return;
- }
- #if !NDEBUG
- std::string str = "Rope particles ";
- str += this->getParticle ().name + " (" + std::to_string (this->getId ()) + ", " + this->getParticle ().particleFile
- + ")";
- glPushDebugGroup (GL_DEBUG_SOURCE_APPLICATION, 0, -1, str.c_str ());
- #endif
- glBindBuffer (GL_ARRAY_BUFFER, m_vbo);
- glBufferData (
- GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (vertexIndex * ROPE_FLOATS_PER_VERTEX * sizeof (float)),
- m_vertices.data (), GL_DYNAMIC_DRAW
- );
- glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, m_ebo);
- glBufferData (
- GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr> (indexOffset * sizeof (uint32_t)), m_indices.data (),
- GL_DYNAMIC_DRAW
- );
- updateMatrices ();
- // REFRACT: blit current scene content into the copy FBO before rendering
- if (m_hasRefract && m_refractFBO) {
- auto sceneFBO = getScene ().getFBO ();
- GLint w = static_cast<GLint> (sceneFBO->getRealWidth ());
- GLint h = static_cast<GLint> (sceneFBO->getRealHeight ());
- glBindFramebuffer (GL_READ_FRAMEBUFFER, sceneFBO->getFramebuffer ());
- glBindFramebuffer (GL_DRAW_FRAMEBUFFER, m_refractFBO->getFramebuffer ());
- glBlitFramebuffer (0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
- }
- glEnable (GL_DEPTH_CLAMP);
- m_pass->render ();
- glDisable (GL_DEPTH_CLAMP);
- #if !NDEBUG
- glPopDebugGroup ();
- #endif
- }
|