diff --git a/Gems/Pointcloud/.gitignore b/Gems/Pointcloud/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/Gems/Pointcloud/Assets/Shaders/Pointclouds/Pointclouds.azsl b/Gems/Pointcloud/Assets/Shaders/Pointclouds/Pointclouds.azsl new file mode 100644 index 00000000..844a6d60 --- /dev/null +++ b/Gems/Pointcloud/Assets/Shaders/Pointclouds/Pointclouds.azsl @@ -0,0 +1,54 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include + +ShaderResourceGroup PerDrawSrg : SRG_PerDraw +{ + float m_pointSize; + float4x4 m_modelMatrix; +} + +struct VSInput +{ + float3 m_position : POSITION; + float4 m_color : COLOR0; +}; + +struct VSOutput +{ + float4 m_position : SV_Position; + float4 m_color : COLOR0; + [[vk::builtin("PointSize")]] float PointSize : PSIZE; +}; + +VSOutput MainVS(VSInput IN) +{ + VSOutput OUT; + const float4 pos = float4(IN.m_position, 1.0); + const float4 pos2 = mul(PerDrawSrg::m_modelMatrix, pos); + OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, pos2); + OUT.m_color = IN.m_color; + OUT.PointSize = PerDrawSrg::m_pointSize; + return OUT; +}; + +struct PSOutput +{ + float4 m_color : SV_Target; +}; + +PSOutput MainPS(VSOutput IN) +{ + PSOutput OUT; + OUT.m_color = IN.m_color; + return OUT; +}; diff --git a/Gems/Pointcloud/Assets/Shaders/Pointclouds/Pointclouds.shader b/Gems/Pointcloud/Assets/Shaders/Pointclouds/Pointclouds.shader new file mode 100644 index 00000000..07b00dee --- /dev/null +++ b/Gems/Pointcloud/Assets/Shaders/Pointclouds/Pointclouds.shader @@ -0,0 +1,28 @@ +{ + "Source": "Pointclouds.azsl", + "DepthStencilState": { + "Depth": { + "Enable": true, + "CompareFunc": "GreaterEqual" + } + }, + "GlobalTargetBlendState" : { + "Enable" : false, + "BlendSource" : "Zero", + "BlendDest" : "One", + "BlendOp" : "Add" + }, + "DrawList": "auxgeom", + "ProgramSettings": { + "EntryPoints": [ + { + "name": "MainVS", + "type": "Vertex" + }, + { + "name": "MainPS", + "type": "Fragment" + } + ] + } +} \ No newline at end of file diff --git a/Gems/Pointcloud/CMakeLists.txt b/Gems/Pointcloud/CMakeLists.txt new file mode 100644 index 00000000..4841612b --- /dev/null +++ b/Gems/Pointcloud/CMakeLists.txt @@ -0,0 +1,29 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +# Query the gem name from the gem.json file if possible +# otherwise fallback to using Pointcloud +o3de_find_ancestor_gem_root(gem_path gem_name "${CMAKE_CURRENT_SOURCE_DIR}") +if (NOT gem_name) + set(gem_name "Pointcloud") +endif() + +# Fallback to using the current source CMakeLists.txt directory as the gem root path +if (NOT gem_path) + set(gem_path ${CMAKE_CURRENT_SOURCE_DIR}) +endif() + +set(gem_json ${gem_path}/gem.json) + +o3de_restricted_path(${gem_json} gem_restricted_path gem_parent_relative_path) + +o3de_pal_dir(pal_dir ${CMAKE_CURRENT_SOURCE_DIR}/Platform/${PAL_PLATFORM_NAME} "${gem_restricted_path}" "${gem_path}" "${gem_parent_relative_path}") + +ly_add_external_target_path(${CMAKE_CURRENT_SOURCE_DIR}/3rdParty) + +add_subdirectory(Code) diff --git a/Gems/Pointcloud/Code/CMakeLists.txt b/Gems/Pointcloud/Code/CMakeLists.txt new file mode 100644 index 00000000..92d88c28 --- /dev/null +++ b/Gems/Pointcloud/Code/CMakeLists.txt @@ -0,0 +1,242 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +# Currently we are in the Code folder: ${CMAKE_CURRENT_LIST_DIR} +# Get the platform specific folder ${pal_dir} for the current folder: ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} +# Note: o3de_pal_dir will take care of the details for us, as this may be a restricted platform +# in which case it will see if that platform is present here or in the restricted folder. +# i.e. It could here in our gem : Gems/Pointcloud/Code/Platform/ or +# //Gems/Pointcloud/Code +o3de_pal_dir(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} "${gem_restricted_path}" "${gem_path}" "${gem_parent_relative_path}") + +# Now that we have the platform abstraction layer (PAL) folder for this folder, thats where we will find the +# traits for this platform. Traits for a platform are defines for things like whether or not something in this gem +# is supported by this platform. +include(${pal_dir}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) + +# Check to see if building the Gem Modules are supported for the current platform +if(NOT PAL_TRAIT_POINTCLOUD_SUPPORTED) + return() +endif() + +# The ${gem_name}.API target declares the common interface that users of this gem should depend on in their targets +ly_add_target( + NAME ${gem_name}.API INTERFACE + NAMESPACE Gem + FILES_CMAKE + pointcloud_api_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Include + BUILD_DEPENDENCIES + INTERFACE + AZ::AzCore + Gem::Atom_RPI.Public + Gem::Atom_Feature_Common + Gem::Atom_Feature_Common.Public + Gem::Atom_Feature_Common.Static +) + +# The ${gem_name}.Private.Object target is an internal target +# It should not be used outside of this Gems CMakeLists.txt +ly_add_target( + NAME ${gem_name}.Private.Object STATIC + NAMESPACE Gem + PLATFORM_INCLUDE_FILES + ${CMAKE_CURRENT_LIST_DIR}/Platform/Common/${PAL_TRAIT_COMPILER_ID}/pointcloud_editor_${PAL_TRAIT_COMPILER_ID_LOWERCASE}.cmake + FILES_CMAKE + pointcloud_private_files.cmake + TARGET_PROPERTIES + O3DE_PRIVATE_TARGET TRUE + INCLUDE_DIRECTORIES + PRIVATE + Include + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzCore + AZ::AzFramework + Gem::Atom_RPI.Public + Gem::Atom_Utils.Static + Gem::Atom_Feature_Common + Gem::AtomLyIntegration_CommonFeatures.Static +) + +# Here add ${gem_name} target, it depends on the Private Object library and Public API interface +ly_add_target( + NAME ${gem_name} ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + PLATFORM_INCLUDE_FILES + ${CMAKE_CURRENT_LIST_DIR}/Platform/Common/${PAL_TRAIT_COMPILER_ID}/pointcloud_editor_${PAL_TRAIT_COMPILER_ID_LOWERCASE}.cmake + FILES_CMAKE + pointcloud_shared_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PUBLIC + Gem::${gem_name}.API + PRIVATE + Gem::${gem_name}.Private.Object +) + +# By default, we will specify that the above target ${gem_name} would be used by +# Client and Server type targets when this gem is enabled. If you don't want it +# active in Clients or Servers by default, delete one of both of the following lines: +ly_create_alias(NAME ${gem_name}.Clients NAMESPACE Gem TARGETS Gem::${gem_name}) +ly_create_alias(NAME ${gem_name}.Servers NAMESPACE Gem TARGETS Gem::${gem_name}) +ly_create_alias(NAME ${gem_name}.Unified NAMESPACE Gem TARGETS Gem::${gem_name}) + +# For the Client and Server variants of ${gem_name} Gem, an alias to the ${gem_name}.API target will be made +ly_create_alias(NAME ${gem_name}.Clients.API NAMESPACE Gem TARGETS Gem::${gem_name}.API) +ly_create_alias(NAME ${gem_name}.Servers.API NAMESPACE Gem TARGETS Gem::${gem_name}.API) +ly_create_alias(NAME ${gem_name}.Unified.API NAMESPACE Gem TARGETS Gem::${gem_name}.API) + +# Add in CMake dependencies for each gem dependency listed in this gem's gem.json file +# for the Clients, Servers and Unified gem variants +o3de_add_variant_dependencies_for_gem_dependencies(GEM_NAME ${gem_name} VARIANTS Clients Servers Unified) + +# If we are on a host platform, we want to add the host tools targets like the ${gem_name}.Editor MODULE target +if(PAL_TRAIT_BUILD_HOST_TOOLS) + # The ${gem_name}.Editor.API target can be used by other gems that want to interact with the ${gem_name}.Editor module + ly_add_target( + NAME ${gem_name}.Editor.API INTERFACE + NAMESPACE Gem + FILES_CMAKE + pointcloud_editor_api_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Include + BUILD_DEPENDENCIES + INTERFACE + AZ::AzToolsFramework + ) + + # The ${gem_name}.Editor.Private.Object target is an internal target + # which is only to be used by this gems CMakeLists.txt and any subdirectories + # Other gems should not use this target + ly_add_target( + NAME ${gem_name}.Editor.Private.Object STATIC + NAMESPACE Gem + PLATFORM_INCLUDE_FILES + ${CMAKE_CURRENT_LIST_DIR}/Platform/Common/${PAL_TRAIT_COMPILER_ID}/pointcloud_editor_${PAL_TRAIT_COMPILER_ID_LOWERCASE}.cmake + FILES_CMAKE + pointcloud_editor_private_files.cmake + TARGET_PROPERTIES + O3DE_PRIVATE_TARGET TRUE + INCLUDE_DIRECTORIES + PRIVATE + Include + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzToolsFramework + $ + Gem::Atom_Utils.Static + Gem::Atom_Feature_Common.Static + Gem::AtomLyIntegration_CommonFeatures.Static + PRIVATE + AZ::AssetBuilderSDK + ) + + ly_add_target( + NAME ${gem_name}.Editor GEM_MODULE + NAMESPACE Gem + AUTOMOC + FILES_CMAKE + pointcloud_editor_shared_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + BUILD_DEPENDENCIES + PUBLIC + Gem::${gem_name}.Editor.API + PRIVATE + Gem::${gem_name}.Editor.Private.Object + ) + + # By default, we will specify that the above target ${gem_name} would be used by + # Tool and Builder type targets when this gem is enabled. If you don't want it + # active in Tools or Builders by default, delete one of both of the following lines: + ly_create_alias(NAME ${gem_name}.Tools NAMESPACE Gem TARGETS Gem::${gem_name}.Editor) + ly_create_alias(NAME ${gem_name}.Builders NAMESPACE Gem TARGETS Gem::${gem_name}.Editor) + + # For the Tools and Builders variants of ${gem_name} Gem, an alias to the ${gem_name}.Editor API target will be made + ly_create_alias(NAME ${gem_name}.Tools.API NAMESPACE Gem TARGETS Gem::${gem_name}.Editor.API) + ly_create_alias(NAME ${gem_name}.Builders.API NAMESPACE Gem TARGETS Gem::${gem_name}.Editor.API) + + # Add in CMake dependencies for each gem dependency listed in this gem's gem.json file + # for the Tools and Builders gem variants + o3de_add_variant_dependencies_for_gem_dependencies(GEM_NAME ${gem_name} VARIANTS Tools Builders) + +endif() + +################################################################################ +# Tests +################################################################################ +# See if globally, tests are supported +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + # We globally support tests, see if we support tests on this platform for ${gem_name}.Tests + if(PAL_TRAIT_POINTCLOUD_TEST_SUPPORTED) + # We support ${gem_name}.Tests on this platform, add dependency on the Private Object target + ly_add_target( + NAME ${gem_name}.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + pointcloud_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + Include + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + AZ::AzFramework + Gem::${gem_name}.Private.Object + ) + + # Add ${gem_name}.Tests to googletest + ly_add_googletest( + NAME Gem::${gem_name}.Tests + ) + endif() + + # If we are a host platform we want to add tools test like editor tests here + if(PAL_TRAIT_BUILD_HOST_TOOLS) + # We are a host platform, see if Editor tests are supported on this platform + if(PAL_TRAIT_POINTCLOUD_EDITOR_TEST_SUPPORTED) + # We support ${gem_name}.Editor.Tests on this platform, add ${gem_name}.Editor.Tests target which depends on + # private ${gem_name}.Editor.Private.Object target + ly_add_target( + NAME ${gem_name}.Editor.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + pointcloud_editor_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + Include + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + Gem::${gem_name}.Editor.Private.Object + ) + + # Add ${gem_name}.Editor.Tests to googletest + ly_add_googletest( + NAME Gem::${gem_name}.Editor.Tests + ) + endif() + endif() +endif() diff --git a/Gems/Pointcloud/Code/Include/Pointcloud/PointcloudAsset.h b/Gems/Pointcloud/Code/Include/Pointcloud/PointcloudAsset.h new file mode 100644 index 00000000..38dcf83e --- /dev/null +++ b/Gems/Pointcloud/Code/Include/Pointcloud/PointcloudAsset.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include + +namespace Pointcloud +{ + class PointcloudAsset final : public AZ::Data::AssetData + { + public: + + //! The vertex data for the pointcloud + struct CloudVertex + { + AZStd::array m_position; + uint32_t m_color; + }; + + struct CloudHeader + { + uint32_t m_magicNumber {0x0}; + uint32_t m_elementSize {0x0}; + uint32_t m_numPoints {0x0}; + }; + + static constexpr uint32_t PointcloudMagicNumber = 0x12345678; + + static constexpr inline const char* DisplayName = "PointcloudAsset"; + static constexpr inline const char* Extension = "pointcloud"; + static constexpr inline const char* Group = "Pointcloud"; + + AZ_RTTI(PointcloudAsset, "{0190c039-385b-7c8a-9172-31e83c091216}", AZ::Data::AssetData) + AZ_CLASS_ALLOCATOR(PointcloudAsset, AZ::SystemAllocator); + + AZStd::vector m_data; + }; + + class PointcloudAssetHandler final : public AzFramework::GenericAssetHandler + { + public: + PointcloudAssetHandler(); + + // AZ::Data::AssetHandler overrides... + AZ::Data::AssetHandler::LoadResult LoadAssetData( + const AZ::Data::Asset& asset, + AZStd::shared_ptr stream, + const AZ::Data::AssetFilterCB& assetLoadFilterCB) override; + bool SaveAssetData(const AZ::Data::Asset& asset, AZ::IO::GenericStream* stream) override; + }; +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Include/Pointcloud/PointcloudFeatureProcessorInterface.h b/Gems/Pointcloud/Code/Include/Pointcloud/PointcloudFeatureProcessorInterface.h new file mode 100644 index 00000000..e7f83ce8 --- /dev/null +++ b/Gems/Pointcloud/Code/Include/Pointcloud/PointcloudFeatureProcessorInterface.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +#include +namespace Pointcloud +{ + class Pointcloud; + + // PointcloudFeatureProcessorInterface provides an interface to the feature processor for code outside of Atom + class PointcloudFeatureProcessorInterface : public AZ::RPI::FeatureProcessor + { + public: + using PointcloudHandle = int; + constexpr static PointcloudHandle InvalidPointcloudHandle = -1; + AZ_RTTI(PointcloudFeatureProcessorInterface, "{8597AF27-EB4E-4363-8889-3BFC2AF5D2EC}", AZ::RPI::FeatureProcessor); + + //! Set the transform of a pointcloud + //! @param handle The handle of the pointcloud obtained from AcquirePointcloud + virtual void SetTransform(const PointcloudHandle& handle, const AZ::Transform& transform) = 0; + + //! Set the point size of a pointcloud + //! @param handle The handle of the pointcloud obtained from AcquirePointcloud + virtual void SetPointSize(const PointcloudHandle& handle, float pointSize) = 0; + + //! Allocate resources and return a handle to the pointcloud + //! @param cloudVertexData The vertex data of the pointcloud + virtual PointcloudHandle AcquirePointcloud(const AZStd::vector& cloudVertexData) = 0; + + //! Set the visibility of a pointcloud + //! @param handle The handle of the pointcloud obtained from AcquirePointcloud + virtual void SetVisibility(const PointcloudHandle& handle, bool visible) = 0; + + //! Release the resources of a pointcloud + //! @param handle The handle of the pointcloud obtained from AcquirePointcloud + virtual void ReleasePointcloud(const PointcloudHandle& handle) = 0; + }; +} // namespace Pointcloud \ No newline at end of file diff --git a/Gems/Pointcloud/Code/Include/Pointcloud/PointcloudTypeIds.h b/Gems/Pointcloud/Code/Include/Pointcloud/PointcloudTypeIds.h new file mode 100644 index 00000000..f458968e --- /dev/null +++ b/Gems/Pointcloud/Code/Include/Pointcloud/PointcloudTypeIds.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +namespace Pointcloud +{ + // System Component TypeIds + inline constexpr const char* PointcloudSystemComponentTypeId = "{3EBCAD0C-39B7-4782-B9CB-D62F35331D87}"; + inline constexpr const char* PointcloudEditorSystemComponentTypeId = "{88835E98-561B-4D74-B7C4-C539E2950E8D}"; + + // Module derived classes TypeIds + inline constexpr const char* PointcloudModuleInterfaceTypeId = "{8077C268-B445-409C-9BBA-DE557794B240}"; + inline constexpr const char* PointcloudModuleTypeId = "{7BFFAD35-81E5-40FF-BE8D-19B0A039266C}"; + // The Editor Module by default is mutually exclusive with the Client Module + // so they use the Same TypeId + inline constexpr const char* PointcloudEditorModuleTypeId = PointcloudModuleTypeId; + + // Interface TypeIds + inline constexpr const char* PointcloudRequestsTypeId = "{86CA76D8-2225-4C50-86E4-B1C8EFDEA8EF}"; +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Platform/Common/Clang/pointcloud_editor_clang.cmake b/Gems/Pointcloud/Code/Platform/Common/Clang/pointcloud_editor_clang.cmake new file mode 100644 index 00000000..51aeee37 --- /dev/null +++ b/Gems/Pointcloud/Code/Platform/Common/Clang/pointcloud_editor_clang.cmake @@ -0,0 +1,4 @@ +set(LY_COMPILE_OPTIONS + PRIVATE + -fexceptions +) diff --git a/Gems/Pointcloud/Code/Platform/Common/GCC/pointcloud_editor_gcc.cmake b/Gems/Pointcloud/Code/Platform/Common/GCC/pointcloud_editor_gcc.cmake new file mode 100644 index 00000000..51aeee37 --- /dev/null +++ b/Gems/Pointcloud/Code/Platform/Common/GCC/pointcloud_editor_gcc.cmake @@ -0,0 +1,4 @@ +set(LY_COMPILE_OPTIONS + PRIVATE + -fexceptions +) diff --git a/Gems/Pointcloud/Code/Platform/Common/msvc/pointcloud_editor_msvc.cmake b/Gems/Pointcloud/Code/Platform/Common/msvc/pointcloud_editor_msvc.cmake new file mode 100644 index 00000000..b0139f1f --- /dev/null +++ b/Gems/Pointcloud/Code/Platform/Common/msvc/pointcloud_editor_msvc.cmake @@ -0,0 +1,4 @@ +set(LY_COMPILE_OPTIONS + PRIVATE + /EHsc +) diff --git a/Gems/Pointcloud/Code/Platform/Linux/PAL_linux.cmake b/Gems/Pointcloud/Code/Platform/Linux/PAL_linux.cmake new file mode 100644 index 00000000..fe389889 --- /dev/null +++ b/Gems/Pointcloud/Code/Platform/Linux/PAL_linux.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(PAL_TRAIT_POINTCLOUD_SUPPORTED TRUE) +set(PAL_TRAIT_POINTCLOUD_TEST_SUPPORTED FALSE) +set(PAL_TRAIT_POINTCLOUD_EDITOR_TEST_SUPPORTED TRUE) diff --git a/Gems/Pointcloud/Code/Platform/Windows/PAL_windows.cmake b/Gems/Pointcloud/Code/Platform/Windows/PAL_windows.cmake new file mode 100644 index 00000000..fe389889 --- /dev/null +++ b/Gems/Pointcloud/Code/Platform/Windows/PAL_windows.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(PAL_TRAIT_POINTCLOUD_SUPPORTED TRUE) +set(PAL_TRAIT_POINTCLOUD_TEST_SUPPORTED FALSE) +set(PAL_TRAIT_POINTCLOUD_EDITOR_TEST_SUPPORTED TRUE) diff --git a/Gems/Pointcloud/Code/Source/3rdParty/happly.h b/Gems/Pointcloud/Code/Source/3rdParty/happly.h new file mode 100644 index 00000000..45b7edbd --- /dev/null +++ b/Gems/Pointcloud/Code/Source/3rdParty/happly.h @@ -0,0 +1,2017 @@ +#pragma once + +/* A header-only implementation of the .ply file format. + * https://github.com/nmwsharp/happly + * By Nicholas Sharp - nsharp@cs.cmu.edu + * + * Version 2, July 20, 2019 + */ + +/* +MIT License + +Copyright (c) 2018 Nick Sharp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + + +// clang-format off +/* + + === Changelog === + + Significant changes to the file recorded here. + + - Version 5 (Aug 22, 2020) Minor: skip blank lines before properties in ASCII files + - Version 4 (Sep 11, 2019) Change internal list format to be flat. Other small perf fixes and cleanup. + - Version 3 (Aug 1, 2019) Add support for big endian and obj_info + - Version 2 (July 20, 2019) Catch exceptions by const reference. + - Version 1 (undated) Initial version. Unnamed changes before version numbering. + +*/ +// clang-format on + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// General namespace wrapping all Happly things. +namespace happly { + +// Enum specifying binary or ASCII filetypes. Binary can be little-endian +// (default) or big endian. +enum class DataFormat { ASCII, Binary, BinaryBigEndian }; + +// Type name strings +// clang-format off +template std::string typeName() { return "unknown"; } +template<> inline std::string typeName() { return "char"; } +template<> inline std::string typeName() { return "uchar"; } +template<> inline std::string typeName() { return "short"; } +template<> inline std::string typeName() { return "ushort"; } +template<> inline std::string typeName() { return "int"; } +template<> inline std::string typeName() { return "uint"; } +template<> inline std::string typeName() { return "float"; } +template<> inline std::string typeName() { return "double"; } + +// Template hackery that makes getProperty() and friends pretty while automatically picking up smaller types +namespace { + +// A pointer for the equivalent/smaller equivalent of a type (eg. when a double is requested a float works too, etc) +// long int is intentionally absent to avoid platform confusion +template struct TypeChain { bool hasChildType = false; typedef T type; }; +template <> struct TypeChain { bool hasChildType = true; typedef int32_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef int16_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef int8_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef uint32_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef uint16_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef uint8_t type; }; +template <> struct TypeChain { bool hasChildType = true; typedef float type; }; + +template struct CanonicalName { typedef T type; }; +template <> struct CanonicalName { typedef int8_t type; }; +template <> struct CanonicalName { typedef uint8_t type; }; +template <> struct CanonicalName { typedef std::conditional::type, int>::value, uint32_t, uint64_t>::type type; }; + +// Used to change behavior of >> for 8bit ints, which does not do what we want. +template struct SerializeType { typedef T type; }; +template <> struct SerializeType { typedef int32_t type; }; +template <> struct SerializeType< int8_t> { typedef int32_t type; }; + +// Give address only if types are same (used below when conditionally copying data) +// last int/char arg is to resolve ambiguous overloads, just always pass 0 and the int version will be preferred +template +S* addressIfSame(T&, char) { + throw std::runtime_error("tried to take address for types that are not same"); + return nullptr;} +template +S* addressIfSame(S& t, int) {return &t;} + +// clang-format on +} // namespace + +/** + * @brief A generic property, which is associated with some element. Can be plain Property or a ListProperty, of some + * type. Generally, the user should not need to interact with these directly, but they are exposed in case someone + * wants to get clever. + */ +class Property { + +public: + /** + * @brief Create a new Property with the given name. + * + * @param name_ + */ + Property(const std::string& name_) : name(name_){}; + virtual ~Property(){}; + + std::string name; + + /** + * @brief Reserve memory. + * + * @param capacity Expected number of elements. + */ + virtual void reserve(size_t capacity) = 0; + + /** + * @brief (ASCII reading) Parse out the next value of this property from a list of tokens. + * + * @param tokens The list of property tokens for the element. + * @param currEntry Index in to tokens, updated after this property is read. + */ + virtual void parseNext(const std::vector& tokens, size_t& currEntry) = 0; + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNext(std::istream& stream) = 0; + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNextBigEndian(std::istream& stream) = 0; + + /** + * @brief (reading) Write a header entry for this property. + * + * @param outStream Stream to write to. + */ + virtual void writeHeader(std::ostream& outStream) = 0; + + /** + * @brief (ASCII writing) write this property for some element to a stream in plaintext + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataASCII(std::ostream& outStream, size_t iElement) = 0; + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinary(std::ostream& outStream, size_t iElement) = 0; + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinaryBigEndian(std::ostream& outStream, size_t iElement) = 0; + + /** + * @brief Number of element entries for this property + * + * @return + */ + virtual size_t size() = 0; + + /** + * @brief A string naming the type of the property + * + * @return + */ + virtual std::string propertyTypeName() = 0; +}; + +namespace { + +/** + * Check if the platform is little endian. + * (not foolproof, but will work on most platforms) + * + * @return true if little endian + */ +bool isLittleEndian() { + int32_t oneVal = 0x1; + char* numPtr = (char*)&oneVal; + return (numPtr[0] == 1); +} + +/** + * Swap endianness. + * + * @param value Value to swap. + * + * @return Swapped value. + */ +template +T swapEndian(T val) { + char* bytes = reinterpret_cast(&val); + for (unsigned int i = 0; i < sizeof(val) / 2; i++) { + std::swap(bytes[sizeof(val) - 1 - i], bytes[i]); + } + return val; +} + +// The following specializations for single-byte types are used to avoid compiler warnings. +template <> int8_t swapEndian(int8_t val) { return val; } +template <> uint8_t swapEndian(uint8_t val) { return val; } + + +// Unpack flattened list from the convention used in TypedListProperty +template +std::vector> unflattenList(const std::vector& flatList, const std::vector flatListStarts) { + size_t outerCount = flatListStarts.size() - 1; + + // Put the output here + std::vector> outLists(outerCount); + + if (outerCount == 0) { + return outLists; // quick out for empty + } + + // Copy each sublist + for (size_t iOuter = 0; iOuter < outerCount; iOuter++) { + size_t iFlatStart = flatListStarts[iOuter]; + size_t iFlatEnd = flatListStarts[iOuter + 1]; + outLists[iOuter].insert(outLists[iOuter].begin(), flatList.begin() + iFlatStart, flatList.begin() + iFlatEnd); + } + + return outLists; +} + + +}; // namespace + + +/** + * @brief A property which takes a single value (not a list). + */ +template +class TypedProperty : public Property { + +public: + /** + * @brief Create a new Property with the given name. + * + * @param name_ + */ + TypedProperty(const std::string& name_) : Property(name_) { + if (typeName() == "unknown") { + // TODO should really be a compile-time error + throw std::runtime_error("Attempted property type does not match any type defined by the .ply format."); + } + }; + + /** + * @brief Create a new property and initialize with data. + * + * @param name_ + * @param data_ + */ + TypedProperty(const std::string& name_, const std::vector& data_) : Property(name_), data(data_) { + if (typeName() == "unknown") { + throw std::runtime_error("Attempted property type does not match any type defined by the .ply format."); + } + }; + + virtual ~TypedProperty() override{}; + + /** + * @brief Reserve memory. + * + * @param capacity Expected number of elements. + */ + virtual void reserve(size_t capacity) override { data.reserve(capacity); } + + /** + * @brief (ASCII reading) Parse out the next value of this property from a list of tokens. + * + * @param tokens The list of property tokens for the element. + * @param currEntry Index in to tokens, updated after this property is read. + */ + virtual void parseNext(const std::vector& tokens, size_t& currEntry) override { + data.emplace_back(); + std::istringstream iss(tokens[currEntry]); + typename SerializeType::type tmp; // usually the same type as T + iss >> tmp; + data.back() = static_cast(tmp); + currEntry++; + }; + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNext(std::istream& stream) override { + data.emplace_back(); + stream.read((char*)&data.back(), sizeof(T)); + } + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNextBigEndian(std::istream& stream) override { + data.emplace_back(); + stream.read((char*)&data.back(), sizeof(T)); + data.back() = swapEndian(data.back()); + } + + /** + * @brief (reading) Write a header entry for this property. + * + * @param outStream Stream to write to. + */ + virtual void writeHeader(std::ostream& outStream) override { + outStream << "property " << typeName() << " " << name << "\n"; + } + + /** + * @brief (ASCII writing) write this property for some element to a stream in plaintext + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataASCII(std::ostream& outStream, size_t iElement) override { + outStream.precision(std::numeric_limits::max_digits10); + outStream << static_cast::type>(data[iElement]); // case is usually a no-op + } + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinary(std::ostream& outStream, size_t iElement) override { + outStream.write((char*)&data[iElement], sizeof(T)); + } + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinaryBigEndian(std::ostream& outStream, size_t iElement) override { + auto value = swapEndian(data[iElement]); + outStream.write((char*)&value, sizeof(T)); + } + + /** + * @brief Number of element entries for this property + * + * @return + */ + virtual size_t size() override { return data.size(); } + + + /** + * @brief A string naming the type of the property + * + * @return + */ + virtual std::string propertyTypeName() override { return typeName(); } + + /** + * @brief The actual data contained in the property + */ + std::vector data; +}; + + +/** + * @brief A property which is a list of value (eg, 3 doubles). Note that lists are always variable length per-element. + */ +template +class TypedListProperty : public Property { + +public: + /** + * @brief Create a new Property with the given name. + * + * @param name_ + */ + TypedListProperty(const std::string& name_, int listCountBytes_) : Property(name_), listCountBytes(listCountBytes_) { + if (typeName() == "unknown") { + throw std::runtime_error("Attempted property type does not match any type defined by the .ply format."); + } + + flattenedIndexStart.push_back(0); + }; + + /** + * @brief Create a new property and initialize with data + * + * @param name_ + * @param data_ + */ + TypedListProperty(const std::string& name_, const std::vector>& data_) : Property(name_) { + if (typeName() == "unknown") { + throw std::runtime_error("Attempted property type does not match any type defined by the .ply format."); + } + + // Populate list with data + flattenedIndexStart.push_back(0); + for (const std::vector& vec : data_) { + for (const T& val : vec) { + flattenedData.emplace_back(val); + } + flattenedIndexStart.push_back(flattenedData.size()); + } + }; + + virtual ~TypedListProperty() override{}; + + /** + * @brief Reserve memory. + * + * @param capacity Expected number of elements. + */ + virtual void reserve(size_t capacity) override { + flattenedData.reserve(3 * capacity); // optimize for triangle meshes + flattenedIndexStart.reserve(capacity + 1); + } + + /** + * @brief (ASCII reading) Parse out the next value of this property from a list of tokens. + * + * @param tokens The list of property tokens for the element. + * @param currEntry Index in to tokens, updated after this property is read. + */ + virtual void parseNext(const std::vector& tokens, size_t& currEntry) override { + + std::istringstream iss(tokens[currEntry]); + size_t count; + iss >> count; + currEntry++; + + size_t currSize = flattenedData.size(); + size_t afterSize = currSize + count; + flattenedData.resize(afterSize); + for (size_t iFlat = currSize; iFlat < afterSize; iFlat++) { + std::istringstream iss2(tokens[currEntry]); + typename SerializeType::type tmp; // usually the same type as T + iss2 >> tmp; + flattenedData[iFlat] = static_cast(tmp); + currEntry++; + } + flattenedIndexStart.emplace_back(afterSize); + } + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNext(std::istream& stream) override { + + // Read the size of the list + size_t count = 0; + stream.read(((char*)&count), listCountBytes); + + // Read list elements + size_t currSize = flattenedData.size(); + size_t afterSize = currSize + count; + flattenedData.resize(afterSize); + if (count > 0) { + stream.read((char*)&flattenedData[currSize], count * sizeof(T)); + } + flattenedIndexStart.emplace_back(afterSize); + } + + /** + * @brief (binary reading) Copy the next value of this property from a stream of bits. + * + * @param stream Stream to read from. + */ + virtual void readNextBigEndian(std::istream& stream) override { + + // Read the size of the list + size_t count = 0; + stream.read(((char*)&count), listCountBytes); + if (listCountBytes == 8) { + count = (size_t)swapEndian((uint64_t)count); + } else if (listCountBytes == 4) { + count = (size_t)swapEndian((uint32_t)count); + } else if (listCountBytes == 2) { + count = (size_t)swapEndian((uint16_t)count); + } + + // Read list elements + size_t currSize = flattenedData.size(); + size_t afterSize = currSize + count; + flattenedData.resize(afterSize); + if (count > 0) { + stream.read((char*)&flattenedData[currSize], count * sizeof(T)); + } + flattenedIndexStart.emplace_back(afterSize); + + // Swap endian order of list elements + for (size_t iFlat = currSize; iFlat < afterSize; iFlat++) { + flattenedData[iFlat] = swapEndian(flattenedData[iFlat]); + } + } + + /** + * @brief (reading) Write a header entry for this property. Note that we already use "uchar" for the list count type. + * + * @param outStream Stream to write to. + */ + virtual void writeHeader(std::ostream& outStream) override { + // NOTE: We ALWAYS use uchar as the list count output type + outStream << "property list uchar " << typeName() << " " << name << "\n"; + } + + /** + * @brief (ASCII writing) write this property for some element to a stream in plaintext + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataASCII(std::ostream& outStream, size_t iElement) override { + size_t dataStart = flattenedIndexStart[iElement]; + size_t dataEnd = flattenedIndexStart[iElement + 1]; + + // Get the number of list elements as a uchar, and ensure the value fits + size_t dataCount = dataEnd - dataStart; + if (dataCount > std::numeric_limits::max()) { + throw std::runtime_error( + "List property has an element with more entries than fit in a uchar. See note in README."); + } + + outStream << dataCount; + outStream.precision(std::numeric_limits::max_digits10); + for (size_t iFlat = dataStart; iFlat < dataEnd; iFlat++) { + outStream << " " << static_cast::type>(flattenedData[iFlat]); // cast is usually a no-op + } + } + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinary(std::ostream& outStream, size_t iElement) override { + size_t dataStart = flattenedIndexStart[iElement]; + size_t dataEnd = flattenedIndexStart[iElement + 1]; + + // Get the number of list elements as a uchar, and ensure the value fits + size_t dataCount = dataEnd - dataStart; + if (dataCount > std::numeric_limits::max()) { + throw std::runtime_error( + "List property has an element with more entries than fit in a uchar. See note in README."); + } + uint8_t count = static_cast(dataCount); + + outStream.write((char*)&count, sizeof(uint8_t)); + outStream.write((char*)&flattenedData[dataStart], count * sizeof(T)); + } + + /** + * @brief (binary writing) copy the bits of this property for some element to a stream + * + * @param outStream Stream to write to. + * @param iElement index of the element to write. + */ + virtual void writeDataBinaryBigEndian(std::ostream& outStream, size_t iElement) override { + size_t dataStart = flattenedIndexStart[iElement]; + size_t dataEnd = flattenedIndexStart[iElement + 1]; + + // Get the number of list elements as a uchar, and ensure the value fits + size_t dataCount = dataEnd - dataStart; + if (dataCount > std::numeric_limits::max()) { + throw std::runtime_error( + "List property has an element with more entries than fit in a uchar. See note in README."); + } + uint8_t count = static_cast(dataCount); + + outStream.write((char*)&count, sizeof(uint8_t)); + for (size_t iFlat = dataStart; iFlat < dataEnd; iFlat++) { + T value = swapEndian(flattenedData[iFlat]); + outStream.write((char*)&value, sizeof(T)); + } + } + + /** + * @brief Number of element entries for this property + * + * @return + */ + virtual size_t size() override { return flattenedIndexStart.size() - 1; } + + + /** + * @brief A string naming the type of the property + * + * @return + */ + virtual std::string propertyTypeName() override { return typeName(); } + + /** + * @brief The (flattened) data for the property, as formed by concatenating all of the individual element lists + * together. + */ + std::vector flattenedData; + + /** + * @brief Indices in to flattenedData. The i'th element gives the index in to flattenedData where the element's data + * begins. A final entry is included which is the length of flattenedData. Size is N_elem + 1. + */ + std::vector flattenedIndexStart; + + /** + * @brief The number of bytes used to store the count for lists of data. + */ + int listCountBytes = -1; +}; + + +/** + * @brief Helper function to construct a new property of the appropriate type. + * + * @param name The name of the property to construct. + * @param typeStr A string naming the type according to the format. + * @param isList Is this a plain property, or a list property? + * @param listCountTypeStr If a list property, the type of the count varible. + * + * @return A new Property with the proper type. + */ +inline std::unique_ptr createPropertyWithType(const std::string& name, const std::string& typeStr, + bool isList, const std::string& listCountTypeStr) { + + // == Figure out how many bytes the list count field has, if this is a list type + // Note: some files seem to use signed types here, we read the width but always parse as if unsigned + int listCountBytes = -1; + if (isList) { + if (listCountTypeStr == "uchar" || listCountTypeStr == "uint8" || listCountTypeStr == "char" || + listCountTypeStr == "int8") { + listCountBytes = 1; + } else if (listCountTypeStr == "ushort" || listCountTypeStr == "uint16" || listCountTypeStr == "short" || + listCountTypeStr == "int16") { + listCountBytes = 2; + } else if (listCountTypeStr == "uint" || listCountTypeStr == "uint32" || listCountTypeStr == "int" || + listCountTypeStr == "int32") { + listCountBytes = 4; + } else { + throw std::runtime_error("Unrecognized list count type: " + listCountTypeStr); + } + } + + // = Unsigned int + + // 8 bit unsigned + if (typeStr == "uchar" || typeStr == "uint8") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 16 bit unsigned + else if (typeStr == "ushort" || typeStr == "uint16") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 32 bit unsigned + else if (typeStr == "uint" || typeStr == "uint32") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // = Signed int + + // 8 bit signed + if (typeStr == "char" || typeStr == "int8") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 16 bit signed + else if (typeStr == "short" || typeStr == "int16") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 32 bit signed + else if (typeStr == "int" || typeStr == "int32") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // = Float + + // 32 bit float + else if (typeStr == "float" || typeStr == "float32") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + // 64 bit float + else if (typeStr == "double" || typeStr == "float64") { + if (isList) { + return std::unique_ptr(new TypedListProperty(name, listCountBytes)); + } else { + return std::unique_ptr(new TypedProperty(name)); + } + } + + else { + throw std::runtime_error("Data type: " + typeStr + " cannot be mapped to .ply format"); + } +} + +/** + * @brief An element (more properly an element type) in the .ply object. Tracks the name of the elemnt type (eg, + * "vertices"), the number of elements of that type (eg, 1244), and any properties associated with that element (eg, + * "position", "color"). + */ +class Element { + +public: + /** + * @brief Create a new element type. + * + * @param name_ Name of the element type (eg, "vertices") + * @param count_ Number of instances of this element. + */ + Element(const std::string& name_, size_t count_) : name(name_), count(count_) {} + + std::string name; + size_t count; + std::vector> properties; + + /** + * @brief Check if a property exists. + * + * @param target The name of the property to get. + * + * @return Whether the target property exists. + */ + bool hasProperty(const std::string& target) { + for (std::unique_ptr& prop : properties) { + if (prop->name == target) { + return true; + } + } + return false; + } + + /** + * @brief Check if a property exists with the requested type. + * + * @tparam T The type of the property + * @param target The name of the property to get. + * + * @return Whether the target property exists. + */ + template + bool hasPropertyType(const std::string& target) { + for (std::unique_ptr& prop : properties) { + if (prop->name == target) { + TypedProperty* castedProp = dynamic_cast*>(prop.get()); + if (castedProp) { + return true; + } + return false; + } + } + return false; + } + + /** + * @brief A list of the names of all properties + * + * @return Property names + */ + std::vector getPropertyNames() { + std::vector names; + for (std::unique_ptr& p : properties) { + names.push_back(p->name); + } + return names; + } + + /** + * @brief Low-level method to get a pointer to a property. Users probably don't need to call this. + * + * @param target The name of the property to get. + * + * @return A (unique_ptr) pointer to the property. + */ + std::unique_ptr& getPropertyPtr(const std::string& target) { + for (std::unique_ptr& prop : properties) { + if (prop->name == target) { + return prop; + } + } + throw std::runtime_error("PLY parser: element " + name + " does not have property " + target); + } + + /** + * @brief Add a new (plain, not list) property for this element type. + * + * @tparam T The type of the property + * @param propertyName The name of the property + * @param data The data for the property. Must have the same length as the number of elements. + */ + template + void addProperty(const std::string& propertyName, const std::vector& data) { + + if (data.size() != count) { + throw std::runtime_error("PLY write: new property " + propertyName + " has size which does not match element"); + } + + // If there is already some property with this name, remove it + for (size_t i = 0; i < properties.size(); i++) { + if (properties[i]->name == propertyName) { + properties.erase(properties.begin() + i); + i--; + } + } + + // Copy to canonical type. Often a no-op, but takes care of standardizing widths across platforms. + std::vector::type> canonicalVec(data.begin(), data.end()); + + properties.push_back( + std::unique_ptr(new TypedProperty::type>(propertyName, canonicalVec))); + } + + /** + * @brief Add a new list property for this element type. + * + * @tparam T The type of the property (eg, "double" for a list of doubles) + * @param propertyName The name of the property + * @param data The data for the property. Outer vector must have the same length as the number of elements. + */ + template + void addListProperty(const std::string& propertyName, const std::vector>& data) { + + if (data.size() != count) { + throw std::runtime_error("PLY write: new property " + propertyName + " has size which does not match element"); + } + + // If there is already some property with this name, remove it + for (size_t i = 0; i < properties.size(); i++) { + if (properties[i]->name == propertyName) { + properties.erase(properties.begin() + i); + i--; + } + } + + // Copy to canonical type. Often a no-op, but takes care of standardizing widths across platforms. + std::vector::type>> canonicalListVec; + for (const std::vector& subList : data) { + canonicalListVec.emplace_back(subList.begin(), subList.end()); + } + + properties.push_back(std::unique_ptr( + new TypedListProperty::type>(propertyName, canonicalListVec))); + } + + /** + * @brief Get a vector of a data from a property for this element. Automatically promotes to larger types. Throws if + * requested data is unavailable. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector getProperty(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + + // Get a copy of the data with auto-promoting type magic + return getDataFromPropertyRecursive(prop.get()); + } + + /** + * @brief Get a vector of a data from a property for this element. Unlike getProperty(), only returns if the ply + * record contains a type that matches T exactly. Throws if * requested data is unavailable. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector getPropertyType(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + TypedProperty* castedProp = dynamic_cast*>(prop.get()); + if (castedProp) { + return castedProp->data; + } + + // No match, failure + throw std::runtime_error("PLY parser: property " + prop->name + " is not of type type " + typeName() + + ". Has type " + prop->propertyTypeName()); + } + + /** + * @brief Get a vector of lists of data from a property for this element. Automatically promotes to larger types. + * Throws if requested data is unavailable. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector> getListProperty(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + + // Get a copy of the data with auto-promoting type magic + return getDataFromListPropertyRecursive(prop.get()); + } + + /** + * @brief Get a vector of a data from a property for this element. Unlike getProperty(), only returns if the ply + * record contains a type that matches T exactly. Throws if * requested data is unavailable. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector> getListPropertyType(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + TypedListProperty* castedProp = dynamic_cast*>(prop.get()); + if (castedProp) { + return unflattenList(castedProp->flattenedData, castedProp->flattenedIndexStart); + } + + // No match, failure + throw std::runtime_error("PLY parser: list property " + prop->name + " is not of type " + typeName() + + ". Has type " + prop->propertyTypeName()); + } + + + /** + * @brief Get a vector of lists of data from a property for this element. Automatically promotes to larger types. + * Unlike getListProperty(), this method will additionally convert between types of different sign (eg, requesting and + * int32 would get data from a uint32); doing so naively converts between signed and unsigned types. This is typically + * useful for data representing indices, which might be stored as signed or unsigned numbers. + * + * @tparam T The type of data requested + * @param propertyName The name of the property to get. + * + * @return The data. + */ + template + std::vector> getListPropertyAnySign(const std::string& propertyName) { + + // Find the property + std::unique_ptr& prop = getPropertyPtr(propertyName); + + // Get a copy of the data with auto-promoting type magic + try { + // First, try the usual approach, looking for a version of the property with the same signed-ness and possibly + // smaller size + return getDataFromListPropertyRecursive(prop.get()); + } catch (const std::runtime_error& orig_e) { + + // If the usual approach fails, look for a version with opposite signed-ness + try { + + // This type has the oppopsite signeness as the input type + typedef typename CanonicalName::type Tcan; + typedef typename std::conditional::value, typename std::make_unsigned::type, + typename std::make_signed::type>::type OppsignType; + + return getDataFromListPropertyRecursive(prop.get()); + + } catch (const std::runtime_error&) { + throw orig_e; + } + + throw orig_e; + } + } + + + /** + * @brief Performs sanity checks on the element, throwing if any fail. + */ + void validate() { + + // Make sure no properties have duplicate names, and no names have whitespace + for (size_t iP = 0; iP < properties.size(); iP++) { + for (char c : properties[iP]->name) { + if (std::isspace(c)) { + throw std::runtime_error("Ply validate: illegal whitespace in name " + properties[iP]->name); + } + } + for (size_t jP = iP + 1; jP < properties.size(); jP++) { + if (properties[iP]->name == properties[jP]->name) { + throw std::runtime_error("Ply validate: multiple properties with name " + properties[iP]->name); + } + } + } + + // Make sure all properties have right length + for (size_t iP = 0; iP < properties.size(); iP++) { + if (properties[iP]->size() != count) { + throw std::runtime_error("Ply validate: property has wrong size. " + properties[iP]->name + + " does not match element size."); + } + } + } + + /** + * @brief Writes out this element's information to the file header. + * + * @param outStream The stream to use. + */ + void writeHeader(std::ostream& outStream) { + + outStream << "element " << name << " " << count << "\n"; + + for (std::unique_ptr& p : properties) { + p->writeHeader(outStream); + } + } + + /** + * @brief (ASCII writing) Writes out all of the data for every element of this element type to the stream, including + * all contained properties. + * + * @param outStream The stream to write to. + */ + void writeDataASCII(std::ostream& outStream) { + // Question: what is the proper output for an element with no properties? Here, we write a blank line, so there is + // one line per element no matter what. + for (size_t iE = 0; iE < count; iE++) { + for (size_t iP = 0; iP < properties.size(); iP++) { + properties[iP]->writeDataASCII(outStream, iE); + if (iP < properties.size() - 1) { + outStream << " "; + } + } + outStream << "\n"; + } + } + + + /** + * @brief (binary writing) Writes out all of the data for every element of this element type to the stream, including + * all contained properties. + * + * @param outStream The stream to write to. + */ + void writeDataBinary(std::ostream& outStream) { + for (size_t iE = 0; iE < count; iE++) { + for (size_t iP = 0; iP < properties.size(); iP++) { + properties[iP]->writeDataBinary(outStream, iE); + } + } + } + + + /** + * @brief (binary writing) Writes out all of the data for every element of this element type to the stream, including + * all contained properties. + * + * @param outStream The stream to write to. + */ + void writeDataBinaryBigEndian(std::ostream& outStream) { + for (size_t iE = 0; iE < count; iE++) { + for (size_t iP = 0; iP < properties.size(); iP++) { + properties[iP]->writeDataBinaryBigEndian(outStream, iE); + } + } + } + + + /** + * @brief Helper function which does the hard work to implement type promotion for data getters. Throws if type + * conversion fails. + * + * @tparam D The desired output type + * @tparam T The current attempt for the actual type of the property + * @param prop The property to get (does not delete nor share pointer) + * + * @return The data, with the requested type + */ + template + std::vector getDataFromPropertyRecursive(Property* prop) { + + typedef typename CanonicalName::type Tcan; + + { // Try to return data of type D from a property of type T + TypedProperty* castedProp = dynamic_cast*>(prop); + if (castedProp) { + // Succeeded, return a buffer of the data (copy while converting type) + std::vector castedVec; + castedVec.reserve(castedProp->data.size()); + for (Tcan& v : castedProp->data) { + castedVec.push_back(static_cast(v)); + } + return castedVec; + } + } + + TypeChain chainType; + if (chainType.hasChildType) { + return getDataFromPropertyRecursive::type>(prop); + } else { + // No smaller type to try, failure + throw std::runtime_error("PLY parser: property " + prop->name + " cannot be coerced to requested type " + + typeName() + ". Has type " + prop->propertyTypeName()); + } + } + + + /** + * @brief Helper function which does the hard work to implement type promotion for list data getters. Throws if type + * conversion fails. + * + * @tparam D The desired output type + * @tparam T The current attempt for the actual type of the property + * @param prop The property to get (does not delete nor share pointer) + * + * @return The data, with the requested type + */ + template + std::vector> getDataFromListPropertyRecursive(Property* prop) { + typedef typename CanonicalName::type Tcan; + + TypedListProperty* castedProp = dynamic_cast*>(prop); + if (castedProp) { + // Succeeded, return a buffer of the data (copy while converting type) + + // Convert to flat buffer of new type + std::vector* castedFlatVec = nullptr; + std::vector castedFlatVecCopy; // we _might_ make a copy here, depending on is_same below + + if (std::is_same, std::vector>::value) { + // just use the array we already have + castedFlatVec = addressIfSame>(castedProp->flattenedData, 0 /* dummy arg to disambiguate */); + } else { + // make a copy + castedFlatVecCopy.reserve(castedProp->flattenedData.size()); + for (Tcan& v : castedProp->flattenedData) { + castedFlatVecCopy.push_back(static_cast(v)); + } + castedFlatVec = &castedFlatVecCopy; + } + + // Unflatten and return + return unflattenList(*castedFlatVec, castedProp->flattenedIndexStart); + } + + TypeChain chainType; + if (chainType.hasChildType) { + return getDataFromListPropertyRecursive::type>(prop); + } else { + // No smaller type to try, failure + throw std::runtime_error("PLY parser: list property " + prop->name + + " cannot be coerced to requested type list " + typeName() + ". Has type list " + + prop->propertyTypeName()); + } + } +}; + + +// Some string helpers +namespace { + +inline std::string trimSpaces(const std::string& input) { + size_t start = 0; + while (start < input.size() && input[start] == ' ') start++; + size_t end = input.size(); + while (end > start && (input[end - 1] == ' ' || input[end - 1] == '\n' || input[end - 1] == '\r')) end--; + return input.substr(start, end - start); +} + +inline std::vector tokenSplit(const std::string& input) { + std::vector result; + size_t curr = 0; + size_t found = 0; + while ((found = input.find_first_of(' ', curr)) != std::string::npos) { + std::string token = input.substr(curr, found - curr); + token = trimSpaces(token); + if (token.size() > 0) { + result.push_back(token); + } + curr = found + 1; + } + std::string token = input.substr(curr); + token = trimSpaces(token); + if (token.size() > 0) { + result.push_back(token); + } + + return result; +} + +inline bool startsWith(const std::string& input, const std::string& query) { + return input.compare(0, query.length(), query) == 0; +} +}; // namespace + + +/** + * @brief Primary class; represents a set of data in the .ply format. + */ +class PLYData { + +public: + /** + * @brief Create an empty PLYData object. + */ + PLYData(){}; + + /** + * @brief Initialize a PLYData by reading from a file. Throws if any failures occur. + * + * @param filename The file to read from. + * @param verbose If true, print useful info about the file to stdout + */ + PLYData(const std::string& filename, bool verbose = false) { + + using std::cout; + using std::endl; + using std::string; + using std::vector; + + if (verbose) cout << "PLY parser: Reading ply file: " << filename << endl; + + // Open a file in binary always, in case it turns out to have binary data. + std::ifstream inStream(filename, std::ios::binary); + if (inStream.fail()) { + throw std::runtime_error("PLY parser: Could not open file " + filename); + } + + parsePLY(inStream, verbose); + + if (verbose) { + cout << " - Finished parsing file." << endl; + } + } + + /** + * @brief Initialize a PLYData by reading from a stringstream. Throws if any failures occur. + * + * @param inStream The stringstream to read from. + * @param verbose If true, print useful info about the file to stdout + */ + PLYData(std::istream& inStream, bool verbose = false) { + + using std::cout; + using std::endl; + + if (verbose) cout << "PLY parser: Reading ply file from stream" << endl; + + parsePLY(inStream, verbose); + + if (verbose) { + cout << " - Finished parsing stream." << endl; + } + } + + /** + * @brief Perform sanity checks on the file, throwing if any fail. + */ + void validate() { + + for (size_t iE = 0; iE < elements.size(); iE++) { + for (char c : elements[iE].name) { + if (std::isspace(c)) { + throw std::runtime_error("Ply validate: illegal whitespace in element name " + elements[iE].name); + } + } + for (size_t jE = iE + 1; jE < elements.size(); jE++) { + if (elements[iE].name == elements[jE].name) { + throw std::runtime_error("Ply validate: duplcate element name " + elements[iE].name); + } + } + } + + // Do a quick validation sanity check + for (Element& e : elements) { + e.validate(); + } + } + + /** + * @brief Write this data to a .ply file. + * + * @param filename The file to write to. + * @param format The format to use (binary or ascii?) + */ + void write(const std::string& filename, DataFormat format = DataFormat::ASCII) { + outputDataFormat = format; + + validate(); + + // Open stream for writing + std::ofstream outStream(filename, std::ios::out | std::ios::binary); + if (!outStream.good()) { + throw std::runtime_error("Ply writer: Could not open output file " + filename + " for writing"); + } + + writePLY(outStream); + } + + /** + * @brief Write this data to an output stream + * + * @param outStream The output stream to write to. + * @param format The format to use (binary or ascii?) + */ + void write(std::ostream& outStream, DataFormat format = DataFormat::ASCII) { + outputDataFormat = format; + + validate(); + + writePLY(outStream); + } + + /** + * @brief Get an element type by name ("vertices") + * + * @param target The name of the element type to get + * + * @return A reference to the element type. + */ + Element& getElement(const std::string& target) { + for (Element& e : elements) { + if (e.name == target) return e; + } + throw std::runtime_error("PLY parser: no element with name: " + target); + } + + + /** + * @brief Check if an element type exists + * + * @param target The name to check for. + * + * @return True if exists. + */ + bool hasElement(const std::string& target) { + for (Element& e : elements) { + if (e.name == target) return true; + } + return false; + } + + + /** + * @brief A list of the names of all elements + * + * @return Element names + */ + std::vector getElementNames() { + std::vector names; + for (Element& e : elements) { + names.push_back(e.name); + } + return names; + } + + + /** + * @brief Add a new element type to the object + * + * @param name The name of the new element type ("vertices"). + * @param count The number of elements of this type. + */ + void addElement(const std::string& name, size_t count) { elements.emplace_back(name, count); } + + // === Common-case helpers + + + /** + * @brief Common-case helper get mesh vertex positions + * + * @param vertexElementName The element name to use (default: "vertex") + * + * @return A vector of vertex positions. + */ + std::vector> getVertexPositions(const std::string& vertexElementName = "vertex") { + + std::vector xPos = getElement(vertexElementName).getProperty("x"); + std::vector yPos = getElement(vertexElementName).getProperty("y"); + std::vector zPos = getElement(vertexElementName).getProperty("z"); + + std::vector> result(xPos.size()); + for (size_t i = 0; i < result.size(); i++) { + result[i][0] = xPos[i]; + result[i][1] = yPos[i]; + result[i][2] = zPos[i]; + } + + return result; + } + + /** + * @brief Common-case helper get mesh vertex colors + * + * @param vertexElementName The element name to use (default: "vertex") + * + * @return A vector of vertex colors (unsigned chars [0,255]). + */ + std::vector> getVertexColors(const std::string& vertexElementName = "vertex") { + + std::vector r = getElement(vertexElementName).getProperty("red"); + std::vector g = getElement(vertexElementName).getProperty("green"); + std::vector b = getElement(vertexElementName).getProperty("blue"); + + std::vector> result(r.size()); + for (size_t i = 0; i < result.size(); i++) { + result[i][0] = r[i]; + result[i][1] = g[i]; + result[i][2] = b[i]; + } + + return result; + } + + /** + * @brief Common-case helper to get face indices for a mesh. If not template type is given, size_t is used. Naively + * converts to requested signedness, which may lead to unexpected values if an unsigned type is used and file contains + * negative values. + * + * @return The indices into the vertex elements for each face. Usually 0-based, though there are no formal rules. + */ + template + std::vector> getFaceIndices() { + + for (const std::string& f : std::vector{"face"}) { + for (const std::string& p : std::vector{"vertex_indices", "vertex_index"}) { + try { + return getElement(f).getListPropertyAnySign(p); + } catch (const std::runtime_error&) { + // that's fine + } + } + } + throw std::runtime_error("PLY parser: could not find face vertex indices attribute under any common name."); + } + + + /** + * @brief Common-case helper set mesh vertex positons. Creates vertex element, if necessary. + * + * @param vertexPositions A vector of vertex positions + */ + void addVertexPositions(std::vector>& vertexPositions) { + + std::string vertexName = "vertex"; + size_t N = vertexPositions.size(); + + // Create the element + if (!hasElement(vertexName)) { + addElement(vertexName, N); + } + + // De-interleave + std::vector xPos(N); + std::vector yPos(N); + std::vector zPos(N); + for (size_t i = 0; i < vertexPositions.size(); i++) { + xPos[i] = vertexPositions[i][0]; + yPos[i] = vertexPositions[i][1]; + zPos[i] = vertexPositions[i][2]; + } + + // Store + getElement(vertexName).addProperty("x", xPos); + getElement(vertexName).addProperty("y", yPos); + getElement(vertexName).addProperty("z", zPos); + } + + /** + * @brief Common-case helper set mesh vertex colors. Creates a vertex element, if necessary. + * + * @param colors A vector of vertex colors (unsigned chars [0,255]). + */ + void addVertexColors(std::vector>& colors) { + + std::string vertexName = "vertex"; + size_t N = colors.size(); + + // Create the element + if (!hasElement(vertexName)) { + addElement(vertexName, N); + } + + // De-interleave + std::vector r(N); + std::vector g(N); + std::vector b(N); + for (size_t i = 0; i < colors.size(); i++) { + r[i] = colors[i][0]; + g[i] = colors[i][1]; + b[i] = colors[i][2]; + } + + // Store + getElement(vertexName).addProperty("red", r); + getElement(vertexName).addProperty("green", g); + getElement(vertexName).addProperty("blue", b); + } + + /** + * @brief Common-case helper set mesh vertex colors. Creates a vertex element, if necessary. + * + * @param colors A vector of vertex colors as floating point [0,1] values. Internally converted to [0,255] chars. + */ + void addVertexColors(std::vector>& colors) { + + std::string vertexName = "vertex"; + size_t N = colors.size(); + + // Create the element + if (!hasElement(vertexName)) { + addElement(vertexName, N); + } + + auto toChar = [](double v) { + if (v < 0.0) v = 0.0; + if (v > 1.0) v = 1.0; + return static_cast(v * 255.); + }; + + // De-interleave + std::vector r(N); + std::vector g(N); + std::vector b(N); + for (size_t i = 0; i < colors.size(); i++) { + r[i] = toChar(colors[i][0]); + g[i] = toChar(colors[i][1]); + b[i] = toChar(colors[i][2]); + } + + // Store + getElement(vertexName).addProperty("red", r); + getElement(vertexName).addProperty("green", g); + getElement(vertexName).addProperty("blue", b); + } + + + /** + * @brief Common-case helper to set face indices. Creates a face element if needed. The input type will be casted to a + * 32 bit integer of the same signedness. + * + * @param indices The indices into the vertex list around each face. + */ + template + void addFaceIndices(std::vector>& indices) { + + std::string faceName = "face"; + size_t N = indices.size(); + + // Create the element + if (!hasElement(faceName)) { + addElement(faceName, N); + } + + // Cast to 32 bit + typedef typename std::conditional::value, int32_t, uint32_t>::type IndType; + std::vector> intInds; + for (std::vector& l : indices) { + std::vector thisInds; + for (T& val : l) { + IndType valConverted = static_cast(val); + if (valConverted != val) { + throw std::runtime_error("Index value " + std::to_string(val) + + " could not be converted to a .ply integer without loss of data. Note that .ply " + "only supports 32-bit ints."); + } + thisInds.push_back(valConverted); + } + intInds.push_back(thisInds); + } + + // Store + getElement(faceName).addListProperty("vertex_indices", intInds); + } + + + /** + * @brief Comments for the file. When writing, each entry will be written as a sequential comment line. + */ + std::vector comments; + + + /** + * @brief obj_info comments for the file. When writing, each entry will be written as a sequential comment line. + */ + std::vector objInfoComments; + +private: + std::vector elements; + const int majorVersion = 1; // I'll buy you a drink if these ever get bumped + const int minorVersion = 0; + + DataFormat inputDataFormat = DataFormat::ASCII; // set when reading from a file + DataFormat outputDataFormat = DataFormat::ASCII; // option for writing files + + + // === Reading === + + /** + * @brief Parse a PLY file from an input stream + * + * @param inStream + * @param verbose + */ + void parsePLY(std::istream& inStream, bool verbose) { + + // == Process the header + parseHeader(inStream, verbose); + + + // === Parse data from a binary file + if (inputDataFormat == DataFormat::Binary) { + parseBinary(inStream, verbose); + } + // === Parse data from an binary file + else if (inputDataFormat == DataFormat::BinaryBigEndian) { + parseBinaryBigEndian(inStream, verbose); + } + // === Parse data from an ASCII file + else if (inputDataFormat == DataFormat::ASCII) { + parseASCII(inStream, verbose); + } + } + + /** + * @brief Read the header for a file + * + * @param inStream + * @param verbose + */ + void parseHeader(std::istream& inStream, bool verbose) { + + using std::cout; + using std::endl; + using std::string; + using std::vector; + + // First two lines are predetermined + { // First line is magic constant + string plyLine; + std::getline(inStream, plyLine); + if (trimSpaces(plyLine) != "ply") { + throw std::runtime_error("PLY parser: File does not appear to be ply file. First line should be 'ply'"); + } + } + + { // second line is version + string styleLine; + std::getline(inStream, styleLine); + vector tokens = tokenSplit(styleLine); + if (tokens.size() != 3) throw std::runtime_error("PLY parser: bad format line"); + std::string formatStr = tokens[0]; + std::string typeStr = tokens[1]; + std::string versionStr = tokens[2]; + + // "format" + if (formatStr != "format") throw std::runtime_error("PLY parser: bad format line"); + + // ascii/binary + if (typeStr == "ascii") { + inputDataFormat = DataFormat::ASCII; + if (verbose) cout << " - Type: ascii" << endl; + } else if (typeStr == "binary_little_endian") { + inputDataFormat = DataFormat::Binary; + if (verbose) cout << " - Type: binary" << endl; + } else if (typeStr == "binary_big_endian") { + inputDataFormat = DataFormat::BinaryBigEndian; + if (verbose) cout << " - Type: binary big endian" << endl; + } else { + throw std::runtime_error("PLY parser: bad format line"); + } + + // version + if (versionStr != "1.0") { + throw std::runtime_error("PLY parser: encountered file with version != 1.0. Don't know how to parse that"); + } + if (verbose) cout << " - Version: " << versionStr << endl; + } + + // Consume header line by line + while (inStream.good()) { + string line; + std::getline(inStream, line); + + // Parse a comment + if (startsWith(line, "comment")) { + string comment = line.substr(8); + if (verbose) cout << " - Comment: " << comment << endl; + comments.push_back(comment); + continue; + } + + // Parse an obj_info comment + if (startsWith(line, "obj_info")) { + string infoComment = line.substr(9); + if (verbose) cout << " - obj_info: " << infoComment << endl; + objInfoComments.push_back(infoComment); + continue; + } + + // Parse an element + else if (startsWith(line, "element")) { + vector tokens = tokenSplit(line); + if (tokens.size() != 3) throw std::runtime_error("PLY parser: Invalid element line"); + string name = tokens[1]; + size_t count; + std::istringstream iss(tokens[2]); + iss >> count; + elements.emplace_back(name, count); + if (verbose) cout << " - Found element: " << name << " (count = " << count << ")" << endl; + continue; + } + + // Parse a property list + else if (startsWith(line, "property list")) { + vector tokens = tokenSplit(line); + if (tokens.size() != 5) throw std::runtime_error("PLY parser: Invalid property list line"); + if (elements.size() == 0) throw std::runtime_error("PLY parser: Found property list without previous element"); + string countType = tokens[2]; + string type = tokens[3]; + string name = tokens[4]; + elements.back().properties.push_back(createPropertyWithType(name, type, true, countType)); + if (verbose) + cout << " - Found list property: " << name << " (count type = " << countType << ", data type = " << type + << ")" << endl; + continue; + } + + // Parse a property + else if (startsWith(line, "property")) { + vector tokens = tokenSplit(line); + if (tokens.size() != 3) throw std::runtime_error("PLY parser: Invalid property line"); + if (elements.size() == 0) throw std::runtime_error("PLY parser: Found property without previous element"); + string type = tokens[1]; + string name = tokens[2]; + elements.back().properties.push_back(createPropertyWithType(name, type, false, "")); + if (verbose) cout << " - Found property: " << name << " (type = " << type << ")" << endl; + continue; + } + + // Parse end of header + else if (startsWith(line, "end_header")) { + break; + } + + // Error! + else { + throw std::runtime_error("Unrecognized header line: " + line); + } + } + } + + /** + * @brief Read the actual data for a file, in ASCII + * + * @param inStream + * @param verbose + */ + void parseASCII(std::istream& inStream, bool verbose) { + + using std::string; + using std::vector; + + // Read all elements + for (Element& elem : elements) { + + if (verbose) { + std::cout << " - Processing element: " << elem.name << std::endl; + } + + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->reserve(elem.count); + } + for (size_t iEntry = 0; iEntry < elem.count; iEntry++) { + + string line; + std::getline(inStream, line); + + // Some .ply files seem to include empty lines before the start of property data (though this is not specified + // in the format description). We attempt to recover and parse such files by skipping any empty lines. + if (!elem.properties.empty()) { // if the element has no properties, the line _should_ be blank, presumably + while (line.empty()) { // skip lines until we hit something nonempty + std::getline(inStream, line); + } + } + + vector tokens = tokenSplit(line); + size_t iTok = 0; + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->parseNext(tokens, iTok); + } + } + } + } + + /** + * @brief Read the actual data for a file, in binary. + * + * @param inStream + * @param verbose + */ + void parseBinary(std::istream& inStream, bool verbose) { + + if (!isLittleEndian()) { + throw std::runtime_error("binary reading assumes little endian system"); + } + + using std::string; + using std::vector; + + // Read all elements + for (Element& elem : elements) { + + if (verbose) { + std::cout << " - Processing element: " << elem.name << std::endl; + } + + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->reserve(elem.count); + } + for (size_t iEntry = 0; iEntry < elem.count; iEntry++) { + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->readNext(inStream); + } + } + } + } + + /** + * @brief Read the actual data for a file, in binary. + * + * @param inStream + * @param verbose + */ + void parseBinaryBigEndian(std::istream& inStream, bool verbose) { + + if (!isLittleEndian()) { + throw std::runtime_error("binary reading assumes little endian system"); + } + + using std::string; + using std::vector; + + // Read all elements + for (Element& elem : elements) { + + if (verbose) { + std::cout << " - Processing element: " << elem.name << std::endl; + } + + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->reserve(elem.count); + } + for (size_t iEntry = 0; iEntry < elem.count; iEntry++) { + for (size_t iP = 0; iP < elem.properties.size(); iP++) { + elem.properties[iP]->readNextBigEndian(inStream); + } + } + } + } + + // === Writing === + + + /** + * @brief write a PLY file to an output stream + * + * @param outStream + */ + void writePLY(std::ostream& outStream) { + + writeHeader(outStream); + + // Write all elements + for (Element& e : elements) { + if (outputDataFormat == DataFormat::Binary) { + if (!isLittleEndian()) { + throw std::runtime_error("binary writing assumes little endian system"); + } + e.writeDataBinary(outStream); + } else if (outputDataFormat == DataFormat::BinaryBigEndian) { + if (!isLittleEndian()) { + throw std::runtime_error("binary writing assumes little endian system"); + } + e.writeDataBinaryBigEndian(outStream); + } else if (outputDataFormat == DataFormat::ASCII) { + e.writeDataASCII(outStream); + } + } + } + + + /** + * @brief Write out a header for a file + * + * @param outStream + */ + void writeHeader(std::ostream& outStream) { + + // Magic line + outStream << "ply\n"; + + // Type line + outStream << "format "; + if (outputDataFormat == DataFormat::Binary) { + outStream << "binary_little_endian "; + } else if (outputDataFormat == DataFormat::BinaryBigEndian) { + outStream << "binary_big_endian "; + } else if (outputDataFormat == DataFormat::ASCII) { + outStream << "ascii "; + } + + // Version number + outStream << majorVersion << "." << minorVersion << "\n"; + + // Write comments + bool hasHapplyComment = false; + std::string happlyComment = "Written with hapPLY (https://github.com/nmwsharp/happly)"; + for (const std::string& comment : comments) { + if (comment == happlyComment) hasHapplyComment = true; + outStream << "comment " << comment << "\n"; + } + if (!hasHapplyComment) { + outStream << "comment " << happlyComment << "\n"; + } + + // Write obj_info comments + for (const std::string& comment : objInfoComments) { + outStream << "obj_info " << comment << "\n"; + } + + // Write elements (and their properties) + for (Element& e : elements) { + e.writeHeader(outStream); + } + + // End header + outStream << "end_header\n"; + } +}; + +} // namespace happly diff --git a/Gems/Pointcloud/Code/Source/Clients/PointcloudAsset.cpp b/Gems/Pointcloud/Code/Source/Clients/PointcloudAsset.cpp new file mode 100644 index 00000000..16765ea3 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Clients/PointcloudAsset.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +namespace Pointcloud +{ + PointcloudAssetHandler::PointcloudAssetHandler() + : AzFramework::GenericAssetHandler( + PointcloudAsset::DisplayName, PointcloudAsset::Group, PointcloudAsset::Extension) + { + } + + bool PointcloudAssetHandler::SaveAssetData(const AZ::Data::Asset& asset, AZ::IO::GenericStream* stream) + { + PointcloudAsset* assetData = asset.GetAs(); + AZ_Assert(assetData, "Asset is not of the expected type."); + const size_t headerSize = sizeof(PointcloudAsset::CloudHeader); + const size_t elementSize = sizeof(PointcloudAsset::CloudVertex); + const size_t numElements = assetData->m_data.size(); + const size_t expectedDataSize = elementSize * numElements; + const size_t expectedSize = headerSize + expectedDataSize; + AZStd::vector headerData(headerSize); + AZStd::vector pointData(expectedDataSize); + + PointcloudAsset::CloudHeader header; + header.m_magicNumber = PointcloudAsset::PointcloudMagicNumber; + header.m_elementSize = elementSize; + header.m_numPoints = static_cast(numElements); + + std::memcpy(headerData.data(), (void*)&header, headerSize); + auto bytesWritten = stream->Write(headerSize, headerData.data()); + AZ_Assert(bytesWritten == headerSize, "Failed to write header data to stream."); + + std::memcpy(pointData.data(), assetData->m_data.data(), expectedDataSize); + bytesWritten += stream->Write(expectedDataSize, pointData.data()); + + AZ_Assert(bytesWritten == expectedSize, "Failed to write point data to stream."); + if (bytesWritten == expectedSize) + { + return true; + } + return false; + } + + AZ::Data::AssetHandler::LoadResult PointcloudAssetHandler::LoadAssetData( + const AZ::Data::Asset& asset, + AZStd::shared_ptr stream, + [[maybe_unused]] const AZ::Data::AssetFilterCB& assetLoadFilterCB) + { + PointcloudAsset* assetData = asset.GetAs(); + AZ_Assert(assetData, "Asset is not of the expected type."); + + if (assetData && stream->GetLength() > 0) + { + PointcloudAsset::CloudHeader header; + stream->Read(sizeof(PointcloudAsset::CloudHeader), (void*)&header); + AZ_Assert(header.m_magicNumber == PointcloudAsset::PointcloudMagicNumber, "Invalid magic number in pointcloud asset file."); + AZ_Assert(header.m_elementSize == sizeof(PointcloudAsset::CloudVertex), "Invalid element size in pointcloud asset file."); + AZ_Printf("Pointcloud", "Loading pointcloud with %d points", header.m_numPoints); + const auto expectedDataSize = header.m_elementSize * header.m_numPoints; + + AZStd::vector rawData(expectedDataSize); + stream->Read(stream->GetLength(), rawData.data()); + + const size_t elementSize = sizeof(PointcloudAsset::CloudVertex); + const size_t numElements = rawData.size() / elementSize; + assetData->m_data.resize(numElements); + std::memcpy(assetData->m_data.data(), rawData.data(), rawData.size()); + return AZ::Data::AssetHandler::LoadResult::LoadComplete; + } + + return AZ::Data::AssetHandler::LoadResult::Error; + } +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Clients/PointcloudComponent.cpp b/Gems/Pointcloud/Code/Source/Clients/PointcloudComponent.cpp new file mode 100644 index 00000000..5a329f57 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Clients/PointcloudComponent.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include "PointcloudComponent.h" +#include +#include +#include +#include +#include +namespace Pointcloud +{ + + PointcloudComponent::PointcloudComponent(const AZ::Data::Asset& pointcloudAsset, const float pointSize) + : m_pointcloudAsset(pointcloudAsset) + , m_pointSize(pointSize) + { + } + + void PointcloudComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("PointcloudAsset", &PointcloudComponent::m_pointcloudAsset) + ->Field("PointSize", &PointcloudComponent::m_pointSize); + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("PointcloudComponent", "PointcloudComponent") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "PointcloudComponent") + ->Attribute(AZ::Edit::Attributes::Category, "RobotecTools") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->DataElement( + AZ::Edit::UIHandlers::Default, + &PointcloudComponent::m_pointcloudAsset, + "Pointcloud Asset", + "Asset containing the pointcloud data") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &PointcloudComponent::m_pointSize, + "Point Size", + "Size of the points in the pointcloud"); + } + } + } + + void PointcloudComponent::Activate() + { + AZ::SystemTickBus::QueueFunction( + [this]() + { + m_scene = AZ::RPI::Scene::GetSceneForEntityId(GetEntityId()); + if (m_scene && m_pointcloudAsset) + { + m_featureProcessor = m_scene->EnableFeatureProcessor(); + AZ_Assert(m_featureProcessor, "Failed to enable PointcloudFeatureProcessorInterface."); + m_pointcloudAsset.QueueLoad(); + m_pointcloudAsset.BlockUntilLoadComplete(); + + AZStd::vector> cloudVertexDataChunks; + if (m_pointcloudAsset.GetId().IsValid() && m_pointcloudAsset.IsReady()) + { + m_pointcloudHandle = m_featureProcessor->AcquirePointcloud(m_pointcloudAsset->m_data); + } + if (m_pointcloudHandle != PointcloudFeatureProcessorInterface::InvalidPointcloudHandle) + { + m_featureProcessor->SetTransform(m_pointcloudHandle, m_entity->GetTransform()->GetWorldTM()); + m_featureProcessor->SetPointSize(m_pointcloudHandle, m_pointSize); + } + } + }); + AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId()); + } + + void PointcloudComponent::Deactivate() + { + AZ::TransformNotificationBus::Handler::BusDisconnect(); + m_featureProcessor->ReleasePointcloud(m_pointcloudHandle); + } + + void PointcloudComponent::OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) + { + AZ_UNUSED(local); + if (m_pointcloudHandle != PointcloudFeatureProcessorInterface::InvalidPointcloudHandle) + { + m_featureProcessor->SetTransform(m_pointcloudHandle, world); + } + } + +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Clients/PointcloudComponent.h b/Gems/Pointcloud/Code/Source/Clients/PointcloudComponent.h new file mode 100644 index 00000000..719f5670 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Clients/PointcloudComponent.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace Pointcloud +{ + + class PointcloudComponent + : public AZ::Component + , private AZ::TransformNotificationBus::Handler + { + public: + AZ_COMPONENT(PointcloudComponent, "{0190c091-83aa-7c6e-a6da-5efea1f23473}"); + PointcloudComponent() = default; + PointcloudComponent(const AZ::Data::Asset& pointcloudAsset, const float pointSize); + ~PointcloudComponent() = default; + + static void Reflect(AZ::ReflectContext* context); + + // Component interface overrides ... + void Activate() override; + void Deactivate() override; + + private: + // AZ::TransformNotificationBus::Handler overrides ... + void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override; + + AZ::Data::Asset m_pointcloudAsset; + float m_pointSize = 1.0f; + PointcloudFeatureProcessorInterface* m_featureProcessor = nullptr; + AZ::RPI::Scene* m_scene = nullptr; + PointcloudFeatureProcessorInterface::PointcloudHandle m_pointcloudHandle = + PointcloudFeatureProcessorInterface::InvalidPointcloudHandle; + }; +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Clients/PointcloudModule.cpp b/Gems/Pointcloud/Code/Source/Clients/PointcloudModule.cpp new file mode 100644 index 00000000..f8668bb8 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Clients/PointcloudModule.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include "PointcloudSystemComponent.h" +#include +#include + +#include + +namespace Pointcloud +{ + class PointcloudModule : public PointcloudModuleInterface + { + public: + AZ_RTTI(PointcloudModule, PointcloudModuleTypeId, PointcloudModuleInterface); + AZ_CLASS_ALLOCATOR(PointcloudModule, AZ::SystemAllocator); + + PointcloudModule() + { + m_descriptors.insert( + m_descriptors.end(), + { + PointcloudSystemComponent::CreateDescriptor(), + }); + } + + AZ::ComponentTypeList GetRequiredSystemComponents() const + { + return AZ::ComponentTypeList{ azrtti_typeid() }; + } + }; +} // namespace Pointcloud + +AZ_DECLARE_MODULE_CLASS(Gem_Pointcloud, Pointcloud::PointcloudModule) diff --git a/Gems/Pointcloud/Code/Source/Clients/PointcloudSystemComponent.cpp b/Gems/Pointcloud/Code/Source/Clients/PointcloudSystemComponent.cpp new file mode 100644 index 00000000..309c77a1 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Clients/PointcloudSystemComponent.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include "PointcloudSystemComponent.h" + +#include + +#include + +#include + +#include + +namespace Pointcloud +{ + AZ_COMPONENT_IMPL(PointcloudSystemComponent, "PointcloudSystemComponent", PointcloudSystemComponentTypeId); + + void PointcloudSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class()->Version(0); + } + + PointcloudFeatureProcessor::Reflect(context); + } + + void PointcloudSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("PointcloudSystemService")); + } + + void PointcloudSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("PointcloudSystemService")); + } + + void PointcloudSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("RPISystem")); + } + + void PointcloudSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + } + + + void PointcloudSystemComponent::Init() + { + } + + void PointcloudSystemComponent::Activate() + { + m_pointcloudAssetHandler = aznew PointcloudAssetHandler(); + m_pointcloudAssetHandler->Register(); + } + + void PointcloudSystemComponent::Deactivate() + { + m_pointcloudAssetHandler->Unregister(); + delete m_pointcloudAssetHandler; + } + +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Clients/PointcloudSystemComponent.h b/Gems/Pointcloud/Code/Source/Clients/PointcloudSystemComponent.h new file mode 100644 index 00000000..76459017 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Clients/PointcloudSystemComponent.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +namespace Pointcloud +{ + class PointcloudSystemComponent + : public AZ::Component + { + public: + AZ_COMPONENT_DECL(PointcloudSystemComponent); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + PointcloudSystemComponent() = default; + ~PointcloudSystemComponent() = default; + + protected: + //////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Init() override; + void Activate() override; + void Deactivate() override; + //////////////////////////////////////////////////////////////////////// + private: + PointcloudAssetHandler* m_pointcloudAssetHandler; + }; + +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/PointcloudModuleInterface.cpp b/Gems/Pointcloud/Code/Source/PointcloudModuleInterface.cpp new file mode 100644 index 00000000..7c8ccb6c --- /dev/null +++ b/Gems/Pointcloud/Code/Source/PointcloudModuleInterface.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include "PointcloudModuleInterface.h" +#include + +#include + +#include "Tools/Components/PointcloudAssetBuilderSystemComponent.h" +#include +#include +namespace Pointcloud +{ + AZ_TYPE_INFO_WITH_NAME_IMPL(PointcloudModuleInterface, "PointcloudModuleInterface", PointcloudModuleInterfaceTypeId); + AZ_RTTI_NO_TYPE_INFO_IMPL(PointcloudModuleInterface, AZ::Module); + AZ_CLASS_ALLOCATOR_IMPL(PointcloudModuleInterface, AZ::SystemAllocator); + + PointcloudModuleInterface::PointcloudModuleInterface() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + // Add ALL components descriptors associated with this gem to m_descriptors. + // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. + // This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert( + m_descriptors.end(), + { + PointcloudSystemComponent::CreateDescriptor(), + PointcloudComponent::CreateDescriptor(), + }); + } + + AZ::ComponentTypeList PointcloudModuleInterface::GetRequiredSystemComponents() const + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + azrtti_typeid(), + }; + } +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/PointcloudModuleInterface.h b/Gems/Pointcloud/Code/Source/PointcloudModuleInterface.h new file mode 100644 index 00000000..1e152317 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/PointcloudModuleInterface.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include + +namespace Pointcloud +{ + class PointcloudModuleInterface + : public AZ::Module + { + public: + AZ_TYPE_INFO_WITH_NAME_DECL(PointcloudModuleInterface) + AZ_RTTI_NO_TYPE_INFO_DECL() + AZ_CLASS_ALLOCATOR_DECL + + PointcloudModuleInterface(); + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override; + }; +}// namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Render/PointcloudFeatureProcessor.cpp b/Gems/Pointcloud/Code/Source/Render/PointcloudFeatureProcessor.cpp new file mode 100644 index 00000000..3f25d12e --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Render/PointcloudFeatureProcessor.cpp @@ -0,0 +1,269 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include "PointcloudFeatureProcessor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace Pointcloud +{ + void PointcloudFeatureProcessor::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class(); + } + } + + void PointcloudFeatureProcessor::Activate() + { + const char* shaderFilePath = "Shaders/Pointclouds/Pointclouds.azshader"; + m_shader = AZ::RPI::LoadCriticalShader(shaderFilePath); + + if (!m_shader) + { + AZ_Error("PointcloudFeatureProcessor", false, "Failed to load required stars shader."); + return; + } + AZ::Data::AssetBus::Handler::BusConnect(m_shader->GetAssetId()); + + m_drawSrgLayout = m_shader->GetAsset()->GetDrawSrgLayout(m_shader->GetSupervariantIndex()); + AZ_Error( + "PointcloudFeatureProcessor", m_drawSrgLayout, "Failed to get the draw shader resource group layout for the pointcloud shader."); + + m_drawListTag = m_shader->GetDrawListTag(); + + auto viewportContextInterface = AZ::Interface::Get(); + AZ_Assert(viewportContextInterface, "PointcloudFeatureProcessor requires the ViewportContextRequestsInterface."); + auto viewportContext = viewportContextInterface->GetViewportContextByScene(GetParentScene()); + AZ_Assert(viewportContext, "PointcloudFeatureProcessor requires a valid ViewportContext."); + + EnableSceneNotification(); + + AZ::RPI::ViewportContextIdNotificationBus::Handler::BusConnect(viewportContext->GetId()); + } + + PointcloudFeatureProcessorInterface::PointcloudHandle PointcloudFeatureProcessor::AcquirePointcloud( + const AZStd::vector& cloudVertexData) + { + AZ_Assert(m_drawSrgLayout, "DrawSrgLayout is not initialized"); + m_currentPointcloudDataIndex++; + auto& pcData = m_pointcloudData[m_currentPointcloudDataIndex]; + pcData.m_index = m_currentPointcloudDataIndex; + const uint32_t elementSize = sizeof(PointcloudAsset::CloudVertex); + const uint32_t elementCount = static_cast(cloudVertexData.size()); + const uint32_t bufferSize = elementCount * elementSize; // bytecount + + AZ_TracePrintf("PointcloudFeatureProcessor", "PointcloudFeatureProcessor SetCloud %d, bytesize %d", elementCount, bufferSize); + pcData.m_pointData = cloudVertexData; + pcData.m_vertices = elementCount; + + AZ::RPI::CommonBufferDescriptor desc; + desc.m_poolType = AZ::RPI::CommonBufferPoolType::ReadWrite; + desc.m_bufferName = AZStd::string::format("PointcloudFeatureProcessor, %d", pcData.m_index); + desc.m_byteCount = bufferSize; + desc.m_elementSize = elementSize; + desc.m_bufferData = pcData.m_pointData.data(); + + pcData.m_cloudVertexBuffer = AZ::RPI::BufferSystemInterface::Get()->CreateBufferFromCommonPool(desc); + + pcData.m_meshStreamBufferViews.front() = + AZ::RHI::StreamBufferView(*pcData.m_cloudVertexBuffer->GetRHIBuffer(), 0, bufferSize, elementSize); + + pcData.m_drawSrg = + AZ::RPI::ShaderResourceGroup::Create(m_shader->GetAsset(), m_shader->GetSupervariantIndex(), m_drawSrgLayout->GetName()); + m_pointSizeIndex.Reset(); + m_modelMatrixIndex.Reset(); + + UpdateDrawPacket(); + return pcData.m_index; + } + + void PointcloudFeatureProcessor::UpdateDrawPacket() + { + for (auto& [_, pcData] : m_pointcloudData) + { + if (m_meshPipelineState && pcData.m_drawSrg && pcData.m_meshStreamBufferViews.front().GetByteCount() != 0) + { + pcData.m_drawPacket = + BuildDrawPacket( pcData.m_drawSrg, m_meshPipelineState, m_drawListTag, pcData.m_meshStreamBufferViews, pcData.m_vertices); + } + } + } + + void PointcloudFeatureProcessor::OnAssetReloaded(AZ::Data::Asset asset) + { + if (asset.GetId() == m_shader->GetAssetId()) + { + UpdateDrawPacket(); + } + } + + void PointcloudFeatureProcessor::OnAssetReady(AZ::Data::Asset asset) + { + if (asset.GetId() == m_shader->GetAssetId()) + { + UpdateDrawPacket(); + } + } + + void PointcloudFeatureProcessor::Deactivate() + { + AZ_Printf("PointcloudFeatureProcessor", "PointcloudFeatureProcessor Deactivated"); + AZ::Data::AssetBus::Handler::BusDisconnect(m_shader->GetAssetId()); + AZ::RPI::ViewportContextIdNotificationBus::Handler::BusDisconnect(); + } + + void PointcloudFeatureProcessor::Simulate([[maybe_unused]] const FeatureProcessor::SimulatePacket& packet) + { + UpdateShaderConstants(); + } + + void PointcloudFeatureProcessor::Render([[maybe_unused]] const FeatureProcessor::RenderPacket& packet) + { + AZ_PROFILE_FUNCTION(AzRender); + + for (auto& [_, pcData] : m_pointcloudData) + { + if (pcData.m_drawPacket) + { + if (!pcData.m_visible) + { + continue; + } + for (auto& view : packet.m_views) + { + if (!view->HasDrawListTag(m_drawListTag)) + { + continue; + } + constexpr float depth = 0.f; + view->AddDrawPacket(pcData.m_drawPacket.get(), depth); + } + } + } + } + + AZ::RHI::ConstPtr PointcloudFeatureProcessor::BuildDrawPacket( + const AZ::Data::Instance& srg, + const AZ::RPI::Ptr& pipelineState, + const AZ::RHI::DrawListTag& drawListTag, + const AZStd::span& streamBufferViews, + uint32_t vertexCount) + { + AZ::RHI::DrawLinear drawLinear; + drawLinear.m_vertexCount = vertexCount; + drawLinear.m_vertexOffset = 0; + drawLinear.m_instanceCount = 1; + drawLinear.m_instanceOffset = 0; + + AZ::RHI::DrawPacketBuilder drawPacketBuilder; + drawPacketBuilder.Begin(nullptr); + drawPacketBuilder.SetDrawArguments(drawLinear); + drawPacketBuilder.AddShaderResourceGroup(srg->GetRHIShaderResourceGroup()); + + AZ::RHI::DrawPacketBuilder::DrawRequest drawRequest; + drawRequest.m_listTag = drawListTag; + drawRequest.m_pipelineState = pipelineState->GetRHIPipelineState(); + drawRequest.m_streamBufferViews = streamBufferViews; + drawPacketBuilder.AddDrawItem(drawRequest); + return drawPacketBuilder.End(); + } + + void PointcloudFeatureProcessor::OnRenderPipelineChanged( + [[maybe_unused]] AZ::RPI::RenderPipeline* renderPipeline, AZ::RPI::SceneNotification::RenderPipelineChangeType changeType) + { + if (changeType == AZ::RPI::SceneNotification::RenderPipelineChangeType::Added) + { + if (!m_meshPipelineState) + { + m_meshPipelineState = aznew AZ::RPI::PipelineStateForDraw; + m_meshPipelineState->Init(m_shader); + + AZ::RHI::InputStreamLayoutBuilder layoutBuilder; + layoutBuilder.AddBuffer() + ->Channel("POSITION", AZ::RHI::Format::R32G32B32_FLOAT) + ->Channel("COLOR", AZ::RHI::Format::R8G8B8A8_UNORM); + layoutBuilder.SetTopology(AZ::RHI::PrimitiveTopology::PointList); + auto inputStreamLayout = layoutBuilder.End(); + + m_meshPipelineState->SetInputStreamLayout(inputStreamLayout); + m_meshPipelineState->SetOutputFromScene(GetParentScene()); + m_meshPipelineState->Finalize(); + + UpdateDrawPacket(); + } + } + else if (changeType == AZ::RPI::SceneNotification::RenderPipelineChangeType::PassChanged) + { + if (m_meshPipelineState) + { + m_meshPipelineState->SetOutputFromScene(GetParentScene()); + m_meshPipelineState->Finalize(); + UpdateDrawPacket(); + } + } + } + + void PointcloudFeatureProcessor::UpdateShaderConstants() + { + for (auto& [_, pcData] : m_pointcloudData) + { + if (pcData.m_needSrgUpdate && pcData.m_drawSrg) + { + pcData.m_needSrgUpdate = false; + AZ::Matrix4x4 orientation = AZ::Matrix4x4::CreateFromTransform(pcData.m_transform); + pcData.m_drawSrg->SetConstant(m_modelMatrixIndex, orientation); + pcData.m_drawSrg->SetConstant(m_pointSizeIndex, pcData.m_pointSize); + pcData.m_drawSrg->Compile(); + } + } + } + + void PointcloudFeatureProcessor::SetTransform(const PointcloudHandle& handle, const AZ::Transform& transform) + { + if (auto it = m_pointcloudData.find(handle); it != m_pointcloudData.end()) + { + it->second.m_transform = transform; + it->second.m_needSrgUpdate = true; + } + } + + void PointcloudFeatureProcessor::SetPointSize(const PointcloudHandle& handle, float pointSize) + { + if (auto it = m_pointcloudData.find(handle); it != m_pointcloudData.end()) + { + it->second.m_pointSize = pointSize; + it->second.m_needSrgUpdate = true; + } + } + + void PointcloudFeatureProcessor::SetVisibility(const PointcloudHandle& handle, bool visible) + { + if (auto it = m_pointcloudData.find(handle); it != m_pointcloudData.end()) + { + it->second.m_visible = visible; + } + } + + void PointcloudFeatureProcessor::ReleasePointcloud(const PointcloudHandle& handle) + { + if (auto it = m_pointcloudData.find(handle); it != m_pointcloudData.end()) + { + m_pointcloudData.erase(it); + } + } + +} // namespace Pointcloud \ No newline at end of file diff --git a/Gems/Pointcloud/Code/Source/Render/PointcloudFeatureProcessor.h b/Gems/Pointcloud/Code/Source/Render/PointcloudFeatureProcessor.h new file mode 100644 index 00000000..440292f5 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Render/PointcloudFeatureProcessor.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include +namespace Pointcloud +{ + class Scene; + class Shader; + + class PointcloudFeatureProcessor + : public PointcloudFeatureProcessorInterface + , protected AZ::RPI::ViewportContextIdNotificationBus::Handler + , protected AZ::Data::AssetBus::Handler + { + public: + AZ_RTTI(PointcloudFeatureProcessor, "{B6EF8776-F7F9-432B-8BD9-D43869FFFC3D}", PointcloudFeatureProcessorInterface); + AZ_CLASS_ALLOCATOR(PointcloudFeatureProcessor, AZ::SystemAllocator) + + static void Reflect(AZ::ReflectContext* context); + + PointcloudFeatureProcessor() = default; + virtual ~PointcloudFeatureProcessor() = default; + + // PointcloudFeatureProcessorInterface overrides + void SetTransform(const PointcloudHandle& handle, const AZ::Transform& transform) override; + void SetPointSize(const PointcloudHandle& handle, float pointSize) override; + PointcloudHandle AcquirePointcloud(const AZStd::vector& cloudVertexData) override; + void SetVisibility(const PointcloudHandle& handle, bool visible) override; + void ReleasePointcloud(const PointcloudHandle& handle) override; + + protected: + // RPI::SceneNotificationBus overrides + void OnRenderPipelineChanged( + AZ::RPI::RenderPipeline* pipeline, AZ::RPI::SceneNotification::RenderPipelineChangeType changeType) override; + + // Data::AssetBus overrides + void OnAssetReloaded(AZ::Data::Asset asset) override; + void OnAssetReady(AZ::Data::Asset asset) override; + + private: + struct PointcloudData + { + PointcloudHandle m_index = 0; + AZ::Data::Asset m_cloudVertexBufferAsset; + AZ::Data::Instance m_cloudVertexBuffer = nullptr; + + AZStd::array m_meshStreamBufferViews; + AZStd::vector m_pointData; + uint32_t m_vertices = 0; + float m_pointSize = 1.0f; + AZ::Transform m_transform = AZ::Transform::CreateIdentity(); + AZ::RHI::ConstPtr m_drawPacket; + AZ::Data::Instance m_drawSrg = nullptr; + bool m_visible = true; + bool m_needSrgUpdate = true; + }; + + // FeatureProcessor overrides + void Activate() override; + void Deactivate() override; + void Simulate(const FeatureProcessor::SimulatePacket& packet) override; + void Render(const FeatureProcessor::RenderPacket& packet) override; + + void UpdateDrawPacket(); + void UpdateShaderConstants(); + + //! build a draw packet to draw the point cloud + AZ::RHI::ConstPtr BuildDrawPacket( + const AZ::Data::Instance& srg, + const AZ::RPI::Ptr& pipelineState, + const AZ::RHI::DrawListTag& drawListTag, + const AZStd::span& streamBufferViews, + uint32_t vertexCount); + + AZ::RPI::Ptr m_meshPipelineState; + AZ::RHI::DrawListTag m_drawListTag; + AZ::Data::Instance m_shader = nullptr; //!< Shader for the pointcloud + AZ::RHI::ShaderInputNameIndex m_pointSizeIndex = "m_pointSize"; + AZ::RHI::ShaderInputNameIndex m_modelMatrixIndex = "m_modelMatrix"; + AZ::RHI::Ptr m_drawSrgLayout; //!< Shader resource group layout for the draw packet + AZStd::unordered_map m_pointcloudData; //!< Map of pointcloud data + PointcloudHandle m_currentPointcloudDataIndex = 0; //!< Index to the next pointcloud data to be created + }; +} // namespace Pointcloud \ No newline at end of file diff --git a/Gems/Pointcloud/Code/Source/Tools/Builders/PointcloudBuilder.cpp b/Gems/Pointcloud/Code/Source/Tools/Builders/PointcloudBuilder.cpp new file mode 100644 index 00000000..84d3f769 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Tools/Builders/PointcloudBuilder.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root + * of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include "PointcloudBuilder.h" +#include +#include + +#include "3rdParty/happly.h" +#include +#include +#include +#include +#include +namespace Pointcloud +{ + + constexpr const char* PointcloudBuilderJobKey = "PointcloudBuilder"; + + PointcloudBuilder::PointcloudBuilder() + { + AssetBuilderSDK::AssetBuilderDesc pointcloudAssetBuilderDescriptor; + pointcloudAssetBuilderDescriptor.m_name = PointcloudBuilderJobKey; + pointcloudAssetBuilderDescriptor.m_version = 1; + pointcloudAssetBuilderDescriptor.m_busId = azrtti_typeid(); + pointcloudAssetBuilderDescriptor.m_analysisFingerprint = AZStd::string::format( + "%u,%lu,%lu", + PointcloudAsset::PointcloudMagicNumber, + sizeof(PointcloudAsset::CloudHeader), + sizeof(PointcloudAsset::CloudVertex)); + pointcloudAssetBuilderDescriptor.m_patterns.push_back( + AssetBuilderSDK::AssetBuilderPattern("*.ply", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); + + pointcloudAssetBuilderDescriptor.m_createJobFunction = [this](auto&& request, auto&& response) + { + return CreateJobs(request, response); + }; + + pointcloudAssetBuilderDescriptor.m_processJobFunction = [this](auto&& request, auto&& response) + { + return ProcessJob(request, response); + }; + + BusConnect(pointcloudAssetBuilderDescriptor.m_busId); + + // Register this builder with the AssetBuilderSDK. + AssetBuilderSDK::AssetBuilderBus::Broadcast( + &AssetBuilderSDK::AssetBuilderBus::Handler::RegisterBuilderInformation, pointcloudAssetBuilderDescriptor); + } + + PointcloudBuilder::~PointcloudBuilder() + { + BusDisconnect(); + } + + void PointcloudBuilder::CreateJobs( + const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const + { + const auto fullSourcePath = AZ::IO::Path(request.m_watchFolder) / AZ::IO::Path(request.m_sourceFile); + + // Create an output job for each platform + for (const AssetBuilderSDK::PlatformInfo& platformInfo : request.m_enabledPlatforms) + { + AssetBuilderSDK::JobDescriptor jobDescriptor; + jobDescriptor.m_critical = false; + jobDescriptor.m_jobKey = "Pointcloud Asset"; + jobDescriptor.SetPlatformIdentifier(platformInfo.m_identifier.c_str()); + + response.m_createJobOutputs.push_back(jobDescriptor); + } + + response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success; + } + + void PointcloudBuilder::ProcessJob( + const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const + { + response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed; + happly::PLYData plyIn(request.m_fullPath.c_str()); + auto vertices = plyIn.getVertexPositions(); + auto colors = plyIn.getVertexColors(); + const auto elementNames = plyIn.getElementNames(); + + AZ::Data::Asset pointcloudAsset; + pointcloudAsset.Create(AZ::Data::AssetId(AZ::Uuid::CreateRandom())); + pointcloudAsset->m_data.reserve(vertices.size()); + for (int i = 0; i < vertices.size(); i++) + { + const auto& vertex = vertices[i]; + AZ::Color color = AZ::Colors::GreenYellow; + if (i < colors.size()) + { + const auto& colorData = colors[i]; + color = AZ::Color( + static_cast(colorData[0]) / 255.0f, + static_cast(colorData[1]) / 255.0f, + static_cast(colorData[2]) / 255.0f, + 1.0f); + } + PointcloudAsset::CloudVertex cloudVertex; + cloudVertex.m_position = { static_cast(vertex[0]), static_cast(vertex[1]), static_cast(vertex[2]) }; + + cloudVertex.m_color = color.ToU32(); + pointcloudAsset->m_data.push_back(cloudVertex); + } + + if (auto assetHandler = AZ::Data::AssetManager::Instance().GetHandler(azrtti_typeid())) + { + auto tempAssetOutputPath = AZ::IO::Path(request.m_tempDirPath) / request.m_sourceFile; + tempAssetOutputPath.ReplaceExtension(PointcloudAsset::Extension); + AZ::IO::FileIOStream outStream( + tempAssetOutputPath.String().c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); + if (!outStream.IsOpen()) + { + AZ_TracePrintf( + AssetBuilderSDK::ErrorWindow, "Error: Failed job %s because file cannot be created.\n", request.m_fullPath.c_str()); + response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed; + return; + } + if (assetHandler->SaveAssetData(pointcloudAsset, &outStream)) + { + // inform the AssetProcessor that we've created a new asset + AssetBuilderSDK::JobProduct pointcloudJobProduct; + pointcloudJobProduct.m_productFileName = tempAssetOutputPath.String(); + pointcloudJobProduct.m_productSubID = 0; + pointcloudJobProduct.m_productAssetType = azrtti_typeid(); + pointcloudJobProduct.m_dependenciesHandled = true; + response.m_outputProducts.push_back(AZStd::move(pointcloudJobProduct)); + response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success; + return; + } + else + { + AZ_Error("PrefabInfoAssetBuilder", false, "The asset could not be saved to file: %s", tempAssetOutputPath.c_str()); + } + } + response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed; + } + +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Tools/Builders/PointcloudBuilder.h b/Gems/Pointcloud/Code/Source/Tools/Builders/PointcloudBuilder.h new file mode 100644 index 00000000..85ea82df --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Tools/Builders/PointcloudBuilder.h @@ -0,0 +1,28 @@ + +#pragma once + +#include +#include +#include + +namespace Pointcloud +{ + [[maybe_unused]] constexpr const char* PointcloudBuilderName = "PointcloudBuilder"; + + class PointcloudBuilder : public AssetBuilderSDK::AssetBuilderCommandBus::Handler + { + public: + AZ_RTTI(PointcloudBuilder, "{0190bb45-d105-7336-8b3e-d039c647115f}"); + + PointcloudBuilder(); + ~PointcloudBuilder(); + + // AssetBuilderSDK::AssetBuilderCommandBus overrides... + void CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const; + void ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const; + void ShutDown() override + { + } + }; + +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudAssetBuilderSystemComponent.cpp b/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudAssetBuilderSystemComponent.cpp new file mode 100644 index 00000000..e7fb3929 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudAssetBuilderSystemComponent.cpp @@ -0,0 +1,58 @@ +#include "PointcloudAssetBuilderSystemComponent.h" +#include "../Builders/PointcloudBuilder.h" +#include +#include + +namespace Pointcloud +{ + + PointcloudAssetBuilderSystemComponent::PointcloudAssetBuilderSystemComponent() = default; + PointcloudAssetBuilderSystemComponent::~PointcloudAssetBuilderSystemComponent() = default; + + void PointcloudAssetBuilderSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class()->Version(0)->Attribute( + AZ::Edit::Attributes::SystemComponentTags, AssetBuilderSDK::ComponentTags::AssetBuilder); + } + } + + void PointcloudAssetBuilderSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("PointcloudAssetBuilderService")); + } + + void PointcloudAssetBuilderSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("PointcloudAssetBuilderService")); + } + + void PointcloudAssetBuilderSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + // This doesn't require any services to exist before startup. + } + + void PointcloudAssetBuilderSystemComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + // If the asset services exist at all, they should be started first so that + // the Sdf builder can register with them correctly. + dependent.push_back(AZ_CRC_CE("AssetDatabaseService")); + dependent.push_back(AZ_CRC_CE("AssetCatalogService")); + } + + void PointcloudAssetBuilderSystemComponent::Activate() + { + m_pointcloudAssetHandler = aznew PointcloudAssetHandler(); + m_pointcloudAssetHandler->Register(); + m_pointcloudBuilder = AZStd::make_unique(); + } + + void PointcloudAssetBuilderSystemComponent::Deactivate() + { + m_pointcloudAssetHandler->Unregister(); + delete m_pointcloudAssetHandler; + m_pointcloudBuilder.reset(); + } + +} // namespace Pointcloud \ No newline at end of file diff --git a/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudAssetBuilderSystemComponent.h b/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudAssetBuilderSystemComponent.h new file mode 100644 index 00000000..1e481d6d --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudAssetBuilderSystemComponent.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +namespace AZ +{ + class ReflectContext; +} +namespace AZ::Data +{ + class AssetHandler; +} + +namespace Pointcloud +{ + class PointcloudBuilder; + + class PointcloudAssetBuilderSystemComponent : public AZ::Component + { + public: + AZ_COMPONENT(PointcloudAssetBuilderSystemComponent, "{0190bb4b-5e26-7898-b3c9-e9f3a50f1177}"); + static void Reflect(AZ::ReflectContext* context); + + PointcloudAssetBuilderSystemComponent(); + ~PointcloudAssetBuilderSystemComponent(); + + private: + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + // Component overrides ... + void Activate() override; + void Deactivate() override; + + // Asset builder instance + AZStd::unique_ptr m_pointcloudBuilder; + + private: + PointcloudAssetHandler* m_pointcloudAssetHandler; + }; +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudEditorComponent.cpp b/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudEditorComponent.cpp new file mode 100644 index 00000000..992438ec --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudEditorComponent.cpp @@ -0,0 +1,151 @@ +#include "PointcloudEditorComponent.h" +#include "Clients/PointcloudComponent.h" +#include +#include +#include +namespace Pointcloud +{ + + void PointcloudEditorComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("TransformService")); + } + + void PointcloudEditorComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(2) + ->Field("Point Size", &PointcloudEditorComponent::m_pointSize) + ->Field("PointcloudAsset", &PointcloudEditorComponent::m_pointcloudAsset) + ->Field("NumPoints", &PointcloudEditorComponent::m_numPoints); + AZ::EditContext* editContext = serializeContext->GetEditContext(); + if (editContext) + { + editContext->Class("PointcloudEditorComponent", "PointcloudEditorComponent") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "PointcloudEditorComponent") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game")) + ->Attribute(AZ::Edit::Attributes::Category, "RobotecTools") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + + ->DataElement( + AZ::Edit::UIHandlers::Default, + &PointcloudEditorComponent::m_pointSize, + "Point Size", + "Size of the points in the pointcloud") + ->Attribute(AZ::Edit::Attributes::Min, 0.0f) + ->Attribute(AZ::Edit::Attributes::Max, 100.0f) + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PointcloudEditorComponent::OnSetPointSize) + ->DataElement( + AZ::Edit::UIHandlers::Default, + &PointcloudEditorComponent::m_pointcloudAsset, + "Pointcloud Asset", + "Asset containing the pointcloud data") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PointcloudEditorComponent::OnAssetChanged) + ->DataElement( + AZ::Edit::UIHandlers::Default, + &PointcloudEditorComponent::m_numPoints, + "NumPoints", + "Number of points in the pointcloud") + ->Attribute(AZ::Edit::Attributes::ReadOnly, true); + } + } + } + + void PointcloudEditorComponent::Activate() + { + m_scene = AZ::RPI::Scene::GetSceneForEntityId(GetEntityId()); + if (m_scene) + { + m_featureProcessor = m_scene->EnableFeatureProcessor(); + + AZ_Assert(m_featureProcessor, "Failed to enable PointcloudFeatureProcessorInterface."); + } + AZ::SystemTickBus::QueueFunction( + [this]() + { + if (m_scene && m_pointcloudAsset) + { + m_featureProcessor = m_scene->EnableFeatureProcessor(); + AZ_Assert(m_featureProcessor, "Failed to enable PointcloudFeatureProcessorInterface."); + m_pointcloudAsset.QueueLoad(); + m_pointcloudAsset.BlockUntilLoadComplete(); + m_numPoints = m_pointcloudAsset->m_data.size(); + AZStd::vector> cloudVertexDataChunks; + + m_pointcloudHandle = m_featureProcessor->AcquirePointcloud(m_pointcloudAsset->m_data); + + if (m_pointcloudHandle != PointcloudFeatureProcessorInterface::InvalidPointcloudHandle) + { + m_featureProcessor->SetTransform(m_pointcloudHandle, m_entity->GetTransform()->GetWorldTM()); + m_featureProcessor->SetPointSize(m_pointcloudHandle, m_pointSize); + } + } + }); + AzToolsFramework::EditorEntityInfoNotificationBus::Handler::BusConnect(); + AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId()); + } + + void PointcloudEditorComponent::Deactivate() + { + AZ::TransformNotificationBus::Handler::BusDisconnect(); + AzToolsFramework::EditorEntityInfoNotificationBus::Handler::BusDisconnect(); + m_featureProcessor->ReleasePointcloud(m_pointcloudHandle); + } + + void PointcloudEditorComponent::BuildGameEntity([[maybe_unused]] AZ::Entity* gameEntity) + { + gameEntity->CreateComponent(m_pointcloudAsset, m_pointSize); + } + + AZ::Crc32 PointcloudEditorComponent::OnSetPointSize() + { + if (m_featureProcessor) + { + m_featureProcessor->SetPointSize(m_pointcloudHandle, m_pointSize); + } + return AZ::Edit::PropertyRefreshLevels::None; + } + AZ::Crc32 PointcloudEditorComponent::OnAssetChanged() + { + if (m_featureProcessor) + { + m_featureProcessor->ReleasePointcloud(m_pointcloudHandle); + if (m_pointcloudAsset.GetId().IsValid()) + { + m_pointcloudAsset.QueueLoad(); + m_pointcloudAsset.BlockUntilLoadComplete(); + if (m_pointcloudAsset.IsReady()) + { + m_pointcloudHandle = m_featureProcessor->AcquirePointcloud(m_pointcloudAsset->m_data); + m_featureProcessor->SetTransform(m_pointcloudHandle, m_entity->GetTransform()->GetWorldTM()); + m_featureProcessor->SetPointSize(m_pointcloudHandle, m_pointSize); + m_numPoints = m_pointcloudAsset->m_data.size(); + } + } + else + { + m_numPoints = 0; + } + } + return AZ::Edit::PropertyRefreshLevels::EntireTree; + } + + void PointcloudEditorComponent::OnEntityInfoUpdatedVisibility(AZ::EntityId entityId, bool visible) + { + if (entityId == GetEntityId()) + { + m_featureProcessor->SetVisibility(m_pointcloudHandle, visible); + } + } + + void PointcloudEditorComponent::OnTransformChanged([[maybe_unused]] const AZ::Transform& local, const AZ::Transform& world) + { + if (m_featureProcessor) + { + m_featureProcessor->SetTransform(m_pointcloudHandle, world); + } + } +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudEditorComponent.h b/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudEditorComponent.h new file mode 100644 index 00000000..68a92e60 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Tools/Components/PointcloudEditorComponent.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Pointcloud +{ + + class PointcloudEditorComponent + : public AzToolsFramework::Components::EditorComponentBase + , private AZ::TransformNotificationBus::Handler + , private AzToolsFramework::EditorEntityInfoNotificationBus::Handler + { + public: + AZ_EDITOR_COMPONENT(PointcloudEditorComponent, "{018fba15-560f-78cb-afb4-cf4d00cefc17}"); + PointcloudEditorComponent() = default; + ~PointcloudEditorComponent() = default; + + static void Reflect(AZ::ReflectContext* context); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + + // EditorComponentBase interface overrides ... + void Activate() override; + void Deactivate() override; + void BuildGameEntity(AZ::Entity* gameEntity) override; + + private: + // AZ::TransformNotificationBus::Handler overrides ... + void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override; + + // AzToolsFramework::EditorEntityInfoNotificationBus overrides ... + void OnEntityInfoUpdatedVisibility(AZ::EntityId entityId, bool visible) override; + + AZ::Crc32 OnSetPointSize(); + AZ::Crc32 OnAssetChanged(); + + float m_pointSize = 1.0f; + uint32_t m_numPoints = 0; + PointcloudFeatureProcessorInterface* m_featureProcessor = nullptr; + AZ::RPI::Scene* m_scene = nullptr; + PointcloudFeatureProcessorInterface::PointcloudHandle m_pointcloudHandle = + PointcloudFeatureProcessorInterface::InvalidPointcloudHandle; + AZ::Data::Asset m_pointcloudAsset; + }; +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Tools/PointcloudEditorModule.cpp b/Gems/Pointcloud/Code/Source/Tools/PointcloudEditorModule.cpp new file mode 100644 index 00000000..37f1421d --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Tools/PointcloudEditorModule.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include "Components/PointcloudEditorComponent.h" +#include "PointcloudEditorSystemComponent.h" +#include "Tools/Components/PointcloudAssetBuilderSystemComponent.h" +#include +#include +namespace Pointcloud +{ + class PointcloudEditorModule : public PointcloudModuleInterface + { + public: + AZ_RTTI(PointcloudEditorModule, PointcloudEditorModuleTypeId, PointcloudModuleInterface); + AZ_CLASS_ALLOCATOR(PointcloudEditorModule, AZ::SystemAllocator); + + PointcloudEditorModule() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + // Add ALL components descriptors associated with this gem to m_descriptors. + // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and + // EditContext. This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert( + m_descriptors.end(), + { PointcloudEditorSystemComponent::CreateDescriptor(), + PointcloudEditorComponent::CreateDescriptor(), + PointcloudAssetBuilderSystemComponent::CreateDescriptor() }); + } + + /** + * Add required SystemComponents to the SystemEntity. + * Non-SystemComponents should not be added here + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + }; + } + }; +} // namespace Pointcloud + +AZ_DECLARE_MODULE_CLASS(Gem_Pointcloud, Pointcloud::PointcloudEditorModule) diff --git a/Gems/Pointcloud/Code/Source/Tools/PointcloudEditorSystemComponent.cpp b/Gems/Pointcloud/Code/Source/Tools/PointcloudEditorSystemComponent.cpp new file mode 100644 index 00000000..38ab4118 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Tools/PointcloudEditorSystemComponent.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include "PointcloudEditorSystemComponent.h" +#include + +#include + +namespace Pointcloud +{ + AZ_COMPONENT_IMPL( + PointcloudEditorSystemComponent, "PointcloudEditorSystemComponent", PointcloudEditorSystemComponentTypeId, BaseSystemComponent); + + void PointcloudEditorSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class()->Version(0); + } + } + + PointcloudEditorSystemComponent::PointcloudEditorSystemComponent() = default; + + PointcloudEditorSystemComponent::~PointcloudEditorSystemComponent() = default; + + void PointcloudEditorSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + BaseSystemComponent::GetProvidedServices(provided); + provided.push_back(AZ_CRC_CE("PointcloudSystemEditorService")); + } + + void PointcloudEditorSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + BaseSystemComponent::GetIncompatibleServices(incompatible); + incompatible.push_back(AZ_CRC_CE("PointcloudSystemEditorService")); + } + + void PointcloudEditorSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + BaseSystemComponent::GetRequiredServices(required); + } + + void PointcloudEditorSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + BaseSystemComponent::GetDependentServices(dependent); + } + + void PointcloudEditorSystemComponent::Activate() + { + PointcloudSystemComponent::Activate(); + AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); + } + + void PointcloudEditorSystemComponent::Deactivate() + { + AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); + PointcloudSystemComponent::Deactivate(); + } + +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Source/Tools/PointcloudEditorSystemComponent.h b/Gems/Pointcloud/Code/Source/Tools/PointcloudEditorSystemComponent.h new file mode 100644 index 00000000..35b0d9f5 --- /dev/null +++ b/Gems/Pointcloud/Code/Source/Tools/PointcloudEditorSystemComponent.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +#include + +namespace Pointcloud +{ + /// System component for Pointcloud editor + class PointcloudEditorSystemComponent + : public PointcloudSystemComponent + , protected AzToolsFramework::EditorEvents::Bus::Handler + { + using BaseSystemComponent = PointcloudSystemComponent; + public: + AZ_COMPONENT_DECL(PointcloudEditorSystemComponent); + + static void Reflect(AZ::ReflectContext* context); + + PointcloudEditorSystemComponent(); + ~PointcloudEditorSystemComponent(); + + private: + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + // AZ::Component + void Activate() override; + void Deactivate() override; + }; +} // namespace Pointcloud diff --git a/Gems/Pointcloud/Code/Tests/Tools/PointcloudEditorTest.cpp b/Gems/Pointcloud/Code/Tests/Tools/PointcloudEditorTest.cpp new file mode 100644 index 00000000..e264029b --- /dev/null +++ b/Gems/Pointcloud/Code/Tests/Tools/PointcloudEditorTest.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include +#include +#include +#include +namespace UnitTest +{ + class PointcloudAssetTest : public LeakDetectionFixture + { + void SetUp() override + { + LeakDetectionFixture::SetUp(); + AZ::Data::AssetManager::Descriptor descriptor; + AZ::Data::AssetManager::Create(descriptor); + m_testHandler = AZStd::make_unique(); + AZ::Data::AssetManager::Instance().RegisterHandler(m_testHandler.get(), AZ::AzTypeInfo::Uuid()); + } + + void TearDown() override + { + AZ::Data::AssetManager::Instance().DispatchEvents(); + AZ::Data::AssetManager::Instance().UnregisterHandler(m_testHandler.get()); + AZ::Data::AssetManager::Destroy(); + + m_testHandler.reset(); + + LeakDetectionFixture::TearDown(); + } + + protected: + AZStd::unique_ptr m_testHandler; + }; + + //! Test the ability to write and read a product pointcloud asset. + TEST_F(PointcloudAssetTest, WritingAndLoadingAsset) + { + AZ::Data::Asset testAsset = + AZ::Data::AssetManager::Instance().CreateAsset(AZ::Data::AssetId(AZ::Uuid::CreateRandom())); + Pointcloud::PointcloudAsset& gold = *testAsset.Get(); + + gold.m_data.resize(3); + + gold.m_data[0].m_position = { 1, 2, 3 }; + gold.m_data[0].m_color = AZ::Colors::Red.ToU32(); + gold.m_data[1].m_position = { 4, 5, 6 }; + gold.m_data[1].m_color = AZ::Colors::Green.ToU32(); + gold.m_data[2].m_position = { 7, 8, 9 }; + gold.m_data[2].m_color = AZ::Colors::Blue.ToU32(); + + AZStd::vector buffer(1024); + AZ::IO::MemoryStream stream(buffer.data(), buffer.size(), buffer.size()); + m_testHandler->SaveAssetData(testAsset, &stream); + + auto assetStream = AZStd::make_shared(); + assetStream->Open(buffer); + AZ::Data::Asset loadedAsset = + AZ::Data::AssetManager::Instance().CreateAsset(AZ::Data::AssetId(AZ::Uuid::CreateRandom())); + m_testHandler->LoadAssetData(loadedAsset, assetStream, nullptr); + + Pointcloud::PointcloudAsset& loaded = *loadedAsset.Get(); + ASSERT_EQ(loaded.m_data.size(), 3); + ASSERT_EQ(loaded.m_data[0].m_position, gold.m_data[0].m_position); + ASSERT_EQ(loaded.m_data[0].m_color, gold.m_data[0].m_color); + ASSERT_EQ(loaded.m_data[1].m_position, gold.m_data[1].m_position); + ASSERT_EQ(loaded.m_data[1].m_color, gold.m_data[1].m_color); + ASSERT_EQ(loaded.m_data[2].m_position, gold.m_data[2].m_position); + ASSERT_EQ(loaded.m_data[2].m_color, gold.m_data[2].m_color); + } + AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); +} // namespace UnitTest diff --git a/Gems/Pointcloud/Code/pointcloud_api_files.cmake b/Gems/Pointcloud/Code/pointcloud_api_files.cmake new file mode 100644 index 00000000..b04d20f8 --- /dev/null +++ b/Gems/Pointcloud/Code/pointcloud_api_files.cmake @@ -0,0 +1,12 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Include/Pointcloud/PointcloudTypeIds.h + Include/Pointcloud/PointcloudFeatureProcessorInterface.h +) diff --git a/Gems/Pointcloud/Code/pointcloud_editor_api_files.cmake b/Gems/Pointcloud/Code/pointcloud_editor_api_files.cmake new file mode 100644 index 00000000..92b518e6 --- /dev/null +++ b/Gems/Pointcloud/Code/pointcloud_editor_api_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Include/Pointcloud/PointcloudAsset.h +) diff --git a/Gems/Pointcloud/Code/pointcloud_editor_private_files.cmake b/Gems/Pointcloud/Code/pointcloud_editor_private_files.cmake new file mode 100644 index 00000000..29c3cb65 --- /dev/null +++ b/Gems/Pointcloud/Code/pointcloud_editor_private_files.cmake @@ -0,0 +1,18 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/Tools/PointcloudEditorSystemComponent.cpp + Source/Tools/PointcloudEditorSystemComponent.h + Source/Tools/Components/PointcloudEditorComponent.cpp + Source/Tools/Components/PointcloudEditorComponent.h + Source/Tools/Components/PointcloudAssetBuilderSystemComponent.cpp + Source/Tools/Components/PointcloudAssetBuilderSystemComponent.h + Source/Tools/Builders/PointcloudBuilder.cpp + Source/Tools/Builders/PointcloudBuilder.h +) diff --git a/Gems/Pointcloud/Code/pointcloud_editor_shared_files.cmake b/Gems/Pointcloud/Code/pointcloud_editor_shared_files.cmake new file mode 100644 index 00000000..79d40f2a --- /dev/null +++ b/Gems/Pointcloud/Code/pointcloud_editor_shared_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/Tools/PointcloudEditorModule.cpp +) diff --git a/Gems/Pointcloud/Code/pointcloud_editor_tests_files.cmake b/Gems/Pointcloud/Code/pointcloud_editor_tests_files.cmake new file mode 100644 index 00000000..e24341c4 --- /dev/null +++ b/Gems/Pointcloud/Code/pointcloud_editor_tests_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Tests/Tools/PointcloudEditorTest.cpp +) diff --git a/Gems/Pointcloud/Code/pointcloud_private_files.cmake b/Gems/Pointcloud/Code/pointcloud_private_files.cmake new file mode 100644 index 00000000..56ca2ab6 --- /dev/null +++ b/Gems/Pointcloud/Code/pointcloud_private_files.cmake @@ -0,0 +1,19 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/PointcloudModuleInterface.cpp + Source/PointcloudModuleInterface.h + Source/Clients/PointcloudSystemComponent.cpp + Source/Clients/PointcloudSystemComponent.h + Source/Render/PointcloudFeatureProcessor.h + Source/Render/PointcloudFeatureProcessor.cpp + Source/Clients/PointcloudAsset.cpp + Source/Clients/PointcloudComponent.cpp + Source/Clients/PointcloudComponent.h +) diff --git a/Gems/Pointcloud/Code/pointcloud_shared_files.cmake b/Gems/Pointcloud/Code/pointcloud_shared_files.cmake new file mode 100644 index 00000000..c0dd7ce9 --- /dev/null +++ b/Gems/Pointcloud/Code/pointcloud_shared_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Source/Clients/PointcloudModule.cpp +) diff --git a/Gems/Pointcloud/Code/pointcloud_tests_files.cmake b/Gems/Pointcloud/Code/pointcloud_tests_files.cmake new file mode 100644 index 00000000..ef5e96b5 --- /dev/null +++ b/Gems/Pointcloud/Code/pointcloud_tests_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Tests/Clients/PointcloudTest.cpp +) diff --git a/Gems/Pointcloud/gem.json b/Gems/Pointcloud/gem.json new file mode 100644 index 00000000..160d7901 --- /dev/null +++ b/Gems/Pointcloud/gem.json @@ -0,0 +1,31 @@ +{ + "gem_name": "Pointcloud", + "version": "1.0.0", + "display_name": "Pointcloud", + "license": "License used i.e. Apache-2.0 or MIT", + "license_url": "Link to the license web site i.e. https://opensource.org/licenses/Apache-2.0", + "origin": "The name of the originator or creator", + "origin_url": "The website for this Gem", + "type": "Code", + "summary": "A short description of this Gem", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "Pointcloud" + ], + "platforms": [ + "" + ], + "icon_path": "preview.png", + "requirements": "Notice of any requirements for this Gem i.e. This requires X other gem", + "documentation_url": "Link to any documentation of your Gem", + "dependencies": [ + "Atom_RPI", + "Atom" + ], + "repo_uri": "", + "compatible_engines": [], + "engine_api_dependencies": [], + "restricted": "Pointcloud" +} diff --git a/Gems/Pointcloud/preview.png b/Gems/Pointcloud/preview.png new file mode 100644 index 00000000..83afae48 Binary files /dev/null and b/Gems/Pointcloud/preview.png differ