From 99452e0cc2449427a2af3cbbc9d1e8a8879bbc63 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Fri, 8 May 2026 16:16:31 +0900 Subject: [PATCH 1/8] Pick model surface particle by sampling random vertex --- code/model/model_flags.h | 1 + code/model/modelread.cpp | 1 + code/particle/EffectHost.h | 1 + code/particle/ParticleEffect.cpp | 4 +- code/particle/ParticleVolume.h | 3 +- code/particle/hosts/EffectHostSubmodel.cpp | 4 ++ code/particle/hosts/EffectHostSubmodel.h | 1 + code/particle/hosts/EffectHostTurret.cpp | 4 ++ code/particle/hosts/EffectHostTurret.h | 1 + code/particle/volumes/ConeVolume.cpp | 2 +- code/particle/volumes/ConeVolume.h | 2 +- .../particle/volumes/LegacyAACuboidVolume.cpp | 2 +- code/particle/volumes/LegacyAACuboidVolume.h | 2 +- code/particle/volumes/ModelSurfaceVolume.cpp | 47 +++++++++++++++++++ code/particle/volumes/ModelSurfaceVolume.h | 31 ++++++++++++ code/particle/volumes/PointVolume.cpp | 2 +- code/particle/volumes/PointVolume.h | 2 +- code/particle/volumes/RingVolume.cpp | 2 +- code/particle/volumes/RingVolume.h | 2 +- code/particle/volumes/SpheroidVolume.cpp | 2 +- code/particle/volumes/SpheroidVolume.h | 2 +- code/source_groups.cmake | 2 + 22 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 code/particle/volumes/ModelSurfaceVolume.cpp create mode 100644 code/particle/volumes/ModelSurfaceVolume.h diff --git a/code/model/model_flags.h b/code/model/model_flags.h index aba8da8a1d5..268f0a8e437 100644 --- a/code/model/model_flags.h +++ b/code/model/model_flags.h @@ -10,6 +10,7 @@ namespace Model { Is_live_debris, // whether current submodel is a live debris model Is_thruster, // is an engine thruster submodel Is_damaged, // is a submodel that represents a damaged submodel (e.g. a -destroyed version of some other submodel) + Is_lod, // is a submodel that is a lower LOD of a different submodel Do_not_scale_detail_distances, // if set should not scale boxes or spheres based on 'model detail' settings Gun_rotation, // for animated weapon models Instant_rotate_accel, // rotating submodels instantly reach their desired velocity diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 44015e877d0..00f87a7fddc 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -3457,6 +3457,7 @@ int model_load(const char* filename, ship_info* sip, ErrorType error_type, bool if (dl2 >= sm1->num_details ) sm1->num_details = dl2+1; sm1->details[dl2] = j; mprintf(( "Submodel '%s' is detail level %d of '%s'\n", sm2->name, dl2 + 1, sm1->name )); + sm2->flags.set(Model::Submodel_flags::Is_lod); lower_to_higher_detail_submodels.emplace(sm2->name, sm1->name); } } diff --git a/code/particle/EffectHost.h b/code/particle/EffectHost.h index fa25ffb155a..9bdea7296d8 100644 --- a/code/particle/EffectHost.h +++ b/code/particle/EffectHost.h @@ -27,6 +27,7 @@ class EffectHost { } virtual std::pair getParentObjAndSig() const { return {-1, -1}; } + virtual int getParentSubmodel() const { return -1; } virtual float getLifetime() const { return -1.f; } diff --git a/code/particle/ParticleEffect.cpp b/code/particle/ParticleEffect.cpp index 36252794c59..8a78aeb7ccf 100644 --- a/code/particle/ParticleEffect.cpp +++ b/code/particle/ParticleEffect.cpp @@ -290,11 +290,11 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s vec3d localPos = posNoise; if (m_spawnVolume != nullptr) { - localPos += m_spawnVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction); + localPos += m_spawnVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction, *source.m_host); } if (m_velocityVolume != nullptr) { - localVelocity += m_velocityVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction) * (m_velocity_scaling.next() * velocityVolumeMultiplier); + localVelocity += m_velocityVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction, *source.m_host) * (m_velocity_scaling.next() * velocityVolumeMultiplier); } if (m_manual_velocity_offset.has_value()) { diff --git a/code/particle/ParticleVolume.h b/code/particle/ParticleVolume.h index c03cfaf55b2..8012239bcdf 100644 --- a/code/particle/ParticleVolume.h +++ b/code/particle/ParticleVolume.h @@ -2,6 +2,7 @@ #include "globalincs/pstypes.h" #include "parse/parselo.h" +#include "particle/EffectHost.h" #include @@ -9,7 +10,7 @@ namespace particle { class ParticleSource; class ParticleVolume { public: - virtual vec3d sampleRandomPoint(const matrix &orientation, const std::tuple& source, float particlesFraction) = 0; + virtual vec3d sampleRandomPoint(const matrix &orientation, const std::tuple& source, float particlesFraction, const EffectHost& host) = 0; virtual void parse() = 0; diff --git a/code/particle/hosts/EffectHostSubmodel.cpp b/code/particle/hosts/EffectHostSubmodel.cpp index 98eec5a828c..8b6355435bf 100644 --- a/code/particle/hosts/EffectHostSubmodel.cpp +++ b/code/particle/hosts/EffectHostSubmodel.cpp @@ -60,6 +60,10 @@ std::pair EffectHostSubmodel::getParentObjAndSig() const { return { m_objnum, m_objsig }; } +int EffectHostSubmodel::getParentSubmodel() const { + return m_submodel; +} + float EffectHostSubmodel::getHostRadius() const { return Objects[m_objnum].radius; } diff --git a/code/particle/hosts/EffectHostSubmodel.h b/code/particle/hosts/EffectHostSubmodel.h index 860774976d4..2dfbfe32c22 100644 --- a/code/particle/hosts/EffectHostSubmodel.h +++ b/code/particle/hosts/EffectHostSubmodel.h @@ -16,6 +16,7 @@ class EffectHostSubmodel : public EffectHost { vec3d getVelocity() const override; std::pair getParentObjAndSig() const override; + int getParentSubmodel() const override; float getHostRadius() const override; diff --git a/code/particle/hosts/EffectHostTurret.cpp b/code/particle/hosts/EffectHostTurret.cpp index 4613fd564d6..747e4dbf519 100644 --- a/code/particle/hosts/EffectHostTurret.cpp +++ b/code/particle/hosts/EffectHostTurret.cpp @@ -74,6 +74,10 @@ std::pair EffectHostTurret::getParentObjAndSig() const { return { m_objnum, m_objsig }; } +int EffectHostTurret::getParentSubmodel() const { + return m_submodel; +} + float EffectHostTurret::getHostRadius() const { return Objects[m_objnum].radius; } diff --git a/code/particle/hosts/EffectHostTurret.h b/code/particle/hosts/EffectHostTurret.h index 2f4c020972d..112486199db 100644 --- a/code/particle/hosts/EffectHostTurret.h +++ b/code/particle/hosts/EffectHostTurret.h @@ -15,6 +15,7 @@ class EffectHostTurret : public EffectHost { vec3d getVelocity() const override; std::pair getParentObjAndSig() const override; + int getParentSubmodel() const override; float getHostRadius() const override; diff --git a/code/particle/volumes/ConeVolume.cpp b/code/particle/volumes/ConeVolume.cpp index d9468353b6d..fb9f34f0152 100644 --- a/code/particle/volumes/ConeVolume.cpp +++ b/code/particle/volumes/ConeVolume.cpp @@ -6,7 +6,7 @@ namespace particle { ConeVolume::ConeVolume(::util::ParsedRandomFloatRange deviation, ::util::ParsedRandomFloatRange length) : m_deviation(deviation), m_length(length), m_modular_curve_instance(m_modular_curves.create_instance()) { } - vec3d ConeVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d ConeVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); //It is surely possible to do this more efficiently. diff --git a/code/particle/volumes/ConeVolume.h b/code/particle/volumes/ConeVolume.h index 5fc47de3c37..ff583bc73ac 100644 --- a/code/particle/volumes/ConeVolume.h +++ b/code/particle/volumes/ConeVolume.h @@ -27,7 +27,7 @@ namespace particle { explicit ConeVolume(::util::ParsedRandomFloatRange deviation, float length); explicit ConeVolume(::util::ParsedRandomFloatRange deviation, ::util::ParsedRandomFloatRange length); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override; }; } \ No newline at end of file diff --git a/code/particle/volumes/LegacyAACuboidVolume.cpp b/code/particle/volumes/LegacyAACuboidVolume.cpp index e48882357fd..ecacd0345ab 100644 --- a/code/particle/volumes/LegacyAACuboidVolume.cpp +++ b/code/particle/volumes/LegacyAACuboidVolume.cpp @@ -5,7 +5,7 @@ namespace particle { LegacyAACuboidVolume::LegacyAACuboidVolume(float normalVariance, float size, bool normalize) : m_normalVariance(normalVariance), m_size(size), m_normalize(normalize), m_modular_curve_instance(m_modular_curves.create_instance()) { } - vec3d LegacyAACuboidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d LegacyAACuboidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); float variance = m_normalVariance * m_modular_curves.get_output(VolumeModularCurveOutput::VARIANCE, curveSource, &m_modular_curve_instance); diff --git a/code/particle/volumes/LegacyAACuboidVolume.h b/code/particle/volumes/LegacyAACuboidVolume.h index ea98b99c1d0..4678630a4d3 100644 --- a/code/particle/volumes/LegacyAACuboidVolume.h +++ b/code/particle/volumes/LegacyAACuboidVolume.h @@ -28,7 +28,7 @@ namespace particle { public: explicit LegacyAACuboidVolume(float normalVariance, float size, bool normalize); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override { UNREACHABLE("Cannot parse Legacy Particle Volume!"); }; diff --git a/code/particle/volumes/ModelSurfaceVolume.cpp b/code/particle/volumes/ModelSurfaceVolume.cpp new file mode 100644 index 00000000000..c47b7633aa3 --- /dev/null +++ b/code/particle/volumes/ModelSurfaceVolume.cpp @@ -0,0 +1,47 @@ +#include "ModelSurfaceVolume.h" + +#include "math/vecmat.h" + +namespace particle { +ModelSurfaceVolume::ModelSurfaceVolume() : m_modular_curve_instance(m_modular_curves.create_instance()) { }; + +vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) { + int obj_num = host.getParentObjAndSig().first; + int submodel = host.getParentSubmodel(); + + vec3d point = ZERO_VECTOR; + + if (obj_num >= 0) { + const polymodel* pm = object_get_model(&Objects[obj_num]); + if (pm != nullptr) { + if (submodel < 0) { + SCP_vector eligible_submodels; + for (size_t i = 0; i < pm->n_models; ++i) { + if (!pm->submodel[i].flags[Model::Submodel_flags::Is_lod, Model::Submodel_flags::Is_damaged, Model::Submodel_flags::Is_live_debris]) + eligible_submodels.emplace_back(i); + } + submodel = eligible_submodels[::util::UniformUIntRange(0, eligible_submodels.size() - 1).next()]; + } + + const bsp_info* submodel_data = &pm->submodel[submodel]; + const auto& geometry_data = *submodel_data->buffer.model_list; + size_t target_vertex = ::util::UniformUIntRange(0, geometry_data.n_verts - 1).next(); + + //This point is, despite its name, not in world space, but in model local space (NOT submodel local though!) + point = geometry_data.vert[target_vertex].world; + } + } + + auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); + + return pointCompensateForOffsetAndRotOffset(point, orientation, + m_modular_curves.get_output(VolumeModularCurveOutput::OFFSET_ROT, curveSource, &m_modular_curve_instance), + m_modular_curves.get_output(VolumeModularCurveOutput::POINT_TO_ROT, curveSource, &m_modular_curve_instance)); +} + +void ModelSurfaceVolume::parse() { + ParticleVolume::parseCommon(); + + m_modular_curves.parse("$Volume Curve:"); +} +} diff --git a/code/particle/volumes/ModelSurfaceVolume.h b/code/particle/volumes/ModelSurfaceVolume.h new file mode 100644 index 00000000000..9e146ba2a41 --- /dev/null +++ b/code/particle/volumes/ModelSurfaceVolume.h @@ -0,0 +1,31 @@ +#pragma once + +#include "particle/ParticleVolume.h" +#include "particle/ParticleEffect.h" + +namespace particle { +class ModelSurfaceVolume : public ParticleVolume { +public: + enum class VolumeModularCurveOutput : uint8_t {OFFSET_ROT, POINT_TO_ROT, NUM_VALUES}; + +private: + constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_subset( + std::array { + std::pair { "Offset Rotate Around Fvec", VolumeModularCurveOutput::OFFSET_ROT }, + std::pair { "Point To Rotate Around Fvec", VolumeModularCurveOutput::POINT_TO_ROT } + }, + std::pair { "Fraction Particles Spawned", modular_curves_self_input{}}); + +public: + MODULAR_CURVE_SET(m_modular_curves, modular_curve_definition); + +private: + modular_curves_entry_instance m_modular_curve_instance; + +public: + explicit ModelSurfaceVolume(); + + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; + void parse() override; +}; +} \ No newline at end of file diff --git a/code/particle/volumes/PointVolume.cpp b/code/particle/volumes/PointVolume.cpp index 05c9b51dcf5..9d55062ca90 100644 --- a/code/particle/volumes/PointVolume.cpp +++ b/code/particle/volumes/PointVolume.cpp @@ -5,7 +5,7 @@ namespace particle { PointVolume::PointVolume() : m_modular_curve_instance(m_modular_curves.create_instance()) { }; - vec3d PointVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d PointVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); return pointCompensateForOffsetAndRotOffset(ZERO_VECTOR, orientation, diff --git a/code/particle/volumes/PointVolume.h b/code/particle/volumes/PointVolume.h index ccfbd52a7fa..6ea532496e8 100644 --- a/code/particle/volumes/PointVolume.h +++ b/code/particle/volumes/PointVolume.h @@ -25,7 +25,7 @@ namespace particle { public: explicit PointVolume(); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override; }; } \ No newline at end of file diff --git a/code/particle/volumes/RingVolume.cpp b/code/particle/volumes/RingVolume.cpp index b38f7119b82..63d69801e7b 100644 --- a/code/particle/volumes/RingVolume.cpp +++ b/code/particle/volumes/RingVolume.cpp @@ -6,7 +6,7 @@ namespace particle { RingVolume::RingVolume() : m_radius(1.f), m_onEdge(false), m_modular_curve_instance(m_modular_curves.create_instance()) { }; RingVolume::RingVolume(float radius, bool onEdge) : m_radius(radius), m_onEdge(onEdge), m_modular_curve_instance(m_modular_curves.create_instance()) { }; - vec3d RingVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d RingVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); vec3d pos; // get an unbiased random point in the sphere diff --git a/code/particle/volumes/RingVolume.h b/code/particle/volumes/RingVolume.h index bdb000ab212..e84d3746358 100644 --- a/code/particle/volumes/RingVolume.h +++ b/code/particle/volumes/RingVolume.h @@ -22,7 +22,7 @@ namespace particle { explicit RingVolume(); explicit RingVolume(float radius, bool onEdge); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override; }; } \ No newline at end of file diff --git a/code/particle/volumes/SpheroidVolume.cpp b/code/particle/volumes/SpheroidVolume.cpp index 95d64cfdce0..f89446494e3 100644 --- a/code/particle/volumes/SpheroidVolume.cpp +++ b/code/particle/volumes/SpheroidVolume.cpp @@ -6,7 +6,7 @@ namespace particle { SpheroidVolume::SpheroidVolume() : m_bias(1.f), m_stretch(1.f), m_radius(1.f), m_modular_curve_instance(m_modular_curves.create_instance()) { }; SpheroidVolume::SpheroidVolume(float bias, float stretch, float radius) : m_bias(bias), m_stretch(stretch), m_radius(radius), m_modular_curve_instance(m_modular_curves.create_instance()) { }; - vec3d SpheroidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + vec3d SpheroidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& /*host*/) { auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); vec3d pos; // get an unbiased random point in the sphere diff --git a/code/particle/volumes/SpheroidVolume.h b/code/particle/volumes/SpheroidVolume.h index 36ea461d232..c5427968ee6 100644 --- a/code/particle/volumes/SpheroidVolume.h +++ b/code/particle/volumes/SpheroidVolume.h @@ -33,7 +33,7 @@ namespace particle { explicit SpheroidVolume(); explicit SpheroidVolume(float bias, float stretch, float radius); - vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) override; void parse() override; }; } \ No newline at end of file diff --git a/code/source_groups.cmake b/code/source_groups.cmake index cd55353b901..3dc3d6e93eb 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1154,6 +1154,8 @@ add_file_folder("Particle\\\\Volumes" particle/volumes/ConeVolume.h particle/volumes/LegacyAACuboidVolume.cpp particle/volumes/LegacyAACuboidVolume.h + particle/volumes/ModelSurfaceVolume.cpp + particle/volumes/ModelSurfaceVolume.h particle/volumes/PointVolume.cpp particle/volumes/PointVolume.h particle/volumes/RingVolume.cpp From 14ba164385a731e26adf4e98a3f1a04c262b722f Mon Sep 17 00:00:00 2001 From: Birk Magnussen <6238428+BMagnu@users.noreply.github.com> Date: Sat, 9 May 2026 21:14:04 +0900 Subject: [PATCH 2/8] Parse and keep buffers --- code/graphics/2d.h | 1 + code/model/modelread.cpp | 7 ++++++- code/model/modelrender.h | 2 ++ code/particle/ParticleParse.cpp | 10 ++++++++-- code/particle/volumes/ModelSurfaceVolume.cpp | 4 +++- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/code/graphics/2d.h b/code/graphics/2d.h index 53dbc1980e2..52b2d29266f 100644 --- a/code/graphics/2d.h +++ b/code/graphics/2d.h @@ -578,6 +578,7 @@ class vertex_buffer size_t stride; size_t vertex_offset; size_t vertex_num_offset; + size_t n_verts; poly_list *model_list; diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 00f87a7fddc..9b23e200b4b 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -72,6 +72,10 @@ SCP_vector Bsp_collision_tree_list; const ubyte* Macro_ubyte_bounds = nullptr; +//If true, CPU-side vertex buffers are deleted once the model is on-GPU. +//This is typically desired for memory reasons, but will prevent certain type of particles. +bool Model_load_clear_CPU_buffers = true; + static int model_initted = 0; #ifndef NDEBUG @@ -1138,7 +1142,8 @@ void create_vertex_buffer(polymodel *pm, const model_read_deferred_tasks& deferr interp_pack_vertex_buffers(pm, i); // release temporary memory - pm->submodel[i].buffer.release(); + if (Model_load_clear_CPU_buffers) + pm->submodel[i].buffer.release(); pm->submodel[i].trans_buffer.release(); } diff --git a/code/model/modelrender.h b/code/model/modelrender.h index 5064b3a35fa..c780226d347 100644 --- a/code/model/modelrender.h +++ b/code/model/modelrender.h @@ -29,6 +29,8 @@ extern color Wireframe_color; extern int Lab_object_detail_level; +extern bool Model_load_clear_CPU_buffers; + typedef enum { TECH_SHIP, TECH_WEAPON, diff --git a/code/particle/ParticleParse.cpp b/code/particle/ParticleParse.cpp index 243e90fb0c4..eef8ff60470 100644 --- a/code/particle/ParticleParse.cpp +++ b/code/particle/ParticleParse.cpp @@ -7,6 +7,8 @@ #include +#include "volumes/ModelSurfaceVolume.h" + namespace particle { // @@ -125,7 +127,7 @@ namespace particle { static std::shared_ptr parseVolume() { - int type = required_string_one_of(4, "Spheroid", "Cone", "Ring", "Point"); //... and future volumes + int type = required_string_one_of(5, "Spheroid", "Cone", "Ring", "Point", "ModelSurface"); //... and future volumes std::shared_ptr volume; switch (type) { @@ -145,6 +147,10 @@ namespace particle { required_string("Point"); volume = std::make_shared(); break; + case 4: + required_string("ModelSurface"); + volume = std::make_shared(); + break; default: UNREACHABLE("Invalid volume type specified!"); } @@ -723,4 +729,4 @@ namespace particle { "Sphere", "Volume" }; -} \ No newline at end of file +} diff --git a/code/particle/volumes/ModelSurfaceVolume.cpp b/code/particle/volumes/ModelSurfaceVolume.cpp index c47b7633aa3..a19fd2c2768 100644 --- a/code/particle/volumes/ModelSurfaceVolume.cpp +++ b/code/particle/volumes/ModelSurfaceVolume.cpp @@ -3,7 +3,9 @@ #include "math/vecmat.h" namespace particle { -ModelSurfaceVolume::ModelSurfaceVolume() : m_modular_curve_instance(m_modular_curves.create_instance()) { }; +ModelSurfaceVolume::ModelSurfaceVolume() : m_modular_curve_instance(m_modular_curves.create_instance()) { + Model_load_clear_CPU_buffers = false; +}; vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction, const EffectHost& host) { int obj_num = host.getParentObjAndSig().first; From f06120ce32213da58812bd4bbc93f6b88342d0c7 Mon Sep 17 00:00:00 2001 From: Birk Magnussen <6238428+BMagnu@users.noreply.github.com> Date: Sat, 9 May 2026 21:30:39 +0900 Subject: [PATCH 3/8] Add local position scaling --- code/particle/ParticleEffect.cpp | 25 +++++++++++++++++++- code/particle/ParticleEffect.h | 2 ++ code/particle/ParticleParse.cpp | 7 ++++++ code/particle/volumes/ModelSurfaceVolume.cpp | 2 +- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/code/particle/ParticleEffect.cpp b/code/particle/ParticleEffect.cpp index 8a78aeb7ccf..7a69bd42e59 100644 --- a/code/particle/ParticleEffect.cpp +++ b/code/particle/ParticleEffect.cpp @@ -185,6 +185,28 @@ void ParticleEffect::sampleNoise(vec3d& noiseTarget, const matrix* orientation, vm_vec_unrotate(&noiseTarget, &noiseSampleLocal, orientation); } +vec3d ParticleEffect::adaptPosition(const vec3d& pos, int parent) const { + if (parent < 0 || !m_local_position_scaling.has_value()) { + return pos; + } + + vec3d pos_local = pos; + + if (!m_parent_local) { + pos_local -= Objects[parent].pos; + vm_vec_rotate(&pos_local, &pos_local, &Objects[parent].orient); + } + + pos_local *= m_local_position_scaling->next(); + + if (!m_parent_local) { + vm_vec_unrotate(&pos_local, &pos_local, &Objects[parent].orient); + vm_vec_add2(&pos_local, &Objects[parent].pos); + } + + return pos_local; +} + /* * In persistent mode (should only ever be used by scripting, really), this function returns pointers to the persistent particles * In non-persistent mode, this function returns the multiplier for the next spawn time. This is because the source cannot know about the curve evaluation that is required to get this factor @@ -213,7 +235,8 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s } } - const auto& [pos, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset); + const auto& [pos_hit, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset); + const vec3d& pos = adaptPosition(pos_hit, parent); vec3d posGlobal = pos; if (m_parent_local && parent >= 0) { diff --git a/code/particle/ParticleEffect.h b/code/particle/ParticleEffect.h index 0824139850e..455e57c7647 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -168,6 +168,7 @@ class ParticleEffect { std::optional<::util::ParsedRandomFloatRange> m_vel_inherit_from_orientation; std::optional<::util::ParsedRandomFloatRange> m_vel_inherit_from_position; + std::optional<::util::ParsedRandomFloatRange> m_local_position_scaling; std::shared_ptr<::particle::ParticleVolume> m_velocityVolume; std::shared_ptr<::particle::ParticleVolume> m_spawnVolume; @@ -186,6 +187,7 @@ class ParticleEffect { float m_distanceCulled; //Kinda deprecated. Only used by the oldest of legacy effects. matrix getNewDirection(const matrix& hostOrientation, const std::optional& normal) const; + vec3d adaptPosition(const vec3d& pos, int parent) const; template auto processSourceInternal(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const; diff --git a/code/particle/ParticleParse.cpp b/code/particle/ParticleParse.cpp index eef8ff60470..3a619e30451 100644 --- a/code/particle/ParticleParse.cpp +++ b/code/particle/ParticleParse.cpp @@ -94,6 +94,12 @@ namespace particle { } } + static void parseLocalPositionScaling(ParticleEffect& effect) { + if (optional_string("+Local position scaling:")) { + effect.m_local_position_scaling = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + } + static void parseParentLocal(ParticleEffect& effect) { if (optional_string("+Remain local to parent:")) { stuff_boolean(&effect.m_parent_local); @@ -378,6 +384,7 @@ namespace particle { parseRadius(effect); parseLength(effect); parseLifetime(effect); + parseLocalPositionScaling(effect); parseParentLocal(effect); parseLightEmissionSettings(effect); diff --git a/code/particle/volumes/ModelSurfaceVolume.cpp b/code/particle/volumes/ModelSurfaceVolume.cpp index a19fd2c2768..5ceb0654945 100644 --- a/code/particle/volumes/ModelSurfaceVolume.cpp +++ b/code/particle/volumes/ModelSurfaceVolume.cpp @@ -18,7 +18,7 @@ vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype( if (pm != nullptr) { if (submodel < 0) { SCP_vector eligible_submodels; - for (size_t i = 0; i < pm->n_models; ++i) { + for (size_t i = 0; i < static_cast(pm->n_models); ++i) { if (!pm->submodel[i].flags[Model::Submodel_flags::Is_lod, Model::Submodel_flags::Is_damaged, Model::Submodel_flags::Is_live_debris]) eligible_submodels.emplace_back(i); } From ea9f5ab74ce9d692109455da4fb9a78a045fc11a Mon Sep 17 00:00:00 2001 From: Birk Magnussen <6238428+BMagnu@users.noreply.github.com> Date: Sat, 9 May 2026 22:04:55 +0900 Subject: [PATCH 4/8] Fix warning --- code/particle/volumes/ModelSurfaceVolume.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/particle/volumes/ModelSurfaceVolume.cpp b/code/particle/volumes/ModelSurfaceVolume.cpp index 5ceb0654945..8c856165b60 100644 --- a/code/particle/volumes/ModelSurfaceVolume.cpp +++ b/code/particle/volumes/ModelSurfaceVolume.cpp @@ -18,7 +18,7 @@ vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype( if (pm != nullptr) { if (submodel < 0) { SCP_vector eligible_submodels; - for (size_t i = 0; i < static_cast(pm->n_models); ++i) { + for (int i = 0; i < pm->n_models; ++i) { if (!pm->submodel[i].flags[Model::Submodel_flags::Is_lod, Model::Submodel_flags::Is_damaged, Model::Submodel_flags::Is_live_debris]) eligible_submodels.emplace_back(i); } From 88d224ad8b995608b777762082b891613526eed6 Mon Sep 17 00:00:00 2001 From: Birk Magnussen <6238428+BMagnu@users.noreply.github.com> Date: Sat, 9 May 2026 22:23:59 +0900 Subject: [PATCH 5/8] Fix warning 2 --- code/particle/volumes/ModelSurfaceVolume.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/particle/volumes/ModelSurfaceVolume.cpp b/code/particle/volumes/ModelSurfaceVolume.cpp index 8c856165b60..f5d46c8ce06 100644 --- a/code/particle/volumes/ModelSurfaceVolume.cpp +++ b/code/particle/volumes/ModelSurfaceVolume.cpp @@ -27,7 +27,7 @@ vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype( const bsp_info* submodel_data = &pm->submodel[submodel]; const auto& geometry_data = *submodel_data->buffer.model_list; - size_t target_vertex = ::util::UniformUIntRange(0, geometry_data.n_verts - 1).next(); + size_t target_vertex = ::util::UniformUIntRange(0, static_cast(geometry_data.n_verts) - 1).next(); //This point is, despite its name, not in world space, but in model local space (NOT submodel local though!) point = geometry_data.vert[target_vertex].world; From b42d5d8620f5cf578cdab2082ba2b85db7b1ac84 Mon Sep 17 00:00:00 2001 From: Birk Magnussen <6238428+BMagnu@users.noreply.github.com> Date: Sat, 9 May 2026 22:51:42 +0900 Subject: [PATCH 6/8] Fix warning 3 --- code/particle/volumes/ModelSurfaceVolume.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/particle/volumes/ModelSurfaceVolume.cpp b/code/particle/volumes/ModelSurfaceVolume.cpp index f5d46c8ce06..a8c1ed8386b 100644 --- a/code/particle/volumes/ModelSurfaceVolume.cpp +++ b/code/particle/volumes/ModelSurfaceVolume.cpp @@ -22,12 +22,12 @@ vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype( if (!pm->submodel[i].flags[Model::Submodel_flags::Is_lod, Model::Submodel_flags::Is_damaged, Model::Submodel_flags::Is_live_debris]) eligible_submodels.emplace_back(i); } - submodel = eligible_submodels[::util::UniformUIntRange(0, eligible_submodels.size() - 1).next()]; + submodel = eligible_submodels[::util::UniformUIntRange(0U, eligible_submodels.size() - 1).next()]; } const bsp_info* submodel_data = &pm->submodel[submodel]; const auto& geometry_data = *submodel_data->buffer.model_list; - size_t target_vertex = ::util::UniformUIntRange(0, static_cast(geometry_data.n_verts) - 1).next(); + size_t target_vertex = ::util::UniformUIntRange(0U, static_cast(geometry_data.n_verts) - 1).next(); //This point is, despite its name, not in world space, but in model local space (NOT submodel local though!) point = geometry_data.vert[target_vertex].world; From 8778af368de1d4e655c36dd7dfcff21ef2b49580 Mon Sep 17 00:00:00 2001 From: Birk Magnussen <6238428+BMagnu@users.noreply.github.com> Date: Sun, 10 May 2026 06:36:16 +0900 Subject: [PATCH 7/8] Fix warning 4 --- code/particle/volumes/ModelSurfaceVolume.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/particle/volumes/ModelSurfaceVolume.cpp b/code/particle/volumes/ModelSurfaceVolume.cpp index a8c1ed8386b..b79d24d1692 100644 --- a/code/particle/volumes/ModelSurfaceVolume.cpp +++ b/code/particle/volumes/ModelSurfaceVolume.cpp @@ -22,12 +22,12 @@ vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype( if (!pm->submodel[i].flags[Model::Submodel_flags::Is_lod, Model::Submodel_flags::Is_damaged, Model::Submodel_flags::Is_live_debris]) eligible_submodels.emplace_back(i); } - submodel = eligible_submodels[::util::UniformUIntRange(0U, eligible_submodels.size() - 1).next()]; + submodel = eligible_submodels[::util::UniformUIntRange(0U, static_cast(eligible_submodels.size()) - 1).next()]; } const bsp_info* submodel_data = &pm->submodel[submodel]; const auto& geometry_data = *submodel_data->buffer.model_list; - size_t target_vertex = ::util::UniformUIntRange(0U, static_cast(geometry_data.n_verts) - 1).next(); + size_t target_vertex = ::util::UniformUIntRange(0U, static_cast(geometry_data.n_verts) - 1).next(); //This point is, despite its name, not in world space, but in model local space (NOT submodel local though!) point = geometry_data.vert[target_vertex].world; From d3cb0388b093d8b0410c26f86422164545a50097 Mon Sep 17 00:00:00 2001 From: Birk Magnussen <6238428+BMagnu@users.noreply.github.com> Date: Sun, 10 May 2026 09:48:11 +0900 Subject: [PATCH 8/8] Add scale parameter --- code/particle/volumes/ModelSurfaceVolume.cpp | 12 +++++++++--- code/particle/volumes/ModelSurfaceVolume.h | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/code/particle/volumes/ModelSurfaceVolume.cpp b/code/particle/volumes/ModelSurfaceVolume.cpp index b79d24d1692..ee24fe800a9 100644 --- a/code/particle/volumes/ModelSurfaceVolume.cpp +++ b/code/particle/volumes/ModelSurfaceVolume.cpp @@ -3,7 +3,7 @@ #include "math/vecmat.h" namespace particle { -ModelSurfaceVolume::ModelSurfaceVolume() : m_modular_curve_instance(m_modular_curves.create_instance()) { +ModelSurfaceVolume::ModelSurfaceVolume() : m_modelScale(::util::UniformFloatRange(1.f)), m_modular_curve_instance(m_modular_curves.create_instance()) { Model_load_clear_CPU_buffers = false; }; @@ -13,6 +13,8 @@ vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype( vec3d point = ZERO_VECTOR; + auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); + if (obj_num >= 0) { const polymodel* pm = object_get_model(&Objects[obj_num]); if (pm != nullptr) { @@ -31,17 +33,21 @@ vec3d ModelSurfaceVolume::sampleRandomPoint(const matrix &orientation, decltype( //This point is, despite its name, not in world space, but in model local space (NOT submodel local though!) point = geometry_data.vert[target_vertex].world; + + point *= m_modelScale.next() * m_modular_curves.get_output(VolumeModularCurveOutput::SCALE_MULT, curveSource, &m_modular_curve_instance); } } - auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); - return pointCompensateForOffsetAndRotOffset(point, orientation, m_modular_curves.get_output(VolumeModularCurveOutput::OFFSET_ROT, curveSource, &m_modular_curve_instance), m_modular_curves.get_output(VolumeModularCurveOutput::POINT_TO_ROT, curveSource, &m_modular_curve_instance)); } void ModelSurfaceVolume::parse() { + if (optional_string("+Scale:")) { + m_modelScale = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + ParticleVolume::parseCommon(); m_modular_curves.parse("$Volume Curve:"); diff --git a/code/particle/volumes/ModelSurfaceVolume.h b/code/particle/volumes/ModelSurfaceVolume.h index 9e146ba2a41..e780f36753c 100644 --- a/code/particle/volumes/ModelSurfaceVolume.h +++ b/code/particle/volumes/ModelSurfaceVolume.h @@ -5,12 +5,12 @@ namespace particle { class ModelSurfaceVolume : public ParticleVolume { -public: - enum class VolumeModularCurveOutput : uint8_t {OFFSET_ROT, POINT_TO_ROT, NUM_VALUES}; + ::util::ParsedRandomFloatRange m_modelScale; + enum class VolumeModularCurveOutput : uint8_t {SCALE_MULT, OFFSET_ROT, POINT_TO_ROT, NUM_VALUES}; -private: constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_subset( std::array { + std::pair { "Scale Mult", VolumeModularCurveOutput::SCALE_MULT }, std::pair { "Offset Rotate Around Fvec", VolumeModularCurveOutput::OFFSET_ROT }, std::pair { "Point To Rotate Around Fvec", VolumeModularCurveOutput::POINT_TO_ROT } },