diff --git a/Assets/PopcornFX_Dependencies.xml b/Assets/PopcornFX_Dependencies.xml index 71a5890..4b6bdd9 100644 --- a/Assets/PopcornFX_Dependencies.xml +++ b/Assets/PopcornFX_Dependencies.xml @@ -37,4 +37,5 @@ + diff --git a/Code/CMakeLists.txt b/Code/CMakeLists.txt index 7cbb325..d127a3e 100644 --- a/Code/CMakeLists.txt +++ b/Code/CMakeLists.txt @@ -3,7 +3,7 @@ # https://www.popcornfx.com/terms-and-conditions/ #---------------------------------------------------------------------------- -set(POPCORNFX_VERSION 2.18.1) +set(POPCORNFX_VERSION 2.15.16) set(POPCORNFX_LICENSE O3DE) if (PK_O3DE_MAJOR_VERSION GREATER_EQUAL 2205) @@ -33,6 +33,22 @@ ly_add_target( Include ) +set(PK_OPTIONAL_REGISTER_NODEABLE_FILES + *.ScriptCanvasNodeable.xml,ScriptCanvasNodeableRegistry_Header.jinja,AutoGenNodeableRegistry.generated.h + *.ScriptCanvasNodeable.xml,ScriptCanvasNodeableRegistry_Source.jinja,AutoGenNodeableRegistry.generated.cpp + ) +set(PK_OPTIONAL_REGISTER_NODEABLE_DEFINES PK_REGISTER_NODEABLE) + +if(DEFINED O3DE_VERSION_MAJOR) + # O3DE 3.x.x and higher no longer need the autogen nodeable registry source files + # Also include special case to handle bug where SDK 23.05.0 engine version was set to the display version + if(O3DE_VERSION_MAJOR GREATER_EQUAL 3 AND + NOT (O3DE_VERSION_MAJOR EQUAL 23 AND O3DE_VERSION_MINOR EQUAL 05 AND O3DE_VERSION_PATCH EQUAL 0)) + unset(PK_OPTIONAL_REGISTER_NODEABLE_FILES) + unset(PK_OPTIONAL_REGISTER_NODEABLE_DEFINES) + endif() +endif() + ly_add_target( NAME PopcornFX.Static STATIC NAMESPACE Gem @@ -46,6 +62,7 @@ ly_add_target( PK_USE_PHYSX PK_USE_EMOTIONFX PK_USE_STARTINGPOINTINPUT + ${PK_OPTIONAL_REGISTER_NODEABLE_DEFINES} INCLUDE_DIRECTORIES PRIVATE Source @@ -72,8 +89,7 @@ ly_add_target( AUTOGEN_RULES *.ScriptCanvasNodeable.xml,ScriptCanvasNodeable_Header.jinja,$path/$fileprefix.generated.h *.ScriptCanvasNodeable.xml,ScriptCanvasNodeable_Source.jinja,$path/$fileprefix.generated.cpp - *.ScriptCanvasNodeable.xml,ScriptCanvasNodeableRegistry_Header.jinja,AutoGenNodeableRegistry.generated.h - *.ScriptCanvasNodeable.xml,ScriptCanvasNodeableRegistry_Source.jinja,AutoGenNodeableRegistry.generated.cpp + ${PK_OPTIONAL_REGISTER_NODEABLE_FILES} ) ly_add_target( diff --git a/Code/Platform/Linux/PAL_linux.cmake b/Code/Platform/Linux/PAL_linux.cmake index 53df0c4..b7ff90f 100644 --- a/Code/Platform/Linux/PAL_linux.cmake +++ b/Code/Platform/Linux/PAL_linux.cmake @@ -6,12 +6,12 @@ set(LY_PACKAGE_SERVER_URLS ${LY_PACKAGE_SERVER_URLS} "https://downloads.popcornfx.com/o3de-packages") if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "x86_64") set(package_name PopcornFX-${POPCORNFX_VERSION}-${POPCORNFX_LICENSE}-linux) - set(pk_package_hash 08e30c12813e29e53eae075f4f9f02a1a308c7e8713bcf73abd9b207659abcd9) - set(pk_package_id Ng3RaXuqyAUWAgHy) + set(pk_package_hash a93262660d1f15cf86f7b5d1f674f9530b574d8b4ad480aceb8d4bb67a091836) + set(pk_package_id kX8RSRt2hZWQZutp) elseif(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "aarch64") set(package_name PopcornFX-${POPCORNFX_VERSION}-${POPCORNFX_LICENSE}-linux-aarch64) - set(pk_package_hash 0a81eb7ca2d717416efe2aa9ea4c5c3014e1b56c2acdb190556b8c490214beb4) - set(pk_package_id ZDEps23sNQA5p7q5) + set(pk_package_hash a93262660d1f15cf86f7b5d1f674f9530b574d8b4ad480aceb8d4bb67a091836_ARM64) + set(pk_package_id kX8RSRt2hZWQZutp_ARM64) else() message(FATAL_ERROR "Unsupported linux architecture ${CMAKE_SYSTEM_PROCESSOR}") endif() diff --git a/Code/Platform/Mac/PAL_mac.cmake b/Code/Platform/Mac/PAL_mac.cmake index 5425749..4f29180 100644 --- a/Code/Platform/Mac/PAL_mac.cmake +++ b/Code/Platform/Mac/PAL_mac.cmake @@ -5,8 +5,8 @@ set(LY_PACKAGE_SERVER_URLS ${LY_PACKAGE_SERVER_URLS} "https://downloads.popcornfx.com/o3de-packages") set(package_name PopcornFX-${POPCORNFX_VERSION}-${POPCORNFX_LICENSE}-mac) -set(pk_package_hash 8e371903268af71a1a362194c7b58f43094b1ff9accc27f0fd4a117df470e760) -set(pk_package_id Kph45AXPVnUw9u6K) +set(pk_package_hash 5146d7599485b397f6e53c8b5cc628bbbb72e9a22b9a0feef60cf524d5e71abb) +set(pk_package_id roDQZpBI1HS7O2SX) ly_associate_package(PACKAGE_NAME ${package_name} TARGETS PopcornFX PACKAGE_HASH ${pk_package_hash}) pk_download_package_ifn(${package_name} ${pk_package_id}) diff --git a/Code/Platform/Windows/PAL_windows.cmake b/Code/Platform/Windows/PAL_windows.cmake index 0435da6..2bcd9d6 100644 --- a/Code/Platform/Windows/PAL_windows.cmake +++ b/Code/Platform/Windows/PAL_windows.cmake @@ -5,8 +5,8 @@ set(LY_PACKAGE_SERVER_URLS ${LY_PACKAGE_SERVER_URLS} "https://downloads.popcornfx.com/o3de-packages") set(package_name PopcornFX-${POPCORNFX_VERSION}-${POPCORNFX_LICENSE}-windows) -set(pk_package_hash 4813982cd76ea251972abd8868f09a71d7f03fe3bd486dc1fc0ecbe243327354) -set(pk_package_id UntkQKUXJ57BnbcB) +set(pk_package_hash 50ef4f2879a49b9e10c438076dce1c9ea5b6f70f92bd61f8abdf8a770c26e968) +set(pk_package_id PsvjDNhF0Trboq2I) ly_associate_package(PACKAGE_NAME ${package_name} TARGETS PopcornFX PACKAGE_HASH ${pk_package_hash}) pk_download_package_ifn(${package_name} ${pk_package_id}) diff --git a/Code/Source/Integration/SceneInterface/SceneInterface.cpp b/Code/Source/Integration/SceneInterface/SceneInterface.cpp index 040696c..5b28537 100644 --- a/Code/Source/Integration/SceneInterface/SceneInterface.cpp +++ b/Code/Source/Integration/SceneInterface/SceneInterface.cpp @@ -9,57 +9,34 @@ #if defined(O3DE_USE_PK) +#include + #if defined(PK_USE_PHYSX) #include - #include - #include #include + #include #include - #include + #if RESOLVE_MATERIAL_PROPERTIES + #include + #endif + #if RESOLVE_CONTACT_OBJECT + #include + #endif #endif -#include - +#include "Integration/PopcornFXIntegrationBus.h" #include "Integration/PopcornFXUtils.h" namespace PopcornFX { - static void PrintPopcornFXPhysicsSurfaceTypesConstants(const AZ::ConsoleCommandContainer&) - { - if (auto *materialManager = AZ::Interface::Get()) - { - AZStd::shared_ptr defaultMaterial = materialManager->GetDefaultMaterial(); - if (defaultMaterial != null) - { - AZ_Printf("PopcornFX", "physics.surfaceTypes.Default %u", AZ::Crc32(defaultMaterial->GetId().ToString())); - } - } - - AZ::Data::AssetCatalogRequests::AssetEnumerationCB popcornFxAssetReloadCb = [](const AZ::Data::AssetId id, const AZ::Data::AssetInfo& info) - { - if (info.m_assetType == ::Physics::MaterialAsset::RTTI_Type()) - { - Physics::MaterialId materialId = Physics::MaterialId::CreateFromAssetId(id); - AZStd::string materialName = info.m_relativePath; - - AZ::StringFunc::Replace(materialName, ".physicsmaterial", ""); - AZ::StringFunc::Replace(materialName, "/", "."); - - AZ_Printf("PopcornFX", "physics.surfaceTypes.%s %u", materialName.c_str(), AZ::Crc32(materialId.ToString())); - } - }; - AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::EnumerateAssets, null, popcornFxAssetReloadCb, null); - } - - AZ_CONSOLEFREEFUNC(PrintPopcornFXPhysicsSurfaceTypesConstants, AZ::ConsoleFunctorFlags::Null, "Print the physics surface types constants to add in your PopcornFX project settings."); - -#if defined(PK_USE_PHYSX) void CSceneInterface::RayTracePacket(const Colliders::STraceFilter &traceFilter, const Colliders::SRayPacket &packet, const Colliders::STracePacket &results) { - AzPhysics::SceneHandle sceneHandle = AzPhysics::InvalidSceneHandle; - AzPhysics::SceneInterface *sceneInterface = AZ::Interface::Get(); +#if defined(PK_USE_PHYSX) + AzPhysics::SceneHandle sceneHandle = AzPhysics::InvalidSceneHandle; + auto *sceneInterface = AZ::Interface::Get(); + if (sceneInterface != null) { #if defined(POPCORNFX_EDITOR) @@ -75,14 +52,6 @@ namespace PopcornFX { if (!PK_VERIFY(sceneHandle != AzPhysics::InvalidSceneHandle)) return; - Physics::MaterialManager *materialManager = null; - if (results.m_ContactSurfaces_Aligned16 != null) - { - materialManager = AZ::Interface::Get(); - if (!PK_VERIFY(materialManager != null)) - return; - } - AzPhysics::CollisionGroup collisionGroup(traceFilter.m_FilterFlags == 0 ? AzPhysics::CollisionGroup::All.GetMask() : traceFilter.m_FilterFlags); const u32 resCount = results.Count(); @@ -109,7 +78,6 @@ namespace PopcornFX { request.m_direction = ToAZ(dir); request.m_distance = _rayDirAndLen.w(); request.m_collisionGroup = collisionGroup; - PK_ASSERT(request.m_reportMultipleHits == false); hitResult = sceneInterface->QueryScene(sceneHandle, &request); } @@ -121,94 +89,81 @@ namespace PopcornFX { request.m_direction = ToAZ(dir); request.m_shapeConfiguration = AZStd::make_shared(packet.m_RaySweepRadii_Aligned16[rayi]); request.m_collisionGroup = collisionGroup; - PK_ASSERT(request.m_reportMultipleHits == false); hitResult = sceneInterface->QueryScene(sceneHandle, &request); } if (!hitResult.m_hits.empty()) { - //m_reportMultipleHits in AzPhysics::RayCastRequest and AzPhysics::ShapeCastRequest are set to false by default, only 1 hit possible + //m_reportMultipleHits in AzPhysics::RayCastRequest and AzPhysics::ShapeCastRequest are set to false, only 1 hit possible const AzPhysics::SceneQueryHit &hit = hitResult.m_hits[0]; results.m_HitTimes_Aligned16[rayi] = hit.m_distance; if (results.m_ContactObjects_Aligned16 != null) { - const bool haveBodyHandle = hit.m_resultFlags & AzPhysics::SceneQuery::ResultFlags::BodyHandle; - if (!haveBodyHandle) - results.m_ContactObjects_Aligned16[rayi] = null; - else - results.m_ContactObjects_Aligned16[rayi] = sceneInterface->GetSimulatedBodyFromHandle(sceneHandle, hit.m_bodyHandle); - } - if (results.m_ContactSurfaces_Aligned16 != null) - { - const bool havePhysicsMaterial = hit.m_resultFlags & AzPhysics::SceneQuery::ResultFlags::Material; - if (!havePhysicsMaterial) - results.m_ContactSurfaces_Aligned16[rayi] = null; - else +#if RESOLVE_CONTACT_OBJECT + Physics::RigidBody *rigidbody = null; + AZ::Entity *entity = null; + EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, hit.m_body->GetEntityId()); + if (entity) { - AZStd::shared_ptr material = AZStd::rtti_pointer_cast(materialManager->GetMaterial(hit.m_physicsMaterialId)); - results.m_ContactSurfaces_Aligned16[rayi] = material.get(); + Physics::RigidBodyRequestBus::EventResult(rigidbody, hit.m_body->GetEntityId(), &Physics::RigidBodyRequests::GetRigidBody); } + if (rigidbody) + results.m_ContactObjects_Aligned16[rayi] = rigidbody; + else +#endif + results.m_ContactObjects_Aligned16[rayi] = CollidableObject::DEFAULT; } if (results.m_ContactPoints_Aligned16 != null) results.m_ContactPoints_Aligned16[rayi].xyz() = ToPk(hit.m_position); if (results.m_ContactNormals_Aligned16 != null) results.m_ContactNormals_Aligned16[rayi].xyz() = ToPk(hit.m_normal); +#if RESOLVE_MATERIAL_PROPERTIES + if (results.m_ContactSurfaces_Aligned16 != null) + { + AZ::Entity *entity = null; + EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, hit.m_entityId); + if (entity) + { + Physics::RigidBody *rigidbody; + Physics::RigidBodyRequestBus::EventResult(rigidbody, hit.m_body->GetEntityId(), &Physics::RigidBodyRequests::GetRigidBody); + if (rigidbody) + { + Physics::MaterialId materialId = rigidbody->GetMaterialIdForShapeHierarchy(hit.m_hitShapeIdHierarchy); + Physics::MaterialProperties *matProperties = null; + Physics::MaterialRequestBus::BroadcastResult(matProperties, &Physics::MaterialRequests::GetPhysicsMaterialProperties, materialId); + results.m_ContactSurfaces_Aligned16[rayi] = matProperties; + } + } + } +#endif //RESOLVE_MATERIAL_PROPERTIES } } +#endif } - void CSceneInterface::ResolveContactMaterials( [[maybe_unused]] const TMemoryView &contactObjects, - const TMemoryView &contactSurfaces, - const TMemoryView &outSurfaceProperties) const - { - PK_ASSERT(contactObjects.Count() == contactSurfaces.Count()); - PK_ASSERT(contactObjects.Count() == outSurfaceProperties.Count()); - - static const PopcornFX::Colliders::SSurfaceProperties kDefaultSurface; - - const u32 materialCount = contactSurfaces.Count(); - for (u32 iMaterial = 0; iMaterial < materialCount; ++iMaterial) - { - PopcornFX::Colliders::SSurfaceProperties &surface = outSurfaceProperties[iMaterial]; - surface = kDefaultSurface; +#if RESOLVE_MATERIAL_PROPERTIES +void CSceneInterface::ResolveContactMaterials( const TMemoryView &contactObjects, + const TMemoryView &contactSurfaces, + const TMemoryView&outSurfaceProperties) const +{ + const u32 materialCount = contactSurfaces.Count(); - PhysX::Material *material = reinterpret_cast(contactSurfaces[iMaterial]); - if (material == null) - continue; - - surface.m_Restitution = material->GetRestitution(); - surface.m_StaticFriction = material->GetStaticFriction(); - surface.m_DynamicFriction = material->GetDynamicFriction(); - surface.m_SurfaceType = AZ::Crc32(material->GetId().ToString()); - -#define REMAP_COMBINE_MODE(__member, __val) \ - switch (__val) \ - { \ - case PhysX::CombineMode::Average: \ - surface.__member = PopcornFX::Colliders::ECombineMode::Combine_Average; \ - break; \ - case PhysX::CombineMode::Minimum: \ - surface.__member = PopcornFX::Colliders::ECombineMode::Combine_Min; \ - break; \ - case PhysX::CombineMode::Maximum: \ - surface.__member = PopcornFX::Colliders::ECombineMode::Combine_Max; \ - break; \ - case PhysX::CombineMode::Multiply: \ - surface.__member = PopcornFX::Colliders::ECombineMode::Combine_Multiply; \ - break; \ - default: \ - PK_ASSERT_NOT_REACHED(); \ - break; \ - } - - REMAP_COMBINE_MODE(m_FrictionCombineMode, material->GetFrictionCombineMode()); - REMAP_COMBINE_MODE(m_RestitutionCombineMode, material->GetRestitutionCombineMode()); - -#undef REMAP_COMBINE_MODE - } + for (u32 iMaterial = 0; iMaterial < materialCount; ++iMaterial) + { + PopcornFX::Colliders::SSurfaceProperties &surface = outSurfaceProperties[iMaterial]; + Physics::MaterialProperties *matProperties = reinterpret_cast(contactSurfaces[iMaterial]); + if (matProperties == null) + continue; + surface.m_Restitution = matProperties->m_restitution; + surface.m_StaticFriction = matProperties->m_friction; + surface.m_DynamicFriction = surface.m_StaticFriction; + surface.m_SurfaceType = AZStd::hash{}(matProperties->m_name); + surface.m_RestitutionCombineMode = Colliders::Combine_Average; + surface.m_FrictionCombineMode = Colliders::Combine_Average; } -#endif //PK_USE_PHYSX +} +#endif //RESOLVE_MATERIAL_PROPERTIES } diff --git a/Code/Source/Integration/SceneInterface/SceneInterface.h b/Code/Source/Integration/SceneInterface/SceneInterface.h index b8297e9..33c6911 100644 --- a/Code/Source/Integration/SceneInterface/SceneInterface.h +++ b/Code/Source/Integration/SceneInterface/SceneInterface.h @@ -8,20 +8,23 @@ #include +#define RESOLVE_MATERIAL_PROPERTIES 0 +#define RESOLVE_CONTACT_OBJECT 0 + namespace PopcornFX { class CSceneInterface : public IParticleScene { public: -#if defined(PK_USE_PHYSX) virtual void RayTracePacket( const Colliders::STraceFilter &traceFilter, const Colliders::SRayPacket &packet, const Colliders::STracePacket &results) override; +#if RESOLVE_MATERIAL_PROPERTIES virtual void ResolveContactMaterials(const TMemoryView &contactObjects, const TMemoryView &contactSurfaces, const TMemoryView&outSurfaceProperties) const override; -#endif //PK_USE_PHYSX +#endif void SetInGameMode(bool inGameMode) { m_InGameMode = inGameMode; } diff --git a/Code/Source/Integration/Startup/PopcornFxStartup.cpp b/Code/Source/Integration/Startup/PopcornFxStartup.cpp index 0e48a0e..5822530 100644 --- a/Code/Source/Integration/Startup/PopcornFxStartup.cpp +++ b/Code/Source/Integration/Startup/PopcornFxStartup.cpp @@ -85,6 +85,23 @@ namespace PopcornFX { }; } }; +#else + class CLogListenerO3DERelease : public ILogListener + { + public: + virtual void Notify(CLog::ELogLevel level, CGuid logClass, const char* message) override + { + const CString s = CString::Format("[%s] ERROR: %s", CLog::LogClassToString(logClass), message); + + switch (level) + { + case PopcornFX::CLog::Level_Error: + case PopcornFX::CLog::Level_ErrorCritical: + AZ_Printf("PopcornFX", s.Data()); + break; + }; + } + }; #endif //---------------------------------------------------------------------------- @@ -95,6 +112,8 @@ namespace PopcornFX { #ifndef PK_RETAIL CLog::AddGlobalListener(PK_NEW(CLogListenerO3DE)); +#else + CLog::AddGlobalListener(PK_NEW(CLogListenerO3DERelease)); #endif } diff --git a/Code/Source/PopcornFXSystemComponent.cpp b/Code/Source/PopcornFXSystemComponent.cpp index 231d7ca..8ee3d59 100644 --- a/Code/Source/PopcornFXSystemComponent.cpp +++ b/Code/Source/PopcornFXSystemComponent.cpp @@ -20,10 +20,14 @@ #include "Asset/PopcornFXAsset.h" #include "Asset/PopcornFXAssetHandler.h" +// Logic for setting PK_REGISTER_NODEABLE is in Code/CMakeLists.txt +#if PK_REGISTER_NODEABLE +// In O3DE versions less than 3.x.x we need to register autogenerated nodeables #include #include REGISTER_SCRIPTCANVAS_AUTOGEN_NODEABLE(PopcornFXStatic); +#endif namespace PopcornFX { diff --git a/Code/popcornfx_autogen_files.cmake b/Code/popcornfx_autogen_files.cmake index a40dc29..68eaa61 100644 --- a/Code/popcornfx_autogen_files.cmake +++ b/Code/popcornfx_autogen_files.cmake @@ -5,10 +5,23 @@ get_property(scriptcanvas_gem_root GLOBAL PROPERTY "@GEMROOT:ScriptCanvas@") +set(PK_OPTIONAL_REGISTER_NODEABLE_FILES + ${scriptcanvas_gem_root}/Code/Include/ScriptCanvas/AutoGen/ScriptCanvasNodeableRegistry_Header.jinja + ${scriptcanvas_gem_root}/Code/Include/ScriptCanvas/AutoGen/ScriptCanvasNodeableRegistry_Source.jinja + ) + +if(DEFINED O3DE_VERSION_MAJOR) + # O3DE 3.x.x and higher no longer need the registry source files + # Also include special case to handle bug where SDK 23.05.0 engine version was set to the display version + if(O3DE_VERSION_MAJOR GREATER_EQUAL 3 AND + NOT (O3DE_VERSION_MAJOR EQUAL 23 AND O3DE_VERSION_MINOR EQUAL 05 AND O3DE_VERSION_PATCH EQUAL 0)) + unset(PK_OPTIONAL_REGISTER_NODEABLE_FILES) + endif() +endif() + set(FILES ${scriptcanvas_gem_root}/Code/Include/ScriptCanvas/AutoGen/ScriptCanvasNodeable_Header.jinja ${scriptcanvas_gem_root}/Code/Include/ScriptCanvas/AutoGen/ScriptCanvasNodeable_Source.jinja - ${scriptcanvas_gem_root}/Code/Include/ScriptCanvas/AutoGen/ScriptCanvasNodeableRegistry_Header.jinja - ${scriptcanvas_gem_root}/Code/Include/ScriptCanvas/AutoGen/ScriptCanvasNodeableRegistry_Source.jinja + ${PK_OPTIONAL_REGISTER_NODEABLE_FILES} ) diff --git a/README.md b/README.md index 83f0a41..cfd2049 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # O3DE PopcornFX Plugin Integrates the **PopcornFX Runtime SDK** into **O3DE** as a Gem. -* **Version:** `v2.18.1` -* **O3DE:** `23.05` +* **Version:** `v2.15.16` +* **O3DE:** `23.05`, `23.10` * **Supported platforms:** `Windows`, `MacOS`, `Linux`, `iOS`, `Android` **Note:** Mobile platforms are in an experimental stage. [Contact-us](http://www.popcornfx.com/contact-us/) to request access. diff --git a/gem.json b/gem.json index 42eb58c..2ab604f 100644 --- a/gem.json +++ b/gem.json @@ -1,13 +1,13 @@ { "gem_name": "PopcornFX", - "display_name": "PopcornFX 2.18.1", + "display_name": "PopcornFX 2.15.16", "license": "Community", "license_url": "https://www.popcornfx.com/popcornfx-community-license", "origin": "Persistant Studios - popcornfx.com", "repo_uri": "https://downloads.popcornfx.com/o3de-repo", - "origin_uri": "https://downloads.popcornfx.com/o3de-repo/PopcornFX-2.18/O3DE_PopcornFXGem_v2.18.1_Win64_Linux64_LinuxARM64_Mac64.zip", - "version": "2.18.1", - "last_updated": "2023-10-26", + "origin_uri": "https://downloads.popcornfx.com/o3de-repo/PopcornFX-2.15/O3DE_PopcornFXGem_v2.15.16_Win64_Linux64_LinuxARM64_Mac64.zip", + "version": "2.15.16", + "last_updated": "2023-12-01", "type": "Code", "summary": "The PopcornFX Gem provides real-time FX solution for particle effects.", "canonical_tags": [