#include #include #include #include "particri/math.hpp" #include "particri/particle.hpp" #define FNL_IMPL #include "particri/forces.hpp" #include "particri/emitter.hpp" #include "particri/system.hpp" namespace py = pybind11; using namespace particri; PYBIND11_MODULE(_particri_core, m) { m.doc() = "Particri C++ High-Performance Particle Engine Core"; // Enums py::enum_(m, "CurveType") .value("Linear", CurveType::Linear) .value("Bell", CurveType::Bell) .value("EaseIn", CurveType::EaseIn) .value("EaseOut", CurveType::EaseOut) .value("GrowAndFade", CurveType::GrowAndFade) .value("Pulse", CurveType::Pulse) .value("Decay", CurveType::Decay) .export_values(); py::enum_(m, "ParticleShape") .value("Bokeh", ParticleShape::Bokeh) .value("Star4", ParticleShape::Star4) .value("Star8", ParticleShape::Star8) .value("Halo", ParticleShape::Halo) .value("Streak", ParticleShape::Streak) .value("Glitter", ParticleShape::Glitter) .value("Custom", ParticleShape::Custom) .export_values(); py::enum_(m, "BlendMode") .value("Additive", BlendMode::Additive) .value("Screen", BlendMode::Screen) .value("Alpha", BlendMode::Alpha) .value("ColorDodge", BlendMode::ColorDodge) .export_values(); py::enum_(m, "EmitterShape") .value("Point", EmitterShape::Point) .value("Circle", EmitterShape::Circle) .value("Ring", EmitterShape::Ring) .value("Box", EmitterShape::Box) .value("Line", EmitterShape::Line) .value("LayerMask", EmitterShape::LayerMask) .export_values(); // Math structs py::class_(m, "Vec2") .def(py::init(), py::arg("x") = 0.0f, py::arg("y") = 0.0f) .def_readwrite("x", &Vec2::x) .def_readwrite("y", &Vec2::y) .def("__repr__", [](const Vec2& v) { return "Vec2(" + std::to_string(v.x) + ", " + std::to_string(v.y) + ")"; }); py::class_(m, "Color4") .def(py::init(), py::arg("r") = 1.0f, py::arg("g") = 1.0f, py::arg("b") = 1.0f, py::arg("a") = 1.0f) .def_readwrite("r", &Color4::r) .def_readwrite("g", &Color4::g) .def_readwrite("b", &Color4::b) .def_readwrite("a", &Color4::a) .def_static("from_hex", &Color4::from_hex, py::arg("hex"), py::arg("alpha") = 1.0f) .def("__repr__", [](const Color4& c) { return "Color4(" + std::to_string(c.r) + ", " + std::to_string(c.g) + ", " + std::to_string(c.b) + ", " + std::to_string(c.a) + ")"; }); // EmitterConfig py::class_(m, "EmitterConfig") .def(py::init<>()) .def_readwrite("shape", &EmitterConfig::shape) .def_readwrite("position", &EmitterConfig::position) .def_readwrite("size", &EmitterConfig::size) .def_readwrite("line_end", &EmitterConfig::line_end) .def_readwrite("rate", &EmitterConfig::rate) .def_readwrite("speed_min", &EmitterConfig::speed_min) .def_readwrite("speed_max", &EmitterConfig::speed_max) .def_readwrite("angle_min", &EmitterConfig::angle_min) .def_readwrite("angle_max", &EmitterConfig::angle_max) .def_readwrite("lifetime_min", &EmitterConfig::lifetime_min) .def_readwrite("lifetime_max", &EmitterConfig::lifetime_max) .def_readwrite("size_start_min", &EmitterConfig::size_start_min) .def_readwrite("size_start_max", &EmitterConfig::size_start_max) .def_readwrite("size_end_min", &EmitterConfig::size_end_min) .def_readwrite("size_end_max", &EmitterConfig::size_end_max) .def_readwrite("color_start", &EmitterConfig::color_start) .def_readwrite("color_start_variance", &EmitterConfig::color_start_variance) .def_readwrite("color_end", &EmitterConfig::color_end) .def_readwrite("particle_shape", &EmitterConfig::particle_shape) .def_readwrite("size_curve", &EmitterConfig::size_curve) .def_readwrite("opacity_curve", &EmitterConfig::opacity_curve) .def_readwrite("inherit_layer_color", &EmitterConfig::inherit_layer_color) .def_readwrite("rotation_speed_min", &EmitterConfig::rotation_speed_min) .def_readwrite("rotation_speed_max", &EmitterConfig::rotation_speed_max) .def_readwrite("twinkle_speed_min", &EmitterConfig::twinkle_speed_min) .def_readwrite("twinkle_speed_max", &EmitterConfig::twinkle_speed_max) .def_readwrite("drag", &EmitterConfig::drag) .def_readwrite("radial_velocity", &EmitterConfig::radial_velocity); // AuxConfig (Trapcode Particular Auxiliary System) py::class_(m, "AuxConfig") .def(py::init<>()) .def_readwrite("enabled", &AuxConfig::enabled) .def_readwrite("rate", &AuxConfig::rate) .def_readwrite("lifetime", &AuxConfig::lifetime) .def_readwrite("size_start", &AuxConfig::size_start) .def_readwrite("size_end", &AuxConfig::size_end) .def_readwrite("color_start", &AuxConfig::color_start) .def_readwrite("color_end", &AuxConfig::color_end) .def_readwrite("inherit_color", &AuxConfig::inherit_color) .def_readwrite("velocity_inheritance", &AuxConfig::velocity_inheritance) .def_readwrite("spread", &AuxConfig::spread) .def_readwrite("shape", &AuxConfig::shape) .def_readwrite("size_curve", &AuxConfig::size_curve) .def_readwrite("opacity_curve", &AuxConfig::opacity_curve) .def_readwrite("emit_at_death", &AuxConfig::emit_at_death); // ParticleSystem py::class_(m, "ParticleSystem") .def(py::init(), py::arg("max_particles") = 20000, py::arg("width") = 1920, py::arg("height") = 1080) .def("add_emitter", &ParticleSystem::add_emitter, py::arg("config"), py::arg("seed") = 42) .def("add_gravity", [](ParticleSystem& s, float gx, float gy) { s.add_force(std::make_shared(Vec2(gx, gy))); }, py::arg("gx"), py::arg("gy")) .def("add_drag", [](ParticleSystem& s, float coeff) { s.add_force(std::make_shared(coeff)); }, py::arg("coeff")) .def("add_vortex", [](ParticleSystem& s, float cx, float cy, float strength, float radial_pull, float radius) { s.add_force(std::make_shared(Vec2(cx, cy), strength, radial_pull, radius)); }, py::arg("cx"), py::arg("cy"), py::arg("strength"), py::arg("radial_pull") = 0.0f, py::arg("radius") = 800.0f) .def("add_attractor", [](ParticleSystem& s, float tx, float ty, float strength, float min_d, float max_d) { s.add_force(std::make_shared(Vec2(tx, ty), strength, min_d, max_d)); }, py::arg("tx"), py::arg("ty"), py::arg("strength"), py::arg("min_distance") = 20.0f, py::arg("max_distance") = 800.0f) .def("add_curl_noise", [](ParticleSystem& s, int seed, float strength, float frequency, float speed) { s.add_force(std::make_shared(seed, strength, frequency, speed)); }, py::arg("seed") = 1337, py::arg("strength") = 100.0f, py::arg("frequency") = 0.003f, py::arg("speed") = 0.3f) .def("add_brownian", [](ParticleSystem& s, float strength, uint32_t seed) { s.add_force(std::make_shared(strength, seed)); }, py::arg("strength") = 30.0f, py::arg("seed") = 42) .def("add_bounce_plane", [](ParticleSystem& s, float coord, float bounce, float friction, bool is_horizontal, bool is_max) { s.add_force(std::make_shared(coord, bounce, friction, is_horizontal, is_max)); }, py::arg("coord") = 900.0f, py::arg("bounce") = 0.65f, py::arg("friction") = 0.85f, py::arg("is_horizontal") = true, py::arg("is_max") = true) .def("set_aux_config", &ParticleSystem::set_aux_config, py::arg("config")) .def("get_aux_config", [](ParticleSystem& s) { return s.aux_config(); }) .def("set_emitter_layer_mask", [](ParticleSystem& s, size_t emitter_idx, py::array_t img_arr, float threshold) { auto em = s.get_emitter(emitter_idx); if (!em) return; auto buf = img_arr.request(); if (buf.ndim != 3 || buf.shape[2] != 4) { throw std::runtime_error("Layer mask array must have shape (H, W, 4) float32"); } int h = static_cast(buf.shape[0]); int w = static_cast(buf.shape[1]); const float* ptr = static_cast(buf.ptr); em->set_layer_mask(ptr, w, h, threshold); }, py::arg("emitter_idx"), py::arg("image_data"), py::arg("threshold") = 0.05f) .def("clear_forces", &ParticleSystem::clear_forces) .def("clear_emitters", &ParticleSystem::clear_emitters) .def("burst", &ParticleSystem::burst, py::arg("emitter_index"), py::arg("count")) .def("update", &ParticleSystem::update, py::arg("dt")) .def("render", &ParticleSystem::render, py::arg("blend_mode") = BlendMode::Additive, py::arg("enable_bloom") = true, py::arg("bloom_threshold") = 0.35f, py::arg("bloom_intensity") = 0.75f, py::arg("bloom_radius") = 8) .def("resize", &ParticleSystem::resize, py::arg("width"), py::arg("height")) .def_property_readonly("width", &ParticleSystem::width) .def_property_readonly("height", &ParticleSystem::height) .def_property_readonly("active_count", &ParticleSystem::active_count) .def("clear_particles", &ParticleSystem::clear_particles) .def("get_image", [](ParticleSystem& s) { int w = s.width(); int h = s.height(); py::array_t result({h, w, 4}); auto buf = result.request(); uint8_t* ptr = static_cast(buf.ptr); s.framebuffer().to_uint8_rgba(ptr); return result; }, "Returns rendered frame as an (H, W, 4) uint8 RGBA NumPy array") .def("get_hdr_image", [](ParticleSystem& s) { int w = s.width(); int h = s.height(); py::array_t result({h, w, 4}); auto buf = result.request(); float* ptr = static_cast(buf.ptr); std::memcpy(ptr, s.framebuffer().data.data(), w * h * 4 * sizeof(float)); return result; }, "Returns rendered frame as an (H, W, 4) float32 HDR RGBA NumPy array"); }