From 699e4e219526112c13b4bd6b14ab0e5329cc3962 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 20 Jan 2023 17:37:41 -0500 Subject: [PATCH 01/20] Begin implementing PntsToGltfConverter --- .../src/PntsToGltfConverter.cpp | 731 ++++++++++++++++++ .../src/PntsToGltfConverter.h | 17 + .../src/registerAllTileContentTypes.cpp | 2 + 3 files changed, 750 insertions(+) create mode 100644 Cesium3DTilesSelection/src/PntsToGltfConverter.cpp create mode 100644 Cesium3DTilesSelection/src/PntsToGltfConverter.h diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp new file mode 100644 index 000000000..299944d92 --- /dev/null +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -0,0 +1,731 @@ +#include "PntsToGltfConverter.h" + +#include "BatchTableToGltfFeatureMetadata.h" +#include "CesiumGeometry/AxisTransforms.h" +#include "draco/attributes/point_attribute.h" +#include "draco/compression/decode.h" +#include "draco/core/decoder_buffer.h" +#include "draco/point_cloud/point_cloud.h" + +#include + +#include +#include + +#include + +namespace Cesium3DTilesSelection { +namespace { +struct PntsHeader { + unsigned char magic[4]; + uint32_t version; + uint32_t byteLength; + uint32_t featureTableJsonByteLength; + uint32_t featureTableBinaryByteLength; + uint32_t batchTableJsonByteLength; + uint32_t batchTableBinaryByteLength; +}; + +void parsePntsHeader( + const gsl::span& pntsBinary, + PntsHeader& header, + uint32_t& headerLength, + GltfConverterResult& result) { + if (pntsBinary.size() < sizeof(PntsHeader)) { + result.errors.emplaceError("The PNTS is invalid because it is too small to " + "include a PNTS header."); + return; + } + + const PntsHeader* pHeader = + reinterpret_cast(pntsBinary.data()); + + header = *pHeader; + headerLength = sizeof(PntsHeader); + + if (pHeader->version != 1) { + result.errors.emplaceError(fmt::format( + "The PNTS file is version {}, which is unsupported.", + pHeader->version)); + return; + } + + if (static_cast(pntsBinary.size()) < pHeader->byteLength) { + result.errors.emplaceError( + "The PNTS is invalid because the total data available is less than the " + "size specified in its header."); + return; + } +} + +// The only semantic that can have a variable component type is the +// BATCH_ID semantic. For parsing purposes, all other semantics are +// assigned component type NONE. +enum ComponentType { NONE, BYTE, UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT }; + +struct PntsSemantic { + bool existsInFeatureTable = false; + uint32_t byteOffset = 0; + ComponentType componentType = ComponentType::NONE; + bool hasDraco = false; + uint32_t dracoId = 0; + draco::PointAttribute* pAttribute = nullptr; +}; + +struct PntsContent { + uint32_t pointsLength = 0; + glm::dvec3 rtcCenter; + glm::dvec3 quantizedVolumeOffset; + glm::dvec3 quantizedVolumeScale; + glm::u8vec4 constantRgba; + uint32_t batchLength = 0; + + PntsSemantic position; + PntsSemantic positionQuantized; + PntsSemantic rgba; + PntsSemantic rgb; + PntsSemantic rgb565; + PntsSemantic normal; + PntsSemantic normalOct16p; + PntsSemantic batchId; + + bool hasDraco = false; + uint32_t dracoByteOffset = 0; + uint32_t dracoByteLength = 0; + + Cesium3DTilesSelection::ErrorList errors; +}; + +void parseDracoExtension( + const rapidjson::Value& dracoExtensionValue, + PntsContent& parsedContent) { + const auto propertiesIt = dracoExtensionValue.FindMember("properties"); + if (propertiesIt == dracoExtensionValue.MemberEnd() || + !propertiesIt->value.IsObject()) { + parsedContent.errors.emplaceError( + "Error parsing Draco compression extension, " + "no valid properties object found."); + return; + } + + const auto byteOffsetIt = dracoExtensionValue.FindMember("byteOffset"); + if (byteOffsetIt == dracoExtensionValue.MemberEnd() || + !byteOffsetIt->value.IsUint64()) { + parsedContent.errors.emplaceError( + "Error parsing Draco compression extension, " + "no valid byteOffset found."); + return; + } + + const auto byteLengthIt = dracoExtensionValue.FindMember("byteLength"); + if (byteLengthIt == dracoExtensionValue.MemberEnd() || + !byteLengthIt->value.IsUint64()) { + parsedContent.errors.emplaceError( + "Error parsing Draco compression extension, " + "no valid byteLength found."); + return; + } + + parsedContent.hasDraco = true; + parsedContent.dracoByteOffset = byteOffsetIt->value.GetUint(); + parsedContent.dracoByteLength = byteLengthIt->value.GetUint(); + + const rapidjson::Value& dracoPropertiesValue = propertiesIt->value; + + auto positionDracoIdIt = dracoPropertiesValue.FindMember("POSITION"); + if (positionDracoIdIt != dracoPropertiesValue.MemberEnd() && + positionDracoIdIt->value.IsInt()) { + parsedContent.position.hasDraco = true; + parsedContent.position.dracoId = positionDracoIdIt->value.GetInt(); + } + + auto rgbaDracoIdIt = dracoPropertiesValue.FindMember("RGBA"); + if (rgbaDracoIdIt != dracoPropertiesValue.MemberEnd() && + rgbaDracoIdIt->value.IsInt()) { + parsedContent.rgba.hasDraco = true; + parsedContent.rgba.dracoId = rgbaDracoIdIt->value.GetInt(); + } + + auto rgbDracoIdIt = dracoPropertiesValue.FindMember("RGB"); + if (rgbDracoIdIt != dracoPropertiesValue.MemberEnd() && + rgbDracoIdIt->value.IsInt()) { + parsedContent.rgb.hasDraco = true; + parsedContent.rgb.dracoId = rgbDracoIdIt->value.GetInt(); + } + + auto normalDracoIdIt = dracoPropertiesValue.FindMember("NORMAL"); + if (normalDracoIdIt != dracoPropertiesValue.MemberEnd() && + normalDracoIdIt->value.IsInt()) { + parsedContent.normal.dracoId = normalDracoIdIt->value.GetInt(); + } + + auto batchIdDracoIdIt = dracoPropertiesValue.FindMember("BATCH_ID"); + if (batchIdDracoIdIt != dracoPropertiesValue.MemberEnd() && + batchIdDracoIdIt->value.IsInt()) { + parsedContent.batchId.dracoId = batchIdDracoIdIt->value.GetInt(); + } + + return; +} + +bool validateJsonArrayValues( + const rapidjson::Value& arrayValue, + uint32_t expectedLength, + std::function validate) { + if (!arrayValue.IsArray()) { + return false; + } + + if (arrayValue.Size() != expectedLength) { + return false; + } + + for (rapidjson::SizeType i = 0; i < expectedLength; i++) { + if (!validate(arrayValue[i])) { + return false; + } + } + + return true; +} + +void parsePositions( + const rapidjson::Document& featureTableJson, + PntsContent& parsedContent) { + const auto positionIt = featureTableJson.FindMember("POSITION"); + if (positionIt != featureTableJson.MemberEnd() && + positionIt->value.IsObject()) { + const auto byteOffsetIt = positionIt->value.FindMember("byteOffset"); + if (byteOffsetIt == positionIt->value.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, POSITION semantic does not have " + "valid byteOffset."); + return; + } + + parsedContent.position.existsInFeatureTable = true; + parsedContent.position.byteOffset = byteOffsetIt->value.GetUint(); + + return; + } + + const auto positionQuantizedIt = + featureTableJson.FindMember("POSITION_QUANTIZED"); + if (positionQuantizedIt != featureTableJson.MemberEnd() && + positionQuantizedIt->value.IsObject()) { + auto quantizedVolumeOffsetIt = + featureTableJson.FindMember("QUANTIZED_VOLUME_OFFSET"); + auto quantizedVolumeScaleIt = + featureTableJson.FindMember("QUANTIZED_VOLUME_SCALE"); + + auto isDouble = [](const rapidjson::Value& value) -> bool { + return value.IsDouble(); + }; + + if (quantizedVolumeOffsetIt == featureTableJson.MemberEnd() || + !validateJsonArrayValues(quantizedVolumeOffsetIt->value, 3, isDouble)) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, POSITION_QUANTIZED is used but " + "no valid QUANTIZED_VOLUME_OFFSET semantic was found."); + return; + } + + if (quantizedVolumeScaleIt == featureTableJson.MemberEnd() || + !validateJsonArrayValues(quantizedVolumeScaleIt->value, 3, isDouble)) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, POSITION_QUANTIZED is used but " + "no valid QUANTIZED_VOLUME_SCALE semantic was found."); + return; + } + + const auto byteOffsetIt = + positionQuantizedIt->value.FindMember("byteOffset"); + if (byteOffsetIt == positionQuantizedIt->value.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, POSITION_QUANTIZED semantic does " + "not have valid byteOffset."); + return; + } + + parsedContent.positionQuantized.existsInFeatureTable = true; + parsedContent.positionQuantized.byteOffset = byteOffsetIt->value.GetUint(); + + auto quantizedVolumeOffset = quantizedVolumeOffsetIt->value.GetArray(); + auto quantizedVolumeScale = quantizedVolumeScaleIt->value.GetArray(); + + parsedContent.quantizedVolumeOffset = glm::dvec3( + quantizedVolumeOffset[0].GetDouble(), + quantizedVolumeOffset[1].GetDouble(), + quantizedVolumeOffset[2].GetDouble()); + + parsedContent.quantizedVolumeScale = glm::dvec3( + quantizedVolumeScale[0].GetDouble(), + quantizedVolumeScale[1].GetDouble(), + quantizedVolumeScale[2].GetDouble()); + + return; + } + + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, no POSITION semantic was found. " + "One of POSITION or POSITION_QUANTIZED must be defined."); + + return; +} + +void parseColors( + const rapidjson::Document& featureTableJson, + PntsContent& parsedContent) { + const auto rgbaIt = featureTableJson.FindMember("RGBA"); + if (rgbaIt != featureTableJson.MemberEnd() && rgbaIt->value.IsObject()) { + const auto byteOffsetIt = rgbaIt->value.FindMember("byteOffset"); + if (byteOffsetIt == rgbaIt->value.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, RGBA semantic does not have valid " + "byteOffset."); + return; + } + + parsedContent.rgba.existsInFeatureTable = true; + parsedContent.rgba.byteOffset = byteOffsetIt->value.GetUint(); + + return; + } + + const auto rgbIt = featureTableJson.FindMember("RGB"); + if (rgbIt != featureTableJson.MemberEnd() && rgbIt->value.IsObject()) { + const auto byteOffsetIt = rgbIt->value.FindMember("byteOffset"); + if (byteOffsetIt == rgbIt->value.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, RGB semantic does not have valid " + "byteOffset."); + return; + } + + parsedContent.rgb.existsInFeatureTable = true; + parsedContent.rgb.byteOffset = byteOffsetIt->value.GetUint(); + + return; + } + + const auto rgb565It = featureTableJson.FindMember("RGB565"); + if (rgb565It != featureTableJson.MemberEnd() && rgb565It->value.IsObject()) { + const auto byteOffsetIt = rgb565It->value.FindMember("byteOffset"); + if (byteOffsetIt == rgb565It->value.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, RGB565 semantic does not have " + "valid byteOffset."); + return; + } + + parsedContent.rgb565.existsInFeatureTable = true; + parsedContent.rgb565.byteOffset = byteOffsetIt->value.GetUint(); + + return; + } + + auto isUint = [](const rapidjson::Value& value) -> bool { + return value.IsUint(); + }; + + const auto constantRgbaIt = featureTableJson.FindMember("CONSTANT_RGBA"); + if (constantRgbaIt != featureTableJson.MemberEnd() && + validateJsonArrayValues(constantRgbaIt->value, 4, isUint)) { + const rapidjson::Value& arrayValue = constantRgbaIt->value; + parsedContent.constantRgba = glm::u8vec4( + arrayValue[0].GetUint(), + arrayValue[1].GetUint(), + arrayValue[2].GetUint(), + arrayValue[3].GetUint()); + } +} + +void parseNormals( + const rapidjson::Document& featureTableJson, + PntsContent& parsedContent) { + const auto normalIt = featureTableJson.FindMember("NORMAL"); + if (normalIt != featureTableJson.MemberEnd() && normalIt->value.IsObject()) { + const auto byteOffsetIt = normalIt->value.FindMember("byteOffset"); + if (byteOffsetIt == normalIt->value.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, NORMAL semantic does not have " + "valid byteOffset."); + return; + } + + parsedContent.normal.existsInFeatureTable = true; + parsedContent.normal.byteOffset = byteOffsetIt->value.GetUint(); + + return; + } + + const auto normalOct16pIt = featureTableJson.FindMember("NORMAL_OCT16P"); + if (normalOct16pIt != featureTableJson.MemberEnd() && + normalOct16pIt->value.IsObject()) { + const auto byteOffsetIt = normalOct16pIt->value.FindMember("byteOffset"); + if (byteOffsetIt == normalOct16pIt->value.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceError("Error parsing PNTS feature table, " + "NORMAL_OCT16P semantic does not have " + "valid byteOffset."); + return; + } + + parsedContent.normalOct16p.existsInFeatureTable = true; + parsedContent.normalOct16p.byteOffset = byteOffsetIt->value.GetUint(); + } +} + +void parseBatchIds( + const rapidjson::Document& featureTableJson, + PntsContent& parsedContent) { + const auto batchIdIt = featureTableJson.FindMember("BATCH_ID"); + if (batchIdIt != featureTableJson.MemberEnd() && + batchIdIt->value.IsObject()) { + const auto byteOffsetIt = batchIdIt->value.FindMember("byteOffset"); + if (byteOffsetIt == batchIdIt->value.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, BATCH_ID semantic does not have " + "valid byteOffset."); + return; + } + + parsedContent.batchId.existsInFeatureTable = true; + parsedContent.batchId.byteOffset = byteOffsetIt->value.GetUint(); + + const auto componentTypeIt = batchIdIt->value.FindMember("componentType"); + if (componentTypeIt != featureTableJson.MemberEnd() && + componentTypeIt->value.IsString()) { + const std::string& componentTypeString = + componentTypeIt->value.GetString(); + + if (componentTypeString == "UNSIGNED_BYTE") { + parsedContent.batchId.componentType = ComponentType::UNSIGNED_BYTE; + } else if (componentTypeString == "UNSIGNED_INT") { + parsedContent.batchId.componentType = ComponentType::UNSIGNED_INT; + } else { + parsedContent.batchId.componentType = ComponentType::UNSIGNED_SHORT; + } + } else { + parsedContent.batchId.componentType = ComponentType::UNSIGNED_SHORT; + } + } + + const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); + if (batchLengthIt != featureTableJson.MemberEnd() && + batchLengthIt->value.IsUint()) { + parsedContent.batchLength = batchLengthIt->value.GetUint(); + } else if (parsedContent.batchId.existsInFeatureTable) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, BATCH_ID semantic is present but " + "no valid BATCH_LENGTH was found."); + } +} + +void parseSemanticsFromFeatureTableJson( + const rapidjson::Document& featureTableJson, + PntsContent& parsedContent) { + parsePositions(featureTableJson, parsedContent); + parseColors(featureTableJson, parsedContent); + parseNormals(featureTableJson, parsedContent); + parseBatchIds(featureTableJson, parsedContent); +} + +void decodeDraco( + const gsl::span& featureTableBinaryData, + PntsContent& parsedContent) { + if (!parsedContent.hasDraco) { + return; + } + + draco::Decoder decoder; + draco::DecoderBuffer buffer; + buffer.Init( + (char*)featureTableBinaryData.data() + parsedContent.dracoByteOffset, + parsedContent.dracoByteLength); + + draco::StatusOr> dracoResult = + decoder.DecodePointCloudFromBuffer(&buffer); + + if (!dracoResult.ok()) { + parsedContent.errors.emplaceError("Error decoding Draco point cloud."); + return; + } + + const std::unique_ptr& pPointCloud = dracoResult.value(); + + if (parsedContent.position.hasDraco) { + draco::PointAttribute* pPositionAttribute = + pPointCloud->attribute(parsedContent.position.dracoId); + if (!pPositionAttribute || + pPositionAttribute->data_type() != draco::DT_FLOAT32 || + pPositionAttribute->num_components() != 3) { + parsedContent.errors.emplaceError( + "Error with decoded Draco point cloud, no valid position attribute."); + return; + } + + parsedContent.position.pAttribute = pPositionAttribute; + } + + // TODO: check for other semantics and metadata +} + +int32_t +createBufferInGltf(CesiumGltf::Model& gltf, std::vector buffer) { + size_t bufferId = gltf.buffers.size(); + CesiumGltf::Buffer& gltfBuffer = gltf.buffers.emplace_back(); + gltfBuffer.byteLength = static_cast(buffer.size()); + gltfBuffer.cesium.data = std::move(buffer); + + return static_cast(bufferId); +} + +int32_t createBufferViewInGltf( + CesiumGltf::Model& gltf, + int32_t bufferId, + int64_t byteLength, + int64_t byteStride) { + size_t bufferViewId = gltf.bufferViews.size(); + CesiumGltf::BufferView& bufferView = gltf.bufferViews.emplace_back(); + bufferView.buffer = bufferId; + bufferView.byteLength = byteLength; + bufferView.byteOffset = 0; + bufferView.byteStride = byteStride; + bufferView.target = CesiumGltf::BufferView::Target::ARRAY_BUFFER; + + return static_cast(bufferViewId); +} + +int32_t createAccessorInGltf( + CesiumGltf::Model& gltf, + int32_t bufferViewId, + int32_t componentType, + int64_t count, + const std::string type) { + size_t accessorId = gltf.accessors.size(); + CesiumGltf::Accessor& accessor = gltf.accessors.emplace_back(); + accessor.bufferView = bufferViewId; + accessor.byteOffset = 0; + accessor.componentType = componentType; + accessor.count = count; + accessor.type = type; + + return static_cast(accessorId); +} + +void createGltfFromFeatureTableData( + const PntsContent& parsedContent, + const gsl::span& featureTableBinaryData, + GltfConverterResult& result) { + uint32_t pointsLength = parsedContent.pointsLength; + + size_t positionsByteStride = sizeof(glm::vec3); + size_t positionsByteLength = pointsLength * positionsByteStride; + + std::vector outPositionsBuffer(positionsByteLength); + gsl::span outPositions( + reinterpret_cast(outPositionsBuffer.data()), + pointsLength); + + if (parsedContent.position.existsInFeatureTable) { + // TODO: check for Draco first + std::memcpy( + outPositionsBuffer.data(), + featureTableBinaryData.data() + parsedContent.position.byteOffset, + positionsByteLength); + } else { + assert(parsedContent.positionQuantized.existsInFeatureTable); + gsl::span quantizedPositions( + reinterpret_cast( + featureTableBinaryData.data() + + parsedContent.positionQuantized.byteOffset), + pointsLength); + + for (size_t i = 0; i < pointsLength; i++) { + const glm::vec3 quantizedPosition( + quantizedPositions[i].x, + quantizedPositions[i].y, + quantizedPositions[i].z); + + outPositions[i] = quantizedPosition * + glm::vec3(parsedContent.quantizedVolumeScale) / + 65535.0f + + glm::vec3(parsedContent.quantizedVolumeOffset); + } + } + + result.model = std::make_optional(); + CesiumGltf::Model& gltf = result.model.value(); + + int32_t positionsBufferId = + createBufferInGltf(gltf, std::move(outPositionsBuffer)); + int32_t positionsBufferViewId = createBufferViewInGltf( + gltf, + positionsBufferId, + static_cast(positionsByteLength), + static_cast(positionsByteStride)); + + int32_t positionAccessorId = createAccessorInGltf( + gltf, + positionsBufferViewId, + CesiumGltf::Accessor::ComponentType::FLOAT, + int64_t(pointsLength), + CesiumGltf::Accessor::Type::VEC3); + + // Create a single mesh with a single primitive under a single node. + + size_t meshId = gltf.meshes.size(); + CesiumGltf::Mesh& mesh = gltf.meshes.emplace_back(); + CesiumGltf::MeshPrimitive& primitive = mesh.primitives.emplace_back(); + primitive.mode = CesiumGltf::MeshPrimitive::Mode::POINTS; + primitive.attributes.emplace( + "POSITION", + static_cast(positionAccessorId)); + CesiumGltf::Node& node = gltf.nodes.emplace_back(); + std::memcpy( + node.matrix.data(), + &CesiumGeometry::AxisTransforms::Z_UP_TO_Y_UP, + sizeof(glm::dmat4)); + node.mesh = static_cast(meshId); +} + +rapidjson::Document parseFeatureTableJson( + const gsl::span& featureTableJsonData, + PntsContent& parsedContent) { + rapidjson::Document document; + document.Parse( + reinterpret_cast(featureTableJsonData.data()), + featureTableJsonData.size()); + if (document.HasParseError()) { + parsedContent.errors.emplaceError(fmt::format( + "Error when parsing feature table JSON, error code {} at byte offset " + "{}", + document.GetParseError(), + document.GetErrorOffset())); + return document; + } + + const auto pointsLengthIt = document.FindMember("POINTS_LENGTH"); + if (pointsLengthIt == document.MemberEnd() || + !pointsLengthIt->value.IsUint()) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, no " + "valid POINTS_LENGTH property was found."); + return document; + } + + parsedContent.pointsLength = pointsLengthIt->value.GetUint(); + + if (parsedContent.pointsLength == 0) { + // This *should* be disallowed by the spec, but it currently isn't. + // In the future, this can be converted to an error. + parsedContent.errors.emplaceWarning( + "The PNTS has a POINTS_LENGTH of zero."); + return document; + } + + parseSemanticsFromFeatureTableJson(document, parsedContent); + if (parsedContent.errors) { + return document; + } + + const auto extensionsIt = document.FindMember("extensions"); + if (extensionsIt != document.MemberEnd() && extensionsIt->value.IsObject()) { + const auto dracoExtensionIt = + extensionsIt->value.FindMember("3DTILES_draco_point_compression"); + if (dracoExtensionIt != extensionsIt->value.MemberEnd() && + dracoExtensionIt->value.IsObject()) { + const rapidjson::Value& dracoExtensionValue = dracoExtensionIt->value; + parseDracoExtension(dracoExtensionValue, parsedContent); + if (parsedContent.errors) { + return document; + } + } + } + + return document; +} + +rapidjson::Document parseBatchTableJson( + const gsl::span& batchTableJsonData, + PntsContent& parsedContent) {} + +void convertPntsContentToGltf( + const gsl::span& pntsBinary, + const PntsHeader& header, + uint32_t headerLength, + GltfConverterResult& result) { + if (header.featureTableJsonByteLength > 0 && + header.featureTableBinaryByteLength > 0) { + const gsl::span featureTableJsonData = + pntsBinary.subspan(headerLength, header.featureTableJsonByteLength); + const gsl::span featureTableBinaryData = + pntsBinary.subspan( + static_cast( + headerLength + header.featureTableJsonByteLength), + header.featureTableBinaryByteLength); + + PntsContent parsedContent; + + rapidjson::Document featureTableJson = + parseFeatureTableJson(featureTableJsonData, parsedContent); + if (parsedContent.errors) { + result.errors.merge(parsedContent.errors); + return; + } + + // If the 3DTILES_draco_point_compression extension is present, + // the batch table's binary may be compressed with the feature + // table's binary. Parse both jsons first in case the extension is there. + if (header.batchTableJsonByteLength > 0) { + const int64_t batchTableStart = headerLength + + header.featureTableJsonByteLength + + header.featureTableBinaryByteLength; + const gsl::span batchTableJsonData = pntsBinary.subspan( + static_cast(batchTableStart), + header.batchTableJsonByteLength); + rapidjson::Document batchTableJson = + parseBatchTableJson(batchTableJsonData, parsedContent); + + const gsl::span batchTableBinaryData = + pntsBinary.subspan( + static_cast( + batchTableStart + header.batchTableJsonByteLength), + header.batchTableBinaryByteLength); + } + + // when parsing the compressed attributes, convert the json and the binary + // to something that can be put into the existing batchtable to metadata + // function + + createGltfFromFeatureTableData( + parsedContent, + featureTableBinaryData, + result); + } +} +} // namespace + +GltfConverterResult PntsToGltfConverter::convert( + const gsl::span& pntsBinary, + const CesiumGltfReader::GltfReaderOptions& /*options*/) { + GltfConverterResult result; + PntsHeader header; + uint32_t headerLength = 0; + parsePntsHeader(pntsBinary, header, headerLength, result); + if (result.errors) { + return result; + } + + convertPntsContentToGltf(pntsBinary, header, headerLength, result); + return result; +} +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.h b/Cesium3DTilesSelection/src/PntsToGltfConverter.h new file mode 100644 index 000000000..47e6aa07b --- /dev/null +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +#include + +#include + +namespace Cesium3DTilesSelection { +struct PntsToGltfConverter { + static GltfConverterResult convert( + const gsl::span& pntsBinary, + const CesiumGltfReader::GltfReaderOptions& options); +}; +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/registerAllTileContentTypes.cpp b/Cesium3DTilesSelection/src/registerAllTileContentTypes.cpp index c43a53cb2..ce9a98020 100644 --- a/Cesium3DTilesSelection/src/registerAllTileContentTypes.cpp +++ b/Cesium3DTilesSelection/src/registerAllTileContentTypes.cpp @@ -1,6 +1,7 @@ #include "B3dmToGltfConverter.h" #include "BinaryToGltfConverter.h" #include "CmptToGltfConverter.h" +#include "PntsToGltfConverter.h" #include #include @@ -11,6 +12,7 @@ void registerAllTileContentTypes() { GltfConverters::registerMagic("glTF", BinaryToGltfConverter::convert); GltfConverters::registerMagic("b3dm", B3dmToGltfConverter::convert); GltfConverters::registerMagic("cmpt", CmptToGltfConverter::convert); + GltfConverters::registerMagic("pnts", PntsToGltfConverter::convert); GltfConverters::registerFileExtension( ".gltf", From 9fe30ee45d05fabf725b0546618e4e1747db2ddb Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 23 Jan 2023 12:01:13 -0500 Subject: [PATCH 02/20] Add unit test --- .../src/PntsToGltfConverter.cpp | 177 ++++++++++-------- .../test/TestPntsToGltfConverter.cpp | 75 ++++++++ .../PointCloud/pointCloudPositionsOnly.pnts | Bin 0 -> 280 bytes 3 files changed, 174 insertions(+), 78 deletions(-) create mode 100644 Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudPositionsOnly.pnts diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index 299944d92..956bc7e04 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -478,6 +478,82 @@ void decodeDraco( // TODO: check for other semantics and metadata } +rapidjson::Document parseFeatureTableJson( + const gsl::span& featureTableJsonData, + PntsContent& parsedContent) { + rapidjson::Document document; + document.Parse( + reinterpret_cast(featureTableJsonData.data()), + featureTableJsonData.size()); + if (document.HasParseError()) { + parsedContent.errors.emplaceError(fmt::format( + "Error when parsing feature table JSON, error code {} at byte offset " + "{}", + document.GetParseError(), + document.GetErrorOffset())); + return document; + } + + const auto pointsLengthIt = document.FindMember("POINTS_LENGTH"); + if (pointsLengthIt == document.MemberEnd() || + !pointsLengthIt->value.IsUint()) { + parsedContent.errors.emplaceError( + "Error parsing PNTS feature table, no " + "valid POINTS_LENGTH property was found."); + return document; + } + + parsedContent.pointsLength = pointsLengthIt->value.GetUint(); + + if (parsedContent.pointsLength == 0) { + // This *should* be disallowed by the spec, but it currently isn't. + // In the future, this can be converted to an error. + parsedContent.errors.emplaceWarning( + "The PNTS has a POINTS_LENGTH of zero."); + return document; + } + + parseSemanticsFromFeatureTableJson(document, parsedContent); + if (parsedContent.errors) { + return document; + } + + const auto extensionsIt = document.FindMember("extensions"); + if (extensionsIt != document.MemberEnd() && extensionsIt->value.IsObject()) { + const auto dracoExtensionIt = + extensionsIt->value.FindMember("3DTILES_draco_point_compression"); + if (dracoExtensionIt != extensionsIt->value.MemberEnd() && + dracoExtensionIt->value.IsObject()) { + const rapidjson::Value& dracoExtensionValue = dracoExtensionIt->value; + parseDracoExtension(dracoExtensionValue, parsedContent); + if (parsedContent.errors) { + return document; + } + } + } + + return document; +} + +rapidjson::Document parseBatchTableJson( + const gsl::span& batchTableJsonData, + PntsContent& parsedContent) { + rapidjson::Document document; + document.Parse( + reinterpret_cast(batchTableJsonData.data()), + batchTableJsonData.size()); + if (document.HasParseError()) { + parsedContent.errors.emplaceWarning(fmt::format( + "Error when parsing batch table JSON, error code {} at byte " + "offset " + "{}. Skip parsing metadata", + document.GetParseError(), + document.GetErrorOffset())); + } + return document; +} + + int32_t createBufferInGltf(CesiumGltf::Model& gltf, std::vector buffer) { size_t bufferId = gltf.buffers.size(); @@ -490,9 +566,9 @@ createBufferInGltf(CesiumGltf::Model& gltf, std::vector buffer) { int32_t createBufferViewInGltf( CesiumGltf::Model& gltf, - int32_t bufferId, - int64_t byteLength, - int64_t byteStride) { + const int32_t bufferId, + const int64_t byteLength, + const int64_t byteStride) { size_t bufferViewId = gltf.bufferViews.size(); CesiumGltf::BufferView& bufferView = gltf.bufferViews.emplace_back(); bufferView.buffer = bufferId; @@ -506,9 +582,9 @@ int32_t createBufferViewInGltf( int32_t createAccessorInGltf( CesiumGltf::Model& gltf, - int32_t bufferViewId, - int32_t componentType, - int64_t count, + const int32_t bufferViewId, + const int32_t componentType, + const int64_t count, const std::string type) { size_t accessorId = gltf.accessors.size(); CesiumGltf::Accessor& accessor = gltf.accessors.emplace_back(); @@ -597,67 +673,6 @@ void createGltfFromFeatureTableData( node.mesh = static_cast(meshId); } -rapidjson::Document parseFeatureTableJson( - const gsl::span& featureTableJsonData, - PntsContent& parsedContent) { - rapidjson::Document document; - document.Parse( - reinterpret_cast(featureTableJsonData.data()), - featureTableJsonData.size()); - if (document.HasParseError()) { - parsedContent.errors.emplaceError(fmt::format( - "Error when parsing feature table JSON, error code {} at byte offset " - "{}", - document.GetParseError(), - document.GetErrorOffset())); - return document; - } - - const auto pointsLengthIt = document.FindMember("POINTS_LENGTH"); - if (pointsLengthIt == document.MemberEnd() || - !pointsLengthIt->value.IsUint()) { - parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, no " - "valid POINTS_LENGTH property was found."); - return document; - } - - parsedContent.pointsLength = pointsLengthIt->value.GetUint(); - - if (parsedContent.pointsLength == 0) { - // This *should* be disallowed by the spec, but it currently isn't. - // In the future, this can be converted to an error. - parsedContent.errors.emplaceWarning( - "The PNTS has a POINTS_LENGTH of zero."); - return document; - } - - parseSemanticsFromFeatureTableJson(document, parsedContent); - if (parsedContent.errors) { - return document; - } - - const auto extensionsIt = document.FindMember("extensions"); - if (extensionsIt != document.MemberEnd() && extensionsIt->value.IsObject()) { - const auto dracoExtensionIt = - extensionsIt->value.FindMember("3DTILES_draco_point_compression"); - if (dracoExtensionIt != extensionsIt->value.MemberEnd() && - dracoExtensionIt->value.IsObject()) { - const rapidjson::Value& dracoExtensionValue = dracoExtensionIt->value; - parseDracoExtension(dracoExtensionValue, parsedContent); - if (parsedContent.errors) { - return document; - } - } - } - - return document; -} - -rapidjson::Document parseBatchTableJson( - const gsl::span& batchTableJsonData, - PntsContent& parsedContent) {} - void convertPntsContentToGltf( const gsl::span& pntsBinary, const PntsHeader& header, @@ -665,15 +680,10 @@ void convertPntsContentToGltf( GltfConverterResult& result) { if (header.featureTableJsonByteLength > 0 && header.featureTableBinaryByteLength > 0) { + PntsContent parsedContent; + const gsl::span featureTableJsonData = pntsBinary.subspan(headerLength, header.featureTableJsonByteLength); - const gsl::span featureTableBinaryData = - pntsBinary.subspan( - static_cast( - headerLength + header.featureTableJsonByteLength), - header.featureTableBinaryByteLength); - - PntsContent parsedContent; rapidjson::Document featureTableJson = parseFeatureTableJson(featureTableJsonData, parsedContent); @@ -683,8 +693,9 @@ void convertPntsContentToGltf( } // If the 3DTILES_draco_point_compression extension is present, - // the batch table's binary may be compressed with the feature - // table's binary. Parse both jsons first in case the extension is there. + // the batch table's binary will be compressed with the feature + // table's binary. Parse both JSONs first in case the extension is there. + rapidjson::Document batchTableJson; if (header.batchTableJsonByteLength > 0) { const int64_t batchTableStart = headerLength + header.featureTableJsonByteLength + @@ -692,8 +703,12 @@ void convertPntsContentToGltf( const gsl::span batchTableJsonData = pntsBinary.subspan( static_cast(batchTableStart), header.batchTableJsonByteLength); - rapidjson::Document batchTableJson = - parseBatchTableJson(batchTableJsonData, parsedContent); + + batchTableJson = parseBatchTableJson(batchTableJsonData, parsedContent); + if (parsedContent.errors) { + result.errors.merge(parsedContent.errors); + return; + } const gsl::span batchTableBinaryData = pntsBinary.subspan( @@ -706,6 +721,12 @@ void convertPntsContentToGltf( // to something that can be put into the existing batchtable to metadata // function + const gsl::span featureTableBinaryData = + pntsBinary.subspan( + static_cast( + headerLength + header.featureTableJsonByteLength), + header.featureTableBinaryByteLength); + createGltfFromFeatureTableData( parsedContent, featureTableBinaryData, diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp new file mode 100644 index 000000000..d3001bbd7 --- /dev/null +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -0,0 +1,75 @@ +#include "PntsToGltfConverter.h" +#include "BatchTableToGltfFeatureMetadata.h" +#include "readFile.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +using namespace CesiumGltf; +using namespace Cesium3DTilesSelection; + +GltfConverterResult loadPnts(const std::filesystem::path& filePath) { + return PntsToGltfConverter::convert(readFile(filePath), {}); +} + +const int32_t vec3ByteLength = sizeof(glm::vec3); + +TEST_CASE("Converts simple point cloud to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudPositionsOnly.pnts"; + + const int32_t pointsLength = 10; + GltfConverterResult result = loadPnts(testFilePath); + + REQUIRE(result.model); + + Model& gltf = *result.model; + + // Check for single buffer + REQUIRE(gltf.buffers.size() == 1); + CHECK(gltf.buffers[0].byteLength == pointsLength * vec3ByteLength); + + // Check for single bufferView + REQUIRE(gltf.bufferViews.size() == 1); + BufferView& bufferView = gltf.bufferViews[0]; + CHECK(bufferView.buffer == 0); + CHECK(bufferView.byteLength == pointsLength * vec3ByteLength); + CHECK(bufferView.byteOffset == 0); + + // Check for single accessors + REQUIRE(gltf.accessors.size() == 1); + Accessor& accessor = gltf.accessors[0]; + CHECK(accessor.bufferView == 0); + CHECK(accessor.byteOffset == 0); + CHECK(accessor.componentType == Accessor::ComponentType::FLOAT); + CHECK(accessor.count == pointsLength); + CHECK(accessor.type == Accessor::Type::VEC3); + + // Check for single mesh primitive + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == 1); + REQUIRE(attributes.find("POSITION") != attributes.end()); + CHECK(attributes.at("POSITION") == 0); + + // Check for single mesh node + REQUIRE(gltf.nodes.size() == 1); + Node& node = gltf.nodes[0]; + CHECK(node.mesh == 0); +} diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudPositionsOnly.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudPositionsOnly.pnts new file mode 100644 index 0000000000000000000000000000000000000000..1ecd9c7af90875bf5422147ca3942ba645c7e4a4 GIT binary patch literal 280 zcmXTOD=B7VU|^5{5;Z`&0*E<)7zC=70{nwLLp=Tcl&q?ik}6A5{nOHlQ%jVr3~F_N za-M!6!SOz>e(oV2N>+vjI!Zwy&hgH!ej%W&7%K6AzTi zi#a?!m1PfNd;SkP&@|J?K{{`a-KKX(?RW4s!r1$P>;pjYeLyu1Ks8`CQ11bt-u*zm m4nQ*$^Ke&Bncl|$eAc8B|)&V$+2Q2POe=3g@a literal 0 HcmV?d00001 From 96fee92d022c5e4b7cbf44c29013aa9c35fb8fba Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 24 Jan 2023 15:24:23 -0500 Subject: [PATCH 03/20] Refactor PntsContent struct --- Cesium3DTilesSelection/CMakeLists.txt | 1 + .../src/PntsToGltfConverter.cpp | 669 ++++++++++++------ .../test/TestPntsToGltfConverter.cpp | 28 +- 3 files changed, 479 insertions(+), 219 deletions(-) diff --git a/Cesium3DTilesSelection/CMakeLists.txt b/Cesium3DTilesSelection/CMakeLists.txt index 08beda942..793a7da73 100644 --- a/Cesium3DTilesSelection/CMakeLists.txt +++ b/Cesium3DTilesSelection/CMakeLists.txt @@ -55,6 +55,7 @@ target_link_libraries(Cesium3DTilesSelection uriparser libmorton expected-lite + ${CESIUM_NATIVE_DRACO_LIBRARY} ) install(TARGETS Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index 956bc7e04..08f656e8b 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -5,6 +5,7 @@ #include "draco/attributes/point_attribute.h" #include "draco/compression/decode.h" #include "draco/core/decoder_buffer.h" +#include "draco/draco_features.h" #include "draco/point_cloud/point_cloud.h" #include @@ -64,34 +65,33 @@ void parsePntsHeader( enum ComponentType { NONE, BYTE, UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT }; struct PntsSemantic { - bool existsInFeatureTable = false; uint32_t byteOffset = 0; ComponentType componentType = ComponentType::NONE; - bool hasDraco = false; - uint32_t dracoId = 0; - draco::PointAttribute* pAttribute = nullptr; + std::optional dracoId = 0; + std::vector data; }; +enum PntsColorType { CONSTANT, RGBA, RGB, RGB565 }; + struct PntsContent { uint32_t pointsLength = 0; - glm::dvec3 rtcCenter; - glm::dvec3 quantizedVolumeOffset; - glm::dvec3 quantizedVolumeScale; - glm::u8vec4 constantRgba; - uint32_t batchLength = 0; + std::optional rtcCenter; + std::optional quantizedVolumeOffset; + std::optional quantizedVolumeScale; + std::optional constantRgba; + std::optional batchLength; PntsSemantic position; - PntsSemantic positionQuantized; - PntsSemantic rgba; - PntsSemantic rgb; - PntsSemantic rgb565; - PntsSemantic normal; - PntsSemantic normalOct16p; - PntsSemantic batchId; - - bool hasDraco = false; - uint32_t dracoByteOffset = 0; - uint32_t dracoByteLength = 0; + std::optional color; + std::optional normal; + std::optional batchId; + + bool positionQuantized = false; + PntsColorType colorType = PntsColorType::CONSTANT; + bool normalOctEncoded = false; + + std::optional dracoByteOffset; + std::optional dracoByteLength; Cesium3DTilesSelection::ErrorList errors; }; @@ -126,46 +126,56 @@ void parseDracoExtension( return; } - parsedContent.hasDraco = true; - parsedContent.dracoByteOffset = byteOffsetIt->value.GetUint(); - parsedContent.dracoByteLength = byteLengthIt->value.GetUint(); + parsedContent.dracoByteOffset = + std::make_optional(byteOffsetIt->value.GetUint()); + parsedContent.dracoByteLength = + std::make_optional(byteLengthIt->value.GetUint()); const rapidjson::Value& dracoPropertiesValue = propertiesIt->value; auto positionDracoIdIt = dracoPropertiesValue.FindMember("POSITION"); if (positionDracoIdIt != dracoPropertiesValue.MemberEnd() && positionDracoIdIt->value.IsInt()) { - parsedContent.position.hasDraco = true; - parsedContent.position.dracoId = positionDracoIdIt->value.GetInt(); - } - - auto rgbaDracoIdIt = dracoPropertiesValue.FindMember("RGBA"); - if (rgbaDracoIdIt != dracoPropertiesValue.MemberEnd() && - rgbaDracoIdIt->value.IsInt()) { - parsedContent.rgba.hasDraco = true; - parsedContent.rgba.dracoId = rgbaDracoIdIt->value.GetInt(); - } - - auto rgbDracoIdIt = dracoPropertiesValue.FindMember("RGB"); - if (rgbDracoIdIt != dracoPropertiesValue.MemberEnd() && - rgbDracoIdIt->value.IsInt()) { - parsedContent.rgb.hasDraco = true; - parsedContent.rgb.dracoId = rgbDracoIdIt->value.GetInt(); + parsedContent.position.dracoId = + std::make_optional(positionDracoIdIt->value.GetInt()); + } + + if (parsedContent.color) { + const PntsColorType& colorType = parsedContent.colorType; + if (colorType == PntsColorType::RGBA) { + auto rgbaDracoIdIt = dracoPropertiesValue.FindMember("RGBA"); + if (rgbaDracoIdIt != dracoPropertiesValue.MemberEnd() && + rgbaDracoIdIt->value.IsInt()) { + parsedContent.color.value().dracoId = + std::make_optional(rgbaDracoIdIt->value.GetInt()); + } + } else if (colorType == PntsColorType::RGB) { + auto rgbDracoIdIt = dracoPropertiesValue.FindMember("RGB"); + if (rgbDracoIdIt != dracoPropertiesValue.MemberEnd() && + rgbDracoIdIt->value.IsInt()) { + parsedContent.color.value().dracoId = + std::make_optional(rgbDracoIdIt->value.GetInt()); + } + } } - auto normalDracoIdIt = dracoPropertiesValue.FindMember("NORMAL"); - if (normalDracoIdIt != dracoPropertiesValue.MemberEnd() && - normalDracoIdIt->value.IsInt()) { - parsedContent.normal.dracoId = normalDracoIdIt->value.GetInt(); + if (parsedContent.normal) { + auto normalDracoIdIt = dracoPropertiesValue.FindMember("NORMAL"); + if (normalDracoIdIt != dracoPropertiesValue.MemberEnd() && + normalDracoIdIt->value.IsInt()) { + parsedContent.normal.value().dracoId = + std::make_optional(normalDracoIdIt->value.GetInt()); + } } - auto batchIdDracoIdIt = dracoPropertiesValue.FindMember("BATCH_ID"); - if (batchIdDracoIdIt != dracoPropertiesValue.MemberEnd() && - batchIdDracoIdIt->value.IsInt()) { - parsedContent.batchId.dracoId = batchIdDracoIdIt->value.GetInt(); + if (parsedContent.batchId) { + auto batchIdDracoIdIt = dracoPropertiesValue.FindMember("BATCH_ID"); + if (batchIdDracoIdIt != dracoPropertiesValue.MemberEnd() && + batchIdDracoIdIt->value.IsInt()) { + parsedContent.batchId.value().dracoId = + std::make_optional(batchIdDracoIdIt->value.GetInt()); + } } - - return; } bool validateJsonArrayValues( @@ -189,7 +199,7 @@ bool validateJsonArrayValues( return true; } -void parsePositions( +void parsePositionsFromFeatureTableJson( const rapidjson::Document& featureTableJson, PntsContent& parsedContent) { const auto positionIt = featureTableJson.FindMember("POSITION"); @@ -204,7 +214,6 @@ void parsePositions( return; } - parsedContent.position.existsInFeatureTable = true; parsedContent.position.byteOffset = byteOffsetIt->value.GetUint(); return; @@ -219,12 +228,12 @@ void parsePositions( auto quantizedVolumeScaleIt = featureTableJson.FindMember("QUANTIZED_VOLUME_SCALE"); - auto isDouble = [](const rapidjson::Value& value) -> bool { - return value.IsDouble(); + auto isNumber = [](const rapidjson::Value& value) -> bool { + return value.IsNumber(); }; if (quantizedVolumeOffsetIt == featureTableJson.MemberEnd() || - !validateJsonArrayValues(quantizedVolumeOffsetIt->value, 3, isDouble)) { + !validateJsonArrayValues(quantizedVolumeOffsetIt->value, 3, isNumber)) { parsedContent.errors.emplaceError( "Error parsing PNTS feature table, POSITION_QUANTIZED is used but " "no valid QUANTIZED_VOLUME_OFFSET semantic was found."); @@ -232,7 +241,7 @@ void parsePositions( } if (quantizedVolumeScaleIt == featureTableJson.MemberEnd() || - !validateJsonArrayValues(quantizedVolumeScaleIt->value, 3, isDouble)) { + !validateJsonArrayValues(quantizedVolumeScaleIt->value, 3, isNumber)) { parsedContent.errors.emplaceError( "Error parsing PNTS feature table, POSITION_QUANTIZED is used but " "no valid QUANTIZED_VOLUME_SCALE semantic was found."); @@ -249,18 +258,18 @@ void parsePositions( return; } - parsedContent.positionQuantized.existsInFeatureTable = true; - parsedContent.positionQuantized.byteOffset = byteOffsetIt->value.GetUint(); + parsedContent.positionQuantized = true; + parsedContent.position.byteOffset = byteOffsetIt->value.GetUint(); auto quantizedVolumeOffset = quantizedVolumeOffsetIt->value.GetArray(); auto quantizedVolumeScale = quantizedVolumeScaleIt->value.GetArray(); - parsedContent.quantizedVolumeOffset = glm::dvec3( + parsedContent.quantizedVolumeOffset = std::make_optional( quantizedVolumeOffset[0].GetDouble(), quantizedVolumeOffset[1].GetDouble(), quantizedVolumeOffset[2].GetDouble()); - parsedContent.quantizedVolumeScale = glm::dvec3( + parsedContent.quantizedVolumeScale = std::make_optional( quantizedVolumeScale[0].GetDouble(), quantizedVolumeScale[1].GetDouble(), quantizedVolumeScale[2].GetDouble()); @@ -275,58 +284,55 @@ void parsePositions( return; } -void parseColors( +void parseColorsFromFeatureTableJson( const rapidjson::Document& featureTableJson, PntsContent& parsedContent) { const auto rgbaIt = featureTableJson.FindMember("RGBA"); if (rgbaIt != featureTableJson.MemberEnd() && rgbaIt->value.IsObject()) { const auto byteOffsetIt = rgbaIt->value.FindMember("byteOffset"); - if (byteOffsetIt == rgbaIt->value.MemberEnd() || - !byteOffsetIt->value.IsUint()) { - parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, RGBA semantic does not have valid " - "byteOffset."); + if (byteOffsetIt != rgbaIt->value.MemberEnd() && + byteOffsetIt->value.IsUint()) { + parsedContent.color = std::make_optional(); + parsedContent.color.value().byteOffset = byteOffsetIt->value.GetUint(); + parsedContent.colorType = PntsColorType::RGBA; return; } - parsedContent.rgba.existsInFeatureTable = true; - parsedContent.rgba.byteOffset = byteOffsetIt->value.GetUint(); - - return; + parsedContent.errors.emplaceWarning( + "Error parsing PNTS feature table, RGBA semantic does not have valid " + "byteOffset. Skip parsing RGBA colors."); } const auto rgbIt = featureTableJson.FindMember("RGB"); if (rgbIt != featureTableJson.MemberEnd() && rgbIt->value.IsObject()) { const auto byteOffsetIt = rgbIt->value.FindMember("byteOffset"); - if (byteOffsetIt == rgbIt->value.MemberEnd() || - !byteOffsetIt->value.IsUint()) { - parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, RGB semantic does not have valid " - "byteOffset."); + if (byteOffsetIt != rgbIt->value.MemberEnd() && + byteOffsetIt->value.IsUint()) { + parsedContent.color = std::make_optional(); + parsedContent.color.value().byteOffset = byteOffsetIt->value.GetUint(); + parsedContent.colorType = PntsColorType::RGB; return; } - parsedContent.rgb.existsInFeatureTable = true; - parsedContent.rgb.byteOffset = byteOffsetIt->value.GetUint(); - - return; + parsedContent.errors.emplaceWarning( + "Error parsing PNTS feature table, RGB semantic does not have valid " + "byteOffset. Skip parsing RGB colors."); } const auto rgb565It = featureTableJson.FindMember("RGB565"); if (rgb565It != featureTableJson.MemberEnd() && rgb565It->value.IsObject()) { const auto byteOffsetIt = rgb565It->value.FindMember("byteOffset"); - if (byteOffsetIt == rgb565It->value.MemberEnd() || - !byteOffsetIt->value.IsUint()) { - parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, RGB565 semantic does not have " - "valid byteOffset."); + if (byteOffsetIt != rgb565It->value.MemberEnd() && + byteOffsetIt->value.IsUint()) { + parsedContent.color = std::make_optional(); + parsedContent.color.value().byteOffset = byteOffsetIt->value.GetUint(); + parsedContent.colorType = PntsColorType::RGB565; return; } - parsedContent.rgb565.existsInFeatureTable = true; - parsedContent.rgb565.byteOffset = byteOffsetIt->value.GetUint(); - - return; + parsedContent.errors.emplaceWarning( + "Error parsing PNTS feature table, RGB565 semantic does not have " + "valid byteOffset. Skip parsing RGB565 colors."); } auto isUint = [](const rapidjson::Value& value) -> bool { @@ -337,7 +343,7 @@ void parseColors( if (constantRgbaIt != featureTableJson.MemberEnd() && validateJsonArrayValues(constantRgbaIt->value, 4, isUint)) { const rapidjson::Value& arrayValue = constantRgbaIt->value; - parsedContent.constantRgba = glm::u8vec4( + parsedContent.constantRgba = std::make_optional( arrayValue[0].GetUint(), arrayValue[1].GetUint(), arrayValue[2].GetUint(), @@ -345,44 +351,43 @@ void parseColors( } } -void parseNormals( +void parseNormalsFromFeatureTableJson( const rapidjson::Document& featureTableJson, PntsContent& parsedContent) { const auto normalIt = featureTableJson.FindMember("NORMAL"); if (normalIt != featureTableJson.MemberEnd() && normalIt->value.IsObject()) { const auto byteOffsetIt = normalIt->value.FindMember("byteOffset"); - if (byteOffsetIt == normalIt->value.MemberEnd() || - !byteOffsetIt->value.IsUint()) { - parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, NORMAL semantic does not have " - "valid byteOffset."); + if (byteOffsetIt != normalIt->value.MemberEnd() && + byteOffsetIt->value.IsUint()) { + parsedContent.normal = std::make_optional(); + parsedContent.normal.value().byteOffset = byteOffsetIt->value.GetUint(); return; } - parsedContent.normal.existsInFeatureTable = true; - parsedContent.normal.byteOffset = byteOffsetIt->value.GetUint(); - - return; + parsedContent.errors.emplaceWarning( + "Error parsing PNTS feature table, NORMAL semantic does not have " + "valid byteOffset. Skip parsing normals."); } const auto normalOct16pIt = featureTableJson.FindMember("NORMAL_OCT16P"); if (normalOct16pIt != featureTableJson.MemberEnd() && normalOct16pIt->value.IsObject()) { const auto byteOffsetIt = normalOct16pIt->value.FindMember("byteOffset"); - if (byteOffsetIt == normalOct16pIt->value.MemberEnd() || - !byteOffsetIt->value.IsUint()) { - parsedContent.errors.emplaceError("Error parsing PNTS feature table, " - "NORMAL_OCT16P semantic does not have " - "valid byteOffset."); + if (byteOffsetIt != normalOct16pIt->value.MemberEnd() && + byteOffsetIt->value.IsUint()) { + parsedContent.normal = std::make_optional(); + parsedContent.normal.value().byteOffset = byteOffsetIt->value.GetUint(); + parsedContent.normalOctEncoded = true; return; } - parsedContent.normalOct16p.existsInFeatureTable = true; - parsedContent.normalOct16p.byteOffset = byteOffsetIt->value.GetUint(); + parsedContent.errors.emplaceWarning( + "Error parsing PNTS feature table, NORMAL_OCT16P semantic does not " + "have valid byteOffset. Skip parsing oct-encoded normals"); } } -void parseBatchIds( +void parseBatchIdsAndLengthFromFeatureTableJson( const rapidjson::Document& featureTableJson, PntsContent& parsedContent) { const auto batchIdIt = featureTableJson.FindMember("BATCH_ID"); @@ -391,14 +396,15 @@ void parseBatchIds( const auto byteOffsetIt = batchIdIt->value.FindMember("byteOffset"); if (byteOffsetIt == batchIdIt->value.MemberEnd() || !byteOffsetIt->value.IsUint()) { - parsedContent.errors.emplaceError( + parsedContent.errors.emplaceWarning( "Error parsing PNTS feature table, BATCH_ID semantic does not have " - "valid byteOffset."); + "valid byteOffset. Skip parsing batch IDs."); return; } - parsedContent.batchId.existsInFeatureTable = true; - parsedContent.batchId.byteOffset = byteOffsetIt->value.GetUint(); + parsedContent.batchId = std::make_optional(); + PntsSemantic& batchId = parsedContent.batchId.value(); + batchId.byteOffset = byteOffsetIt->value.GetUint(); const auto componentTypeIt = batchIdIt->value.FindMember("componentType"); if (componentTypeIt != featureTableJson.MemberEnd() && @@ -407,22 +413,23 @@ void parseBatchIds( componentTypeIt->value.GetString(); if (componentTypeString == "UNSIGNED_BYTE") { - parsedContent.batchId.componentType = ComponentType::UNSIGNED_BYTE; + batchId.componentType = ComponentType::UNSIGNED_BYTE; } else if (componentTypeString == "UNSIGNED_INT") { - parsedContent.batchId.componentType = ComponentType::UNSIGNED_INT; + batchId.componentType = ComponentType::UNSIGNED_INT; } else { - parsedContent.batchId.componentType = ComponentType::UNSIGNED_SHORT; + batchId.componentType = ComponentType::UNSIGNED_SHORT; } } else { - parsedContent.batchId.componentType = ComponentType::UNSIGNED_SHORT; + batchId.componentType = ComponentType::UNSIGNED_SHORT; } } const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); if (batchLengthIt != featureTableJson.MemberEnd() && batchLengthIt->value.IsUint()) { - parsedContent.batchLength = batchLengthIt->value.GetUint(); - } else if (parsedContent.batchId.existsInFeatureTable) { + parsedContent.batchLength = + std::make_optional(batchLengthIt->value.GetUint()); + } else if (parsedContent.batchId) { parsedContent.errors.emplaceError( "Error parsing PNTS feature table, BATCH_ID semantic is present but " "no valid BATCH_LENGTH was found."); @@ -432,50 +439,39 @@ void parseBatchIds( void parseSemanticsFromFeatureTableJson( const rapidjson::Document& featureTableJson, PntsContent& parsedContent) { - parsePositions(featureTableJson, parsedContent); - parseColors(featureTableJson, parsedContent); - parseNormals(featureTableJson, parsedContent); - parseBatchIds(featureTableJson, parsedContent); -} - -void decodeDraco( - const gsl::span& featureTableBinaryData, - PntsContent& parsedContent) { - if (!parsedContent.hasDraco) { + parsePositionsFromFeatureTableJson(featureTableJson, parsedContent); + if (parsedContent.errors) { return; } - draco::Decoder decoder; - draco::DecoderBuffer buffer; - buffer.Init( - (char*)featureTableBinaryData.data() + parsedContent.dracoByteOffset, - parsedContent.dracoByteLength); - - draco::StatusOr> dracoResult = - decoder.DecodePointCloudFromBuffer(&buffer); + parseColorsFromFeatureTableJson(featureTableJson, parsedContent); + if (parsedContent.errors) { + return; + } - if (!dracoResult.ok()) { - parsedContent.errors.emplaceError("Error decoding Draco point cloud."); + parseNormalsFromFeatureTableJson(featureTableJson, parsedContent); + if (parsedContent.errors) { return; } - const std::unique_ptr& pPointCloud = dracoResult.value(); + parseBatchIdsAndLengthFromFeatureTableJson(featureTableJson, parsedContent); + if (parsedContent.errors) { + return; + } - if (parsedContent.position.hasDraco) { - draco::PointAttribute* pPositionAttribute = - pPointCloud->attribute(parsedContent.position.dracoId); - if (!pPositionAttribute || - pPositionAttribute->data_type() != draco::DT_FLOAT32 || - pPositionAttribute->num_components() != 3) { - parsedContent.errors.emplaceError( - "Error with decoded Draco point cloud, no valid position attribute."); - return; - } + auto isNumber = [](const rapidjson::Value& value) -> bool { + return value.IsNumber(); + }; - parsedContent.position.pAttribute = pPositionAttribute; + const auto rtcIt = featureTableJson.FindMember("RTC_CENTER"); + if (rtcIt != featureTableJson.MemberEnd() && + validateJsonArrayValues(rtcIt->value, 3, isNumber)) { + const rapidjson::Value& rtcValue = rtcIt->value; + parsedContent.rtcCenter = std::make_optional( + rtcValue[0].GetDouble(), + rtcValue[1].GetDouble(), + rtcValue[2].GetDouble()); } - - // TODO: check for other semantics and metadata } rapidjson::Document parseFeatureTableJson( @@ -553,6 +549,268 @@ rapidjson::Document parseBatchTableJson( return document; } +bool validateDracoAttribute( + const draco::PointAttribute* const pAttribute, + const draco::DataType expectedDataType, + const int32_t expectedNumComponents) { + return pAttribute && pAttribute->data_type() == expectedDataType && + pAttribute->num_components() == expectedNumComponents; +} + +void decodeDraco( + const gsl::span& featureTableBinaryData, + PntsContent& parsedContent) { + if (!parsedContent.dracoByteOffset || !parsedContent.dracoByteLength) { + return; + } + + draco::Decoder decoder; + draco::DecoderBuffer buffer; + buffer.Init( + (char*)featureTableBinaryData.data() + + parsedContent.dracoByteOffset.value(), + parsedContent.dracoByteLength.value()); + + draco::StatusOr> dracoResult = + decoder.DecodePointCloudFromBuffer(&buffer); + + if (!dracoResult.ok()) { + parsedContent.errors.emplaceError("Error decoding Draco point cloud."); + return; + } + + const std::unique_ptr& pPointCloud = dracoResult.value(); + const uint32_t pointsLength = parsedContent.pointsLength; + + if (parsedContent.position.dracoId) { + draco::PointAttribute* pPositionAttribute = + pPointCloud->attribute(parsedContent.position.dracoId.value()); + if (!validateDracoAttribute(pPositionAttribute, draco::DT_FLOAT32, 3)) { + parsedContent.errors.emplaceError( + "Error with decoded Draco point cloud, no valid position attribute."); + return; + } + + std::vector& positionData = parsedContent.position.data; + positionData.resize(pointsLength * sizeof(glm::vec3)); + + gsl::span outPositions( + reinterpret_cast(positionData.data()), + pointsLength); + + draco::DataBuffer* decodedBuffer = pPositionAttribute->buffer(); + int64_t decodedByteOffset = pPositionAttribute->byte_offset(); + int64_t decodedByteStride = pPositionAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + outPositions[i] = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + decodedByteStride * i); + } + } + + if (parsedContent.color) { + PntsSemantic& color = parsedContent.color.value(); + if (color.dracoId) { + std::vector& colorData = color.data; + draco::PointAttribute* pColorAttribute = + pPointCloud->attribute(color.dracoId.value()); + if (parsedContent.colorType == PntsColorType::RGBA && + validateDracoAttribute(pColorAttribute, draco::DT_UINT8, 4)) { + colorData.resize(pointsLength * sizeof(glm::u8vec4)); + + gsl::span outColors( + reinterpret_cast(colorData.data()), + pointsLength); + + draco::DataBuffer* decodedBuffer = pColorAttribute->buffer(); + int64_t decodedByteOffset = pColorAttribute->byte_offset(); + int64_t decodedByteStride = pColorAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + outColors[i] = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + + decodedByteStride * i); + } + } else if ( + parsedContent.colorType == PntsColorType::RGB && + validateDracoAttribute(pColorAttribute, draco::DT_UINT8, 3)) { + colorData.resize(pointsLength * sizeof(glm::u8vec3)); + gsl::span outColors( + reinterpret_cast(colorData.data()), + pointsLength); + + draco::DataBuffer* decodedBuffer = pColorAttribute->buffer(); + int64_t decodedByteOffset = pColorAttribute->byte_offset(); + int64_t decodedByteStride = pColorAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + outColors[i] = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + + decodedByteStride * i); + } + } else { + parsedContent.errors.emplaceWarning( + "Warning: decoded Draco point cloud did not contain a valid color " + "attribute. Skip parsing colors."); + parsedContent.color = std::nullopt; + parsedContent.colorType = PntsColorType::CONSTANT; + } + } + } + + if (parsedContent.normal) { + PntsSemantic& normal = parsedContent.normal.value(); + draco::PointAttribute* pNormalAttribute = + pPointCloud->attribute(normal.dracoId.value()); + if (validateDracoAttribute(pNormalAttribute, draco::DT_FLOAT32, 3)) { + std::vector& normalData = normal.data; + normalData.resize(pointsLength * sizeof(glm::vec3)); + + gsl::span outNormals( + reinterpret_cast(normalData.data()), + pointsLength); + + draco::DataBuffer* decodedBuffer = pNormalAttribute->buffer(); + int64_t decodedByteOffset = pNormalAttribute->byte_offset(); + int64_t decodedByteStride = pNormalAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + outNormals[i] = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + decodedByteStride * i); + } + } else { + parsedContent.errors.emplaceWarning( + "Warning: decoded Draco point cloud did not contain valid normal " + "attribute. Skip parsing normals."); + parsedContent.normal = std::nullopt; + } + } + + if (parsedContent.batchId) { + PntsSemantic& batchId = parsedContent.batchId.value(); + if (batchId.dracoId) { + draco::PointAttribute* pBatchIdAttribute = + pPointCloud->attribute(batchId.dracoId.value()); + std::vector& batchIdData = batchId.data; + ComponentType componentType = batchId.componentType; + if (componentType == ComponentType::UNSIGNED_BYTE && + validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT8, 1)) { + batchIdData.resize(pointsLength * sizeof(uint8_t)); + gsl::span outBatchIds( + reinterpret_cast(batchIdData.data()), + pointsLength); + + draco::DataBuffer* decodedBuffer = pBatchIdAttribute->buffer(); + int64_t decodedByteOffset = pBatchIdAttribute->byte_offset(); + int64_t decodedByteStride = pBatchIdAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + outBatchIds[i] = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + + decodedByteStride * i); + } + } else if ( + componentType == ComponentType::UNSIGNED_INT && + validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT32, 1)) { + batchIdData.resize(pointsLength * sizeof(uint32_t)); + gsl::span outBatchIds( + reinterpret_cast(batchIdData.data()), + pointsLength); + + draco::DataBuffer* decodedBuffer = pBatchIdAttribute->buffer(); + int64_t decodedByteOffset = pBatchIdAttribute->byte_offset(); + int64_t decodedByteStride = pBatchIdAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + outBatchIds[i] = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + + decodedByteStride * i); + } + } else if ( + (componentType == ComponentType::UNSIGNED_SHORT || + componentType == ComponentType::NONE) && + validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT16, 1)) { + batchIdData.resize(pointsLength * sizeof(uint16_t)); + gsl::span outBatchIds( + reinterpret_cast(batchIdData.data()), + pointsLength); + + draco::DataBuffer* decodedBuffer = pBatchIdAttribute->buffer(); + int64_t decodedByteOffset = pBatchIdAttribute->byte_offset(); + int64_t decodedByteStride = pBatchIdAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + outBatchIds[i] = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + + decodedByteStride * i); + } + } else { + parsedContent.errors.emplaceWarning( + "Warning: decoded Draco point cloud did not contain a valid batch " + "id " + "attribute. Skip parsing batch IDs."); + parsedContent.batchId = std::nullopt; + } + } + } + + // TODO: check for metadata +} + +void parsePositionsFromFeatureTableBinary( + const gsl::span& featureTableBinaryData, + PntsContent& parsedContent) { + std::vector& positionData = parsedContent.position.data; + if (positionData.size() > 0) { + // If data isn't empty, it must have been decoded from Draco. + return; + } + + uint32_t pointsLength = parsedContent.pointsLength; + size_t positionsByteStride = sizeof(glm::vec3); + size_t positionsByteLength = pointsLength * positionsByteStride; + + gsl::span outPositions( + reinterpret_cast(positionData.data()), + pointsLength); + + if (parsedContent.positionQuantized) { + // PERFORMANCE_IDEA: In the future, it would be more performant to detect if + // the recipient rendering engine can handle dequantization on its own and + // avoid dequantizing here. + gsl::span quantizedPositions( + reinterpret_cast( + featureTableBinaryData.data() + parsedContent.position.byteOffset), + pointsLength); + + glm::vec3 quantizedVolumeScale = + glm::vec3(parsedContent.quantizedVolumeScale.value()); + glm::vec3 quantizedVolumeOffset = + glm::vec3(parsedContent.quantizedVolumeOffset.value()); + + for (size_t i = 0; i < pointsLength; i++) { + const glm::vec3 quantizedPosition( + quantizedPositions[i].x, + quantizedPositions[i].y, + quantizedPositions[i].z); + + outPositions[i] = quantizedPosition * quantizedVolumeScale / 65535.0f + + quantizedVolumeOffset; + } + } else { + std::memcpy( + positionData.data(), + featureTableBinaryData.data() + parsedContent.position.byteOffset, + positionsByteLength); + } +} + +void parseFeatureTableBinary( + const gsl::span& featureTableBinaryData, + PntsContent& parsedContent) { + decodeDraco(featureTableBinaryData, parsedContent); + parsePositionsFromFeatureTableBinary(featureTableBinaryData, parsedContent); +} int32_t createBufferInGltf(CesiumGltf::Model& gltf, std::vector buffer) { @@ -597,80 +855,62 @@ int32_t createAccessorInGltf( return static_cast(accessorId); } -void createGltfFromFeatureTableData( +void addPositionAttributeToPrimitiveInGltf( const PntsContent& parsedContent, - const gsl::span& featureTableBinaryData, - GltfConverterResult& result) { - uint32_t pointsLength = parsedContent.pointsLength; - - size_t positionsByteStride = sizeof(glm::vec3); - size_t positionsByteLength = pointsLength * positionsByteStride; - - std::vector outPositionsBuffer(positionsByteLength); - gsl::span outPositions( - reinterpret_cast(outPositionsBuffer.data()), - pointsLength); - - if (parsedContent.position.existsInFeatureTable) { - // TODO: check for Draco first - std::memcpy( - outPositionsBuffer.data(), - featureTableBinaryData.data() + parsedContent.position.byteOffset, - positionsByteLength); - } else { - assert(parsedContent.positionQuantized.existsInFeatureTable); - gsl::span quantizedPositions( - reinterpret_cast( - featureTableBinaryData.data() + - parsedContent.positionQuantized.byteOffset), - pointsLength); - - for (size_t i = 0; i < pointsLength; i++) { - const glm::vec3 quantizedPosition( - quantizedPositions[i].x, - quantizedPositions[i].y, - quantizedPositions[i].z); - - outPositions[i] = quantizedPosition * - glm::vec3(parsedContent.quantizedVolumeScale) / - 65535.0f + - glm::vec3(parsedContent.quantizedVolumeOffset); - } - } - - result.model = std::make_optional(); - CesiumGltf::Model& gltf = result.model.value(); - + CesiumGltf::Model& gltf, + CesiumGltf::MeshPrimitive& primitive) { + const int64_t count = static_cast(parsedContent.pointsLength); + const int64_t positionsByteStride = static_cast(sizeof(glm ::vec3)); + const int64_t positionsByteLength = + static_cast(positionsByteStride * count); int32_t positionsBufferId = - createBufferInGltf(gltf, std::move(outPositionsBuffer)); + createBufferInGltf(gltf, std::move(parsedContent.position.data)); int32_t positionsBufferViewId = createBufferViewInGltf( gltf, positionsBufferId, - static_cast(positionsByteLength), - static_cast(positionsByteStride)); - + positionsByteLength, + positionsByteStride); int32_t positionAccessorId = createAccessorInGltf( gltf, positionsBufferViewId, CesiumGltf::Accessor::ComponentType::FLOAT, - int64_t(pointsLength), + count, CesiumGltf::Accessor::Type::VEC3); - // Create a single mesh with a single primitive under a single node. - - size_t meshId = gltf.meshes.size(); - CesiumGltf::Mesh& mesh = gltf.meshes.emplace_back(); - CesiumGltf::MeshPrimitive& primitive = mesh.primitives.emplace_back(); - primitive.mode = CesiumGltf::MeshPrimitive::Mode::POINTS; primitive.attributes.emplace( "POSITION", static_cast(positionAccessorId)); +} + +void createGltfFromParsedContent( + const PntsContent& parsedContent, + GltfConverterResult& result) { + result.model = std::make_optional(); + CesiumGltf::Model& gltf = result.model.value(); + + // Create a single node with a single mesh, with a single primitive. CesiumGltf::Node& node = gltf.nodes.emplace_back(); std::memcpy( node.matrix.data(), &CesiumGeometry::AxisTransforms::Z_UP_TO_Y_UP, sizeof(glm::dmat4)); + + size_t meshId = gltf.meshes.size(); + CesiumGltf::Mesh& mesh = gltf.meshes.emplace_back(); node.mesh = static_cast(meshId); + + CesiumGltf::MeshPrimitive& primitive = mesh.primitives.emplace_back(); + primitive.mode = CesiumGltf::MeshPrimitive::Mode::POINTS; + + addPositionAttributeToPrimitiveInGltf(parsedContent, gltf, primitive); + + if (parsedContent.rtcCenter) { + // Add the RTC_CENTER value to the glTF as a CESIUM_RTC extension. + auto& cesiumRTC = + result.model->addExtension(); + glm::dvec3 rtcCenter = parsedContent.rtcCenter.value(); + cesiumRTC.center = {rtcCenter.x, rtcCenter.y, rtcCenter.z}; + } } void convertPntsContentToGltf( @@ -709,28 +949,21 @@ void convertPntsContentToGltf( result.errors.merge(parsedContent.errors); return; } - - const gsl::span batchTableBinaryData = - pntsBinary.subspan( - static_cast( - batchTableStart + header.batchTableJsonByteLength), - header.batchTableBinaryByteLength); } - // when parsing the compressed attributes, convert the json and the binary - // to something that can be put into the existing batchtable to metadata - // function - const gsl::span featureTableBinaryData = pntsBinary.subspan( static_cast( headerLength + header.featureTableJsonByteLength), header.featureTableBinaryByteLength); - createGltfFromFeatureTableData( - parsedContent, - featureTableBinaryData, - result); + /*const gsl::span batchTableBinaryData = + pntsBinary.subspan( + static_cast(batchTableStart + header.batchTableJsonByteLength), + header.batchTableBinaryByteLength);*/ + + parseFeatureTableBinary(featureTableBinaryData, parsedContent); + createGltfFromParsedContent(parsedContent, result); } } } // namespace diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index d3001bbd7..7dc21caa3 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -1,5 +1,5 @@ -#include "PntsToGltfConverter.h" #include "BatchTableToGltfFeatureMetadata.h" +#include "PntsToGltfConverter.h" #include "readFile.h" #include @@ -20,6 +20,32 @@ using namespace CesiumGltf; using namespace Cesium3DTilesSelection; +template +static void +checkBuffer(const gsl::span& values, const gsl::span& expected) { + REQUIRE(values.size() == expected.size()); + if constexpr (std::is_same_v) { + for (size_t i = 0; i < value.size(); ++i) { + const glm::vec3& value = values[i]; + const glm::vec3& expectedValue = expected[i]; + CHECK( + value.x == Approx(expectedValue.x) && + value.y == Approx(expectedValue.y) && + value.z == Approx(expectedValue.z)); + } + } else if constexpr (std::is_same_v) { + const glm::vec4& value = values[i]; + const glm::vec4& expectedValue = expected[i]; + CHECK( + value.x == Approx(expectedValue.x) && + value.y == Approx(expectedValue.y) && + value.z == Approx(expectedValue.z) && + value.w == Approx(expectedValue.w)); + } + + // TODO: check batch ids +} + GltfConverterResult loadPnts(const std::filesystem::path& filePath) { return PntsToGltfConverter::convert(readFile(filePath), {}); } From 933430f2687f11194017eb171e3494e3c7bdb86e Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 25 Jan 2023 16:04:19 -0500 Subject: [PATCH 04/20] Fix draco warnings preventing build, add color parsing and unit tests --- .../src/PntsToGltfConverter.cpp | 420 +++++++++++++----- .../test/TestPntsToGltfConverter.cpp | 293 ++++++++++-- .../PointCloud/pointCloudConstantRGBA.pnts | Bin 0 -> 280 bytes .../PointCloud/pointCloudPositionsOnly.pnts | Bin 280 -> 256 bytes .../test/data/PointCloud/pointCloudRGBA.pnts | Bin 0 -> 312 bytes 5 files changed, 556 insertions(+), 157 deletions(-) create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudConstantRGBA.pnts create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudRGBA.pnts diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index 08f656e8b..d6ba08664 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -2,11 +2,20 @@ #include "BatchTableToGltfFeatureMetadata.h" #include "CesiumGeometry/AxisTransforms.h" -#include "draco/attributes/point_attribute.h" -#include "draco/compression/decode.h" -#include "draco/core/decoder_buffer.h" -#include "draco/draco_features.h" -#include "draco/point_cloud/point_cloud.h" + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4127 4018 4804) +#endif + +#include +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(pop) +#endif #include @@ -14,6 +23,7 @@ #include #include +#include namespace Cesium3DTilesSelection { namespace { @@ -66,8 +76,7 @@ enum ComponentType { NONE, BYTE, UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT }; struct PntsSemantic { uint32_t byteOffset = 0; - ComponentType componentType = ComponentType::NONE; - std::optional dracoId = 0; + std::optional dracoId; std::vector data; }; @@ -82,102 +91,27 @@ struct PntsContent { std::optional batchLength; PntsSemantic position; - std::optional color; - std::optional normal; - std::optional batchId; + // required by glTF spec + glm::vec3 positionMin = glm::vec3(std::numeric_limits::max()); + glm::vec3 positionMax = glm::vec3(std::numeric_limits::min()); bool positionQuantized = false; + + std::optional color; PntsColorType colorType = PntsColorType::CONSTANT; + + std::optional normal; bool normalOctEncoded = false; + std::optional batchId; + ComponentType batchIdComponentType = ComponentType::NONE; + std::optional dracoByteOffset; std::optional dracoByteLength; Cesium3DTilesSelection::ErrorList errors; }; -void parseDracoExtension( - const rapidjson::Value& dracoExtensionValue, - PntsContent& parsedContent) { - const auto propertiesIt = dracoExtensionValue.FindMember("properties"); - if (propertiesIt == dracoExtensionValue.MemberEnd() || - !propertiesIt->value.IsObject()) { - parsedContent.errors.emplaceError( - "Error parsing Draco compression extension, " - "no valid properties object found."); - return; - } - - const auto byteOffsetIt = dracoExtensionValue.FindMember("byteOffset"); - if (byteOffsetIt == dracoExtensionValue.MemberEnd() || - !byteOffsetIt->value.IsUint64()) { - parsedContent.errors.emplaceError( - "Error parsing Draco compression extension, " - "no valid byteOffset found."); - return; - } - - const auto byteLengthIt = dracoExtensionValue.FindMember("byteLength"); - if (byteLengthIt == dracoExtensionValue.MemberEnd() || - !byteLengthIt->value.IsUint64()) { - parsedContent.errors.emplaceError( - "Error parsing Draco compression extension, " - "no valid byteLength found."); - return; - } - - parsedContent.dracoByteOffset = - std::make_optional(byteOffsetIt->value.GetUint()); - parsedContent.dracoByteLength = - std::make_optional(byteLengthIt->value.GetUint()); - - const rapidjson::Value& dracoPropertiesValue = propertiesIt->value; - - auto positionDracoIdIt = dracoPropertiesValue.FindMember("POSITION"); - if (positionDracoIdIt != dracoPropertiesValue.MemberEnd() && - positionDracoIdIt->value.IsInt()) { - parsedContent.position.dracoId = - std::make_optional(positionDracoIdIt->value.GetInt()); - } - - if (parsedContent.color) { - const PntsColorType& colorType = parsedContent.colorType; - if (colorType == PntsColorType::RGBA) { - auto rgbaDracoIdIt = dracoPropertiesValue.FindMember("RGBA"); - if (rgbaDracoIdIt != dracoPropertiesValue.MemberEnd() && - rgbaDracoIdIt->value.IsInt()) { - parsedContent.color.value().dracoId = - std::make_optional(rgbaDracoIdIt->value.GetInt()); - } - } else if (colorType == PntsColorType::RGB) { - auto rgbDracoIdIt = dracoPropertiesValue.FindMember("RGB"); - if (rgbDracoIdIt != dracoPropertiesValue.MemberEnd() && - rgbDracoIdIt->value.IsInt()) { - parsedContent.color.value().dracoId = - std::make_optional(rgbDracoIdIt->value.GetInt()); - } - } - } - - if (parsedContent.normal) { - auto normalDracoIdIt = dracoPropertiesValue.FindMember("NORMAL"); - if (normalDracoIdIt != dracoPropertiesValue.MemberEnd() && - normalDracoIdIt->value.IsInt()) { - parsedContent.normal.value().dracoId = - std::make_optional(normalDracoIdIt->value.GetInt()); - } - } - - if (parsedContent.batchId) { - auto batchIdDracoIdIt = dracoPropertiesValue.FindMember("BATCH_ID"); - if (batchIdDracoIdIt != dracoPropertiesValue.MemberEnd() && - batchIdDracoIdIt->value.IsInt()) { - parsedContent.batchId.value().dracoId = - std::make_optional(batchIdDracoIdIt->value.GetInt()); - } - } -} - bool validateJsonArrayValues( const rapidjson::Value& arrayValue, uint32_t expectedLength, @@ -413,14 +347,14 @@ void parseBatchIdsAndLengthFromFeatureTableJson( componentTypeIt->value.GetString(); if (componentTypeString == "UNSIGNED_BYTE") { - batchId.componentType = ComponentType::UNSIGNED_BYTE; + parsedContent.batchIdComponentType = ComponentType::UNSIGNED_BYTE; } else if (componentTypeString == "UNSIGNED_INT") { - batchId.componentType = ComponentType::UNSIGNED_INT; + parsedContent.batchIdComponentType = ComponentType::UNSIGNED_INT; } else { - batchId.componentType = ComponentType::UNSIGNED_SHORT; + parsedContent.batchIdComponentType = ComponentType::UNSIGNED_SHORT; } } else { - batchId.componentType = ComponentType::UNSIGNED_SHORT; + parsedContent.batchIdComponentType = ComponentType::UNSIGNED_SHORT; } } @@ -474,6 +408,88 @@ void parseSemanticsFromFeatureTableJson( } } +void parseDracoExtensionFromFeatureTableJson( + const rapidjson::Value& dracoExtensionValue, + PntsContent& parsedContent) { + const auto propertiesIt = dracoExtensionValue.FindMember("properties"); + if (propertiesIt == dracoExtensionValue.MemberEnd() || + !propertiesIt->value.IsObject()) { + parsedContent.errors.emplaceError( + "Error parsing Draco compression extension, " + "no valid properties object found."); + return; + } + + const auto byteOffsetIt = dracoExtensionValue.FindMember("byteOffset"); + if (byteOffsetIt == dracoExtensionValue.MemberEnd() || + !byteOffsetIt->value.IsUint64()) { + parsedContent.errors.emplaceError( + "Error parsing Draco compression extension, " + "no valid byteOffset found."); + return; + } + + const auto byteLengthIt = dracoExtensionValue.FindMember("byteLength"); + if (byteLengthIt == dracoExtensionValue.MemberEnd() || + !byteLengthIt->value.IsUint64()) { + parsedContent.errors.emplaceError( + "Error parsing Draco compression extension, " + "no valid byteLength found."); + return; + } + + parsedContent.dracoByteOffset = + std::make_optional(byteOffsetIt->value.GetUint()); + parsedContent.dracoByteLength = + std::make_optional(byteLengthIt->value.GetUint()); + + const rapidjson::Value& dracoPropertiesValue = propertiesIt->value; + + auto positionDracoIdIt = dracoPropertiesValue.FindMember("POSITION"); + if (positionDracoIdIt != dracoPropertiesValue.MemberEnd() && + positionDracoIdIt->value.IsInt()) { + parsedContent.position.dracoId = + std::make_optional(positionDracoIdIt->value.GetInt()); + } + + if (parsedContent.color) { + const PntsColorType& colorType = parsedContent.colorType; + if (colorType == PntsColorType::RGBA) { + auto rgbaDracoIdIt = dracoPropertiesValue.FindMember("RGBA"); + if (rgbaDracoIdIt != dracoPropertiesValue.MemberEnd() && + rgbaDracoIdIt->value.IsInt()) { + parsedContent.color.value().dracoId = + std::make_optional(rgbaDracoIdIt->value.GetInt()); + } + } else if (colorType == PntsColorType::RGB) { + auto rgbDracoIdIt = dracoPropertiesValue.FindMember("RGB"); + if (rgbDracoIdIt != dracoPropertiesValue.MemberEnd() && + rgbDracoIdIt->value.IsInt()) { + parsedContent.color.value().dracoId = + std::make_optional(rgbDracoIdIt->value.GetInt()); + } + } + } + + if (parsedContent.normal) { + auto normalDracoIdIt = dracoPropertiesValue.FindMember("NORMAL"); + if (normalDracoIdIt != dracoPropertiesValue.MemberEnd() && + normalDracoIdIt->value.IsInt()) { + parsedContent.normal.value().dracoId = + std::make_optional(normalDracoIdIt->value.GetInt()); + } + } + + if (parsedContent.batchId) { + auto batchIdDracoIdIt = dracoPropertiesValue.FindMember("BATCH_ID"); + if (batchIdDracoIdIt != dracoPropertiesValue.MemberEnd() && + batchIdDracoIdIt->value.IsInt()) { + parsedContent.batchId.value().dracoId = + std::make_optional(batchIdDracoIdIt->value.GetInt()); + } + } +} + rapidjson::Document parseFeatureTableJson( const gsl::span& featureTableJsonData, PntsContent& parsedContent) { @@ -504,8 +520,6 @@ rapidjson::Document parseFeatureTableJson( if (parsedContent.pointsLength == 0) { // This *should* be disallowed by the spec, but it currently isn't. // In the future, this can be converted to an error. - parsedContent.errors.emplaceWarning( - "The PNTS has a POINTS_LENGTH of zero."); return document; } @@ -521,7 +535,9 @@ rapidjson::Document parseFeatureTableJson( if (dracoExtensionIt != extensionsIt->value.MemberEnd() && dracoExtensionIt->value.IsObject()) { const rapidjson::Value& dracoExtensionValue = dracoExtensionIt->value; - parseDracoExtension(dracoExtensionValue, parsedContent); + parseDracoExtensionFromFeatureTableJson( + dracoExtensionValue, + parsedContent); if (parsedContent.errors) { return document; } @@ -602,6 +618,7 @@ void decodeDraco( int64_t decodedByteOffset = pPositionAttribute->byte_offset(); int64_t decodedByteStride = pPositionAttribute->byte_stride(); + // TODO: min max for (uint32_t i = 0; i < pointsLength; ++i) { outPositions[i] = *reinterpret_cast( decodedBuffer->data() + decodedByteOffset + decodedByteStride * i); @@ -692,7 +709,7 @@ void decodeDraco( draco::PointAttribute* pBatchIdAttribute = pPointCloud->attribute(batchId.dracoId.value()); std::vector& batchIdData = batchId.data; - ComponentType componentType = batchId.componentType; + ComponentType componentType = parsedContent.batchIdComponentType; if (componentType == ComponentType::UNSIGNED_BYTE && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT8, 1)) { batchIdData.resize(pointsLength * sizeof(uint8_t)); @@ -766,9 +783,10 @@ void parsePositionsFromFeatureTableBinary( return; } - uint32_t pointsLength = parsedContent.pointsLength; - size_t positionsByteStride = sizeof(glm::vec3); - size_t positionsByteLength = pointsLength * positionsByteStride; + const uint32_t pointsLength = parsedContent.pointsLength; + const size_t positionsByteStride = sizeof(glm::vec3); + const size_t positionsByteLength = pointsLength * positionsByteStride; + positionData.resize(positionsByteLength); gsl::span outPositions( reinterpret_cast(positionData.data()), @@ -777,15 +795,16 @@ void parsePositionsFromFeatureTableBinary( if (parsedContent.positionQuantized) { // PERFORMANCE_IDEA: In the future, it would be more performant to detect if // the recipient rendering engine can handle dequantization on its own and - // avoid dequantizing here. - gsl::span quantizedPositions( + // if so, use the KHR_mesh_quantization extension to avoid dequantizing + // here. + const gsl::span quantizedPositions( reinterpret_cast( featureTableBinaryData.data() + parsedContent.position.byteOffset), pointsLength); - glm::vec3 quantizedVolumeScale = + const glm::vec3 quantizedVolumeScale = glm::vec3(parsedContent.quantizedVolumeScale.value()); - glm::vec3 quantizedVolumeOffset = + const glm::vec3 quantizedVolumeOffset = glm::vec3(parsedContent.quantizedVolumeOffset.value()); for (size_t i = 0; i < pointsLength; i++) { @@ -798,10 +817,86 @@ void parsePositionsFromFeatureTableBinary( quantizedVolumeOffset; } } else { + // The position accessor min / max is required by the glTF spec, so + // a for loop is used instead of std::memcpy. + auto binaryByteOffset = + featureTableBinaryData.data() + parsedContent.position.byteOffset; + const gsl::span positions( + reinterpret_cast(binaryByteOffset), + pointsLength); + for (size_t i = 0; i < pointsLength; i++) { + outPositions[i] = positions[i]; + + parsedContent.positionMin = + glm::min(parsedContent.positionMin, positions[i]); + parsedContent.positionMax = + glm::max(parsedContent.positionMax, positions[i]); + } + } +} + +void parseColorsFromFeatureTableBinary( + const gsl::span& featureTableBinaryData, + PntsContent& parsedContent) { + PntsSemantic& color = parsedContent.color.value(); + std::vector& colorData = color.data; + if (colorData.size() > 0) { + // If data isn't empty, it must have been decoded from Draco. + return; + } + + uint32_t pointsLength = parsedContent.pointsLength; + + if (parsedContent.colorType == PntsColorType::RGBA) { + const size_t colorsByteStride = sizeof(glm::u8vec4); + const size_t colorsByteLength = pointsLength * colorsByteStride; + colorData.resize(colorsByteLength); + + gsl::span outColors( + reinterpret_cast(colorData.data()), + pointsLength); + + std::memcpy( + colorData.data(), + featureTableBinaryData.data() + color.byteOffset, + colorsByteLength); + } else if (parsedContent.colorType == PntsColorType::RGB) { + const size_t colorsByteStride = sizeof(glm::u8vec3); + const size_t colorsByteLength = pointsLength * colorsByteStride; + colorData.resize(colorsByteLength); + std::memcpy( - positionData.data(), - featureTableBinaryData.data() + parsedContent.position.byteOffset, - positionsByteLength); + colorData.data(), + featureTableBinaryData.data() + color.byteOffset, + colorsByteLength); + } else if (parsedContent.colorType == PntsColorType::RGB565) { + const size_t colorsByteStride = sizeof(glm::vec3); + const size_t colorsByteLength = pointsLength * colorsByteStride; + colorData.resize(colorsByteLength); + + gsl::span outColors( + reinterpret_cast(colorData.data()), + pointsLength); + + const gsl::span compressedColors( + reinterpret_cast( + featureTableBinaryData.data() + color.byteOffset), + pointsLength); + + const uint16_t mask5 = (1 << 5) - 1; + const uint16_t mask6 = (1 << 6) - 1; + const float normalize5 = 1.0f / 31.0f; // normalize [0, 31] to [0, 1] + const float normalize6 = 1.0f / 63.0f; // normalize [0, 63] to [0, 1] + + for (size_t i = 0; i < pointsLength; i++) { + const uint16_t compressedColor = compressedColors[i]; + const uint16_t red = compressedColor >> 11; + const uint16_t green = (compressedColor >> 5) & mask6; + const uint16_t blue = compressedColor & mask5; + + outColors[i] = + glm::vec3(red * normalize5, green * normalize6, blue * normalize5); + } } } @@ -810,10 +905,13 @@ void parseFeatureTableBinary( PntsContent& parsedContent) { decodeDraco(featureTableBinaryData, parsedContent); parsePositionsFromFeatureTableBinary(featureTableBinaryData, parsedContent); + if (parsedContent.color) { + parseColorsFromFeatureTableBinary(featureTableBinaryData, parsedContent); + } } int32_t -createBufferInGltf(CesiumGltf::Model& gltf, std::vector buffer) { +createBufferInGltf(CesiumGltf::Model& gltf, std::vector& buffer) { size_t bufferId = gltf.buffers.size(); CesiumGltf::Buffer& gltfBuffer = gltf.buffers.emplace_back(); gltfBuffer.byteLength = static_cast(buffer.size()); @@ -855,16 +953,13 @@ int32_t createAccessorInGltf( return static_cast(accessorId); } -void addPositionAttributeToPrimitiveInGltf( - const PntsContent& parsedContent, - CesiumGltf::Model& gltf, - CesiumGltf::MeshPrimitive& primitive) { +void addPositionsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { const int64_t count = static_cast(parsedContent.pointsLength); const int64_t positionsByteStride = static_cast(sizeof(glm ::vec3)); const int64_t positionsByteLength = static_cast(positionsByteStride * count); int32_t positionsBufferId = - createBufferInGltf(gltf, std::move(parsedContent.position.data)); + createBufferInGltf(gltf, parsedContent.position.data); int32_t positionsBufferViewId = createBufferViewInGltf( gltf, positionsBufferId, @@ -877,13 +972,88 @@ void addPositionAttributeToPrimitiveInGltf( count, CesiumGltf::Accessor::Type::VEC3); + CesiumGltf::Accessor& accessor = gltf.accessors[positionAccessorId]; + accessor.min = { + parsedContent.positionMin.x, + parsedContent.positionMin.y, + parsedContent.positionMin.z, + }; + accessor.max = { + parsedContent.positionMax.x, + parsedContent.positionMax.y, + parsedContent.positionMax.z, + }; + + CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; primitive.attributes.emplace( "POSITION", static_cast(positionAccessorId)); } +void addColorsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { + if (parsedContent.color) { + PntsSemantic& color = parsedContent.color.value(); + + const int64_t count = static_cast(parsedContent.pointsLength); + int64_t colorsByteStride = 0; + std::string type; + bool isTranslucent = false; + + if (parsedContent.colorType == PntsColorType::RGBA) { + colorsByteStride = static_cast(sizeof(glm::u8vec4)); + type = CesiumGltf::Accessor::Type::VEC4; + isTranslucent = true; + } else { + colorsByteStride = static_cast(sizeof(glm::u8vec3)); + type = CesiumGltf::Accessor::Type::VEC3; + } + + const int64_t colorsByteLength = + static_cast(colorsByteStride * count); + int32_t colorsBufferId = createBufferInGltf(gltf, color.data); + int32_t colorsBufferViewId = createBufferViewInGltf( + gltf, + colorsBufferId, + colorsByteLength, + colorsByteStride); + int32_t colorsAccessorId = createAccessorInGltf( + gltf, + colorsBufferViewId, + CesiumGltf::Accessor::ComponentType::UNSIGNED_BYTE, + count, + type); + + CesiumGltf::Accessor& accessor = gltf.accessors[colorsAccessorId]; + accessor.normalized = true; + + CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; + primitive.attributes.emplace( + "COLOR_0", + static_cast(colorsAccessorId)); + + if (isTranslucent) { + CesiumGltf::Material& material = gltf.materials[primitive.material]; + material.alphaMode = CesiumGltf::Material::AlphaMode::BLEND; + } + + return; + } + + if (parsedContent.constantRgba) { + // Map RGBA from [0, 255] to [0, 1] + glm::vec4 materialColor(parsedContent.constantRgba.value()); + materialColor /= 255.0f; + + CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; + CesiumGltf::Material& material = gltf.materials[primitive.material]; + material.pbrMetallicRoughness.value().baseColorFactor = + {materialColor.x, materialColor.y, materialColor.z, materialColor.w}; + material.alphaMode = CesiumGltf::Material::AlphaMode::BLEND; + } +} + void createGltfFromParsedContent( - const PntsContent& parsedContent, + PntsContent& parsedContent, GltfConverterResult& result) { result.model = std::make_optional(); CesiumGltf::Model& gltf = result.model.value(); @@ -902,10 +1072,24 @@ void createGltfFromParsedContent( CesiumGltf::MeshPrimitive& primitive = mesh.primitives.emplace_back(); primitive.mode = CesiumGltf::MeshPrimitive::Mode::POINTS; - addPositionAttributeToPrimitiveInGltf(parsedContent, gltf, primitive); + size_t materialId = gltf.materials.size(); + CesiumGltf::Material& material = gltf.materials.emplace_back(); + material.pbrMetallicRoughness = + std::make_optional(); + // These values are borrowed from CesiumJS. + material.pbrMetallicRoughness.value().metallicFactor = 0; + material.pbrMetallicRoughness.value().roughnessFactor = 0.9; + + primitive.material = static_cast(materialId); + + addPositionsToGltf(parsedContent, gltf); + addColorsToGltf(parsedContent, gltf); if (parsedContent.rtcCenter) { // Add the RTC_CENTER value to the glTF as a CESIUM_RTC extension. + // This matches what B3dmToGltfConverter does. In the future, + // this can be added instead to the translation component of + // the root node, as suggested in the 3D Tiles migration guide. auto& cesiumRTC = result.model->addExtension(); glm::dvec3 rtcCenter = parsedContent.rtcCenter.value(); diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index 7dc21caa3..4e8ea7294 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -21,60 +22,130 @@ using namespace CesiumGltf; using namespace Cesium3DTilesSelection; template -static void -checkBuffer(const gsl::span& values, const gsl::span& expected) { - REQUIRE(values.size() == expected.size()); +static void checkBufferContents( + const std::vector& buffer, + const std::vector& expected) { + REQUIRE(buffer.size() == expected.size() * sizeof(Type)); + const int32_t byteStride = sizeof(Type); if constexpr (std::is_same_v) { - for (size_t i = 0; i < value.size(); ++i) { - const glm::vec3& value = values[i]; + for (size_t i = 0; i < expected.size(); ++i) { + const glm::vec3& value = + *reinterpret_cast(buffer.data() + i * byteStride); const glm::vec3& expectedValue = expected[i]; - CHECK( - value.x == Approx(expectedValue.x) && - value.y == Approx(expectedValue.y) && - value.z == Approx(expectedValue.z)); + CHECK(value.x == Approx(expectedValue.x)); + CHECK(value.y == Approx(expectedValue.y)); + CHECK(value.z == Approx(expectedValue.z)); } } else if constexpr (std::is_same_v) { - const glm::vec4& value = values[i]; - const glm::vec4& expectedValue = expected[i]; - CHECK( - value.x == Approx(expectedValue.x) && - value.y == Approx(expectedValue.y) && - value.z == Approx(expectedValue.z) && - value.w == Approx(expectedValue.w)); + for (size_t i = 0; i < expected.size(); ++i) { + const glm::vec4& value = + *reinterpret_cast(buffer.data() + i * byteStride); + const glm::vec4& expectedValue = expected[i]; + CHECK(value.x == Approx(expectedValue.x)); + CHECK(value.y == Approx(expectedValue.y)); + CHECK(value.z == Approx(expectedValue.z)); + CHECK(value.w == Approx(expectedValue.w)); + } + } else if constexpr (std::is_same_v) { + for (size_t i = 0; i < expected.size(); ++i) { + const glm::u8vec4& value = + *reinterpret_cast(buffer.data() + i * byteStride); + const glm::u8vec4& expectedValue = expected[i]; + CHECK(value.x == expectedValue.x); + CHECK(value.y == expectedValue.y); + CHECK(value.z == expectedValue.z); + CHECK(value.w == expectedValue.w); + } + } else { + FAIL("Buffer check has not been implemented for the given type.") } - // TODO: check batch ids } +template +static void checkAttribute( + const Model& gltf, + const MeshPrimitive& primitive, + const std::string& attributeSemantic, + const uint32_t expectedCount) { + const auto& attributes = primitive.attributes; + REQUIRE(attributes.find(attributeSemantic) != attributes.end()); + + const int32_t accessorId = attributes.at(attributeSemantic); + REQUIRE(accessorId >= 0); + REQUIRE(accessorId < gltf.accessors.size()); + const Accessor& accessor = gltf.accessors[accessorId]; + + int32_t expectedComponentType = -1; + std::string expectedType; + + if constexpr (std::is_same_v) { + expectedComponentType = Accessor::ComponentType::FLOAT; + expectedType = Accessor::Type::VEC3; + } else { + FAIL("Accessor check has not been implemented for the given type."); + } + + CHECK(accessor.byteOffset == 0); + CHECK(accessor.componentType == expectedComponentType); + CHECK(accessor.count == expectedCount); + CHECK(accessor.type == expectedType); + + const int32_t expectedByteLength = expectedCount * sizeof(Type); + + const int32_t bufferViewId = accessor.bufferView; + REQUIRE(bufferViewId >= 0); + REQUIRE(bufferViewId < gltf.bufferViews.size()); + const BufferView& bufferView = gltf.bufferViews[bufferViewId]; + CHECK(bufferView.byteLength == expectedByteLength); + CHECK(bufferView.byteOffset == 0); + + const int32_t bufferId = bufferView.buffer; + REQUIRE(bufferId >= 0); + REQUIRE(bufferId < gltf.buffers.size()); + + const Buffer& buffer = gltf.buffers[bufferId]; + CHECK(buffer.byteLength == expectedByteLength); + CHECK(static_cast(buffer.cesium.data.size()) == buffer.byteLength); +} + GltfConverterResult loadPnts(const std::filesystem::path& filePath) { return PntsToGltfConverter::convert(readFile(filePath), {}); } -const int32_t vec3ByteLength = sizeof(glm::vec3); - TEST_CASE("Converts simple point cloud to glTF") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "PointCloud" / "pointCloudPositionsOnly.pnts"; + const int32_t pointsLength = 8; - const int32_t pointsLength = 10; GltfConverterResult result = loadPnts(testFilePath); REQUIRE(result.model); - Model& gltf = *result.model; - // Check for single buffer - REQUIRE(gltf.buffers.size() == 1); - CHECK(gltf.buffers[0].byteLength == pointsLength * vec3ByteLength); + // Check for single mesh node + REQUIRE(gltf.nodes.size() == 1); + Node& node = gltf.nodes[0]; + CHECK(node.mesh == 0); - // Check for single bufferView - REQUIRE(gltf.bufferViews.size() == 1); - BufferView& bufferView = gltf.bufferViews[0]; - CHECK(bufferView.buffer == 0); - CHECK(bufferView.byteLength == pointsLength * vec3ByteLength); - CHECK(bufferView.byteOffset == 0); + // Check for single mesh primitive + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.material == 0); - // Check for single accessors + // Check for single material + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(material.pbrMetallicRoughness); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == 1); + REQUIRE(attributes.find("POSITION") != attributes.end()); + CHECK(attributes.at("POSITION") == 0); + + // Check for single accessor REQUIRE(gltf.accessors.size() == 1); Accessor& accessor = gltf.accessors[0]; CHECK(accessor.bufferView == 0); @@ -83,19 +154,163 @@ TEST_CASE("Converts simple point cloud to glTF") { CHECK(accessor.count == pointsLength); CHECK(accessor.type == Accessor::Type::VEC3); - // Check for single mesh primitive + const glm::vec3 expectedMin( + -3.2968313694000244, + -4.033046722412109, + -3.522307872772217); + CHECK(accessor.min[0] == Approx(expectedMin.x)); + CHECK(accessor.min[1] == Approx(expectedMin.y)); + CHECK(accessor.min[2] == Approx(expectedMin.z)); + + const glm::vec3 expectedMax( + 3.2968313694000244, + 4.033046722412109, + 3.522307872772217); + CHECK(accessor.max[0] == Approx(expectedMax.x)); + CHECK(accessor.max[1] == Approx(expectedMax.y)); + CHECK(accessor.max[2] == Approx(expectedMax.z)); + + // Check for single bufferView + REQUIRE(gltf.bufferViews.size() == 1); + BufferView& bufferView = gltf.bufferViews[0]; + CHECK(bufferView.buffer == 0); + CHECK(bufferView.byteLength == pointsLength * sizeof(glm::vec3)); + CHECK(bufferView.byteOffset == 0); + + // Check for single buffer + REQUIRE(gltf.buffers.size() == 1); + Buffer& buffer = gltf.buffers[0]; + CHECK(buffer.byteLength == pointsLength * sizeof(glm::vec3)); + CHECK(static_cast(buffer.cesium.data.size()) == buffer.byteLength); + + const std::vector expectedPositions = { + glm::vec3(-2.4975082874298096, -0.3252686858177185, -3.522307872772217), + glm::vec3(2.345669984817505, 0.9171584248542786, -3.522307872772217), + glm::vec3(-3.2968313694000244, 2.790619373321533, 0.30552753806114197), + glm::vec3(1.54634690284729, 4.033046722412109, 0.30552753806114197), + glm::vec3(-1.54634690284729, -4.033046722412109, -0.30552753806114197), + glm::vec3(3.2968313694000244, -2.790619373321533, -0.30552753806114197), + glm::vec3(-2.345669984817505, -0.9171584248542786, 3.522307872772217), + glm::vec3(2.4975082874298096, 0.3252686858177185, 3.522307872772217)}; + + checkBufferContents(buffer.cesium.data, expectedPositions); + + // Check for RTC extension + REQUIRE(gltf.hasExtension()); + const auto& rtcExtension = + result.model->getExtension(); + const glm::vec3 expectedRtcCenter( + 1215012.8828876738, + -4736313.051199594, + 4081605.22126042); + CHECK(rtcExtension->center[0] == Approx(expectedRtcCenter.x)); + CHECK(rtcExtension->center[1] == Approx(expectedRtcCenter.y)); + CHECK(rtcExtension->center[2] == Approx(expectedRtcCenter.z)); +} + +TEST_CASE("Converts point cloud with RGBA to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudRGBA.pnts"; + const int32_t pointsLength = 8; + const int32_t expectedAttributeCount = 2; + + GltfConverterResult result = loadPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + REQUIRE(gltf.meshes.size() == 1); Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(material.alphaMode == Material::AlphaMode::BLEND); + + REQUIRE(gltf.accessors.size() == expectedAttributeCount); + REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); + REQUIRE(gltf.buffers.size() == expectedAttributeCount); + auto attributes = primitive.attributes; - REQUIRE(attributes.size() == 1); - REQUIRE(attributes.find("POSITION") != attributes.end()); - CHECK(attributes.at("POSITION") == 0); + REQUIRE(attributes.size() == expectedAttributeCount); - // Check for single mesh node - REQUIRE(gltf.nodes.size() == 1); - Node& node = gltf.nodes[0]; - CHECK(node.mesh == 0); + // Check that position attribute is present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + + // Check color attribute more thoroughly + REQUIRE(attributes.find("COLOR_0") != attributes.end()); + int32_t colorAccessorId = attributes.at("COLOR_0"); + REQUIRE(colorAccessorId >= 0); + REQUIRE(colorAccessorId < expectedAttributeCount); + + Accessor& colorAccessor = gltf.accessors[colorAccessorId]; + CHECK(colorAccessor.byteOffset == 0); + CHECK(colorAccessor.componentType == Accessor::ComponentType::UNSIGNED_BYTE); + CHECK(colorAccessor.count == pointsLength); + CHECK(colorAccessor.type == Accessor::Type::VEC4); + CHECK(colorAccessor.normalized); + + int32_t colorBufferViewId = colorAccessor.bufferView; + REQUIRE(colorBufferViewId >= 0); + REQUIRE(colorBufferViewId < expectedAttributeCount); + + BufferView& colorBufferView = gltf.bufferViews[colorBufferViewId]; + CHECK(colorBufferView.byteLength == pointsLength * sizeof(glm::u8vec4)); + CHECK(colorBufferView.byteOffset == 0); + + int32_t colorBufferId = colorBufferView.buffer; + REQUIRE(colorBufferId >= 0); + REQUIRE(colorBufferId < expectedAttributeCount); + + Buffer& colorBuffer = gltf.buffers[colorBufferId]; + CHECK(colorBuffer.byteLength == pointsLength * sizeof(glm::u8vec4)); + CHECK( + static_cast(colorBuffer.cesium.data.size()) == + colorBuffer.byteLength); + + const std::vector expectedColors = { + glm::u8vec4(139, 151, 182, 108), + glm::u8vec4(153, 218, 138, 108), + glm::u8vec4(108, 159, 164, 49), + glm::u8vec4(111, 75, 227, 7), + glm::u8vec4(245, 69, 97, 61), + glm::u8vec4(201, 207, 134, 61), + glm::u8vec4(144, 100, 236, 107), + glm::u8vec4(18, 86, 22, 82)}; + + checkBufferContents(colorBuffer.cesium.data, expectedColors); +} + +TEST_CASE("Converts point cloud with CONSTANT_RGBA") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudConstantRGBA.pnts"; + GltfConverterResult result = loadPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(gltf.buffers.size() == 1); + CHECK(gltf.bufferViews.size() == 1); + CHECK(gltf.accessors.size() == 1); + + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + REQUIRE(material.pbrMetallicRoughness); + MaterialPBRMetallicRoughness& pbrMetallicRoughness = + material.pbrMetallicRoughness.value(); + const auto& baseColorFactor = pbrMetallicRoughness.baseColorFactor; + + // Check that CONSTANT_RGBA is stored in the material base color + const glm::vec4 expectedConstantRGBA = + glm::vec4(1.0f, 1.0f, 0.0f, 51.0f / 255.0f); + CHECK(baseColorFactor[0] == Approx(expectedConstantRGBA.x)); + CHECK(baseColorFactor[1] == Approx(expectedConstantRGBA.y)); + CHECK(baseColorFactor[2] == Approx(expectedConstantRGBA.z)); + CHECK(baseColorFactor[3] == Approx(expectedConstantRGBA.w)); + + CHECK(material.alphaMode == Material::AlphaMode::BLEND); } diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudConstantRGBA.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudConstantRGBA.pnts new file mode 100644 index 0000000000000000000000000000000000000000..9a9efd835c989c9497399b3dc3453e517624954b GIT binary patch literal 280 zcmXTOD=B7VU|^5{5>tS50uXZmF$h#E1^5ShhIsn>DOpu3B~_NB`lqE8rW&7%K6AzTii#a?!m1PfNd;SkP&@|J?K{{`a-KKX(?RW4s q!r1$P>;pjYeLyu1Ks8`CQ11bt-u*zm4nQ*{u}X literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudPositionsOnly.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudPositionsOnly.pnts index 1ecd9c7af90875bf5422147ca3942ba645c7e4a4..58f2e7774ebb60abf840acae9175d5ecb28cf5bf 100644 GIT binary patch delta 42 qcmbQi)W9TIkXKU7$N&UDq6SDMOcb?oLfB^ueFBf?L diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudRGBA.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudRGBA.pnts new file mode 100644 index 0000000000000000000000000000000000000000..3d6859933b07ace06406e988e6d2f0c862345d16 GIT binary patch literal 312 zcmXTOD=B7VU|_HS5>tS50}yinF$h#E1^5ShhIsn>DOpu3B~_NB`lqE8r^L!9HCUHw8_gOseI4UG&<4GfL+EG&#H zEX>W!jV*L^P0WqWj17(T3``9TEiFwgO>|5QEDX&IO!bV642{eTOpIb{6~I9Ey8MB8 zoy+#sG< H8tM)JD28a= literal 0 HcmV?d00001 From fc9f63379171729b9868389e84cb87c40ff688b8 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 25 Jan 2023 17:53:00 -0500 Subject: [PATCH 05/20] Add remaining color attribute tests --- .../src/PntsToGltfConverter.cpp | 42 ++-- .../test/TestPntsToGltfConverter.cpp | 205 ++++++++++++++++-- .../test/data/PointCloud/pointCloudRGB.pnts | Bin 0 -> 304 bytes .../data/PointCloud/pointCloudRGB565.pnts | Bin 0 -> 296 bytes 4 files changed, 207 insertions(+), 40 deletions(-) create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudRGB.pnts create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudRGB565.pnts diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index d6ba08664..46340c383 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -76,7 +76,7 @@ enum ComponentType { NONE, BYTE, UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT }; struct PntsSemantic { uint32_t byteOffset = 0; - std::optional dracoId; + std::optional dracoId; std::vector data; }; @@ -972,7 +972,8 @@ void addPositionsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { count, CesiumGltf::Accessor::Type::VEC3); - CesiumGltf::Accessor& accessor = gltf.accessors[positionAccessorId]; + CesiumGltf::Accessor& accessor = + gltf.accessors[static_cast(positionAccessorId)]; accessor.min = { parsedContent.positionMin.x, parsedContent.positionMin.y, @@ -985,9 +986,7 @@ void addPositionsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { }; CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; - primitive.attributes.emplace( - "POSITION", - static_cast(positionAccessorId)); + primitive.attributes.emplace("POSITION", positionAccessorId); } void addColorsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { @@ -996,15 +995,25 @@ void addColorsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { const int64_t count = static_cast(parsedContent.pointsLength); int64_t colorsByteStride = 0; + int32_t componentType = 0; std::string type; bool isTranslucent = false; + bool isNormalized = false; if (parsedContent.colorType == PntsColorType::RGBA) { colorsByteStride = static_cast(sizeof(glm::u8vec4)); + componentType = CesiumGltf::Accessor::ComponentType::UNSIGNED_BYTE; type = CesiumGltf::Accessor::Type::VEC4; isTranslucent = true; - } else { + isNormalized = true; + } else if (parsedContent.colorType == PntsColorType::RGB) { colorsByteStride = static_cast(sizeof(glm::u8vec3)); + componentType = CesiumGltf::Accessor::ComponentType::UNSIGNED_BYTE; + isNormalized = true; + type = CesiumGltf::Accessor::Type::VEC3; + } else if (parsedContent.colorType == PntsColorType::RGB565) { + colorsByteStride = static_cast(sizeof(glm::vec3)); + componentType = CesiumGltf::Accessor::ComponentType::FLOAT; type = CesiumGltf::Accessor::Type::VEC3; } @@ -1019,33 +1028,28 @@ void addColorsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { int32_t colorsAccessorId = createAccessorInGltf( gltf, colorsBufferViewId, - CesiumGltf::Accessor::ComponentType::UNSIGNED_BYTE, + componentType, count, type); - CesiumGltf::Accessor& accessor = gltf.accessors[colorsAccessorId]; - accessor.normalized = true; + CesiumGltf::Accessor& accessor = + gltf.accessors[static_cast(colorsAccessorId)]; + accessor.normalized = isNormalized; CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; - primitive.attributes.emplace( - "COLOR_0", - static_cast(colorsAccessorId)); + primitive.attributes.emplace("COLOR_0", colorsAccessorId); if (isTranslucent) { - CesiumGltf::Material& material = gltf.materials[primitive.material]; + CesiumGltf::Material& material = gltf.materials[static_cast(primitive.material)]; material.alphaMode = CesiumGltf::Material::AlphaMode::BLEND; } - - return; - } - - if (parsedContent.constantRgba) { + } else if (parsedContent.constantRgba) { // Map RGBA from [0, 255] to [0, 1] glm::vec4 materialColor(parsedContent.constantRgba.value()); materialColor /= 255.0f; CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; - CesiumGltf::Material& material = gltf.materials[primitive.material]; + CesiumGltf::Material& material = gltf.materials[static_cast(primitive.material)]; material.pbrMetallicRoughness.value().baseColorFactor = {materialColor.x, materialColor.y, materialColor.z, materialColor.w}; material.alphaMode = CesiumGltf::Material::AlphaMode::BLEND; diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index 4e8ea7294..21fed42c0 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -46,20 +46,17 @@ static void checkBufferContents( CHECK(value.z == Approx(expectedValue.z)); CHECK(value.w == Approx(expectedValue.w)); } - } else if constexpr (std::is_same_v) { + } else if constexpr ( + std::is_same_v || std::is_same_v) { for (size_t i = 0; i < expected.size(); ++i) { - const glm::u8vec4& value = - *reinterpret_cast(buffer.data() + i * byteStride); - const glm::u8vec4& expectedValue = expected[i]; - CHECK(value.x == expectedValue.x); - CHECK(value.y == expectedValue.y); - CHECK(value.z == expectedValue.z); - CHECK(value.w == expectedValue.w); + const Type& value = + *reinterpret_cast(buffer.data() + i * byteStride); + const Type& expectedValue = expected[i]; + CHECK(value == expectedValue); } } else { - FAIL("Buffer check has not been implemented for the given type.") + FAIL("Buffer check has not been implemented for the given type."); } - // TODO: check batch ids } template @@ -184,14 +181,14 @@ TEST_CASE("Converts simple point cloud to glTF") { CHECK(static_cast(buffer.cesium.data.size()) == buffer.byteLength); const std::vector expectedPositions = { - glm::vec3(-2.4975082874298096, -0.3252686858177185, -3.522307872772217), - glm::vec3(2.345669984817505, 0.9171584248542786, -3.522307872772217), - glm::vec3(-3.2968313694000244, 2.790619373321533, 0.30552753806114197), - glm::vec3(1.54634690284729, 4.033046722412109, 0.30552753806114197), - glm::vec3(-1.54634690284729, -4.033046722412109, -0.30552753806114197), - glm::vec3(3.2968313694000244, -2.790619373321533, -0.30552753806114197), - glm::vec3(-2.345669984817505, -0.9171584248542786, 3.522307872772217), - glm::vec3(2.4975082874298096, 0.3252686858177185, 3.522307872772217)}; + glm::vec3(-2.4975082, -0.3252686, -3.5223078), + glm::vec3(2.3456699, 0.9171584, -3.5223078), + glm::vec3(-3.2968313, 2.7906193, 0.3055275), + glm::vec3(1.5463469, 4.03304672, 0.3055275), + glm::vec3(-1.5463469, -4.03304672, -0.3055275), + glm::vec3(3.2968313, -2.7906193, -0.3055275), + glm::vec3(-2.3456699, -0.9171584, 3.5223078), + glm::vec3(2.4975082, 0.3252686, 3.5223078)}; checkBufferContents(buffer.cesium.data, expectedPositions); @@ -200,9 +197,9 @@ TEST_CASE("Converts simple point cloud to glTF") { const auto& rtcExtension = result.model->getExtension(); const glm::vec3 expectedRtcCenter( - 1215012.8828876738, - -4736313.051199594, - 4081605.22126042); + 1215012.8828876, + -4736313.0511995, + 4081605.2212604); CHECK(rtcExtension->center[0] == Approx(expectedRtcCenter.x)); CHECK(rtcExtension->center[1] == Approx(expectedRtcCenter.y)); CHECK(rtcExtension->center[2] == Approx(expectedRtcCenter.z)); @@ -285,18 +282,184 @@ TEST_CASE("Converts point cloud with RGBA to glTF") { checkBufferContents(colorBuffer.cesium.data, expectedColors); } +TEST_CASE("Converts point cloud with RGB to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudRGB.pnts"; + const int32_t pointsLength = 8; + const int32_t expectedAttributeCount = 2; + + GltfConverterResult result = loadPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(material.alphaMode == Material::AlphaMode::OPAQUE); + + REQUIRE(gltf.accessors.size() == expectedAttributeCount); + REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); + REQUIRE(gltf.buffers.size() == expectedAttributeCount); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == expectedAttributeCount); + + // Check that position attribute is present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + + // Check color attribute more thoroughly + REQUIRE(attributes.find("COLOR_0") != attributes.end()); + int32_t colorAccessorId = attributes.at("COLOR_0"); + REQUIRE(colorAccessorId >= 0); + REQUIRE(colorAccessorId < expectedAttributeCount); + + Accessor& colorAccessor = gltf.accessors[colorAccessorId]; + CHECK(colorAccessor.byteOffset == 0); + CHECK(colorAccessor.componentType == Accessor::ComponentType::UNSIGNED_BYTE); + CHECK(colorAccessor.count == pointsLength); + CHECK(colorAccessor.type == Accessor::Type::VEC3); + CHECK(colorAccessor.normalized); + + int32_t colorBufferViewId = colorAccessor.bufferView; + REQUIRE(colorBufferViewId >= 0); + REQUIRE(colorBufferViewId < expectedAttributeCount); + + BufferView& colorBufferView = gltf.bufferViews[colorBufferViewId]; + CHECK(colorBufferView.byteLength == pointsLength * sizeof(glm::u8vec3)); + CHECK(colorBufferView.byteOffset == 0); + + int32_t colorBufferId = colorBufferView.buffer; + REQUIRE(colorBufferId >= 0); + REQUIRE(colorBufferId < expectedAttributeCount); + + Buffer& colorBuffer = gltf.buffers[colorBufferId]; + CHECK(colorBuffer.byteLength == pointsLength * sizeof(glm::u8vec3)); + CHECK( + static_cast(colorBuffer.cesium.data.size()) == + colorBuffer.byteLength); + + const std::vector expectedColors = { + glm::u8vec3(139, 151, 182), + glm::u8vec3(153, 218, 138), + glm::u8vec3(108, 159, 164), + glm::u8vec3(111, 75, 227), + glm::u8vec3(245, 69, 97), + glm::u8vec3(201, 207, 134), + glm::u8vec3(144, 100, 236), + glm::u8vec3(18, 86, 22)}; + + checkBufferContents(colorBuffer.cesium.data, expectedColors); +} + +TEST_CASE("Converts point cloud with RGB565 to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudRGB565.pnts"; + const int32_t pointsLength = 8; + const int32_t expectedAttributeCount = 2; + + GltfConverterResult result = loadPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(material.alphaMode == Material::AlphaMode::OPAQUE); + + REQUIRE(gltf.accessors.size() == expectedAttributeCount); + REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); + REQUIRE(gltf.buffers.size() == expectedAttributeCount); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == expectedAttributeCount); + + // Check that position attribute is present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + + // Check color attribute more thoroughly + REQUIRE(attributes.find("COLOR_0") != attributes.end()); + int32_t colorAccessorId = attributes.at("COLOR_0"); + REQUIRE(colorAccessorId >= 0); + REQUIRE(colorAccessorId < expectedAttributeCount); + + Accessor& colorAccessor = gltf.accessors[colorAccessorId]; + CHECK(colorAccessor.byteOffset == 0); + CHECK(colorAccessor.componentType == Accessor::ComponentType::FLOAT); + CHECK(colorAccessor.count == pointsLength); + CHECK(colorAccessor.type == Accessor::Type::VEC3); + CHECK(!colorAccessor.normalized); + + int32_t colorBufferViewId = colorAccessor.bufferView; + REQUIRE(colorBufferViewId >= 0); + REQUIRE(colorBufferViewId < expectedAttributeCount); + + BufferView& colorBufferView = gltf.bufferViews[colorBufferViewId]; + CHECK(colorBufferView.byteLength == pointsLength * sizeof(glm::vec3)); + CHECK(colorBufferView.byteOffset == 0); + + int32_t colorBufferId = colorBufferView.buffer; + REQUIRE(colorBufferId >= 0); + REQUIRE(colorBufferId < expectedAttributeCount); + + Buffer& colorBuffer = gltf.buffers[colorBufferId]; + CHECK(colorBuffer.byteLength == pointsLength * sizeof(glm::vec3)); + CHECK( + static_cast(colorBuffer.cesium.data.size()) == + colorBuffer.byteLength); + + const std::vector expectedColors = { + glm::vec3(0.5483871, 0.5873016, 0.7096773), + glm::vec3(0.5806451, 0.8571428, 0.5161290), + glm::vec3(0.4193548, 0.6190476, 0.6451612), + glm::vec3(0.4193548, 0.2857142, 0.8709677), + glm::vec3(0.9354838, 0.2698412, 0.3548386), + glm::vec3(0.7741935, 0.8095238, 0.5161290), + glm::vec3(0.5483871, 0.3809523, 0.9032257), + glm::vec3(0.0645161, 0.3333333, 0.0645161)}; + + checkBufferContents(colorBuffer.cesium.data, expectedColors); +} + TEST_CASE("Converts point cloud with CONSTANT_RGBA") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "PointCloud" / "pointCloudConstantRGBA.pnts"; GltfConverterResult result = loadPnts(testFilePath); + const int32_t pointsLength = 8; REQUIRE(result.model); Model& gltf = *result.model; + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.material == 0); + CHECK(gltf.buffers.size() == 1); CHECK(gltf.bufferViews.size() == 1); CHECK(gltf.accessors.size() == 1); + checkAttribute(gltf, primitive, "POSITION", pointsLength); + REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; REQUIRE(material.pbrMetallicRoughness); diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudRGB.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudRGB.pnts new file mode 100644 index 0000000000000000000000000000000000000000..f6ebcd1ed112ea656d8da09aa0534b9810265814 GIT binary patch literal 304 zcmXTOD=B7VU|=u+5>tS51rT!pF$h#E1^5ShhIsn>DOpu3B~_NB`lqE8rA4UP2-Obrb!Eln*=bW98^49yHo^^A-Rjm!*8jACmQAVBxJ{DFC$ z%l6gfCLSo27jt-eD$5?k_WU1oplPO&gLK{;yG`$o+V9|Ngt7Mn*$05)`+#a3fNH>O ypxy&Oz59WB9e`#!0L`=mvAd^ln|Z4%Xa16W@5f(V6HlIRn~?HGC`_yx>Hq-I`(|YT literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudRGB565.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudRGB565.pnts new file mode 100644 index 0000000000000000000000000000000000000000..69e94b31f64c507a977b5584171ba6cb5db6f455 GIT binary patch literal 296 zcmXTOD=B7VU|`Sy5>tS50T6QlF$h#E1^5ShhIsn>DOpu3B~_NB`lqE8racbJd~^~fQmz$ Date: Thu, 26 Jan 2023 14:55:42 -0500 Subject: [PATCH 06/20] Add unit tests for normals, oct encoded normals, and positions --- .../src/PntsToGltfConverter.cpp | 208 ++++++++--- .../test/TestPntsToGltfConverter.cpp | 350 +++++++++++++----- .../data/PointCloud/pointCloudNormals.pnts | Bin 0 -> 424 bytes .../pointCloudNormalsOctEncoded.pnts | Bin 0 -> 352 bytes .../data/PointCloud/pointCloudQuantized.pnts | Bin 0 -> 312 bytes 5 files changed, 405 insertions(+), 153 deletions(-) create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudNormals.pnts create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudNormalsOctEncoded.pnts create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudQuantized.pnts diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index 46340c383..2d8115ffe 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -18,6 +18,8 @@ #endif #include +#include +#include #include #include @@ -93,7 +95,7 @@ struct PntsContent { PntsSemantic position; // required by glTF spec glm::vec3 positionMin = glm::vec3(std::numeric_limits::max()); - glm::vec3 positionMax = glm::vec3(std::numeric_limits::min()); + glm::vec3 positionMax = glm::vec3(std::numeric_limits::lowest()); bool positionQuantized = false; @@ -793,7 +795,7 @@ void parsePositionsFromFeatureTableBinary( pointsLength); if (parsedContent.positionQuantized) { - // PERFORMANCE_IDEA: In the future, it would be more performant to detect if + // PERFORMANCE_IDEA: In the future, it might be more performant to detect if // the recipient rendering engine can handle dequantization on its own and // if so, use the KHR_mesh_quantization extension to avoid dequantizing // here. @@ -807,30 +809,34 @@ void parsePositionsFromFeatureTableBinary( const glm::vec3 quantizedVolumeOffset = glm::vec3(parsedContent.quantizedVolumeOffset.value()); + const glm::vec3 quantizedPositionScalar = quantizedVolumeScale / 65535.0f; + for (size_t i = 0; i < pointsLength; i++) { const glm::vec3 quantizedPosition( quantizedPositions[i].x, quantizedPositions[i].y, quantizedPositions[i].z); - outPositions[i] = quantizedPosition * quantizedVolumeScale / 65535.0f + - quantizedVolumeOffset; + const glm::vec3 dequantizedPosition = + quantizedPosition * quantizedPositionScalar + quantizedVolumeOffset; + outPositions[i] = dequantizedPosition; + parsedContent.positionMin = + glm::min(parsedContent.positionMin, dequantizedPosition); + parsedContent.positionMax = + glm::max(parsedContent.positionMax, dequantizedPosition); } } else { // The position accessor min / max is required by the glTF spec, so // a for loop is used instead of std::memcpy. - auto binaryByteOffset = - featureTableBinaryData.data() + parsedContent.position.byteOffset; const gsl::span positions( - reinterpret_cast(binaryByteOffset), + reinterpret_cast( + featureTableBinaryData.data() + parsedContent.position.byteOffset), pointsLength); for (size_t i = 0; i < pointsLength; i++) { - outPositions[i] = positions[i]; - - parsedContent.positionMin = - glm::min(parsedContent.positionMin, positions[i]); - parsedContent.positionMax = - glm::max(parsedContent.positionMax, positions[i]); + const glm::vec3 position = positions[i]; + outPositions[i] = position; + parsedContent.positionMin = glm::min(parsedContent.positionMin, position); + parsedContent.positionMax = glm::max(parsedContent.positionMax, position); } } } @@ -845,7 +851,7 @@ void parseColorsFromFeatureTableBinary( return; } - uint32_t pointsLength = parsedContent.pointsLength; + const uint32_t pointsLength = parsedContent.pointsLength; if (parsedContent.colorType == PntsColorType::RGBA) { const size_t colorsByteStride = sizeof(glm::u8vec4); @@ -887,11 +893,13 @@ void parseColorsFromFeatureTableBinary( const uint16_t mask6 = (1 << 6) - 1; const float normalize5 = 1.0f / 31.0f; // normalize [0, 31] to [0, 1] const float normalize6 = 1.0f / 63.0f; // normalize [0, 63] to [0, 1] + const uint16_t shift11 = 11; + const uint16_t shift5 = 5; for (size_t i = 0; i < pointsLength; i++) { const uint16_t compressedColor = compressedColors[i]; - const uint16_t red = compressedColor >> 11; - const uint16_t green = (compressedColor >> 5) & mask6; + const uint16_t red = compressedColor >> shift11; + const uint16_t green = (compressedColor >> shift5) & mask6; const uint16_t blue = compressedColor & mask5; outColors[i] = @@ -900,6 +908,63 @@ void parseColorsFromFeatureTableBinary( } } +void parseNormalsFromFeatureTableBinary( + const gsl::span& featureTableBinaryData, + PntsContent& parsedContent) { + PntsSemantic& normal = parsedContent.normal.value(); + std::vector& normalData = normal.data; + if (normalData.size() > 0) { + // If data isn't empty, it must have been decoded from Draco. + return; + } + + const uint32_t pointsLength = parsedContent.pointsLength; + const size_t normalsByteStride = sizeof(glm::vec3); + const size_t normalsByteLength = pointsLength * normalsByteStride; + normalData.resize(normalsByteLength); + + if (parsedContent.normalOctEncoded) { + const gsl::span encodedNormals( + reinterpret_cast( + featureTableBinaryData.data() + normal.byteOffset), + pointsLength); + + gsl::span outNormals( + reinterpret_cast(normalData.data()), + pointsLength); + + constexpr uint8_t rangeMax = 255; + + for (size_t i = 0; i < pointsLength; i++) { + const glm::u8vec2 encodedNormal = encodedNormals[i]; + + // TODO: This is copied from QuantizedMeshLoader. It should really + // be put in its own module, e.g. CesiumUtility::AttributeCompression + glm::dvec3 decodedNormal; + decodedNormal.x = + CesiumUtility::Math::fromSNorm(encodedNormal.x, rangeMax); + decodedNormal.y = + CesiumUtility::Math::fromSNorm(encodedNormal.y, rangeMax); + decodedNormal.z = + 1.0 - (glm::abs(decodedNormal.x) + glm::abs(decodedNormal.y)); + + if (decodedNormal.z < 0.0) { + const double oldVX = decodedNormal.x; + decodedNormal.x = (1.0 - glm::abs(decodedNormal.y)) * + CesiumUtility::Math::signNotZero(oldVX); + decodedNormal.y = (1.0 - glm::abs(oldVX)) * + CesiumUtility::Math::signNotZero(decodedNormal.y); + } + outNormals[i] = glm::vec3(glm::normalize(decodedNormal)); + } + } else { + std::memcpy( + normalData.data(), + featureTableBinaryData.data() + normal.byteOffset, + normalsByteLength); + } +} + void parseFeatureTableBinary( const gsl::span& featureTableBinaryData, PntsContent& parsedContent) { @@ -908,6 +973,9 @@ void parseFeatureTableBinary( if (parsedContent.color) { parseColorsFromFeatureTableBinary(featureTableBinaryData, parsedContent); } + if (parsedContent.normal) { + parseNormalsFromFeatureTableBinary(featureTableBinaryData, parsedContent); + } } int32_t @@ -955,25 +1023,20 @@ int32_t createAccessorInGltf( void addPositionsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { const int64_t count = static_cast(parsedContent.pointsLength); - const int64_t positionsByteStride = static_cast(sizeof(glm ::vec3)); - const int64_t positionsByteLength = - static_cast(positionsByteStride * count); - int32_t positionsBufferId = - createBufferInGltf(gltf, parsedContent.position.data); - int32_t positionsBufferViewId = createBufferViewInGltf( - gltf, - positionsBufferId, - positionsByteLength, - positionsByteStride); - int32_t positionAccessorId = createAccessorInGltf( + const int64_t byteStride = static_cast(sizeof(glm ::vec3)); + const int64_t byteLength = static_cast(byteStride * count); + int32_t bufferId = createBufferInGltf(gltf, parsedContent.position.data); + int32_t bufferViewId = + createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); + int32_t accessorId = createAccessorInGltf( gltf, - positionsBufferViewId, + bufferViewId, CesiumGltf::Accessor::ComponentType::FLOAT, count, CesiumGltf::Accessor::Type::VEC3); CesiumGltf::Accessor& accessor = - gltf.accessors[static_cast(positionAccessorId)]; + gltf.accessors[static_cast(accessorId)]; accessor.min = { parsedContent.positionMin.x, parsedContent.positionMin.y, @@ -986,7 +1049,7 @@ void addPositionsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { }; CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; - primitive.attributes.emplace("POSITION", positionAccessorId); + primitive.attributes.emplace("POSITION", accessorId); } void addColorsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { @@ -994,65 +1057,71 @@ void addColorsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { PntsSemantic& color = parsedContent.color.value(); const int64_t count = static_cast(parsedContent.pointsLength); - int64_t colorsByteStride = 0; + int64_t byteStride = 0; int32_t componentType = 0; std::string type; bool isTranslucent = false; bool isNormalized = false; if (parsedContent.colorType == PntsColorType::RGBA) { - colorsByteStride = static_cast(sizeof(glm::u8vec4)); + byteStride = static_cast(sizeof(glm::u8vec4)); componentType = CesiumGltf::Accessor::ComponentType::UNSIGNED_BYTE; type = CesiumGltf::Accessor::Type::VEC4; isTranslucent = true; isNormalized = true; } else if (parsedContent.colorType == PntsColorType::RGB) { - colorsByteStride = static_cast(sizeof(glm::u8vec3)); + byteStride = static_cast(sizeof(glm::u8vec3)); componentType = CesiumGltf::Accessor::ComponentType::UNSIGNED_BYTE; isNormalized = true; type = CesiumGltf::Accessor::Type::VEC3; } else if (parsedContent.colorType == PntsColorType::RGB565) { - colorsByteStride = static_cast(sizeof(glm::vec3)); + byteStride = static_cast(sizeof(glm::vec3)); componentType = CesiumGltf::Accessor::ComponentType::FLOAT; type = CesiumGltf::Accessor::Type::VEC3; } - const int64_t colorsByteLength = - static_cast(colorsByteStride * count); - int32_t colorsBufferId = createBufferInGltf(gltf, color.data); - int32_t colorsBufferViewId = createBufferViewInGltf( - gltf, - colorsBufferId, - colorsByteLength, - colorsByteStride); - int32_t colorsAccessorId = createAccessorInGltf( - gltf, - colorsBufferViewId, - componentType, - count, - type); + const int64_t byteLength = static_cast(byteStride * count); + int32_t bufferId = createBufferInGltf(gltf, color.data); + int32_t bufferViewId = + createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); + int32_t accessorId = + createAccessorInGltf(gltf, bufferViewId, componentType, count, type); CesiumGltf::Accessor& accessor = - gltf.accessors[static_cast(colorsAccessorId)]; + gltf.accessors[static_cast(accessorId)]; accessor.normalized = isNormalized; CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; - primitive.attributes.emplace("COLOR_0", colorsAccessorId); + primitive.attributes.emplace("COLOR_0", accessorId); if (isTranslucent) { - CesiumGltf::Material& material = gltf.materials[static_cast(primitive.material)]; + CesiumGltf::Material& material = + gltf.materials[static_cast(primitive.material)]; material.alphaMode = CesiumGltf::Material::AlphaMode::BLEND; } - } else if (parsedContent.constantRgba) { - // Map RGBA from [0, 255] to [0, 1] - glm::vec4 materialColor(parsedContent.constantRgba.value()); - materialColor /= 255.0f; + } +} + +void addNormalsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { + if (parsedContent.normal) { + PntsSemantic& normal = parsedContent.normal.value(); + + const int64_t count = static_cast(parsedContent.pointsLength); + const int64_t byteStride = static_cast(sizeof(glm ::vec3)); + const int64_t byteLength = static_cast(byteStride * count); + + int32_t bufferId = createBufferInGltf(gltf, normal.data); + int32_t bufferViewId = + createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); + int32_t accessorId = createAccessorInGltf( + gltf, + bufferViewId, + CesiumGltf::Accessor::ComponentType::FLOAT, + count, + CesiumGltf::Accessor::Type::VEC3); CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; - CesiumGltf::Material& material = gltf.materials[static_cast(primitive.material)]; - material.pbrMetallicRoughness.value().baseColorFactor = - {materialColor.x, materialColor.y, materialColor.z, materialColor.w}; - material.alphaMode = CesiumGltf::Material::AlphaMode::BLEND; + primitive.attributes.emplace("NORMAL", accessorId); } } @@ -1087,7 +1156,26 @@ void createGltfFromParsedContent( primitive.material = static_cast(materialId); addPositionsToGltf(parsedContent, gltf); - addColorsToGltf(parsedContent, gltf); + + if (parsedContent.color) { + addColorsToGltf(parsedContent, gltf); + } else if (parsedContent.constantRgba) { + // Map RGBA from [0, 255] to [0, 1] + glm::vec4 materialColor(parsedContent.constantRgba.value()); + materialColor /= 255.0f; + + material.pbrMetallicRoughness.value().baseColorFactor = + {materialColor.x, materialColor.y, materialColor.z, materialColor.w}; + material.alphaMode = CesiumGltf::Material::AlphaMode::BLEND; + } + + if (parsedContent.normal) { + addNormalsToGltf(parsedContent, gltf); + } else { + // Points without normals should be rendered without lighting, which we + // can indicate with the KHR_materials_unlit extension. + material.addExtension(); + } if (parsedContent.rtcCenter) { // Add the RTC_CENTER value to the glTF as a CESIUM_RTC extension. diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index 21fed42c0..504e5d0dd 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -5,10 +5,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -20,6 +22,7 @@ using namespace CesiumGltf; using namespace Cesium3DTilesSelection; +using namespace CesiumUtility; template static void checkBufferContents( @@ -32,19 +35,20 @@ static void checkBufferContents( const glm::vec3& value = *reinterpret_cast(buffer.data() + i * byteStride); const glm::vec3& expectedValue = expected[i]; - CHECK(value.x == Approx(expectedValue.x)); - CHECK(value.y == Approx(expectedValue.y)); - CHECK(value.z == Approx(expectedValue.z)); + CHECK(Math::equalsEpsilon( + static_cast(value), + static_cast(expectedValue), + Math::Epsilon6)); } } else if constexpr (std::is_same_v) { for (size_t i = 0; i < expected.size(); ++i) { const glm::vec4& value = *reinterpret_cast(buffer.data() + i * byteStride); const glm::vec4& expectedValue = expected[i]; - CHECK(value.x == Approx(expectedValue.x)); - CHECK(value.y == Approx(expectedValue.y)); - CHECK(value.z == Approx(expectedValue.z)); - CHECK(value.w == Approx(expectedValue.w)); + CHECK(Math::equalsEpsilon( + static_cast(value), + static_cast(expectedValue), + Math::Epsilon6)); } } else if constexpr ( std::is_same_v || std::is_same_v) { @@ -70,8 +74,9 @@ static void checkAttribute( const int32_t accessorId = attributes.at(attributeSemantic); REQUIRE(accessorId >= 0); - REQUIRE(accessorId < gltf.accessors.size()); - const Accessor& accessor = gltf.accessors[accessorId]; + const uint32_t accessorIdUint = static_cast(accessorId); + REQUIRE(accessorIdUint < gltf.accessors.size()); + const Accessor& accessor = gltf.accessors[accessorIdUint]; int32_t expectedComponentType = -1; std::string expectedType; @@ -79,6 +84,12 @@ static void checkAttribute( if constexpr (std::is_same_v) { expectedComponentType = Accessor::ComponentType::FLOAT; expectedType = Accessor::Type::VEC3; + } else if constexpr (std::is_same_v) { + expectedComponentType = Accessor::ComponentType::UNSIGNED_BYTE; + expectedType = Accessor::Type::VEC3; + } else if constexpr (std::is_same_v) { + expectedComponentType = Accessor::ComponentType::UNSIGNED_BYTE; + expectedType = Accessor::Type::VEC4; } else { FAIL("Accessor check has not been implemented for the given type."); } @@ -92,16 +103,18 @@ static void checkAttribute( const int32_t bufferViewId = accessor.bufferView; REQUIRE(bufferViewId >= 0); - REQUIRE(bufferViewId < gltf.bufferViews.size()); - const BufferView& bufferView = gltf.bufferViews[bufferViewId]; + const uint32_t bufferViewIdUint = static_cast(bufferViewId); + REQUIRE(bufferViewIdUint < gltf.bufferViews.size()); + const BufferView& bufferView = gltf.bufferViews[bufferViewIdUint]; CHECK(bufferView.byteLength == expectedByteLength); CHECK(bufferView.byteOffset == 0); const int32_t bufferId = bufferView.buffer; REQUIRE(bufferId >= 0); - REQUIRE(bufferId < gltf.buffers.size()); + const uint32_t bufferIdUint = static_cast(bufferId); + REQUIRE(bufferIdUint < gltf.buffers.size()); - const Buffer& buffer = gltf.buffers[bufferId]; + const Buffer& buffer = gltf.buffers[static_cast(bufferIdUint)]; CHECK(buffer.byteLength == expectedByteLength); CHECK(static_cast(buffer.cesium.data.size()) == buffer.byteLength); } @@ -123,6 +136,24 @@ TEST_CASE("Converts simple point cloud to glTF") { // Check for single mesh node REQUIRE(gltf.nodes.size() == 1); Node& node = gltf.nodes[0]; + std::vector expectedMatrix = { + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0}; + CHECK(node.matrix == expectedMatrix); CHECK(node.mesh == 0); // Check for single mesh primitive @@ -136,6 +167,7 @@ TEST_CASE("Converts simple point cloud to glTF") { REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; CHECK(material.pbrMetallicRoughness); + CHECK(material.hasExtension()); auto attributes = primitive.attributes; REQUIRE(attributes.size() == 1); @@ -151,18 +183,12 @@ TEST_CASE("Converts simple point cloud to glTF") { CHECK(accessor.count == pointsLength); CHECK(accessor.type == Accessor::Type::VEC3); - const glm::vec3 expectedMin( - -3.2968313694000244, - -4.033046722412109, - -3.522307872772217); + const glm::vec3 expectedMin(-3.2968313, -4.0330467, -3.5223078); CHECK(accessor.min[0] == Approx(expectedMin.x)); CHECK(accessor.min[1] == Approx(expectedMin.y)); CHECK(accessor.min[2] == Approx(expectedMin.z)); - const glm::vec3 expectedMax( - 3.2968313694000244, - 4.033046722412109, - 3.522307872772217); + const glm::vec3 expectedMax(3.2968313, 4.0330467, 3.5223078); CHECK(accessor.max[0] == Approx(expectedMax.x)); CHECK(accessor.max[1] == Approx(expectedMax.y)); CHECK(accessor.max[2] == Approx(expectedMax.z)); @@ -227,6 +253,7 @@ TEST_CASE("Converts point cloud with RGBA to glTF") { REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; CHECK(material.alphaMode == Material::AlphaMode::BLEND); + CHECK(material.hasExtension()); REQUIRE(gltf.accessors.size() == expectedAttributeCount); REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); @@ -235,39 +262,20 @@ TEST_CASE("Converts point cloud with RGBA to glTF") { auto attributes = primitive.attributes; REQUIRE(attributes.size() == expectedAttributeCount); - // Check that position attribute is present + // Check that position and color attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); // Check color attribute more thoroughly - REQUIRE(attributes.find("COLOR_0") != attributes.end()); - int32_t colorAccessorId = attributes.at("COLOR_0"); - REQUIRE(colorAccessorId >= 0); - REQUIRE(colorAccessorId < expectedAttributeCount); - + uint32_t colorAccessorId = static_cast(attributes.at("COLOR_0")); Accessor& colorAccessor = gltf.accessors[colorAccessorId]; - CHECK(colorAccessor.byteOffset == 0); - CHECK(colorAccessor.componentType == Accessor::ComponentType::UNSIGNED_BYTE); - CHECK(colorAccessor.count == pointsLength); - CHECK(colorAccessor.type == Accessor::Type::VEC4); CHECK(colorAccessor.normalized); - int32_t colorBufferViewId = colorAccessor.bufferView; - REQUIRE(colorBufferViewId >= 0); - REQUIRE(colorBufferViewId < expectedAttributeCount); - + uint32_t colorBufferViewId = static_cast(colorAccessor.bufferView); BufferView& colorBufferView = gltf.bufferViews[colorBufferViewId]; - CHECK(colorBufferView.byteLength == pointsLength * sizeof(glm::u8vec4)); - CHECK(colorBufferView.byteOffset == 0); - - int32_t colorBufferId = colorBufferView.buffer; - REQUIRE(colorBufferId >= 0); - REQUIRE(colorBufferId < expectedAttributeCount); + uint32_t colorBufferId = static_cast(colorBufferView.buffer); Buffer& colorBuffer = gltf.buffers[colorBufferId]; - CHECK(colorBuffer.byteLength == pointsLength * sizeof(glm::u8vec4)); - CHECK( - static_cast(colorBuffer.cesium.data.size()) == - colorBuffer.byteLength); const std::vector expectedColors = { glm::u8vec4(139, 151, 182, 108), @@ -304,6 +312,7 @@ TEST_CASE("Converts point cloud with RGB to glTF") { REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; CHECK(material.alphaMode == Material::AlphaMode::OPAQUE); + CHECK(material.hasExtension()); REQUIRE(gltf.accessors.size() == expectedAttributeCount); REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); @@ -312,39 +321,20 @@ TEST_CASE("Converts point cloud with RGB to glTF") { auto attributes = primitive.attributes; REQUIRE(attributes.size() == expectedAttributeCount); - // Check that position attribute is present + // Check that position and color attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); // Check color attribute more thoroughly - REQUIRE(attributes.find("COLOR_0") != attributes.end()); - int32_t colorAccessorId = attributes.at("COLOR_0"); - REQUIRE(colorAccessorId >= 0); - REQUIRE(colorAccessorId < expectedAttributeCount); - + uint32_t colorAccessorId = static_cast(attributes.at("COLOR_0")); Accessor& colorAccessor = gltf.accessors[colorAccessorId]; - CHECK(colorAccessor.byteOffset == 0); - CHECK(colorAccessor.componentType == Accessor::ComponentType::UNSIGNED_BYTE); - CHECK(colorAccessor.count == pointsLength); - CHECK(colorAccessor.type == Accessor::Type::VEC3); CHECK(colorAccessor.normalized); - int32_t colorBufferViewId = colorAccessor.bufferView; - REQUIRE(colorBufferViewId >= 0); - REQUIRE(colorBufferViewId < expectedAttributeCount); - + uint32_t colorBufferViewId = static_cast(colorAccessor.bufferView); BufferView& colorBufferView = gltf.bufferViews[colorBufferViewId]; - CHECK(colorBufferView.byteLength == pointsLength * sizeof(glm::u8vec3)); - CHECK(colorBufferView.byteOffset == 0); - - int32_t colorBufferId = colorBufferView.buffer; - REQUIRE(colorBufferId >= 0); - REQUIRE(colorBufferId < expectedAttributeCount); + uint32_t colorBufferId = static_cast(colorBufferView.buffer); Buffer& colorBuffer = gltf.buffers[colorBufferId]; - CHECK(colorBuffer.byteLength == pointsLength * sizeof(glm::u8vec3)); - CHECK( - static_cast(colorBuffer.cesium.data.size()) == - colorBuffer.byteLength); const std::vector expectedColors = { glm::u8vec3(139, 151, 182), @@ -381,6 +371,7 @@ TEST_CASE("Converts point cloud with RGB565 to glTF") { REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; CHECK(material.alphaMode == Material::AlphaMode::OPAQUE); + CHECK(material.hasExtension()); REQUIRE(gltf.accessors.size() == expectedAttributeCount); REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); @@ -389,39 +380,21 @@ TEST_CASE("Converts point cloud with RGB565 to glTF") { auto attributes = primitive.attributes; REQUIRE(attributes.size() == expectedAttributeCount); - // Check that position attribute is present + // Check that position and color attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); // Check color attribute more thoroughly - REQUIRE(attributes.find("COLOR_0") != attributes.end()); - int32_t colorAccessorId = attributes.at("COLOR_0"); - REQUIRE(colorAccessorId >= 0); - REQUIRE(colorAccessorId < expectedAttributeCount); - + // Check color attribute more thoroughly + uint32_t colorAccessorId = static_cast(attributes.at("COLOR_0")); Accessor& colorAccessor = gltf.accessors[colorAccessorId]; - CHECK(colorAccessor.byteOffset == 0); - CHECK(colorAccessor.componentType == Accessor::ComponentType::FLOAT); - CHECK(colorAccessor.count == pointsLength); - CHECK(colorAccessor.type == Accessor::Type::VEC3); CHECK(!colorAccessor.normalized); - int32_t colorBufferViewId = colorAccessor.bufferView; - REQUIRE(colorBufferViewId >= 0); - REQUIRE(colorBufferViewId < expectedAttributeCount); - + uint32_t colorBufferViewId = static_cast(colorAccessor.bufferView); BufferView& colorBufferView = gltf.bufferViews[colorBufferViewId]; - CHECK(colorBufferView.byteLength == pointsLength * sizeof(glm::vec3)); - CHECK(colorBufferView.byteOffset == 0); - - int32_t colorBufferId = colorBufferView.buffer; - REQUIRE(colorBufferId >= 0); - REQUIRE(colorBufferId < expectedAttributeCount); + uint32_t colorBufferId = static_cast(colorBufferView.buffer); Buffer& colorBuffer = gltf.buffers[colorBufferId]; - CHECK(colorBuffer.byteLength == pointsLength * sizeof(glm::vec3)); - CHECK( - static_cast(colorBuffer.cesium.data.size()) == - colorBuffer.byteLength); const std::vector expectedColors = { glm::vec3(0.5483871, 0.5873016, 0.7096773), @@ -476,4 +449,195 @@ TEST_CASE("Converts point cloud with CONSTANT_RGBA") { CHECK(baseColorFactor[3] == Approx(expectedConstantRGBA.w)); CHECK(material.alphaMode == Material::AlphaMode::BLEND); + CHECK(material.hasExtension()); +} + +TEST_CASE("Converts point cloud with normals to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudNormals.pnts"; + const int32_t pointsLength = 8; + const int32_t expectedAttributeCount = 3; + + GltfConverterResult result = loadPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(!material.hasExtension()); + + REQUIRE(gltf.accessors.size() == expectedAttributeCount); + REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); + REQUIRE(gltf.buffers.size() == expectedAttributeCount); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == expectedAttributeCount); + + // Check that position, color, and normal attributes are present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "NORMAL", pointsLength); + + // Check normal attribute more thoroughly + uint32_t normalAccessorId = static_cast(attributes.at("NORMAL")); + Accessor& normalAccessor = gltf.accessors[normalAccessorId]; + + uint32_t normalBufferViewId = + static_cast(normalAccessor.bufferView); + BufferView& normalBufferView = gltf.bufferViews[normalBufferViewId]; + + uint32_t normalBufferId = static_cast(normalBufferView.buffer); + Buffer& normalBuffer = gltf.buffers[normalBufferId]; + + const std::vector expectedNormals = { + glm::vec3(-0.9854088, 0.1667507, 0.0341110), + glm::vec3(-0.5957704, 0.5378777, 0.5964436), + glm::vec3(-0.5666092, -0.7828890, -0.2569800), + glm::vec3(-0.5804154, -0.7226123, 0.3754320), + glm::vec3(-0.8535281, -0.1291752, -0.5047805), + glm::vec3(0.7557975, 0.1243999, 0.6428800), + glm::vec3(0.1374090, -0.2333731, -0.9626296), + glm::vec3(-0.0633145, 0.9630424, 0.2618022)}; + + checkBufferContents(normalBuffer.cesium.data, expectedNormals); +} + +TEST_CASE("Converts point cloud with oct-encoded normals to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = + testFilePath / "PointCloud" / "pointCloudNormalsOctEncoded.pnts"; + const int32_t pointsLength = 8; + const int32_t expectedAttributeCount = 3; + + GltfConverterResult result = loadPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(!material.hasExtension()); + + REQUIRE(gltf.accessors.size() == expectedAttributeCount); + REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); + REQUIRE(gltf.buffers.size() == expectedAttributeCount); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == expectedAttributeCount); + + // Check that position, color, and normal attributes are present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "NORMAL", pointsLength); + + // Check normal attribute more thoroughly + uint32_t normalAccessorId = static_cast(attributes.at("NORMAL")); + Accessor& normalAccessor = gltf.accessors[normalAccessorId]; + CHECK(!normalAccessor.normalized); + + uint32_t normalBufferViewId = + static_cast(normalAccessor.bufferView); + BufferView& normalBufferView = gltf.bufferViews[normalBufferViewId]; + + uint32_t normalBufferId = static_cast(normalBufferView.buffer); + Buffer& normalBuffer = gltf.buffers[normalBufferId]; + + const std::vector expectedNormals = { + glm::vec3(-0.9856477, 0.1634960, 0.0420418), + glm::vec3(-0.5901730, 0.5359042, 0.6037402), + glm::vec3(-0.5674310, -0.7817938, -0.2584963), + glm::vec3(-0.5861990, -0.7179291, 0.3754308), + glm::vec3(-0.8519385, -0.1283743, -0.5076620), + glm::vec3(0.7587127, 0.1254564, 0.6392304), + glm::vec3(0.1354662, -0.2292506, -0.9638947), + glm::vec3(-0.0656172, 0.9640687, 0.2574214)}; + + checkBufferContents(normalBuffer.cesium.data, expectedNormals); +} + +TEST_CASE("Converts point cloud with quantized positions to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudQuantized.pnts"; + const int32_t pointsLength = 8; + const int32_t expectedAttributeCount = 2; + + GltfConverterResult result = loadPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(!gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(material.hasExtension()); + + REQUIRE(gltf.accessors.size() == expectedAttributeCount); + REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); + REQUIRE(gltf.buffers.size() == expectedAttributeCount); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == expectedAttributeCount); + + // Check that position and color attributes are present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + + // Check position attribute more thoroughly + uint32_t positionAccessorId = + static_cast(attributes.at("POSITION")); + Accessor& positionAccessor = gltf.accessors[positionAccessorId]; + CHECK(!positionAccessor.normalized); + + const glm::vec3 expectedMin(1215009.59, -4736317.08, 4081601.7); + CHECK(positionAccessor.min[0] == Approx(expectedMin.x)); + CHECK(positionAccessor.min[1] == Approx(expectedMin.y)); + CHECK(positionAccessor.min[2] == Approx(expectedMin.z)); + + const glm::vec3 expectedMax(1215016.18, -4736309.02, 4081608.74); + CHECK(positionAccessor.max[0] == Approx(expectedMax.x)); + CHECK(positionAccessor.max[1] == Approx(expectedMax.y)); + CHECK(positionAccessor.max[2] == Approx(expectedMax.z)); + + uint32_t positionBufferViewId = + static_cast(positionAccessor.bufferView); + BufferView& positionBufferView = gltf.bufferViews[positionBufferViewId]; + + uint32_t positionBufferId = static_cast(positionBufferView.buffer); + Buffer& positionBuffer = gltf.buffers[positionBufferId]; + + const std::vector expectedPositions = { + glm::vec3(1215010.39, -4736313.38, 4081601.7), + glm::vec3(1215015.23, -4736312.13, 4081601.7), + glm::vec3(1215009.59, -4736310.26, 4081605.53), + glm::vec3(1215014.43, -4736309.02, 4081605.53), + glm::vec3(1215011.34, -4736317.08, 4081604.92), + glm::vec3(1215016.18, -4736315.84, 4081604.92), + glm::vec3(1215010.54, -4736313.97, 4081608.74), + glm::vec3(1215015.38, -4736312.73, 4081608.74)}; + + checkBufferContents(positionBuffer.cesium.data, expectedPositions); } diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudNormals.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudNormals.pnts new file mode 100644 index 0000000000000000000000000000000000000000..cb03c6bdb009faf9f360345b90b700396df9df64 GIT binary patch literal 424 zcmXTOD=B7VU|?7QB-Q}w8$iqf#2`?u6yP818RF^hr({*FlvG)g>YtWYoLZt}Wl*c5 z6y)xNE^KKA6!-HF@^$nKEb~q+}Is zXk=(=U}&UgVPRxpVQyw_Y@wrTVs30^Y-p@!U}|V+X=!R{qGMuUVQ6Mxs%KW-bm!T{3)E^So@=9Vwnzi7ARflQn9 z1E<_R*f&q;IG`D!x_?2t;(_#K`i7wh_DK^Vt zZSQjXZP#b!%wO`D@9W9)ZEvm$vBqAxd-rE)%khKGHT$<8(6VFQ!EKw>BC$Vu6Q_O0 z1_^sVUcvoY*&h3!OlsaIJxyf4!e5L1i!2Vi95?g?D2M}`rF$h#E1^5ShhIsn>DOpu3B~_NB`lqE8rwt_{hdP$%>vL>7#e}J2KamWg#^d@xca$=cqmy}01XRqj(2wT z3vmrnvWhk|GBh>L&bm>zIxt2!{Ze98Vr zyDbl7+N2*i<@Uk8c|ykl%?Q=~3)&SAyuVfGkZ3J$zh{T9gYhGM``gh|_xrQXI&e#L z(LPJDSq^J^m)mcm_gH K{c5ml6chlv9eKh4 literal 0 HcmV?d00001 diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudQuantized.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudQuantized.pnts new file mode 100644 index 0000000000000000000000000000000000000000..3b1c9a2cc7299026df7f383c1d334965745120da GIT binary patch literal 312 zcmXTOD=B7VU|_HS5@&$42M}`rF$h#E1^5ShhIsn>#RrBu`h|E#xwRVyV`mZbWp zr4^@^C|McQ>L>-dJE04kSOCQX{5}0bg5!N${oF%5l&matl;Fn1hxz-2`ntvkJ3IQg zDp^Gv8t8ygEEYBXZf?P@As|IYhNcDv=6V(uMiv(4X6D8gI=Uw2#%9Kb7J3GzhK81w zrj{l;CI%LUW(Ed&Mn;B4W(Fokv9$_7AmFgN{E8~~o~r3rRA*}EAHUc>b-CU1i|v^a z2PAYWQm^z|=vI7oZpqNP^=^M~ht{p`>Dy-B>dKkFB;Wh-SJ%Xo=i4Tvyb%f$s|K5< Gpa1~QM_^w7 literal 0 HcmV?d00001 From 3fdc5ac176aafb8136ede309dcf58924811fa6ae Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 27 Jan 2023 15:32:26 -0500 Subject: [PATCH 07/20] Add AttributeCompression library, add batch IDs to pnts glTF --- .../src/PntsToGltfConverter.cpp | 378 +++++++++++------- .../src/QuantizedMeshLoader.cpp | 23 +- .../test/TestPntsToGltfConverter.cpp | 281 ++++++++++--- .../data/PointCloud/pointCloudBatched.pnts | Bin 0 -> 824 bytes .../CesiumUtility/AttributeCompression.h | 84 ++++ 5 files changed, 549 insertions(+), 217 deletions(-) create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudBatched.pnts create mode 100644 CesiumUtility/include/CesiumUtility/AttributeCompression.h diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index 2d8115ffe..cb528a1e2 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -27,6 +28,9 @@ #include #include +using namespace CesiumGltf; +using namespace CesiumUtility; + namespace Cesium3DTilesSelection { namespace { struct PntsHeader { @@ -71,17 +75,18 @@ void parsePntsHeader( } } -// The only semantic that can have a variable component type is the -// BATCH_ID semantic. For parsing purposes, all other semantics are -// assigned component type NONE. -enum ComponentType { NONE, BYTE, UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT }; - struct PntsSemantic { uint32_t byteOffset = 0; std::optional dracoId; std::vector data; }; +struct DracoBatchTableValue { + + std::optional dracoId; + std::vector data; +}; + enum PntsColorType { CONSTANT, RGBA, RGB, RGB565 }; struct PntsContent { @@ -106,11 +111,13 @@ struct PntsContent { bool normalOctEncoded = false; std::optional batchId; - ComponentType batchIdComponentType = ComponentType::NONE; + std::optional batchIdComponentType; std::optional dracoByteOffset; std::optional dracoByteLength; + std::unordered_map dracoBatchTableValues; + Cesium3DTilesSelection::ErrorList errors; }; @@ -349,14 +356,18 @@ void parseBatchIdsAndLengthFromFeatureTableJson( componentTypeIt->value.GetString(); if (componentTypeString == "UNSIGNED_BYTE") { - parsedContent.batchIdComponentType = ComponentType::UNSIGNED_BYTE; + parsedContent.batchIdComponentType = + Accessor::ComponentType::UNSIGNED_BYTE; } else if (componentTypeString == "UNSIGNED_INT") { - parsedContent.batchIdComponentType = ComponentType::UNSIGNED_INT; + parsedContent.batchIdComponentType = + Accessor::ComponentType::UNSIGNED_INT; } else { - parsedContent.batchIdComponentType = ComponentType::UNSIGNED_SHORT; + parsedContent.batchIdComponentType = + Accessor::ComponentType::UNSIGNED_SHORT; } } else { - parsedContent.batchIdComponentType = ComponentType::UNSIGNED_SHORT; + parsedContent.batchIdComponentType = + Accessor::ComponentType::UNSIGNED_SHORT; } } @@ -563,7 +574,35 @@ rapidjson::Document parseBatchTableJson( "{}. Skip parsing metadata", document.GetParseError(), document.GetErrorOffset())); + return document; } + + const auto extensionsIt = document.FindMember("extensions"); + if (extensionsIt != document.MemberEnd() && extensionsIt->value.IsObject()) { + const auto dracoExtensionIt = + extensionsIt->value.FindMember("3DTILES_draco_point_compression"); + if (dracoExtensionIt != extensionsIt->value.MemberEnd() && + dracoExtensionIt->value.IsObject()) { + const rapidjson::Value& dracoExtensionValue = dracoExtensionIt->value; + auto& dracoBatchTableValues = parsedContent.dracoBatchTableValues; + for (auto propertyIt = dracoExtensionValue.MemberBegin(); + propertyIt != dracoExtensionValue.MemberEnd(); + ++propertyIt) { + std::string name = propertyIt->name.GetString(); + if (propertyIt->value.IsInt()) { + PntsSemantic metadataSemantic; + metadataSemantic.dracoId = propertyIt->value.GetInt(); + dracoBatchTableValues.insert(std::make_pair(name, metadataSemantic)); + } else { + parsedContent.errors.emplaceWarning( + "Error parsing metadata property " + name + + " due to invalid Draco ID. The values for this property will not " + "be parsed properly."); + } + } + } + } + return document; } @@ -605,7 +644,8 @@ void decodeDraco( pPointCloud->attribute(parsedContent.position.dracoId.value()); if (!validateDracoAttribute(pPositionAttribute, draco::DT_FLOAT32, 3)) { parsedContent.errors.emplaceError( - "Error with decoded Draco point cloud, no valid position attribute."); + "Error with decoded Draco point cloud, no valid position " + "attribute."); return; } @@ -620,10 +660,13 @@ void decodeDraco( int64_t decodedByteOffset = pPositionAttribute->byte_offset(); int64_t decodedByteStride = pPositionAttribute->byte_stride(); - // TODO: min max for (uint32_t i = 0; i < pointsLength; ++i) { - outPositions[i] = *reinterpret_cast( + const glm::vec3 position = *reinterpret_cast( decodedBuffer->data() + decodedByteOffset + decodedByteStride * i); + outPositions[i] = position; + + parsedContent.positionMin = glm::min(position, parsedContent.positionMin); + parsedContent.positionMax = glm::max(position, parsedContent.positionMax); } } @@ -669,7 +712,8 @@ void decodeDraco( } } else { parsedContent.errors.emplaceWarning( - "Warning: decoded Draco point cloud did not contain a valid color " + "Warning: decoded Draco point cloud did not contain a valid " + "color " "attribute. Skip parsing colors."); parsedContent.color = std::nullopt; parsedContent.colorType = PntsColorType::CONSTANT; @@ -711,8 +755,13 @@ void decodeDraco( draco::PointAttribute* pBatchIdAttribute = pPointCloud->attribute(batchId.dracoId.value()); std::vector& batchIdData = batchId.data; - ComponentType componentType = parsedContent.batchIdComponentType; - if (componentType == ComponentType::UNSIGNED_BYTE && + + int32_t componentType = 0; + if (parsedContent.batchIdComponentType) { + componentType = parsedContent.batchIdComponentType.value(); + } + + if (componentType == Accessor::ComponentType::UNSIGNED_BYTE && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT8, 1)) { batchIdData.resize(pointsLength * sizeof(uint8_t)); gsl::span outBatchIds( @@ -729,7 +778,7 @@ void decodeDraco( decodedByteStride * i); } } else if ( - componentType == ComponentType::UNSIGNED_INT && + componentType == Accessor::ComponentType::UNSIGNED_INT && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT32, 1)) { batchIdData.resize(pointsLength * sizeof(uint32_t)); gsl::span outBatchIds( @@ -746,8 +795,8 @@ void decodeDraco( decodedByteStride * i); } } else if ( - (componentType == ComponentType::UNSIGNED_SHORT || - componentType == ComponentType::NONE) && + (componentType == 0 || + componentType == Accessor::ComponentType::UNSIGNED_SHORT) && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT16, 1)) { batchIdData.resize(pointsLength * sizeof(uint16_t)); gsl::span outBatchIds( @@ -765,7 +814,8 @@ void decodeDraco( } } else { parsedContent.errors.emplaceWarning( - "Warning: decoded Draco point cloud did not contain a valid batch " + "Warning: decoded Draco point cloud did not contain a valid " + "batch " "id " "attribute. Skip parsing batch IDs."); parsedContent.batchId = std::nullopt; @@ -795,10 +845,10 @@ void parsePositionsFromFeatureTableBinary( pointsLength); if (parsedContent.positionQuantized) { - // PERFORMANCE_IDEA: In the future, it might be more performant to detect if - // the recipient rendering engine can handle dequantization on its own and - // if so, use the KHR_mesh_quantization extension to avoid dequantizing - // here. + // PERFORMANCE_IDEA: In the future, it might be more performant to detect + // if the recipient rendering engine can handle dequantization on its own + // and if so, use the KHR_mesh_quantization extension to avoid + // dequantizing here. const gsl::span quantizedPositions( reinterpret_cast( featureTableBinaryData.data() + parsedContent.position.byteOffset), @@ -889,21 +939,10 @@ void parseColorsFromFeatureTableBinary( featureTableBinaryData.data() + color.byteOffset), pointsLength); - const uint16_t mask5 = (1 << 5) - 1; - const uint16_t mask6 = (1 << 6) - 1; - const float normalize5 = 1.0f / 31.0f; // normalize [0, 31] to [0, 1] - const float normalize6 = 1.0f / 63.0f; // normalize [0, 63] to [0, 1] - const uint16_t shift11 = 11; - const uint16_t shift5 = 5; - for (size_t i = 0; i < pointsLength; i++) { const uint16_t compressedColor = compressedColors[i]; - const uint16_t red = compressedColor >> shift11; - const uint16_t green = (compressedColor >> shift5) & mask6; - const uint16_t blue = compressedColor & mask5; - outColors[i] = - glm::vec3(red * normalize5, green * normalize6, blue * normalize5); + glm::vec3(AttributeCompression::decodeRGB565(compressedColor)); } } } @@ -933,29 +972,11 @@ void parseNormalsFromFeatureTableBinary( reinterpret_cast(normalData.data()), pointsLength); - constexpr uint8_t rangeMax = 255; - for (size_t i = 0; i < pointsLength; i++) { const glm::u8vec2 encodedNormal = encodedNormals[i]; - - // TODO: This is copied from QuantizedMeshLoader. It should really - // be put in its own module, e.g. CesiumUtility::AttributeCompression - glm::dvec3 decodedNormal; - decodedNormal.x = - CesiumUtility::Math::fromSNorm(encodedNormal.x, rangeMax); - decodedNormal.y = - CesiumUtility::Math::fromSNorm(encodedNormal.y, rangeMax); - decodedNormal.z = - 1.0 - (glm::abs(decodedNormal.x) + glm::abs(decodedNormal.y)); - - if (decodedNormal.z < 0.0) { - const double oldVX = decodedNormal.x; - decodedNormal.x = (1.0 - glm::abs(decodedNormal.y)) * - CesiumUtility::Math::signNotZero(oldVX); - decodedNormal.y = (1.0 - glm::abs(oldVX)) * - CesiumUtility::Math::signNotZero(decodedNormal.y); - } - outNormals[i] = glm::vec3(glm::normalize(decodedNormal)); + outNormals[i] = glm::vec3(CesiumUtility::AttributeCompression::octDecode( + encodedNormal.x, + encodedNormal.y)); } } else { std::memcpy( @@ -965,6 +986,31 @@ void parseNormalsFromFeatureTableBinary( } } +void parseBatchIdsFromFeatureTableBinary( + const gsl::span& featureTableBinaryData, + PntsContent& parsedContent) { + PntsSemantic& batchId = parsedContent.batchId.value(); + std::vector& batchIdData = batchId.data; + if (batchIdData.size() > 0) { + // If data isn't empty, it must have been decoded from Draco. + return; + } + + const uint32_t pointsLength = parsedContent.pointsLength; + size_t batchIdsByteStride = sizeof(uint16_t); + if (parsedContent.batchIdComponentType) { + batchIdsByteStride = Accessor::computeByteSizeOfComponent( + parsedContent.batchIdComponentType.value()); + } + const size_t batchIdsByteLength = pointsLength * batchIdsByteStride; + batchIdData.resize(batchIdsByteLength); + + std::memcpy( + batchIdData.data(), + featureTableBinaryData.data() + batchId.byteOffset, + batchIdsByteLength); +} + void parseFeatureTableBinary( const gsl::span& featureTableBinaryData, PntsContent& parsedContent) { @@ -976,12 +1022,14 @@ void parseFeatureTableBinary( if (parsedContent.normal) { parseNormalsFromFeatureTableBinary(featureTableBinaryData, parsedContent); } + if (parsedContent.batchId) { + parseBatchIdsFromFeatureTableBinary(featureTableBinaryData, parsedContent); + } } -int32_t -createBufferInGltf(CesiumGltf::Model& gltf, std::vector& buffer) { +int32_t createBufferInGltf(Model& gltf, std::vector& buffer) { size_t bufferId = gltf.buffers.size(); - CesiumGltf::Buffer& gltfBuffer = gltf.buffers.emplace_back(); + Buffer& gltfBuffer = gltf.buffers.emplace_back(); gltfBuffer.byteLength = static_cast(buffer.size()); gltfBuffer.cesium.data = std::move(buffer); @@ -989,29 +1037,29 @@ createBufferInGltf(CesiumGltf::Model& gltf, std::vector& buffer) { } int32_t createBufferViewInGltf( - CesiumGltf::Model& gltf, + Model& gltf, const int32_t bufferId, const int64_t byteLength, const int64_t byteStride) { size_t bufferViewId = gltf.bufferViews.size(); - CesiumGltf::BufferView& bufferView = gltf.bufferViews.emplace_back(); + BufferView& bufferView = gltf.bufferViews.emplace_back(); bufferView.buffer = bufferId; bufferView.byteLength = byteLength; bufferView.byteOffset = 0; bufferView.byteStride = byteStride; - bufferView.target = CesiumGltf::BufferView::Target::ARRAY_BUFFER; + bufferView.target = BufferView::Target::ARRAY_BUFFER; return static_cast(bufferViewId); } int32_t createAccessorInGltf( - CesiumGltf::Model& gltf, + Model& gltf, const int32_t bufferViewId, const int32_t componentType, const int64_t count, const std::string type) { size_t accessorId = gltf.accessors.size(); - CesiumGltf::Accessor& accessor = gltf.accessors.emplace_back(); + Accessor& accessor = gltf.accessors.emplace_back(); accessor.bufferView = bufferViewId; accessor.byteOffset = 0; accessor.componentType = componentType; @@ -1021,7 +1069,7 @@ int32_t createAccessorInGltf( return static_cast(accessorId); } -void addPositionsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { +void addPositionsToGltf(PntsContent& parsedContent, Model& gltf) { const int64_t count = static_cast(parsedContent.pointsLength); const int64_t byteStride = static_cast(sizeof(glm ::vec3)); const int64_t byteLength = static_cast(byteStride * count); @@ -1031,12 +1079,11 @@ void addPositionsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { int32_t accessorId = createAccessorInGltf( gltf, bufferViewId, - CesiumGltf::Accessor::ComponentType::FLOAT, + Accessor::ComponentType::FLOAT, count, - CesiumGltf::Accessor::Type::VEC3); + Accessor::Type::VEC3); - CesiumGltf::Accessor& accessor = - gltf.accessors[static_cast(accessorId)]; + Accessor& accessor = gltf.accessors[static_cast(accessorId)]; accessor.min = { parsedContent.positionMin.x, parsedContent.positionMin.y, @@ -1048,105 +1095,127 @@ void addPositionsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { parsedContent.positionMax.z, }; - CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; + MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; primitive.attributes.emplace("POSITION", accessorId); } -void addColorsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { - if (parsedContent.color) { - PntsSemantic& color = parsedContent.color.value(); +void addColorsToGltf(PntsContent& parsedContent, Model& gltf) { + PntsSemantic& color = parsedContent.color.value(); - const int64_t count = static_cast(parsedContent.pointsLength); - int64_t byteStride = 0; - int32_t componentType = 0; - std::string type; - bool isTranslucent = false; - bool isNormalized = false; - - if (parsedContent.colorType == PntsColorType::RGBA) { - byteStride = static_cast(sizeof(glm::u8vec4)); - componentType = CesiumGltf::Accessor::ComponentType::UNSIGNED_BYTE; - type = CesiumGltf::Accessor::Type::VEC4; - isTranslucent = true; - isNormalized = true; - } else if (parsedContent.colorType == PntsColorType::RGB) { - byteStride = static_cast(sizeof(glm::u8vec3)); - componentType = CesiumGltf::Accessor::ComponentType::UNSIGNED_BYTE; - isNormalized = true; - type = CesiumGltf::Accessor::Type::VEC3; - } else if (parsedContent.colorType == PntsColorType::RGB565) { - byteStride = static_cast(sizeof(glm::vec3)); - componentType = CesiumGltf::Accessor::ComponentType::FLOAT; - type = CesiumGltf::Accessor::Type::VEC3; - } + const int64_t count = static_cast(parsedContent.pointsLength); + int64_t byteStride = 0; + int32_t componentType = 0; + std::string type; + bool isTranslucent = false; + bool isNormalized = false; - const int64_t byteLength = static_cast(byteStride * count); - int32_t bufferId = createBufferInGltf(gltf, color.data); - int32_t bufferViewId = - createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); - int32_t accessorId = - createAccessorInGltf(gltf, bufferViewId, componentType, count, type); + if (parsedContent.colorType == PntsColorType::RGBA) { + byteStride = static_cast(sizeof(glm::u8vec4)); + componentType = Accessor::ComponentType::UNSIGNED_BYTE; + type = Accessor::Type::VEC4; + isTranslucent = true; + isNormalized = true; + } else if (parsedContent.colorType == PntsColorType::RGB) { + byteStride = static_cast(sizeof(glm::u8vec3)); + componentType = Accessor::ComponentType::UNSIGNED_BYTE; + isNormalized = true; + type = Accessor::Type::VEC3; + } else if (parsedContent.colorType == PntsColorType::RGB565) { + byteStride = static_cast(sizeof(glm::vec3)); + componentType = Accessor::ComponentType::FLOAT; + type = Accessor::Type::VEC3; + } - CesiumGltf::Accessor& accessor = - gltf.accessors[static_cast(accessorId)]; - accessor.normalized = isNormalized; + const int64_t byteLength = static_cast(byteStride * count); + int32_t bufferId = createBufferInGltf(gltf, color.data); + int32_t bufferViewId = + createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); + int32_t accessorId = + createAccessorInGltf(gltf, bufferViewId, componentType, count, type); - CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; - primitive.attributes.emplace("COLOR_0", accessorId); + Accessor& accessor = gltf.accessors[static_cast(accessorId)]; + accessor.normalized = isNormalized; - if (isTranslucent) { - CesiumGltf::Material& material = - gltf.materials[static_cast(primitive.material)]; - material.alphaMode = CesiumGltf::Material::AlphaMode::BLEND; - } + MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; + primitive.attributes.emplace("COLOR_0", accessorId); + + if (isTranslucent) { + Material& material = + gltf.materials[static_cast(primitive.material)]; + material.alphaMode = Material::AlphaMode::BLEND; } } -void addNormalsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { - if (parsedContent.normal) { - PntsSemantic& normal = parsedContent.normal.value(); +void addNormalsToGltf(PntsContent& parsedContent, Model& gltf) { + PntsSemantic& normal = parsedContent.normal.value(); - const int64_t count = static_cast(parsedContent.pointsLength); - const int64_t byteStride = static_cast(sizeof(glm ::vec3)); - const int64_t byteLength = static_cast(byteStride * count); - - int32_t bufferId = createBufferInGltf(gltf, normal.data); - int32_t bufferViewId = - createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); - int32_t accessorId = createAccessorInGltf( - gltf, - bufferViewId, - CesiumGltf::Accessor::ComponentType::FLOAT, - count, - CesiumGltf::Accessor::Type::VEC3); - - CesiumGltf::MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; - primitive.attributes.emplace("NORMAL", accessorId); + const int64_t count = static_cast(parsedContent.pointsLength); + const int64_t byteStride = static_cast(sizeof(glm ::vec3)); + const int64_t byteLength = static_cast(byteStride * count); + + int32_t bufferId = createBufferInGltf(gltf, normal.data); + int32_t bufferViewId = + createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); + int32_t accessorId = createAccessorInGltf( + gltf, + bufferViewId, + Accessor::ComponentType::FLOAT, + count, + Accessor::Type::VEC3); + + MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; + primitive.attributes.emplace("NORMAL", accessorId); +} + +void addBatchIdsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { + PntsSemantic& batchId = parsedContent.batchId.value(); + + const int64_t count = static_cast(parsedContent.pointsLength); + int32_t componentType = Accessor::ComponentType::UNSIGNED_SHORT; + if (parsedContent.batchIdComponentType) { + componentType = parsedContent.batchIdComponentType.value(); } + const int64_t byteStride = + Accessor::computeByteSizeOfComponent(componentType); + const int64_t byteLength = static_cast(byteStride * count); + + int32_t bufferId = createBufferInGltf(gltf, batchId.data); + int32_t bufferViewId = + createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); + int32_t accessorId = createAccessorInGltf( + gltf, + bufferViewId, + componentType, + count, + Accessor::Type::SCALAR); + + MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; + // This will be renamed by BatchTableToGltfFeatureMetadata. + primitive.attributes.emplace("_BATCHID", accessorId); } void createGltfFromParsedContent( PntsContent& parsedContent, GltfConverterResult& result) { result.model = std::make_optional(); - CesiumGltf::Model& gltf = result.model.value(); + Model& gltf = result.model.value(); // Create a single node with a single mesh, with a single primitive. - CesiumGltf::Node& node = gltf.nodes.emplace_back(); + Node& node = gltf.nodes.emplace_back(); std::memcpy( node.matrix.data(), &CesiumGeometry::AxisTransforms::Z_UP_TO_Y_UP, sizeof(glm::dmat4)); size_t meshId = gltf.meshes.size(); - CesiumGltf::Mesh& mesh = gltf.meshes.emplace_back(); + Mesh& mesh = gltf.meshes.emplace_back(); node.mesh = static_cast(meshId); - CesiumGltf::MeshPrimitive& primitive = mesh.primitives.emplace_back(); - primitive.mode = CesiumGltf::MeshPrimitive::Mode::POINTS; + MeshPrimitive& primitive = mesh.primitives.emplace_back(); + primitive.mode = MeshPrimitive::Mode::POINTS; size_t materialId = gltf.materials.size(); - CesiumGltf::Material& material = gltf.materials.emplace_back(); + Material& material = gltf.materials.emplace_back(); material.pbrMetallicRoughness = std::make_optional(); // These values are borrowed from CesiumJS. @@ -1177,6 +1246,10 @@ void createGltfFromParsedContent( material.addExtension(); } + if (parsedContent.batchId) { + addBatchIdsToGltf(parsedContent, gltf); + } + if (parsedContent.rtcCenter) { // Add the RTC_CENTER value to the glTF as a CESIUM_RTC extension. // This matches what B3dmToGltfConverter does. In the future, @@ -1211,11 +1284,12 @@ void convertPntsContentToGltf( // If the 3DTILES_draco_point_compression extension is present, // the batch table's binary will be compressed with the feature // table's binary. Parse both JSONs first in case the extension is there. + const int64_t batchTableStart = headerLength + + header.featureTableJsonByteLength + + header.featureTableBinaryByteLength; + rapidjson::Document batchTableJson; if (header.batchTableJsonByteLength > 0) { - const int64_t batchTableStart = headerLength + - header.featureTableJsonByteLength + - header.featureTableBinaryByteLength; const gsl::span batchTableJsonData = pntsBinary.subspan( static_cast(batchTableStart), header.batchTableJsonByteLength); @@ -1233,13 +1307,25 @@ void convertPntsContentToGltf( headerLength + header.featureTableJsonByteLength), header.featureTableBinaryByteLength); - /*const gsl::span batchTableBinaryData = - pntsBinary.subspan( - static_cast(batchTableStart + header.batchTableJsonByteLength), - header.batchTableBinaryByteLength);*/ - parseFeatureTableBinary(featureTableBinaryData, parsedContent); createGltfFromParsedContent(parsedContent, result); + + if (header.batchTableJsonByteLength > 0) { + gsl::span batchTableBinaryData; + // check if data was manipulated earlier + if (header.batchTableBinaryByteLength > 0) { + batchTableBinaryData = pntsBinary.subspan( + static_cast( + batchTableStart + header.batchTableJsonByteLength), + header.batchTableBinaryByteLength); + } + + result.errors.merge(BatchTableToGltfFeatureMetadata::convert( + featureTableJson, + batchTableJson, + batchTableBinaryData, + result.model.value())); + } } } } // namespace diff --git a/Cesium3DTilesSelection/src/QuantizedMeshLoader.cpp b/Cesium3DTilesSelection/src/QuantizedMeshLoader.cpp index 7910342b5..326acbc11 100644 --- a/Cesium3DTilesSelection/src/QuantizedMeshLoader.cpp +++ b/Cesium3DTilesSelection/src/QuantizedMeshLoader.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -151,26 +152,6 @@ static T readValue( return defaultValue; } -static glm::dvec3 octDecode(uint8_t x, uint8_t y) { - constexpr uint8_t rangeMax = 255; - - glm::dvec3 result; - - result.x = CesiumUtility::Math::fromSNorm(x, rangeMax); - result.y = CesiumUtility::Math::fromSNorm(y, rangeMax); - result.z = 1.0 - (glm::abs(result.x) + glm::abs(result.y)); - - if (result.z < 0.0) { - const double oldVX = result.x; - result.x = - (1.0 - glm::abs(result.y)) * CesiumUtility::Math::signNotZero(oldVX); - result.y = - (1.0 - glm::abs(oldVX)) * CesiumUtility::Math::signNotZero(result.y); - } - - return glm::normalize(result); -} - static QuantizedMeshMetadataResult processMetadata(const QuadtreeTileID& tileID, gsl::span json); @@ -620,7 +601,7 @@ static void decodeNormals( size_t normalOutputIndex = 0; for (size_t i = 0; i < encoded.size(); i += 2) { - glm::dvec3 normal = octDecode( + glm::dvec3 normal = AttributeCompression::octDecode( static_cast(encoded[i]), static_cast(encoded[i + 1])); decoded[normalOutputIndex++] = static_cast(normal.x); diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index 504e5d0dd..fb7331e3b 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -50,7 +50,15 @@ static void checkBufferContents( static_cast(expectedValue), Math::Epsilon6)); } + } else if constexpr (std::is_floating_point_v) { + for (size_t i = 0; i < expected.size(); ++i) { + const Type& value = + *reinterpret_cast(buffer.data() + i * byteStride); + const Type& expectedValue = expected[i]; + CHECK(value == Approx(expectedValue)); + } } else if constexpr ( + std::is_integral_v || std::is_same_v || std::is_same_v || std::is_same_v) { for (size_t i = 0; i < expected.size(); ++i) { const Type& value = @@ -75,7 +83,6 @@ static void checkAttribute( const int32_t accessorId = attributes.at(attributeSemantic); REQUIRE(accessorId >= 0); const uint32_t accessorIdUint = static_cast(accessorId); - REQUIRE(accessorIdUint < gltf.accessors.size()); const Accessor& accessor = gltf.accessors[accessorIdUint]; int32_t expectedComponentType = -1; @@ -90,6 +97,9 @@ static void checkAttribute( } else if constexpr (std::is_same_v) { expectedComponentType = Accessor::ComponentType::UNSIGNED_BYTE; expectedType = Accessor::Type::VEC4; + } else if constexpr (std::is_same_v) { + expectedComponentType = Accessor::ComponentType::UNSIGNED_BYTE; + expectedType = Accessor::Type::SCALAR; } else { FAIL("Accessor check has not been implemented for the given type."); } @@ -99,12 +109,12 @@ static void checkAttribute( CHECK(accessor.count == expectedCount); CHECK(accessor.type == expectedType); - const int32_t expectedByteLength = expectedCount * sizeof(Type); + const int64_t expectedByteLength = + static_cast(expectedCount * sizeof(Type)); const int32_t bufferViewId = accessor.bufferView; REQUIRE(bufferViewId >= 0); const uint32_t bufferViewIdUint = static_cast(bufferViewId); - REQUIRE(bufferViewIdUint < gltf.bufferViews.size()); const BufferView& bufferView = gltf.bufferViews[bufferViewIdUint]; CHECK(bufferView.byteLength == expectedByteLength); CHECK(bufferView.byteOffset == 0); @@ -112,8 +122,6 @@ static void checkAttribute( const int32_t bufferId = bufferView.buffer; REQUIRE(bufferId >= 0); const uint32_t bufferIdUint = static_cast(bufferId); - REQUIRE(bufferIdUint < gltf.buffers.size()); - const Buffer& buffer = gltf.buffers[static_cast(bufferIdUint)]; CHECK(buffer.byteLength == expectedByteLength); CHECK(static_cast(buffer.cesium.data.size()) == buffer.byteLength); @@ -161,6 +169,7 @@ TEST_CASE("Converts simple point cloud to glTF") { Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); CHECK(primitive.material == 0); // Check for single material @@ -452,6 +461,76 @@ TEST_CASE("Converts point cloud with CONSTANT_RGBA") { CHECK(material.hasExtension()); } +TEST_CASE("Converts point cloud with quantized positions to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudQuantized.pnts"; + const int32_t pointsLength = 8; + const int32_t expectedAttributeCount = 2; + + GltfConverterResult result = loadPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(!gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(material.hasExtension()); + + REQUIRE(gltf.accessors.size() == expectedAttributeCount); + REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); + REQUIRE(gltf.buffers.size() == expectedAttributeCount); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == expectedAttributeCount); + + // Check that position and color attributes are present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + + // Check position attribute more thoroughly + uint32_t positionAccessorId = + static_cast(attributes.at("POSITION")); + Accessor& positionAccessor = gltf.accessors[positionAccessorId]; + CHECK(!positionAccessor.normalized); + + const glm::vec3 expectedMin(1215009.59, -4736317.08, 4081601.7); + CHECK(positionAccessor.min[0] == Approx(expectedMin.x)); + CHECK(positionAccessor.min[1] == Approx(expectedMin.y)); + CHECK(positionAccessor.min[2] == Approx(expectedMin.z)); + + const glm::vec3 expectedMax(1215016.18, -4736309.02, 4081608.74); + CHECK(positionAccessor.max[0] == Approx(expectedMax.x)); + CHECK(positionAccessor.max[1] == Approx(expectedMax.y)); + CHECK(positionAccessor.max[2] == Approx(expectedMax.z)); + + uint32_t positionBufferViewId = + static_cast(positionAccessor.bufferView); + BufferView& positionBufferView = gltf.bufferViews[positionBufferViewId]; + + uint32_t positionBufferId = static_cast(positionBufferView.buffer); + Buffer& positionBuffer = gltf.buffers[positionBufferId]; + + const std::vector expectedPositions = { + glm::vec3(1215010.39, -4736313.38, 4081601.7), + glm::vec3(1215015.23, -4736312.13, 4081601.7), + glm::vec3(1215009.59, -4736310.26, 4081605.53), + glm::vec3(1215014.43, -4736309.02, 4081605.53), + glm::vec3(1215011.34, -4736317.08, 4081604.92), + glm::vec3(1215016.18, -4736315.84, 4081604.92), + glm::vec3(1215010.54, -4736313.97, 4081608.74), + glm::vec3(1215015.38, -4736312.73, 4081608.74)}; + + checkBufferContents(positionBuffer.cesium.data, expectedPositions); +} + TEST_CASE("Converts point cloud with normals to glTF") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "PointCloud" / "pointCloudNormals.pnts"; @@ -572,72 +651,174 @@ TEST_CASE("Converts point cloud with oct-encoded normals to glTF") { checkBufferContents(normalBuffer.cesium.data, expectedNormals); } -TEST_CASE("Converts point cloud with quantized positions to glTF") { +std::set getUniqueBufferViewIds( + const std::vector& accessors, + std::optional featureTable) { + std::set result; + for (auto it = accessors.begin(); it != accessors.end(); it++) { + result.insert(it->bufferView); + } + + if (featureTable) { + auto& properties = featureTable.value().properties; + for (auto it = properties.begin(); it != properties.end(); it++) { + auto& property = it->second; + result.insert(property.bufferView); + if (property.arrayOffsetBufferView >= 0) { + result.insert(property.arrayOffsetBufferView); + } + if (property.stringOffsetBufferView >= 0) { + result.insert(property.stringOffsetBufferView); + } + } + } + + return result; +} + +std::set +getUniqueBufferIds(const std::vector& bufferViews) { + std::set result; + for (auto it = bufferViews.begin(); it != bufferViews.end(); it++) { + result.insert(it->buffer); + } + + return result; +} + +static void checkModelFeatureMetadataExtension(Model& gltf) { + REQUIRE(gltf.hasExtension()); + const auto pExtension = gltf.getExtension(); + + // Check the schema + REQUIRE(pExtension->schema); + REQUIRE(pExtension->schema->classes.size() == 1); + + auto firstClassIt = pExtension->schema->classes.begin(); + CHECK(firstClassIt->first == "default"); + + CesiumGltf::Class& defaultClass = firstClassIt->second; + REQUIRE(defaultClass.properties.size() == 3); + + auto nameItClass = defaultClass.properties.find("name"); + REQUIRE(nameItClass != defaultClass.properties.end()); + auto dimensionsItClass = defaultClass.properties.find("dimensions"); + REQUIRE(dimensionsItClass != defaultClass.properties.end()); + auto idItClass = defaultClass.properties.find("id"); + REQUIRE(idItClass != defaultClass.properties.end()); + + CHECK(nameItClass->second.type == "STRING"); + CHECK(dimensionsItClass->second.type == "ARRAY"); + REQUIRE(dimensionsItClass->second.componentType); + CHECK(dimensionsItClass->second.componentType.value() == "FLOAT32"); + CHECK(idItClass->second.type == "UINT32"); + + // Check the feature table + auto firstFeatureTableIt = pExtension->featureTables.begin(); + REQUIRE(firstFeatureTableIt != pExtension->featureTables.end()); + + FeatureTable& featureTable = firstFeatureTableIt->second; + CHECK(featureTable.classProperty == "default"); + REQUIRE(featureTable.properties.size() == 3); + + auto nameItTable = featureTable.properties.find("name"); + REQUIRE(nameItTable != featureTable.properties.end()); + auto dimensionsItTable = featureTable.properties.find("dimensions"); + REQUIRE(dimensionsItTable != featureTable.properties.end()); + auto idItTable = featureTable.properties.find("id"); + REQUIRE(idItTable != featureTable.properties.end()); + + CHECK(nameItTable->second.bufferView >= 0); + CHECK( + nameItTable->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(dimensionsItTable->second.bufferView >= 0); + CHECK( + dimensionsItTable->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(idItTable->second.bufferView >= 0); + CHECK( + idItTable->second.bufferView < + static_cast(gltf.bufferViews.size())); + + std::set bufferViewSet = + getUniqueBufferViewIds(gltf.accessors, featureTable); + CHECK(bufferViewSet.size() == gltf.bufferViews.size()); +} + +TEST_CASE( + "Converts point cloud with batch IDs to glTF with EXT_feature_metadata") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; - testFilePath = testFilePath / "PointCloud" / "pointCloudQuantized.pnts"; + testFilePath = testFilePath / "PointCloud" / "pointCloudBatched.pnts"; const int32_t pointsLength = 8; - const int32_t expectedAttributeCount = 2; GltfConverterResult result = loadPnts(testFilePath); REQUIRE(result.model); Model& gltf = *result.model; - CHECK(!gltf.hasExtension()); CHECK(gltf.nodes.size() == 1); - REQUIRE(gltf.meshes.size() == 1); Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; - REQUIRE(gltf.materials.size() == 1); - Material& material = gltf.materials[0]; - CHECK(material.hasExtension()); - - REQUIRE(gltf.accessors.size() == expectedAttributeCount); - REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); - REQUIRE(gltf.buffers.size() == expectedAttributeCount); + REQUIRE(primitive.hasExtension()); + const auto primitiveExtension = + primitive.getExtension(); + REQUIRE(primitiveExtension->featureIdAttributes.size() == 1); + FeatureIDAttribute& attribute = primitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + CHECK(attribute.featureIds.attribute == "_FEATURE_ID_0"); + + CHECK(gltf.materials.size() == 1); + + // The file has three metadata properties: + // - "name": string array in JSON + // - "dimensions": vec3 array in binary + // - "id": int array in binary + checkModelFeatureMetadataExtension(gltf); + + // There are only three accessors (one per primitive attribute)... + REQUIRE(gltf.accessors.size() == 3); + + // ...but there are four additional buffer views: + // - string data buffer view + // - string offsets buffer view + // - first binary property buffer view + // - second binary property buffer view + REQUIRE(gltf.bufferViews.size() == 7); + + // There are also three added buffers: + // - binary data in the batch table + // - string data of the property in the json value + // - string offsets for the same property + REQUIRE(gltf.buffers.size() == 6); + std::set bufferSet = getUniqueBufferIds(gltf.bufferViews); + CHECK(bufferSet.size() == 6); auto attributes = primitive.attributes; - REQUIRE(attributes.size() == expectedAttributeCount); + REQUIRE(attributes.size() == 3); - // Check that position and color attributes are present + // Check that position, normal, and feature ID attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); - - // Check position attribute more thoroughly - uint32_t positionAccessorId = - static_cast(attributes.at("POSITION")); - Accessor& positionAccessor = gltf.accessors[positionAccessorId]; - CHECK(!positionAccessor.normalized); - - const glm::vec3 expectedMin(1215009.59, -4736317.08, 4081601.7); - CHECK(positionAccessor.min[0] == Approx(expectedMin.x)); - CHECK(positionAccessor.min[1] == Approx(expectedMin.y)); - CHECK(positionAccessor.min[2] == Approx(expectedMin.z)); + checkAttribute(gltf, primitive, "NORMAL", pointsLength); + checkAttribute(gltf, primitive, "_FEATURE_ID_0", pointsLength); - const glm::vec3 expectedMax(1215016.18, -4736309.02, 4081608.74); - CHECK(positionAccessor.max[0] == Approx(expectedMax.x)); - CHECK(positionAccessor.max[1] == Approx(expectedMax.y)); - CHECK(positionAccessor.max[2] == Approx(expectedMax.z)); + // Check feature ID attribute more thoroughly + uint32_t featureIdAccessorId = + static_cast(attributes.at("_FEATURE_ID_0")); + Accessor& featureIdAccessor = gltf.accessors[featureIdAccessorId]; - uint32_t positionBufferViewId = - static_cast(positionAccessor.bufferView); - BufferView& positionBufferView = gltf.bufferViews[positionBufferViewId]; + uint32_t featureIdBufferViewId = + static_cast(featureIdAccessor.bufferView); + BufferView& featureIdBufferView = gltf.bufferViews[featureIdBufferViewId]; - uint32_t positionBufferId = static_cast(positionBufferView.buffer); - Buffer& positionBuffer = gltf.buffers[positionBufferId]; + uint32_t featureIdBufferId = + static_cast(featureIdBufferView.buffer); + Buffer& featureIdBuffer = gltf.buffers[featureIdBufferId]; - const std::vector expectedPositions = { - glm::vec3(1215010.39, -4736313.38, 4081601.7), - glm::vec3(1215015.23, -4736312.13, 4081601.7), - glm::vec3(1215009.59, -4736310.26, 4081605.53), - glm::vec3(1215014.43, -4736309.02, 4081605.53), - glm::vec3(1215011.34, -4736317.08, 4081604.92), - glm::vec3(1215016.18, -4736315.84, 4081604.92), - glm::vec3(1215010.54, -4736313.97, 4081608.74), - glm::vec3(1215015.38, -4736312.73, 4081608.74)}; + const std::vector expectedFeatureIDs = {5, 5, 6, 6, 7, 0, 3, 1}; - checkBufferContents(positionBuffer.cesium.data, expectedPositions); + checkBufferContents(featureIdBuffer.cesium.data, expectedFeatureIDs); } diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudBatched.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudBatched.pnts new file mode 100644 index 0000000000000000000000000000000000000000..db6b93950fab544d7989ee2c8e9c9d8287de5d58 GIT binary patch literal 824 zcmXTOD=B7VU|_IdW?*;%q)!0x2Ow?$;%cP;|6tD$Pk%opt7@gB%92$7w6x;X5+y5x zS{)@n{~%vSA9NW@GoX}{V~Ddyyr&Dgq@ksej#6@dZb5!tYF6H9v#Vc-Ymkyvw4srqsez%9o`r>x zg@w79xv_Mp{1p%rHPJ-frX)&fvKL6k)e^9fr(Kp%vEq>YZX8s za@E`ePYOcq*_P?qwJfze@JUX`VbvWwhlK&A2VB~!4$LiIvVYNT%LAD<=?6}^eXwty z&~ZRBLUsRwcEtnlZxuQuTFcw-+2QM8{7B#acJ$Q!{;abO+!9^1&r)ob!`j~E_6MD7 z_HRF+WyiXM+cv93Vt@1|PWz4x683()g8Q?wJ@!AD)Vxo6n#g{IzZUx!Ssbvt)DX45 z-Hm0RuO;LDPt}h08At!wo}8>=|7{D8UEI&SeNXes_KU7=-20`o%>E-+n;k1F8yhHNlAAiRXpaCV2NDgy#HU`CQW(vAqOEXN(@FW3D2naKRogIA~gOqB)5qHP( zlWkJ9h5g-&%JvTbMeI#Z3)y*38$o*&u-XG^1EVpu= + +namespace CesiumUtility { +/** + * @brief Functions to handle compressed attributes in different formats + */ +class CESIUMUTILITY_API AttributeCompression final { + +public: + /** + * @brief Decodes a unit-length vector in 'oct' encoding to a normalized + * 3-component vector. + * + * @param x The x component of the oct-encoded unit length vector. + * @param y The y component of the oct-encoded unit length vector. + * @param rangeMax The maximum value of the SNORM range. The encoded vector is + * stored in log2(rangeMax+1) bits. + * @returns The decoded and normalized vector. + */ + template < + typename T, + class = typename std::enable_if::value>::type> + static glm::dvec3 octDecodeInRange(T x, T y, T rangeMax) { + glm::dvec3 result; + + result.x = CesiumUtility::Math::fromSNorm(x, rangeMax); + result.y = CesiumUtility::Math::fromSNorm(y, rangeMax); + result.z = 1.0 - (glm::abs(result.x) + glm::abs(result.y)); + + if (result.z < 0.0) { + const double oldVX = result.x; + result.x = + (1.0 - glm::abs(result.y)) * CesiumUtility::Math::signNotZero(oldVX); + result.y = + (1.0 - glm::abs(oldVX)) * CesiumUtility::Math::signNotZero(result.y); + } + + return glm::normalize(result); + } + + /** + * Decodes a unit-length vector in 2 byte 'oct' encoding to a normalized + * 3-component vector. + * + * @param x The x component of the oct-encoded unit length vector. + * @param y The y component of the oct-encoded unit length vector. + * @returns The decoded and normalized vector. + * + * @exception {DeveloperError} x and y must be an unsigned normalized integer between 0 and 255. + * + * @see AttributeCompression::octDecodeInRange + */ + static glm::dvec3 octDecode(uint8_t x, uint8_t y) { + constexpr uint8_t rangeMax = 255; + return AttributeCompression::octDecodeInRange(x, y, rangeMax); + } + + /** + * @brief Decodes a RGB565-encoded color to a 3-component vector + * containing the normalized RGB values. + * + * @param value The RGB565-encoded value. + * @returns The normalized RGB values. + */ + static glm::dvec3 decodeRGB565(const uint16_t value) { + constexpr uint16_t mask5 = (1 << 5) - 1; + constexpr uint16_t mask6 = (1 << 6) - 1; + constexpr float normalize5 = 1.0f / 31.0f; // normalize [0, 31] to [0, 1] + constexpr float normalize6 = 1.0f / 63.0f; // normalize [0, 63] to [0, 1] + + const uint16_t red = static_cast(value >> 11); + const uint16_t green = static_cast(value >> 5 & mask6); + const uint16_t blue = value & mask5; + + return glm::dvec3(red * normalize5, green * normalize6, blue * normalize5); + }; +}; + +} // namespace CesiumUtility From 63ee02417b93c8157ff1cd331c7feb944392bc88 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 27 Jan 2023 17:48:04 -0500 Subject: [PATCH 08/20] Refactor BatchTableToGltfFeatureMetadata to handle pnts --- .../src/B3dmToGltfConverter.cpp | 2 +- .../src/BatchTableToGltfFeatureMetadata.cpp | 168 ++++++++++--- .../src/BatchTableToGltfFeatureMetadata.h | 8 +- .../src/PntsToGltfConverter.cpp | 24 +- .../test/ConvertTileToGltf.h | 21 ++ .../test/TestPntsToGltfConverter.cpp | 221 +++++++----------- ...tUpgradeBatchTableToExtFeatureMetadata.cpp | 221 ++++++++++++++++-- .../pointCloudWithPerPointProperties.pnts | Bin 0 -> 592 bytes 8 files changed, 462 insertions(+), 203 deletions(-) create mode 100644 Cesium3DTilesSelection/test/ConvertTileToGltf.h create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudWithPerPointProperties.pnts diff --git a/Cesium3DTilesSelection/src/B3dmToGltfConverter.cpp b/Cesium3DTilesSelection/src/B3dmToGltfConverter.cpp index 7b708cd54..6ee772ca5 100644 --- a/Cesium3DTilesSelection/src/B3dmToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/B3dmToGltfConverter.cpp @@ -220,7 +220,7 @@ void convertB3dmMetadataToGltfFeatureMetadata( } // upgrade batch table to glTF feature metadata and append the result - result.errors.merge(BatchTableToGltfFeatureMetadata::convert( + result.errors.merge(BatchTableToGltfFeatureMetadata::convertFromB3dm( featureTableJson, batchTableJson, batchTableBinaryData, diff --git a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp index 8d814347f..c736d3b2b 100644 --- a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp +++ b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp @@ -45,7 +45,7 @@ struct CompatibleTypes { }; struct BinaryProperty { - int64_t b3dmByteOffset; + int64_t batchTableByteOffset; int64_t gltfByteOffset; int64_t byteLength; }; @@ -55,8 +55,8 @@ struct GltfFeatureTableType { size_t typeSize; }; -const std::map b3dmComponentTypeToGltfType = - { +const std::map + batchTableComponentTypeToGltfType = { {"BYTE", GltfFeatureTableType{"INT8", sizeof(int8_t)}}, {"UNSIGNED_BYTE", GltfFeatureTableType{"UINT8", sizeof(uint8_t)}}, {"SHORT", GltfFeatureTableType{"INT16", sizeof(int16_t)}}, @@ -1258,8 +1258,8 @@ void updateExtensionWithBinaryProperty( const std::string& componentType = componentTypeIt->value.GetString(); const std::string& type = typeIt->value.GetString(); - auto convertedTypeIt = b3dmComponentTypeToGltfType.find(componentType); - if (convertedTypeIt == b3dmComponentTypeToGltfType.end()) { + auto convertedTypeIt = batchTableComponentTypeToGltfType.find(componentType); + if (convertedTypeIt == batchTableComponentTypeToGltfType.end()) { return; } const GltfFeatureTableType& gltfType = convertedTypeIt->second; @@ -1297,7 +1297,7 @@ void updateExtensionWithBinaryProperty( featureTableProperty.bufferView = static_cast(gltf.bufferViews.size() - 1); - binaryProperty.b3dmByteOffset = byteOffset; + binaryProperty.batchTableByteOffset = byteOffset; binaryProperty.gltfByteOffset = gltfBufferOffset; binaryProperty.byteLength = static_cast(bufferView.byteLength); } @@ -1379,37 +1379,12 @@ void updateExtensionWithBatchTableHierarchy( } } -} // namespace - -ErrorList BatchTableToGltfFeatureMetadata::convert( - const rapidjson::Document& featureTableJson, +void convertBatchTableToGltfFeatureMetadataExtension( const rapidjson::Document& batchTableJson, const gsl::span& batchTableBinaryData, - CesiumGltf::Model& gltf) { - // Check to make sure a char of rapidjson is 1 byte - static_assert( - sizeof(rapidjson::Value::Ch) == 1, - "RapidJson::Value::Ch is not 1 byte"); - - ErrorList result; - - // Parse the b3dm batch table and convert it to the EXT_feature_metadata - // extension. - - // If the feature table is missing the BATCH_LENGTH semantic, ignore the batch - // table completely. - const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); - if (batchLengthIt == featureTableJson.MemberEnd() || - !batchLengthIt->value.IsInt64()) { - result.emplaceWarning( - "The B3DM has a batch table, but it is being ignored because there is " - "no BATCH_LENGTH semantic in the feature table or it is not an " - "integer."); - return result; - } - - const int64_t batchLength = batchLengthIt->value.GetInt64(); - + CesiumGltf::Model& gltf, + const int64_t featureCount, + ErrorList& result) { // Add the binary part of the batch table - if any - to the glTF as a buffer. // We will reallign this buffer later on int32_t gltfBufferIndex = -1; @@ -1430,7 +1405,7 @@ ErrorList BatchTableToGltfFeatureMetadata::convert( FeatureTable& featureTable = modelExtension.featureTables.emplace("default", FeatureTable()) .first->second; - featureTable.count = batchLength; + featureTable.count = featureCount; featureTable.classProperty = "default"; // Convert each regular property in the batch table @@ -1499,10 +1474,129 @@ ErrorList BatchTableToGltfFeatureMetadata::convert( for (const BinaryProperty& binaryProperty : binaryProperties) { std::memcpy( buffer.cesium.data.data() + binaryProperty.gltfByteOffset, - batchTableBinaryData.data() + binaryProperty.b3dmByteOffset, + batchTableBinaryData.data() + binaryProperty.batchTableByteOffset, static_cast(binaryProperty.byteLength)); } } +} + +} // namespace + +ErrorList BatchTableToGltfFeatureMetadata::convertFromPnts( + const rapidjson::Document& featureTableJson, + const rapidjson::Document& batchTableJson, + const gsl::span& batchTableBinaryData, + CesiumGltf::Model& gltf) { + // Check to make sure a char of rapidjson is 1 byte + static_assert( + sizeof(rapidjson::Value::Ch) == 1, + "RapidJson::Value::Ch is not 1 byte"); + + ErrorList result; + + // Parse the pnts batch table and convert it to the EXT_feature_metadata + // extension. + + const auto pointsLengthIt = featureTableJson.FindMember("POINTS_LENGTH"); + if (pointsLengthIt == featureTableJson.MemberEnd() || + !pointsLengthIt->value.IsInt64()) { + result.emplaceError("The PNTS cannot be parsed because there is no valid " + "POINTS_LENGTH semantic."); + return result; + } + + int64_t featureCount = 0; + const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); + const auto batchIdIt = featureTableJson.FindMember("BATCH_ID"); + + // If the feature table is missing the BATCH_LENGTH semantic, the batch table + // corresponds to per-point properties. + if (batchLengthIt != featureTableJson.MemberEnd() && + batchLengthIt->value.IsInt64()) { + featureCount = batchLengthIt->value.GetInt64(); + } else if ( + batchIdIt != featureTableJson.MemberEnd() && + batchIdIt->value.IsObject()) { + result.emplaceWarning( + "The PNTS has a batch table, but it is being ignored because there " + "is no valid BATCH_LENGTH semantic in the feature table, and " + "the BATCH_ID semantic is defined."); + return result; + } else { + featureCount = pointsLengthIt->value.GetInt64(); + } + + convertBatchTableToGltfFeatureMetadataExtension( + batchTableJson, + batchTableBinaryData, + gltf, + featureCount, + result); + + // Create the EXT_feature_metadata extension for the single mesh primitive. + assert(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + + assert(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + ExtensionMeshPrimitiveExtFeatureMetadata& extension = + primitive.addExtension(); + FeatureIDAttribute& attribute = extension.featureIdAttributes.emplace_back(); + attribute.featureTable = "default"; + + auto primitiveBatchIdIt = primitive.attributes.find("_BATCHID"); + if (primitiveBatchIdIt != primitive.attributes.end()) { + // If _BATCHID is present, rename the _BATCHID attribute to _FEATURE_ID_0 + primitive.attributes["_FEATURE_ID_0"] = primitiveBatchIdIt->second; + primitive.attributes.erase("_BATCHID"); + + attribute.featureIds.attribute = "_FEATURE_ID_0"; + } else { + // Otherwise, use implicit feature IDs to indicate the metadata is stored in + // per-point properties. + attribute.featureIds.constant = 0; + attribute.featureIds.divisor = 1; + } + + return result; +} + +ErrorList BatchTableToGltfFeatureMetadata::convertFromB3dm( + const rapidjson::Document& featureTableJson, + const rapidjson::Document& batchTableJson, + const gsl::span& batchTableBinaryData, + CesiumGltf::Model& gltf) { + // Check to make sure a char of rapidjson is 1 byte + static_assert( + sizeof(rapidjson::Value::Ch) == 1, + "RapidJson::Value::Ch is not 1 byte"); + + ErrorList result; + + // Parse the b3dm batch table and convert it to the EXT_feature_metadata + // extension. + + // If the feature table is missing the BATCH_LENGTH semantic, ignore the batch + // table completely. + const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); + if (batchLengthIt == featureTableJson.MemberEnd() || + !batchLengthIt->value.IsInt64()) { + result.emplaceWarning( + "The B3DM has a batch table, but it is being ignored because there is " + "no BATCH_LENGTH semantic in the feature table or it is not an " + "integer."); + return result; + } + + const int64_t batchLength = batchLengthIt->value.GetInt64(); + + convertBatchTableToGltfFeatureMetadataExtension( + batchTableJson, + batchTableBinaryData, + gltf, + batchLength, + result); // Create an EXT_feature_metadata extension for each primitive with a _BATCHID // attribute. diff --git a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.h b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.h index 136118870..65008f913 100644 --- a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.h +++ b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.h @@ -10,7 +10,13 @@ namespace Cesium3DTilesSelection { struct BatchTableToGltfFeatureMetadata { - static ErrorList convert( + static ErrorList convertFromB3dm( + const rapidjson::Document& featureTableJson, + const rapidjson::Document& batchTableJson, + const gsl::span& batchTableBinaryData, + CesiumGltf::Model& gltf); + + static ErrorList convertFromPnts( const rapidjson::Document& featureTableJson, const rapidjson::Document& batchTableJson, const gsl::span& batchTableBinaryData, diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index cb528a1e2..51053897a 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -95,7 +96,6 @@ struct PntsContent { std::optional quantizedVolumeOffset; std::optional quantizedVolumeScale; std::optional constantRgba; - std::optional batchLength; PntsSemantic position; // required by glTF spec @@ -330,7 +330,7 @@ void parseNormalsFromFeatureTableJson( } } -void parseBatchIdsAndLengthFromFeatureTableJson( +void parseBatchIdsFromFeatureTableJson( const rapidjson::Document& featureTableJson, PntsContent& parsedContent) { const auto batchIdIt = featureTableJson.FindMember("BATCH_ID"); @@ -370,17 +370,6 @@ void parseBatchIdsAndLengthFromFeatureTableJson( Accessor::ComponentType::UNSIGNED_SHORT; } } - - const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); - if (batchLengthIt != featureTableJson.MemberEnd() && - batchLengthIt->value.IsUint()) { - parsedContent.batchLength = - std::make_optional(batchLengthIt->value.GetUint()); - } else if (parsedContent.batchId) { - parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, BATCH_ID semantic is present but " - "no valid BATCH_LENGTH was found."); - } } void parseSemanticsFromFeatureTableJson( @@ -401,7 +390,7 @@ void parseSemanticsFromFeatureTableJson( return; } - parseBatchIdsAndLengthFromFeatureTableJson(featureTableJson, parsedContent); + parseBatchIdsFromFeatureTableJson(featureTableJson, parsedContent); if (parsedContent.errors) { return; } @@ -999,8 +988,9 @@ void parseBatchIdsFromFeatureTableBinary( const uint32_t pointsLength = parsedContent.pointsLength; size_t batchIdsByteStride = sizeof(uint16_t); if (parsedContent.batchIdComponentType) { - batchIdsByteStride = Accessor::computeByteSizeOfComponent( - parsedContent.batchIdComponentType.value()); + int8_t componentByteSize = Accessor::computeByteSizeOfComponent( + parsedContent.batchIdComponentType.value()); + batchIdsByteStride = static_cast(componentByteSize); } const size_t batchIdsByteLength = pointsLength * batchIdsByteStride; batchIdData.resize(batchIdsByteLength); @@ -1320,7 +1310,7 @@ void convertPntsContentToGltf( header.batchTableBinaryByteLength); } - result.errors.merge(BatchTableToGltfFeatureMetadata::convert( + result.errors.merge(BatchTableToGltfFeatureMetadata::convertFromPnts( featureTableJson, batchTableJson, batchTableBinaryData, diff --git a/Cesium3DTilesSelection/test/ConvertTileToGltf.h b/Cesium3DTilesSelection/test/ConvertTileToGltf.h new file mode 100644 index 000000000..2011ca5cb --- /dev/null +++ b/Cesium3DTilesSelection/test/ConvertTileToGltf.h @@ -0,0 +1,21 @@ +#pragma once + +#include "B3dmToGltfConverter.h" +#include "PntsToGltfConverter.h" +#include "readFile.h" + +#include + +namespace Cesium3DTilesSelection { + +class ConvertTileToGltf { +public: + static GltfConverterResult fromB3dm(const std::filesystem::path& filePath) { + return B3dmToGltfConverter::convert(readFile(filePath), {}); + } + + static GltfConverterResult fromPnts(const std::filesystem::path& filePath) { + return PntsToGltfConverter::convert(readFile(filePath), {}); + } +}; +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index fb7331e3b..a1373e36e 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -1,6 +1,5 @@ #include "BatchTableToGltfFeatureMetadata.h" -#include "PntsToGltfConverter.h" -#include "readFile.h" +#include "ConvertTileToGltf.h" #include #include @@ -127,16 +126,12 @@ static void checkAttribute( CHECK(static_cast(buffer.cesium.data.size()) == buffer.byteLength); } -GltfConverterResult loadPnts(const std::filesystem::path& filePath) { - return PntsToGltfConverter::convert(readFile(filePath), {}); -} - TEST_CASE("Converts simple point cloud to glTF") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "PointCloud" / "pointCloudPositionsOnly.pnts"; const int32_t pointsLength = 8; - GltfConverterResult result = loadPnts(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); REQUIRE(result.model); Model& gltf = *result.model; @@ -144,23 +139,13 @@ TEST_CASE("Converts simple point cloud to glTF") { // Check for single mesh node REQUIRE(gltf.nodes.size() == 1); Node& node = gltf.nodes[0]; + // clang-format off std::vector expectedMatrix = { - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - -1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0}; + 1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, -1.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + // clang-format on CHECK(node.matrix == expectedMatrix); CHECK(node.mesh == 0); @@ -246,7 +231,7 @@ TEST_CASE("Converts point cloud with RGBA to glTF") { const int32_t pointsLength = 8; const int32_t expectedAttributeCount = 2; - GltfConverterResult result = loadPnts(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); REQUIRE(result.model); Model& gltf = *result.model; @@ -305,7 +290,7 @@ TEST_CASE("Converts point cloud with RGB to glTF") { const int32_t pointsLength = 8; const int32_t expectedAttributeCount = 2; - GltfConverterResult result = loadPnts(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); REQUIRE(result.model); Model& gltf = *result.model; @@ -364,7 +349,7 @@ TEST_CASE("Converts point cloud with RGB565 to glTF") { const int32_t pointsLength = 8; const int32_t expectedAttributeCount = 2; - GltfConverterResult result = loadPnts(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); REQUIRE(result.model); Model& gltf = *result.model; @@ -421,7 +406,7 @@ TEST_CASE("Converts point cloud with RGB565 to glTF") { TEST_CASE("Converts point cloud with CONSTANT_RGBA") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "PointCloud" / "pointCloudConstantRGBA.pnts"; - GltfConverterResult result = loadPnts(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); const int32_t pointsLength = 8; REQUIRE(result.model); @@ -467,7 +452,7 @@ TEST_CASE("Converts point cloud with quantized positions to glTF") { const int32_t pointsLength = 8; const int32_t expectedAttributeCount = 2; - GltfConverterResult result = loadPnts(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); REQUIRE(result.model); Model& gltf = *result.model; @@ -537,7 +522,7 @@ TEST_CASE("Converts point cloud with normals to glTF") { const int32_t pointsLength = 8; const int32_t expectedAttributeCount = 3; - GltfConverterResult result = loadPnts(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); REQUIRE(result.model); Model& gltf = *result.model; @@ -597,7 +582,7 @@ TEST_CASE("Converts point cloud with oct-encoded normals to glTF") { const int32_t pointsLength = 8; const int32_t expectedAttributeCount = 3; - GltfConverterResult result = loadPnts(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); REQUIRE(result.model); Model& gltf = *result.model; @@ -651,31 +636,6 @@ TEST_CASE("Converts point cloud with oct-encoded normals to glTF") { checkBufferContents(normalBuffer.cesium.data, expectedNormals); } -std::set getUniqueBufferViewIds( - const std::vector& accessors, - std::optional featureTable) { - std::set result; - for (auto it = accessors.begin(); it != accessors.end(); it++) { - result.insert(it->bufferView); - } - - if (featureTable) { - auto& properties = featureTable.value().properties; - for (auto it = properties.begin(); it != properties.end(); it++) { - auto& property = it->second; - result.insert(property.bufferView); - if (property.arrayOffsetBufferView >= 0) { - result.insert(property.arrayOffsetBufferView); - } - if (property.stringOffsetBufferView >= 0) { - result.insert(property.stringOffsetBufferView); - } - } - } - - return result; -} - std::set getUniqueBufferIds(const std::vector& bufferViews) { std::set result; @@ -686,77 +646,21 @@ getUniqueBufferIds(const std::vector& bufferViews) { return result; } -static void checkModelFeatureMetadataExtension(Model& gltf) { - REQUIRE(gltf.hasExtension()); - const auto pExtension = gltf.getExtension(); - - // Check the schema - REQUIRE(pExtension->schema); - REQUIRE(pExtension->schema->classes.size() == 1); - - auto firstClassIt = pExtension->schema->classes.begin(); - CHECK(firstClassIt->first == "default"); - - CesiumGltf::Class& defaultClass = firstClassIt->second; - REQUIRE(defaultClass.properties.size() == 3); - - auto nameItClass = defaultClass.properties.find("name"); - REQUIRE(nameItClass != defaultClass.properties.end()); - auto dimensionsItClass = defaultClass.properties.find("dimensions"); - REQUIRE(dimensionsItClass != defaultClass.properties.end()); - auto idItClass = defaultClass.properties.find("id"); - REQUIRE(idItClass != defaultClass.properties.end()); - - CHECK(nameItClass->second.type == "STRING"); - CHECK(dimensionsItClass->second.type == "ARRAY"); - REQUIRE(dimensionsItClass->second.componentType); - CHECK(dimensionsItClass->second.componentType.value() == "FLOAT32"); - CHECK(idItClass->second.type == "UINT32"); - - // Check the feature table - auto firstFeatureTableIt = pExtension->featureTables.begin(); - REQUIRE(firstFeatureTableIt != pExtension->featureTables.end()); - - FeatureTable& featureTable = firstFeatureTableIt->second; - CHECK(featureTable.classProperty == "default"); - REQUIRE(featureTable.properties.size() == 3); - - auto nameItTable = featureTable.properties.find("name"); - REQUIRE(nameItTable != featureTable.properties.end()); - auto dimensionsItTable = featureTable.properties.find("dimensions"); - REQUIRE(dimensionsItTable != featureTable.properties.end()); - auto idItTable = featureTable.properties.find("id"); - REQUIRE(idItTable != featureTable.properties.end()); - - CHECK(nameItTable->second.bufferView >= 0); - CHECK( - nameItTable->second.bufferView < - static_cast(gltf.bufferViews.size())); - CHECK(dimensionsItTable->second.bufferView >= 0); - CHECK( - dimensionsItTable->second.bufferView < - static_cast(gltf.bufferViews.size())); - CHECK(idItTable->second.bufferView >= 0); - CHECK( - idItTable->second.bufferView < - static_cast(gltf.bufferViews.size())); - - std::set bufferViewSet = - getUniqueBufferViewIds(gltf.accessors, featureTable); - CHECK(bufferViewSet.size() == gltf.bufferViews.size()); -} - TEST_CASE( "Converts point cloud with batch IDs to glTF with EXT_feature_metadata") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "PointCloud" / "pointCloudBatched.pnts"; const int32_t pointsLength = 8; - GltfConverterResult result = loadPnts(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); REQUIRE(result.model); Model& gltf = *result.model; + // The correctness of the model extension is thoroughly tested in + // TestUpgradeBatchTableTo + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); REQUIRE(gltf.meshes.size() == 1); Mesh& mesh = gltf.meshes[0]; @@ -774,25 +678,23 @@ TEST_CASE( CHECK(gltf.materials.size() == 1); // The file has three metadata properties: - // - "name": string array in JSON - // - "dimensions": vec3 array in binary - // - "id": int array in binary - checkModelFeatureMetadataExtension(gltf); - - // There are only three accessors (one per primitive attribute)... + // - "name": string scalars in JSON + // - "dimensions": float vec3s in binary + // - "id": int scalars in binary + + // There are only three accessors (one per primitive attribute), + // but there are four additional buffer views: + // - "name" string data buffer view + // - "name" string offsets buffer view + // - "dimensions" buffer view + // - "id" buffer view REQUIRE(gltf.accessors.size() == 3); - - // ...but there are four additional buffer views: - // - string data buffer view - // - string offsets buffer view - // - first binary property buffer view - // - second binary property buffer view REQUIRE(gltf.bufferViews.size() == 7); // There are also three added buffers: // - binary data in the batch table - // - string data of the property in the json value - // - string offsets for the same property + // - string data of "name" + // - string offsets for the data for "name" REQUIRE(gltf.buffers.size() == 6); std::set bufferSet = getUniqueBufferIds(gltf.bufferViews); CHECK(bufferSet.size() == 6); @@ -819,6 +721,63 @@ TEST_CASE( Buffer& featureIdBuffer = gltf.buffers[featureIdBufferId]; const std::vector expectedFeatureIDs = {5, 5, 6, 6, 7, 0, 3, 1}; - checkBufferContents(featureIdBuffer.cesium.data, expectedFeatureIDs); } + +TEST_CASE("Converts point cloud with per-point properties to glTF with " + "EXT_feature_metadata") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = + testFilePath / "PointCloud" / "pointCloudWithPerPointProperties.pnts"; + const int32_t pointsLength = 8; + + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(gltf.nodes.size() == 1); + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + REQUIRE(primitive.hasExtension()); + const auto primitiveExtension = + primitive.getExtension(); + REQUIRE(primitiveExtension->featureIdAttributes.size() == 1); + FeatureIDAttribute& attribute = primitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + CHECK(attribute.featureIds.constant == 0); + CHECK(attribute.featureIds.divisor == 1); + CHECK(!attribute.featureIds.attribute); + + CHECK(gltf.materials.size() == 1); + + // The file has three binary metadata properties: + // - "temperature": float scalars + // - "secondaryColor": float vec3s + // - "id": unsigned short scalars + // checkFeatureMetadataExtensionForBatchedPointCloud(gltf); + + // There are only two accessors (one per primitive attribute). + REQUIRE(gltf.accessors.size() == 2); + + // There are three additional buffer views: + // - temperature buffer view + // - secondary color buffer view + // - id buffer view + REQUIRE(gltf.bufferViews.size() == 5); + + // There is only one added buffer containing all the binary values. + REQUIRE(gltf.buffers.size() == 3); + std::set bufferSet = getUniqueBufferIds(gltf.bufferViews); + CHECK(bufferSet.size() == 3); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == 2); + + // Check that position and color attributes are present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); +} diff --git a/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp b/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp index d8c31f9ee..b47183daa 100644 --- a/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp +++ b/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp @@ -1,6 +1,5 @@ -#include "B3dmToGltfConverter.h" #include "BatchTableToGltfFeatureMetadata.h" -#include "readFile.h" +#include "ConvertTileToGltf.h" #include #include @@ -137,7 +136,7 @@ static void createTestForScalarJson( scalarProperty, batchTableJson.GetAllocator()); - auto errors = BatchTableToGltfFeatureMetadata::convert( + auto errors = BatchTableToGltfFeatureMetadata::convertFromB3dm( featureTableJson, batchTableJson, gsl::span(), @@ -213,7 +212,7 @@ static void createTestForArrayJson( fixedArrayProperties, batchTableJson.GetAllocator()); - auto errors = BatchTableToGltfFeatureMetadata::convert( + auto errors = BatchTableToGltfFeatureMetadata::convertFromB3dm( featureTableJson, batchTableJson, gsl::span(), @@ -246,15 +245,34 @@ static void createTestForArrayJson( totalInstances); } -GltfConverterResult loadB3dm(const std::filesystem::path& filePath) { - return B3dmToGltfConverter::convert(readFile(filePath), {}); +std::set getUniqueBufferViewIds( + const std::vector& accessors, + FeatureTable& featureTable) { + std::set result; + for (auto it = accessors.begin(); it != accessors.end(); it++) { + result.insert(it->bufferView); + } + + auto& properties = featureTable.properties; + for (auto it = properties.begin(); it != properties.end(); it++) { + auto& property = it->second; + result.insert(property.bufferView); + if (property.arrayOffsetBufferView >= 0) { + result.insert(property.arrayOffsetBufferView); + } + if (property.stringOffsetBufferView >= 0) { + result.insert(property.stringOffsetBufferView); + } + } + + return result; } -TEST_CASE("Converts simple batch table to EXT_feature_metadata") { +TEST_CASE("Converts JSON B3DM batch table to EXT_feature_metadata") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "BatchTables" / "batchedWithJson.b3dm"; - GltfConverterResult result = loadB3dm(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromB3dm(testFilePath); REQUIRE(result.model); @@ -434,12 +452,12 @@ TEST_CASE("Converts simple batch table to EXT_feature_metadata") { } } -TEST_CASE("Convert binary batch table to EXT_feature_metadata") { +TEST_CASE("Convert binary B3DM batch table to EXT_feature_metadata") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "BatchTables" / "batchedWithBatchTableBinary.b3dm"; - GltfConverterResult result = loadB3dm(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromB3dm(testFilePath); REQUIRE(!result.errors); REQUIRE(result.model != std::nullopt); @@ -578,12 +596,183 @@ TEST_CASE("Convert binary batch table to EXT_feature_metadata") { } } +TEST_CASE("Converts batched PNTS batch table to EXT_feature_metadata") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudBatched.pnts"; + + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + ExtensionModelExtFeatureMetadata* pExtension = + gltf.getExtension(); + REQUIRE(pExtension); + + // Check the schema + REQUIRE(pExtension->schema); + REQUIRE(pExtension->schema->classes.size() == 1); + + auto firstClassIt = pExtension->schema->classes.begin(); + CHECK(firstClassIt->first == "default"); + + CesiumGltf::Class& defaultClass = firstClassIt->second; + REQUIRE(defaultClass.properties.size() == 3); + + auto nameItClass = defaultClass.properties.find("name"); + REQUIRE(nameItClass != defaultClass.properties.end()); + auto dimensionsItClass = defaultClass.properties.find("dimensions"); + REQUIRE(dimensionsItClass != defaultClass.properties.end()); + auto idItClass = defaultClass.properties.find("id"); + REQUIRE(idItClass != defaultClass.properties.end()); + + CHECK(nameItClass->second.type == "STRING"); + CHECK(dimensionsItClass->second.type == "ARRAY"); + REQUIRE(dimensionsItClass->second.componentType); + CHECK(dimensionsItClass->second.componentType.value() == "FLOAT32"); + CHECK(idItClass->second.type == "UINT32"); + + // Check the feature table + auto firstFeatureTableIt = pExtension->featureTables.begin(); + REQUIRE(firstFeatureTableIt != pExtension->featureTables.end()); + + FeatureTable& featureTable = firstFeatureTableIt->second; + CHECK(featureTable.classProperty == "default"); + REQUIRE(featureTable.properties.size() == 3); + + auto nameItTable = featureTable.properties.find("name"); + REQUIRE(nameItTable != featureTable.properties.end()); + auto dimensionsItTable = featureTable.properties.find("dimensions"); + REQUIRE(dimensionsItTable != featureTable.properties.end()); + auto idItTable = featureTable.properties.find("id"); + REQUIRE(idItTable != featureTable.properties.end()); + + CHECK(nameItTable->second.bufferView >= 0); + CHECK( + nameItTable->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(dimensionsItTable->second.bufferView >= 0); + CHECK( + dimensionsItTable->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(idItTable->second.bufferView >= 0); + CHECK( + idItTable->second.bufferView < + static_cast(gltf.bufferViews.size())); + + std::set bufferViewSet = + getUniqueBufferViewIds(gltf.accessors, featureTable); + CHECK(bufferViewSet.size() == gltf.bufferViews.size()); + + // Check the mesh primitives + CHECK(!gltf.meshes.empty()); + + for (Mesh& mesh : gltf.meshes) { + CHECK(!mesh.primitives.empty()); + for (MeshPrimitive& primitive : mesh.primitives) { + CHECK( + primitive.attributes.find("_FEATURE_ID_0") != + primitive.attributes.end()); + + ExtensionMeshPrimitiveExtFeatureMetadata* pPrimitiveExtension = + primitive.getExtension(); + REQUIRE(pPrimitiveExtension); + REQUIRE(pPrimitiveExtension->featureIdAttributes.size() == 1); + + FeatureIDAttribute& attribute = + pPrimitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureIds.attribute == "_FEATURE_ID_0"); + CHECK(attribute.featureTable == "default"); + } + } + // TODO + /* + // Check metadata values + { + std::vector expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + checkScalarProperty( + *result.model, + featureTable, + defaultClass, + "id", + "INT8", + expected, + expected.size()); + } + + { + std::vector expected = { + 11.762595914304256, + 13.992324123159051, + 7.490081690251827, + 13.484312580898404, + 11.481756005436182, + 7.836617760360241, + 9.338438434526324, + 13.513022359460592, + 13.74609257467091, + 10.145220385864377}; + checkScalarProperty( + *result.model, + featureTable, + defaultClass, + "Height", + "FLOAT64", + expected, + expected.size()); + } + + { + std::vector expected = { + -1.3196595204101946, + -1.3196739888070643, + -1.3196641114334025, + -1.3196579305297966, + -1.3196585149509301, + -1.319678877969692, + -1.3196612732428445, + -1.3196718857616954, + -1.3196471198757775, + -1.319644104024109}; + checkScalarProperty( + *result.model, + featureTable, + defaultClass, + "Longitude", + "FLOAT64", + expected, + expected.size()); + } + + { + std::vector expected = { + 0.6988582109, + 0.6988498770649103, + 0.6988533339856887, + 0.6988691467754378, + 0.698848878034009, + 0.6988592976292447, + 0.6988600642191055, + 0.6988670019309562, + 0.6988523191715889, + 0.6988697375823105}; + checkScalarProperty( + *result.model, + featureTable, + defaultClass, + "Latitude", + "FLOAT64", + expected, + expected.size()); + }*/ +} + TEST_CASE("Upgrade json nested json metadata to string") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "BatchTables" / "batchedWithStringAndNestedJson.b3dm"; - GltfConverterResult result = loadB3dm(testFilePath); + GltfConverterResult result = ConvertTileToGltf::fromB3dm(testFilePath); REQUIRE(!result.errors); REQUIRE(result.model != std::nullopt); @@ -671,7 +860,7 @@ TEST_CASE("Upgrade bool json to boolean binary") { boolProperties, batchTableJson.GetAllocator()); - auto errors = BatchTableToGltfFeatureMetadata::convert( + auto errors = BatchTableToGltfFeatureMetadata::convertFromB3dm( featureTableJson, batchTableJson, gsl::span(), @@ -1213,7 +1402,7 @@ TEST_CASE("Converts Feature Classes 3DTILES_batch_table_hierarchy example to " rapidjson::Document batchTableParsed; batchTableParsed.Parse(batchTableJson.data(), batchTableJson.size()); - auto errors = BatchTableToGltfFeatureMetadata::convert( + auto errors = BatchTableToGltfFeatureMetadata::convertFromB3dm( featureTableParsed, batchTableParsed, gsl::span(), @@ -1343,7 +1532,7 @@ TEST_CASE("Converts Feature Hierarchy 3DTILES_batch_table_hierarchy example to " rapidjson::Document batchTableParsed; batchTableParsed.Parse(batchTableJson.data(), batchTableJson.size()); - BatchTableToGltfFeatureMetadata::convert( + BatchTableToGltfFeatureMetadata::convertFromB3dm( featureTableParsed, batchTableParsed, gsl::span(), @@ -1537,7 +1726,7 @@ TEST_CASE( auto pLog = std::make_shared(3); spdlog::default_logger()->sinks().emplace_back(pLog); - BatchTableToGltfFeatureMetadata::convert( + BatchTableToGltfFeatureMetadata::convertFromB3dm( featureTableParsed, batchTableParsed, gsl::span(), @@ -1629,7 +1818,7 @@ TEST_CASE( auto pLog = std::make_shared(3); spdlog::default_logger()->sinks().emplace_back(pLog); - auto errors = BatchTableToGltfFeatureMetadata::convert( + auto errors = BatchTableToGltfFeatureMetadata::convertFromB3dm( featureTableParsed, batchTableParsed, gsl::span(), diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudWithPerPointProperties.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudWithPerPointProperties.pnts new file mode 100644 index 0000000000000000000000000000000000000000..638bad7d521be641ca3c6e5f11f1ae2fd1f7cc76 GIT binary patch literal 592 zcmXTOD=B7VU|z~9p^Bskv3)z3Y|L&?gbRsjSU7!(d5VFx5ONF2ro(J(eh9E6d@ zU~(`qINd#c+ss>CIrEp~dq4i_nt1Yj+k})iLSbUnN+qee1*t`eC8b5F= Date: Mon, 30 Jan 2023 11:49:36 -0500 Subject: [PATCH 09/20] Add AttributeCompression tests --- .../src/BatchTableToGltfFeatureMetadata.cpp | 128 +++++++++--------- .../CesiumUtility/AttributeCompression.h | 6 +- .../test/TestAttributeCompression.cpp | 69 ++++++++++ 3 files changed, 135 insertions(+), 68 deletions(-) create mode 100644 CesiumUtility/test/TestAttributeCompression.cpp diff --git a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp index c736d3b2b..3b1b1b542 100644 --- a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp +++ b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp @@ -1482,6 +1482,69 @@ void convertBatchTableToGltfFeatureMetadataExtension( } // namespace +ErrorList BatchTableToGltfFeatureMetadata::convertFromB3dm( + const rapidjson::Document& featureTableJson, + const rapidjson::Document& batchTableJson, + const gsl::span& batchTableBinaryData, + CesiumGltf::Model& gltf) { + // Check to make sure a char of rapidjson is 1 byte + static_assert( + sizeof(rapidjson::Value::Ch) == 1, + "RapidJson::Value::Ch is not 1 byte"); + + ErrorList result; + + // Parse the b3dm batch table and convert it to the EXT_feature_metadata + // extension. + + // If the feature table is missing the BATCH_LENGTH semantic, ignore the batch + // table completely. + const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); + if (batchLengthIt == featureTableJson.MemberEnd() || + !batchLengthIt->value.IsInt64()) { + result.emplaceWarning( + "The B3DM has a batch table, but it is being ignored because there is " + "no BATCH_LENGTH semantic in the feature table or it is not an " + "integer."); + return result; + } + + const int64_t batchLength = batchLengthIt->value.GetInt64(); + + convertBatchTableToGltfFeatureMetadataExtension( + batchTableJson, + batchTableBinaryData, + gltf, + batchLength, + result); + + // Create an EXT_feature_metadata extension for each primitive with a _BATCHID + // attribute. + for (Mesh& mesh : gltf.meshes) { + for (MeshPrimitive& primitive : mesh.primitives) { + auto batchIDIt = primitive.attributes.find("_BATCHID"); + if (batchIDIt == primitive.attributes.end()) { + // This primitive has no batch ID, ignore it. + continue; + } + + // Rename the _BATCHID attribute to _FEATURE_ID_0 + primitive.attributes["_FEATURE_ID_0"] = batchIDIt->second; + primitive.attributes.erase("_BATCHID"); + + // Create a feature extension + ExtensionMeshPrimitiveExtFeatureMetadata& extension = + primitive.addExtension(); + FeatureIDAttribute& attribute = + extension.featureIdAttributes.emplace_back(); + attribute.featureTable = "default"; + attribute.featureIds.attribute = "_FEATURE_ID_0"; + } + } + + return result; +} + ErrorList BatchTableToGltfFeatureMetadata::convertFromPnts( const rapidjson::Document& featureTableJson, const rapidjson::Document& batchTableJson, @@ -1519,7 +1582,7 @@ ErrorList BatchTableToGltfFeatureMetadata::convertFromPnts( batchIdIt->value.IsObject()) { result.emplaceWarning( "The PNTS has a batch table, but it is being ignored because there " - "is no valid BATCH_LENGTH semantic in the feature table, and " + "is no valid BATCH_LENGTH semantic in the feature table even though " "the BATCH_ID semantic is defined."); return result; } else { @@ -1561,67 +1624,4 @@ ErrorList BatchTableToGltfFeatureMetadata::convertFromPnts( return result; } - -ErrorList BatchTableToGltfFeatureMetadata::convertFromB3dm( - const rapidjson::Document& featureTableJson, - const rapidjson::Document& batchTableJson, - const gsl::span& batchTableBinaryData, - CesiumGltf::Model& gltf) { - // Check to make sure a char of rapidjson is 1 byte - static_assert( - sizeof(rapidjson::Value::Ch) == 1, - "RapidJson::Value::Ch is not 1 byte"); - - ErrorList result; - - // Parse the b3dm batch table and convert it to the EXT_feature_metadata - // extension. - - // If the feature table is missing the BATCH_LENGTH semantic, ignore the batch - // table completely. - const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); - if (batchLengthIt == featureTableJson.MemberEnd() || - !batchLengthIt->value.IsInt64()) { - result.emplaceWarning( - "The B3DM has a batch table, but it is being ignored because there is " - "no BATCH_LENGTH semantic in the feature table or it is not an " - "integer."); - return result; - } - - const int64_t batchLength = batchLengthIt->value.GetInt64(); - - convertBatchTableToGltfFeatureMetadataExtension( - batchTableJson, - batchTableBinaryData, - gltf, - batchLength, - result); - - // Create an EXT_feature_metadata extension for each primitive with a _BATCHID - // attribute. - for (Mesh& mesh : gltf.meshes) { - for (MeshPrimitive& primitive : mesh.primitives) { - auto batchIDIt = primitive.attributes.find("_BATCHID"); - if (batchIDIt == primitive.attributes.end()) { - // This primitive has no batch ID, ignore it. - continue; - } - - // Rename the _BATCHID attribute to _FEATURE_ID_0 - primitive.attributes["_FEATURE_ID_0"] = batchIDIt->second; - primitive.attributes.erase("_BATCHID"); - - // Create a feature extension - ExtensionMeshPrimitiveExtFeatureMetadata& extension = - primitive.addExtension(); - FeatureIDAttribute& attribute = - extension.featureIdAttributes.emplace_back(); - attribute.featureTable = "default"; - attribute.featureIds.attribute = "_FEATURE_ID_0"; - } - } - - return result; -} } // namespace Cesium3DTilesSelection diff --git a/CesiumUtility/include/CesiumUtility/AttributeCompression.h b/CesiumUtility/include/CesiumUtility/AttributeCompression.h index e2878c8b9..6670550a1 100644 --- a/CesiumUtility/include/CesiumUtility/AttributeCompression.h +++ b/CesiumUtility/include/CesiumUtility/AttributeCompression.h @@ -44,15 +44,13 @@ class CESIUMUTILITY_API AttributeCompression final { } /** - * Decodes a unit-length vector in 2 byte 'oct' encoding to a normalized + * @brief Decodes a unit-length vector in 2 byte 'oct' encoding to a normalized * 3-component vector. * * @param x The x component of the oct-encoded unit length vector. * @param y The y component of the oct-encoded unit length vector. * @returns The decoded and normalized vector. * - * @exception {DeveloperError} x and y must be an unsigned normalized integer between 0 and 255. - * * @see AttributeCompression::octDecodeInRange */ static glm::dvec3 octDecode(uint8_t x, uint8_t y) { @@ -74,7 +72,7 @@ class CESIUMUTILITY_API AttributeCompression final { constexpr float normalize6 = 1.0f / 63.0f; // normalize [0, 63] to [0, 1] const uint16_t red = static_cast(value >> 11); - const uint16_t green = static_cast(value >> 5 & mask6); + const uint16_t green = static_cast((value >> 5) & mask6); const uint16_t blue = value & mask5; return glm::dvec3(red * normalize5, green * normalize6, blue * normalize5); diff --git a/CesiumUtility/test/TestAttributeCompression.cpp b/CesiumUtility/test/TestAttributeCompression.cpp new file mode 100644 index 000000000..7f5f29b1f --- /dev/null +++ b/CesiumUtility/test/TestAttributeCompression.cpp @@ -0,0 +1,69 @@ +#include "CesiumUtility/AttributeCompression.h" +#include "CesiumUtility/Math.h" + +#include + +using namespace CesiumUtility; + +TEST_CASE("AttributeCompression::octDecode") { + const std::vector input{ + glm::u8vec2(128, 128), + glm::u8vec2(255, 255), + glm::u8vec2(128, 255), + glm::u8vec2(128, 0), + glm::u8vec2(255, 128), + glm::u8vec2(0, 128), + glm::u8vec2(170, 170), + glm::u8vec2(170, 85), + glm::u8vec2(85, 85), + glm::u8vec2(85, 170), + glm::u8vec2(213, 213), + glm::u8vec2(213, 42), + glm::u8vec2(42, 42), + glm::u8vec2(42, 213), + }; + const std::vector expected{ + glm::dvec3(0.0, 0.0, 1.0), + glm::dvec3(0.0, 0.0, -1.0), + glm::dvec3(0.0, 1.0, 0.0), + glm::dvec3(0.0, -1.0, 0.0), + glm::dvec3(1.0, 0.0, 0.0), + glm::dvec3(-1.0, 0.0, 0.0), + glm::normalize(glm::dvec3(1.0, 1.0, 1.0)), + glm::normalize(glm::dvec3(1.0, -1.0, 1.0)), + glm::normalize(glm::dvec3(-1.0, -1.0, 1.0)), + glm::normalize(glm::dvec3(-1.0, 1.0, 1.0)), + glm::normalize(glm::dvec3(1.0, 1.0, -1.0)), + glm::normalize(glm::dvec3(1.0, -1.0, -1.0)), + glm::normalize(glm::dvec3(-1.0, -1.0, -1.0)), + glm::normalize(glm::dvec3(-1.0, 1.0, -1.0)), + }; + + for (size_t i = 0; i < expected.size(); i++) { + const glm::dvec3 value = + AttributeCompression::octDecode(input[i].x, input[i].y); + CHECK(Math::equalsEpsilon(value, expected[i], Math::Epsilon1)); + } +} + +TEST_CASE("AttributeCompression::decodeRGB565") { + const std::vector input{ + 0, + // 0b00001_000001_00001 + 2081, + // 0b10000_100000_01000 + 33800, + // 0b11111_111111_11111 + 65535, + }; + const std::vector expected{ + glm::dvec3(0.0), + glm::dvec3(1.0 / 31.0, 1.0 / 63.0, 1.0 / 31.0), + glm::dvec3(16.0 / 31.0, 32.0 / 63.0, 8.0 / 31.0), + glm::dvec3(1.0)}; + + for (size_t i = 0; i < expected.size(); i++) { + const glm::dvec3 value = AttributeCompression::decodeRGB565(input[i]); + CHECK(Math::equalsEpsilon(value, expected[i], Math::Epsilon6)); + } +} From ed5bd6e0586a4e8e59e5897dc885919c1f8a841a Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 30 Jan 2023 13:54:10 -0500 Subject: [PATCH 10/20] Add tests for pnts metadata --- .../test/TestPntsToGltfConverter.cpp | 32 +- ...tUpgradeBatchTableToExtFeatureMetadata.cpp | 291 +++++++++++++----- .../CesiumUtility/AttributeCompression.h | 4 +- 3 files changed, 231 insertions(+), 96 deletions(-) diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index a1373e36e..82a97a3ff 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -658,7 +658,7 @@ TEST_CASE( Model& gltf = *result.model; // The correctness of the model extension is thoroughly tested in - // TestUpgradeBatchTableTo + // TestUpgradeBatchTableToExtFeatureMetadata CHECK(gltf.hasExtension()); CHECK(gltf.nodes.size() == 1); @@ -667,9 +667,9 @@ TEST_CASE( REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; - REQUIRE(primitive.hasExtension()); - const auto primitiveExtension = + auto primitiveExtension = primitive.getExtension(); + REQUIRE(primitiveExtension); REQUIRE(primitiveExtension->featureIdAttributes.size() == 1); FeatureIDAttribute& attribute = primitiveExtension->featureIdAttributes[0]; CHECK(attribute.featureTable == "default"); @@ -681,9 +681,8 @@ TEST_CASE( // - "name": string scalars in JSON // - "dimensions": float vec3s in binary // - "id": int scalars in binary - - // There are only three accessors (one per primitive attribute), - // but there are four additional buffer views: + // There are three accessors (one per primitive attribute) + // and four additional buffer views: // - "name" string data buffer view // - "name" string offsets buffer view // - "dimensions" buffer view @@ -736,21 +735,26 @@ TEST_CASE("Converts point cloud with per-point properties to glTF with " REQUIRE(result.model); Model& gltf = *result.model; + // The correctness of the model extension is thoroughly tested in + // TestUpgradeBatchTableToExtFeatureMetadata + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); REQUIRE(gltf.meshes.size() == 1); Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; - REQUIRE(primitive.hasExtension()); - const auto primitiveExtension = + auto primitiveExtension = primitive.getExtension(); + REQUIRE(primitiveExtension); REQUIRE(primitiveExtension->featureIdAttributes.size() == 1); FeatureIDAttribute& attribute = primitiveExtension->featureIdAttributes[0]; CHECK(attribute.featureTable == "default"); + // Check for implicit feature IDs + CHECK(!attribute.featureIds.attribute); CHECK(attribute.featureIds.constant == 0); CHECK(attribute.featureIds.divisor == 1); - CHECK(!attribute.featureIds.attribute); CHECK(gltf.materials.size() == 1); @@ -758,15 +762,12 @@ TEST_CASE("Converts point cloud with per-point properties to glTF with " // - "temperature": float scalars // - "secondaryColor": float vec3s // - "id": unsigned short scalars - // checkFeatureMetadataExtensionForBatchedPointCloud(gltf); - - // There are only two accessors (one per primitive attribute). - REQUIRE(gltf.accessors.size() == 2); - - // There are three additional buffer views: + // There are two accessors (one per primitive attribute) + // and three additional buffer views: // - temperature buffer view // - secondary color buffer view // - id buffer view + REQUIRE(gltf.accessors.size() == 2); REQUIRE(gltf.bufferViews.size() == 5); // There is only one added buffer containing all the binary values. @@ -776,6 +777,7 @@ TEST_CASE("Converts point cloud with per-point properties to glTF with " auto attributes = primitive.attributes; REQUIRE(attributes.size() == 2); + REQUIRE(attributes.find("_FEATURE_ID_0") == attributes.end()); // Check that position and color attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); diff --git a/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp b/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp index b47183daa..c5b2fcfbc 100644 --- a/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp +++ b/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp @@ -619,18 +619,20 @@ TEST_CASE("Converts batched PNTS batch table to EXT_feature_metadata") { CesiumGltf::Class& defaultClass = firstClassIt->second; REQUIRE(defaultClass.properties.size() == 3); - auto nameItClass = defaultClass.properties.find("name"); - REQUIRE(nameItClass != defaultClass.properties.end()); - auto dimensionsItClass = defaultClass.properties.find("dimensions"); - REQUIRE(dimensionsItClass != defaultClass.properties.end()); - auto idItClass = defaultClass.properties.find("id"); - REQUIRE(idItClass != defaultClass.properties.end()); - - CHECK(nameItClass->second.type == "STRING"); - CHECK(dimensionsItClass->second.type == "ARRAY"); - REQUIRE(dimensionsItClass->second.componentType); - CHECK(dimensionsItClass->second.componentType.value() == "FLOAT32"); - CHECK(idItClass->second.type == "UINT32"); + { + auto nameIt = defaultClass.properties.find("name"); + REQUIRE(nameIt != defaultClass.properties.end()); + auto dimensionsIt = defaultClass.properties.find("dimensions"); + REQUIRE(dimensionsIt != defaultClass.properties.end()); + auto idIt = defaultClass.properties.find("id"); + REQUIRE(idIt != defaultClass.properties.end()); + + CHECK(nameIt->second.type == "STRING"); + CHECK(dimensionsIt->second.type == "ARRAY"); + REQUIRE(dimensionsIt->second.componentType); + CHECK(dimensionsIt->second.componentType.value() == "FLOAT32"); + CHECK(idIt->second.type == "UINT32"); + } // Check the feature table auto firstFeatureTableIt = pExtension->featureTables.begin(); @@ -640,25 +642,27 @@ TEST_CASE("Converts batched PNTS batch table to EXT_feature_metadata") { CHECK(featureTable.classProperty == "default"); REQUIRE(featureTable.properties.size() == 3); - auto nameItTable = featureTable.properties.find("name"); - REQUIRE(nameItTable != featureTable.properties.end()); - auto dimensionsItTable = featureTable.properties.find("dimensions"); - REQUIRE(dimensionsItTable != featureTable.properties.end()); - auto idItTable = featureTable.properties.find("id"); - REQUIRE(idItTable != featureTable.properties.end()); - - CHECK(nameItTable->second.bufferView >= 0); - CHECK( - nameItTable->second.bufferView < - static_cast(gltf.bufferViews.size())); - CHECK(dimensionsItTable->second.bufferView >= 0); - CHECK( - dimensionsItTable->second.bufferView < - static_cast(gltf.bufferViews.size())); - CHECK(idItTable->second.bufferView >= 0); - CHECK( - idItTable->second.bufferView < - static_cast(gltf.bufferViews.size())); + { + auto nameIt = featureTable.properties.find("name"); + REQUIRE(nameIt != featureTable.properties.end()); + auto dimensionsIt = featureTable.properties.find("dimensions"); + REQUIRE(dimensionsIt != featureTable.properties.end()); + auto idIt = featureTable.properties.find("id"); + REQUIRE(idIt != featureTable.properties.end()); + + CHECK(nameIt->second.bufferView >= 0); + CHECK( + nameIt->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(dimensionsIt->second.bufferView >= 0); + CHECK( + dimensionsIt->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(idIt->second.bufferView >= 0); + CHECK( + idIt->second.bufferView < + static_cast(gltf.bufferViews.size())); + } std::set bufferViewSet = getUniqueBufferViewIds(gltf.accessors, featureTable); @@ -685,86 +689,215 @@ TEST_CASE("Converts batched PNTS batch table to EXT_feature_metadata") { CHECK(attribute.featureTable == "default"); } } - // TODO - /* + // Check metadata values { - std::vector expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - checkScalarProperty( + std::vector expected = { + "section0", + "section1", + "section2", + "section3", + "section4", + "section5", + "section6", + "section7"}; + checkScalarProperty( + *result.model, + featureTable, + defaultClass, + "name", + "STRING", + expected, + expected.size()); + } + + { + std::vector> expected = { + {0.1182744f, 0.7206326f, 0.6399210f}, + {0.5820198f, 0.1433532f, 0.5373732f}, + {0.9446688f, 0.7586156f, 0.5218483f}, + {0.1059076f, 0.4146619f, 0.4736004f}, + {0.2645556f, 0.1863323f, 0.7742336f}, + {0.7369181f, 0.4561503f, 0.2165503f}, + {0.5684339f, 0.1352181f, 0.0187897f}, + {0.3241409f, 0.6176354f, 0.1496748f}}; + + checkArrayProperty( + *result.model, + featureTable, + defaultClass, + "dimensions", + 3, + "FLOAT32", + expected, + expected.size()); + } + + { + std::vector expected = {0, 1, 2, 3, 4, 5, 6, 7}; + checkScalarProperty( *result.model, featureTable, defaultClass, "id", - "INT8", + "UINT32", expected, expected.size()); } +} + +TEST_CASE("Converts per-point PNTS batch table to EXT_feature_metadata") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = + testFilePath / "PointCloud" / "pointCloudWithPerPointProperties.pnts"; + + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + ExtensionModelExtFeatureMetadata* pExtension = + gltf.getExtension(); + REQUIRE(pExtension); + + // Check the schema + REQUIRE(pExtension->schema); + REQUIRE(pExtension->schema->classes.size() == 1); + + auto firstClassIt = pExtension->schema->classes.begin(); + CHECK(firstClassIt->first == "default"); + + CesiumGltf::Class& defaultClass = firstClassIt->second; + REQUIRE(defaultClass.properties.size() == 3); { - std::vector expected = { - 11.762595914304256, - 13.992324123159051, - 7.490081690251827, - 13.484312580898404, - 11.481756005436182, - 7.836617760360241, - 9.338438434526324, - 13.513022359460592, - 13.74609257467091, - 10.145220385864377}; - checkScalarProperty( + auto temperatureIt = defaultClass.properties.find("temperature"); + REQUIRE(temperatureIt != defaultClass.properties.end()); + auto secondaryColorIt = defaultClass.properties.find("secondaryColor"); + REQUIRE(secondaryColorIt != defaultClass.properties.end()); + auto idIt = defaultClass.properties.find("id"); + REQUIRE(idIt != defaultClass.properties.end()); + + CHECK(temperatureIt->second.type == "FLOAT32"); + CHECK(secondaryColorIt->second.type == "ARRAY"); + REQUIRE(secondaryColorIt->second.componentType); + CHECK(secondaryColorIt->second.componentType.value() == "FLOAT32"); + CHECK(idIt->second.type == "UINT16"); + } + + // Check the feature table + auto firstFeatureTableIt = pExtension->featureTables.begin(); + REQUIRE(firstFeatureTableIt != pExtension->featureTables.end()); + + FeatureTable& featureTable = firstFeatureTableIt->second; + CHECK(featureTable.classProperty == "default"); + REQUIRE(featureTable.properties.size() == 3); + + { + auto temperatureIt = featureTable.properties.find("temperature"); + REQUIRE(temperatureIt != featureTable.properties.end()); + auto secondaryColorIt = featureTable.properties.find("secondaryColor"); + REQUIRE(secondaryColorIt != featureTable.properties.end()); + auto idIt = featureTable.properties.find("id"); + REQUIRE(idIt != featureTable.properties.end()); + + CHECK(temperatureIt->second.bufferView >= 0); + CHECK( + temperatureIt->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(secondaryColorIt->second.bufferView >= 0); + CHECK( + secondaryColorIt->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(idIt->second.bufferView >= 0); + CHECK( + idIt->second.bufferView < + static_cast(gltf.bufferViews.size())); + } + + std::set bufferViewSet = + getUniqueBufferViewIds(gltf.accessors, featureTable); + CHECK(bufferViewSet.size() == gltf.bufferViews.size()); + + // Check the mesh primitives + CHECK(!gltf.meshes.empty()); + + for (Mesh& mesh : gltf.meshes) { + CHECK(!mesh.primitives.empty()); + for (MeshPrimitive& primitive : mesh.primitives) { + CHECK( + primitive.attributes.find("_FEATURE_ID_0") == + primitive.attributes.end()); + + ExtensionMeshPrimitiveExtFeatureMetadata* pPrimitiveExtension = + primitive.getExtension(); + REQUIRE(pPrimitiveExtension); + REQUIRE(pPrimitiveExtension->featureIdAttributes.size() == 1); + + FeatureIDAttribute& attribute = + pPrimitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + // Check for implicit feature IDs + CHECK(!attribute.featureIds.attribute); + CHECK(attribute.featureIds.constant == 0); + CHECK(attribute.featureIds.divisor == 1); + } + } + + // Check metadata values + { + std::vector expected = { + 0.2883332f, + 0.4338732f, + 0.1750928f, + 0.1430827f, + 0.1156976f, + 0.3274261f, + 0.1337213f, + 0.0207673f}; + checkScalarProperty( *result.model, featureTable, defaultClass, - "Height", - "FLOAT64", + "temperature", + "FLOAT32", expected, expected.size()); } { - std::vector expected = { - -1.3196595204101946, - -1.3196739888070643, - -1.3196641114334025, - -1.3196579305297966, - -1.3196585149509301, - -1.319678877969692, - -1.3196612732428445, - -1.3196718857616954, - -1.3196471198757775, - -1.319644104024109}; - checkScalarProperty( + std::vector> expected = { + {0.0202183f, 0, 0}, + {0.3682415f, 0, 0}, + {0.8326198f, 0, 0}, + {0.9571551f, 0, 0}, + {0.7781567f, 0, 0}, + {0.1403507f, 0, 0}, + {0.8700121f, 0, 0}, + {0.8700872f, 0, 0}}; + + checkArrayProperty( *result.model, featureTable, defaultClass, - "Longitude", - "FLOAT64", + "secondaryColor", + 3, + "FLOAT32", expected, expected.size()); } { - std::vector expected = { - 0.6988582109, - 0.6988498770649103, - 0.6988533339856887, - 0.6988691467754378, - 0.698848878034009, - 0.6988592976292447, - 0.6988600642191055, - 0.6988670019309562, - 0.6988523191715889, - 0.6988697375823105}; - checkScalarProperty( + std::vector expected = {0, 1, 2, 3, 4, 5, 6, 7}; + checkScalarProperty( *result.model, featureTable, defaultClass, - "Latitude", - "FLOAT64", + "id", + "UINT16", expected, expected.size()); - }*/ + } } TEST_CASE("Upgrade json nested json metadata to string") { diff --git a/CesiumUtility/include/CesiumUtility/AttributeCompression.h b/CesiumUtility/include/CesiumUtility/AttributeCompression.h index 6670550a1..1c468b855 100644 --- a/CesiumUtility/include/CesiumUtility/AttributeCompression.h +++ b/CesiumUtility/include/CesiumUtility/AttributeCompression.h @@ -44,8 +44,8 @@ class CESIUMUTILITY_API AttributeCompression final { } /** - * @brief Decodes a unit-length vector in 2 byte 'oct' encoding to a normalized - * 3-component vector. + * @brief Decodes a unit-length vector in 2 byte 'oct' encoding to a + * normalized 3-component vector. * * @param x The x component of the oct-encoded unit length vector. * @param y The y component of the oct-encoded unit length vector. From 91ef08f33d37c4c1e3a2931b6ba8b69870cb12fa Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 31 Jan 2023 11:58:24 -0500 Subject: [PATCH 11/20] Add support for decoding Draco metadata (untested) --- .../src/BatchTableToGltfFeatureMetadata.cpp | 6 +- .../src/PntsToGltfConverter.cpp | 508 ++++++++++++------ .../test/TestPntsToGltfConverter.cpp | 59 ++ .../test/data/PointCloud/pointCloudDraco.pnts | Bin 0 -> 1016 bytes 4 files changed, 418 insertions(+), 155 deletions(-) create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudDraco.pnts diff --git a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp index 3b1b1b542..556da0f79 100644 --- a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp +++ b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp @@ -1223,13 +1223,13 @@ void updateExtensionWithBinaryProperty( assert( gltfBufferIndex >= 0 && "gltfBufferIndex is negative. Need to allocate buffer before " - "convert the binary property"); + "converting the binary property"); const auto& byteOffsetIt = propertyValue.FindMember("byteOffset"); if (byteOffsetIt == propertyValue.MemberEnd() || !byteOffsetIt->value.IsInt64()) { result.emplaceWarning(fmt::format( - "Skip convert {}. The binary property doesn't have required " + "Skip converting {}. The binary property doesn't have required " "byteOffset.", propertyName)); return; @@ -1239,7 +1239,7 @@ void updateExtensionWithBinaryProperty( if (componentTypeIt == propertyValue.MemberEnd() || !componentTypeIt->value.IsString()) { result.emplaceWarning(fmt::format( - "Skip convert {}. The binary property doesn't have required " + "Skip converting {}. The binary property doesn't have required " "componentType.", propertyName)); return; diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index 51053897a..e1a605e94 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -76,16 +76,109 @@ void parsePntsHeader( } } +struct MetadataProperty { +public: + enum ComponentType { + BYTE, + UNSIGNED_BYTE, + SHORT, + UNSIGNED_SHORT, + INT, + UNSIGNED_INT, + FLOAT, + DOUBLE + }; + + enum Type { SCALAR, VEC2, VEC3, VEC4 }; + + static std::optional + componentTypeFromDracoDataType(const draco::DataType dataType) { + switch (dataType) { + case draco::DT_INT8: + return ComponentType::BYTE; + case draco::DT_UINT8: + return ComponentType::UNSIGNED_BYTE; + case draco::DT_INT16: + return ComponentType::SHORT; + case draco::DT_UINT16: + return ComponentType::UNSIGNED_SHORT; + case draco::DT_INT32: + return ComponentType::INT; + case draco::DT_UINT32: + return ComponentType::UNSIGNED_INT; + case draco::DT_FLOAT32: + return ComponentType::FLOAT; + case draco::DT_FLOAT64: + return ComponentType::DOUBLE; + default: + return std::nullopt; + } + } + + static size_t sizeOfComponentType(ComponentType componentType) { + switch (componentType) { + case ComponentType::BYTE: + case ComponentType::UNSIGNED_BYTE: + return sizeof(uint8_t); + case ComponentType::SHORT: + case ComponentType::UNSIGNED_SHORT: + return sizeof(uint16_t); + case ComponentType::INT: + case ComponentType::UNSIGNED_INT: + return sizeof(uint32_t); + case ComponentType::FLOAT: + return sizeof(float); + case ComponentType::DOUBLE: + return sizeof(double); + default: + return 0; + } + }; + + static std::optional typeFromNumberOfComponents(int8_t numComponents) { + switch (numComponents) { + case 1: + return Type::SCALAR; + case 2: + return Type::VEC2; + case 3: + return Type::VEC3; + case 4: + return Type::VEC4; + default: + return std::nullopt; + } + } +}; + +const std::map + stringToMetadataComponentType{ + {"BYTE", MetadataProperty::ComponentType::BYTE}, + {"UNSIGNED_BYTE", MetadataProperty::ComponentType::UNSIGNED_BYTE}, + {"SHORT", MetadataProperty::ComponentType::SHORT}, + {"UNSIGNED_SHORT", MetadataProperty::ComponentType::UNSIGNED_SHORT}, + {"INT", MetadataProperty::ComponentType::INT}, + {"UNSIGNED_INT", MetadataProperty::ComponentType::UNSIGNED_INT}, + {"FLOAT", MetadataProperty::ComponentType::FLOAT}, + {"DOUBLE", MetadataProperty::ComponentType::DOUBLE}, + }; + +const std::map stringToMetadataType{ + {"SCALAR", MetadataProperty::Type::SCALAR}, + {"VEC2", MetadataProperty::Type::VEC2}, + {"VEC3", MetadataProperty::Type::VEC3}, + {"VEC4", MetadataProperty::Type::VEC4}}; + struct PntsSemantic { uint32_t byteOffset = 0; std::optional dracoId; std::vector data; }; -struct DracoBatchTableValue { - - std::optional dracoId; - std::vector data; +struct DracoMetadataSemantic { + int32_t dracoId; + MetadataProperty::ComponentType componentType; + MetadataProperty::Type type; }; enum PntsColorType { CONSTANT, RGBA, RGB, RGB565 }; @@ -116,9 +209,11 @@ struct PntsContent { std::optional dracoByteOffset; std::optional dracoByteLength; - std::unordered_map dracoBatchTableValues; + std::map dracoMetadataSemantics; + std::vector dracoBatchTableBinary; Cesium3DTilesSelection::ErrorList errors; + bool metadataHasErrors = false; }; bool validateJsonArrayValues( @@ -522,6 +617,8 @@ rapidjson::Document parseFeatureTableJson( if (parsedContent.pointsLength == 0) { // This *should* be disallowed by the spec, but it currently isn't. // In the future, this can be converted to an error. + parsedContent.errors.emplaceWarning("The PNTS has a POINTS_LENGTH of zero. " + "Skip parsing feature table."); return document; } @@ -540,15 +637,107 @@ rapidjson::Document parseFeatureTableJson( parseDracoExtensionFromFeatureTableJson( dracoExtensionValue, parsedContent); - if (parsedContent.errors) { - return document; - } } } return document; } +void parseDracoExtensionFromBatchTableJson( + const rapidjson::Document& batchTableJson, + PntsContent& parsedContent) { + const auto extensionsIt = batchTableJson.FindMember("extensions"); + if (extensionsIt == batchTableJson.MemberEnd() || + !extensionsIt->value.IsObject()) { + return; + } + + const auto dracoExtensionIt = + extensionsIt->value.FindMember("3DTILES_draco_point_compression"); + if (dracoExtensionIt == extensionsIt->value.MemberEnd() || + !dracoExtensionIt->value.IsObject()) { + return; + } + + const rapidjson::Value& dracoExtension = dracoExtensionIt->value; + for (auto dracoPropertyIt = dracoExtension.MemberBegin(); + dracoPropertyIt != dracoExtension.MemberEnd(); + ++dracoPropertyIt) { + const std::string name = dracoPropertyIt->name.GetString(); + + // Validate the property against the batch table first. If there are + // any issues with the batch table, skip parsing metadata altogether. + auto batchTablePropertyIt = batchTableJson.FindMember(name.c_str()); + if (batchTablePropertyIt == batchTableJson.MemberEnd() || + !batchTablePropertyIt->value.IsObject()) { + parsedContent.errors.emplaceWarning(fmt::format( + "Warning: the metadata property {} is in the " + "3DTILES_draco_point_compression extension but not in the batch " + "table itself.", + name)); + continue; + } + const rapidjson::Value& batchTableProperty = batchTablePropertyIt->value; + auto byteOffsetIt = batchTableProperty.FindMember("byteOffset"); + if (byteOffsetIt == batchTableProperty.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceWarning(fmt::format( + "Error parsing batch table, the metadata property {} doesn't have a " + "valid byteOffset. Skip parsing metadata.", + name)); + parsedContent.metadataHasErrors = true; + return; + } + + std::string componentType; + auto componentTypeIt = batchTableProperty.FindMember("componentType"); + if (componentTypeIt != batchTableProperty.MemberEnd() && + componentTypeIt->value.IsString()) { + componentType = componentTypeIt->value.GetString(); + } + if (stringToMetadataComponentType.find(componentType) == + stringToMetadataComponentType.end()) { + parsedContent.errors.emplaceWarning(fmt::format( + "Error parsing batch table, the metadata property {} doesn't have a " + "valid componentType. Skip parsing metadata.", + name)); + parsedContent.metadataHasErrors = true; + return; + } + + std::string type; + auto typeIt = batchTableProperty.FindMember("type"); + if (typeIt != batchTableProperty.MemberEnd() && typeIt->value.IsString()) { + type = typeIt->value.GetString(); + } + if (stringToMetadataType.find(type) == stringToMetadataType.end()) { + parsedContent.errors.emplaceWarning(fmt::format( + "Error parsing batch table, the metadata property {} doesn't have a " + "valid type. Skip parsing metadata.", + name)); + parsedContent.metadataHasErrors = true; + return; + } + + if (!dracoPropertyIt->value.IsInt()) { + parsedContent.errors.emplaceWarning(fmt::format( + "Error parsing batch table with 3DTILES_draco_compression extension, " + "the metadata property {} doesn't have a valid draco ID. Skip " + "parsing metadata.", + name)); + parsedContent.metadataHasErrors = true; + return; + } + + DracoMetadataSemantic semantic; + semantic.dracoId = dracoPropertyIt->value.GetInt(); + semantic.componentType = stringToMetadataComponentType.at(componentType); + semantic.type = stringToMetadataType.at(type); + + parsedContent.dracoMetadataSemantics.insert({name, semantic}); + } +} + rapidjson::Document parseBatchTableJson( const gsl::span& batchTableJsonData, PntsContent& parsedContent) { @@ -566,31 +755,7 @@ rapidjson::Document parseBatchTableJson( return document; } - const auto extensionsIt = document.FindMember("extensions"); - if (extensionsIt != document.MemberEnd() && extensionsIt->value.IsObject()) { - const auto dracoExtensionIt = - extensionsIt->value.FindMember("3DTILES_draco_point_compression"); - if (dracoExtensionIt != extensionsIt->value.MemberEnd() && - dracoExtensionIt->value.IsObject()) { - const rapidjson::Value& dracoExtensionValue = dracoExtensionIt->value; - auto& dracoBatchTableValues = parsedContent.dracoBatchTableValues; - for (auto propertyIt = dracoExtensionValue.MemberBegin(); - propertyIt != dracoExtensionValue.MemberEnd(); - ++propertyIt) { - std::string name = propertyIt->name.GetString(); - if (propertyIt->value.IsInt()) { - PntsSemantic metadataSemantic; - metadataSemantic.dracoId = propertyIt->value.GetInt(); - dracoBatchTableValues.insert(std::make_pair(name, metadataSemantic)); - } else { - parsedContent.errors.emplaceWarning( - "Error parsing metadata property " + name + - " due to invalid Draco ID. The values for this property will not " - "be parsed properly."); - } - } - } - } + parseDracoExtensionFromBatchTableJson(document, parsedContent); return document; } @@ -603,8 +768,97 @@ bool validateDracoAttribute( pAttribute->num_components() == expectedNumComponents; } +bool validateDracoMetadataAttribute( + const draco::PointAttribute* const pAttribute, + const DracoMetadataSemantic semantic) { + if (!pAttribute) { + return false; + } + + if (MetadataProperty::componentTypeFromDracoDataType( + pAttribute->data_type()) != semantic.componentType) { + return false; + } + + return MetadataProperty::typeFromNumberOfComponents( + pAttribute->num_components()) != semantic.type; +} + +template +void getDracoData( + const draco::PointAttribute* pAttribute, + std::vector& data, + const uint32_t pointsLength) { + data.resize(pointsLength * sizeof(T)); + + gsl::span outData(reinterpret_cast(data.data()), pointsLength); + + draco::DataBuffer* decodedBuffer = pAttribute->buffer(); + int64_t decodedByteOffset = pAttribute->byte_offset(); + int64_t decodedByteStride = pAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + outData[i] = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + decodedByteStride * i); + } +} + +void decodeDracoMetadata( + const std::unique_ptr& pPointCloud, + rapidjson::Document& batchTableJson, + PntsContent& parsedContent) { + const uint64_t pointsLength = parsedContent.pointsLength; + std::vector& data = parsedContent.dracoBatchTableBinary; + + auto& dracoMetadataSemantics = parsedContent.dracoMetadataSemantics; + for (auto dracoSemanticIt = dracoMetadataSemantics.begin(); + dracoSemanticIt != dracoMetadataSemantics.end(); + dracoSemanticIt++) { + DracoMetadataSemantic dracoSemantic = dracoSemanticIt->second; + draco::PointAttribute* pAttribute = + pPointCloud->attribute(dracoSemantic.dracoId); + if (!validateDracoMetadataAttribute(pAttribute, dracoSemantic)) { + parsedContent.errors.emplaceWarning(fmt::format( + "Error decoding the {} metadata property in the " + "3DTILES_draco_compression extension. Skip parsing metadata.", + dracoSemanticIt->first)); + parsedContent.metadataHasErrors = true; + return; + } + + const size_t byteOffset = data.size(); + + // These checks do not test for validity since the batch table and extension + // were validated in parseDracoExtensionFromBatchTable. + auto batchTableSemanticIt = + batchTableJson.FindMember(dracoSemanticIt->first.c_str()); + rapidjson::Value& batchTableSemantic = + batchTableSemanticIt->value.GetObject(); + auto byteOffsetIt = batchTableSemantic.FindMember("byteOffset"); + byteOffsetIt->value.SetUint(static_cast(byteOffset)); + + const size_t metadataByteStride = + MetadataProperty::sizeOfComponentType(dracoSemantic.componentType) * + static_cast(pAttribute->num_components()); + data.resize(byteOffset + pointsLength * metadataByteStride); + + draco::DataBuffer* decodedBuffer = pAttribute->buffer(); + int64_t decodedByteOffset = pAttribute->byte_offset(); + int64_t decodedByteStride = pAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + std::memcpy( + data.data() + byteOffset + i * metadataByteStride, + decodedBuffer->data() + decodedByteOffset + decodedByteStride * i, + metadataByteStride); + } + } +} + void decodeDraco( const gsl::span& featureTableBinaryData, + rapidjson::Document& batchTableJson, + const gsl::span& batchTableBinaryData, PntsContent& parsedContent) { if (!parsedContent.dracoByteOffset || !parsedContent.dracoByteLength) { return; @@ -662,48 +916,19 @@ void decodeDraco( if (parsedContent.color) { PntsSemantic& color = parsedContent.color.value(); if (color.dracoId) { - std::vector& colorData = color.data; draco::PointAttribute* pColorAttribute = pPointCloud->attribute(color.dracoId.value()); if (parsedContent.colorType == PntsColorType::RGBA && validateDracoAttribute(pColorAttribute, draco::DT_UINT8, 4)) { - colorData.resize(pointsLength * sizeof(glm::u8vec4)); - - gsl::span outColors( - reinterpret_cast(colorData.data()), - pointsLength); - - draco::DataBuffer* decodedBuffer = pColorAttribute->buffer(); - int64_t decodedByteOffset = pColorAttribute->byte_offset(); - int64_t decodedByteStride = pColorAttribute->byte_stride(); - - for (uint32_t i = 0; i < pointsLength; ++i) { - outColors[i] = *reinterpret_cast( - decodedBuffer->data() + decodedByteOffset + - decodedByteStride * i); - } + getDracoData(pColorAttribute, color.data, pointsLength); } else if ( parsedContent.colorType == PntsColorType::RGB && validateDracoAttribute(pColorAttribute, draco::DT_UINT8, 3)) { - colorData.resize(pointsLength * sizeof(glm::u8vec3)); - gsl::span outColors( - reinterpret_cast(colorData.data()), - pointsLength); - - draco::DataBuffer* decodedBuffer = pColorAttribute->buffer(); - int64_t decodedByteOffset = pColorAttribute->byte_offset(); - int64_t decodedByteStride = pColorAttribute->byte_stride(); - - for (uint32_t i = 0; i < pointsLength; ++i) { - outColors[i] = *reinterpret_cast( - decodedBuffer->data() + decodedByteOffset + - decodedByteStride * i); - } + getDracoData(pColorAttribute, color.data, pointsLength); } else { parsedContent.errors.emplaceWarning( "Warning: decoded Draco point cloud did not contain a valid " - "color " - "attribute. Skip parsing colors."); + "color attribute. Skip parsing colors."); parsedContent.color = std::nullopt; parsedContent.colorType = PntsColorType::CONSTANT; } @@ -715,21 +940,7 @@ void decodeDraco( draco::PointAttribute* pNormalAttribute = pPointCloud->attribute(normal.dracoId.value()); if (validateDracoAttribute(pNormalAttribute, draco::DT_FLOAT32, 3)) { - std::vector& normalData = normal.data; - normalData.resize(pointsLength * sizeof(glm::vec3)); - - gsl::span outNormals( - reinterpret_cast(normalData.data()), - pointsLength); - - draco::DataBuffer* decodedBuffer = pNormalAttribute->buffer(); - int64_t decodedByteOffset = pNormalAttribute->byte_offset(); - int64_t decodedByteStride = pNormalAttribute->byte_stride(); - - for (uint32_t i = 0; i < pointsLength; ++i) { - outNormals[i] = *reinterpret_cast( - decodedBuffer->data() + decodedByteOffset + decodedByteStride * i); - } + getDracoData(pNormalAttribute, normal.data, pointsLength); } else { parsedContent.errors.emplaceWarning( "Warning: decoded Draco point cloud did not contain valid normal " @@ -743,7 +954,6 @@ void decodeDraco( if (batchId.dracoId) { draco::PointAttribute* pBatchIdAttribute = pPointCloud->attribute(batchId.dracoId.value()); - std::vector& batchIdData = batchId.data; int32_t componentType = 0; if (parsedContent.batchIdComponentType) { @@ -752,67 +962,41 @@ void decodeDraco( if (componentType == Accessor::ComponentType::UNSIGNED_BYTE && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT8, 1)) { - batchIdData.resize(pointsLength * sizeof(uint8_t)); - gsl::span outBatchIds( - reinterpret_cast(batchIdData.data()), - pointsLength); - - draco::DataBuffer* decodedBuffer = pBatchIdAttribute->buffer(); - int64_t decodedByteOffset = pBatchIdAttribute->byte_offset(); - int64_t decodedByteStride = pBatchIdAttribute->byte_stride(); - - for (uint32_t i = 0; i < pointsLength; ++i) { - outBatchIds[i] = *reinterpret_cast( - decodedBuffer->data() + decodedByteOffset + - decodedByteStride * i); - } + getDracoData(pBatchIdAttribute, batchId.data, pointsLength); } else if ( componentType == Accessor::ComponentType::UNSIGNED_INT && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT32, 1)) { - batchIdData.resize(pointsLength * sizeof(uint32_t)); - gsl::span outBatchIds( - reinterpret_cast(batchIdData.data()), - pointsLength); - - draco::DataBuffer* decodedBuffer = pBatchIdAttribute->buffer(); - int64_t decodedByteOffset = pBatchIdAttribute->byte_offset(); - int64_t decodedByteStride = pBatchIdAttribute->byte_stride(); - - for (uint32_t i = 0; i < pointsLength; ++i) { - outBatchIds[i] = *reinterpret_cast( - decodedBuffer->data() + decodedByteOffset + - decodedByteStride * i); - } + getDracoData(pBatchIdAttribute, batchId.data, pointsLength); } else if ( (componentType == 0 || componentType == Accessor::ComponentType::UNSIGNED_SHORT) && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT16, 1)) { - batchIdData.resize(pointsLength * sizeof(uint16_t)); - gsl::span outBatchIds( - reinterpret_cast(batchIdData.data()), - pointsLength); - - draco::DataBuffer* decodedBuffer = pBatchIdAttribute->buffer(); - int64_t decodedByteOffset = pBatchIdAttribute->byte_offset(); - int64_t decodedByteStride = pBatchIdAttribute->byte_stride(); - - for (uint32_t i = 0; i < pointsLength; ++i) { - outBatchIds[i] = *reinterpret_cast( - decodedBuffer->data() + decodedByteOffset + - decodedByteStride * i); - } + getDracoData(pBatchIdAttribute, batchId.data, pointsLength); } else { parsedContent.errors.emplaceWarning( "Warning: decoded Draco point cloud did not contain a valid " - "batch " - "id " - "attribute. Skip parsing batch IDs."); + "batch id attribute. Skip parsing batch IDs."); parsedContent.batchId = std::nullopt; } } } - // TODO: check for metadata + if (batchTableJson.HasParseError() || parsedContent.metadataHasErrors) { + return; + } + + // Not all metadata attributes may be compressed. Copy the binary of the + // uncompressed attributes first before appending the decoded data. + size_t batchTableBinaryByteLength = batchTableBinaryData.size(); + if (batchTableBinaryByteLength > 0) { + parsedContent.dracoBatchTableBinary.resize(batchTableBinaryByteLength); + std::memcpy( + parsedContent.dracoBatchTableBinary.data(), + batchTableBinaryData.data(), + batchTableBinaryByteLength); + } + + decodeDracoMetadata(pPointCloud, batchTableJson, parsedContent); } void parsePositionsFromFeatureTableBinary( @@ -1003,9 +1187,20 @@ void parseBatchIdsFromFeatureTableBinary( void parseFeatureTableBinary( const gsl::span& featureTableBinaryData, + rapidjson::Document& batchTableJson, + const gsl::span& batchTableBinaryData, PntsContent& parsedContent) { - decodeDraco(featureTableBinaryData, parsedContent); + decodeDraco( + featureTableBinaryData, + batchTableJson, + batchTableBinaryData, + parsedContent); + if (parsedContent.errors) { + return; + } + parsePositionsFromFeatureTableBinary(featureTableBinaryData, parsedContent); + if (parsedContent.color) { parseColorsFromFeatureTableBinary(featureTableBinaryData, parsedContent); } @@ -1259,11 +1454,9 @@ void convertPntsContentToGltf( GltfConverterResult& result) { if (header.featureTableJsonByteLength > 0 && header.featureTableBinaryByteLength > 0) { - PntsContent parsedContent; - const gsl::span featureTableJsonData = pntsBinary.subspan(headerLength, header.featureTableJsonByteLength); - + PntsContent parsedContent; rapidjson::Document featureTableJson = parseFeatureTableJson(featureTableJsonData, parsedContent); if (parsedContent.errors) { @@ -1277,18 +1470,12 @@ void convertPntsContentToGltf( const int64_t batchTableStart = headerLength + header.featureTableJsonByteLength + header.featureTableBinaryByteLength; - rapidjson::Document batchTableJson; if (header.batchTableJsonByteLength > 0) { const gsl::span batchTableJsonData = pntsBinary.subspan( static_cast(batchTableStart), header.batchTableJsonByteLength); - batchTableJson = parseBatchTableJson(batchTableJsonData, parsedContent); - if (parsedContent.errors) { - result.errors.merge(parsedContent.errors); - return; - } } const gsl::span featureTableBinaryData = @@ -1297,25 +1484,42 @@ void convertPntsContentToGltf( headerLength + header.featureTableJsonByteLength), header.featureTableBinaryByteLength); - parseFeatureTableBinary(featureTableBinaryData, parsedContent); + gsl::span batchTableBinaryData; + if (header.batchTableBinaryByteLength > 0) { + batchTableBinaryData = pntsBinary.subspan( + static_cast( + batchTableStart + header.batchTableJsonByteLength), + header.batchTableBinaryByteLength); + } + + parseFeatureTableBinary( + featureTableBinaryData, + batchTableJson, + batchTableBinaryData, + parsedContent); + + if (parsedContent.errors) { + result.errors.merge(parsedContent.errors); + return; + } + createGltfFromParsedContent(parsedContent, result); - if (header.batchTableJsonByteLength > 0) { - gsl::span batchTableBinaryData; - // check if data was manipulated earlier - if (header.batchTableBinaryByteLength > 0) { - batchTableBinaryData = pntsBinary.subspan( - static_cast( - batchTableStart + header.batchTableJsonByteLength), - header.batchTableBinaryByteLength); - } + if (batchTableJson.HasParseError() || parsedContent.metadataHasErrors) { + result.errors.merge(parsedContent.errors); + return; + } - result.errors.merge(BatchTableToGltfFeatureMetadata::convertFromPnts( - featureTableJson, - batchTableJson, - batchTableBinaryData, - result.model.value())); + if (!parsedContent.dracoBatchTableBinary.empty()) { + batchTableBinaryData = + gsl::span(parsedContent.dracoBatchTableBinary); } + + result.errors.merge(BatchTableToGltfFeatureMetadata::convertFromPnts( + featureTableJson, + batchTableJson, + batchTableBinaryData, + result.model.value())); } } } // namespace diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index 82a97a3ff..9edabe207 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -783,3 +783,62 @@ TEST_CASE("Converts point cloud with per-point properties to glTF with " checkAttribute(gltf, primitive, "POSITION", pointsLength); checkAttribute(gltf, primitive, "COLOR_0", pointsLength); } +/* +TEST_CASE("Converts point cloud with Draco compression to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudDraco.pnts"; + const int32_t pointsLength = 8; + const int32_t expectedAttributeCount = 3; + + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(!material.hasExtension()); + + REQUIRE(gltf.accessors.size() == expectedAttributeCount); + REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); + REQUIRE(gltf.buffers.size() == expectedAttributeCount); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == expectedAttributeCount); + + // Check that position, color, and normal attributes are present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "NORMAL", pointsLength); + + // Check each attribute more thoroughly + { + /* uint32_t accessorId = static_cast(attributes.at("POSITION")); + Accessor& accessor = gltf.accessors[accessorId]; + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + const std::vector expected = { + glm::vec3(-0.9856477, 0.1634960, 0.0420418), + glm::vec3(-0.5901730, 0.5359042, 0.6037402), + glm::vec3(-0.5674310, -0.7817938, -0.2584963), + glm::vec3(-0.5861990, -0.7179291, 0.3754308), + glm::vec3(-0.8519385, -0.1283743, -0.5076620), + glm::vec3(0.7587127, 0.1254564, 0.6392304), + glm::vec3(0.1354662, -0.2292506, -0.9638947), + glm::vec3(-0.0656172, 0.9640687, 0.2574214)}; + checkBufferContents(buffer.cesium.data, expected); + } +}*/ diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudDraco.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudDraco.pnts new file mode 100644 index 0000000000000000000000000000000000000000..91c37703dcc4e54718422eba45d5b55c8de7b575 GIT binary patch literal 1016 zcmbu7O=uHA6vt<>sZFukf<=1}8s?&hwPbhmv01cjOtwi%+^xwfs7Q#(wh7wZuvwwC zjoP9XJor`A)DKWkQdAT?C?1Tr;>Ck_Q}HHVTJX@?i}6kTqOBm}ECcVo`Mv*}9sWhb zE@6bw7X~2~f;{XPEx;Nj`mn7VrGjach%`!sh!#)CsdUcDWX*KZEEsk=YYr7Hy+j#` z6fLu;TXsROH}tEixE5CxB6(PnjP4{7#}Y+N?g}M{#FtreXvEglfq{~4*Abc~bYsxY z6G`y-%Ru~=7+p@oSzU2;`R9PuC;e(%(NgJztVFdKkwlQt!s)QAXmXNB2RNSdc{sjP z6nRk$_ydB-Zu15NzrYEd9v{aAgTA1b^?F3k@9}l=JjeSzUjAU&4J(oig;fVUB0?_M zcnxZT!~qqIA#ymIFk+k#Ih{_78XTB$I53={mH_U7_wK2@H^P?py3$9Uj9vWNQ|%po z%CvnrySQmilC=ThCx zH27~%+WHVUG-DsNbTAo9N_y5bav5tRY#uQ!B6(R-$Pvj`rtVt?ukaH~sP|1nH*9UB z2x6o+p@uX9UEkAGIFtw_3COIR@vlmI<*-0?=*9lpyGKdIqlz3!r($aI57(*N)%^`S C${pzd literal 0 HcmV?d00001 From b67d3c7512fb5ea3678ff320e91fa2dc9ca966fd Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 31 Jan 2023 15:56:39 -0500 Subject: [PATCH 12/20] Add unit tests and fixes for Draco decoding --- .../src/PntsToGltfConverter.cpp | 177 ++++++---- .../test/TestPntsToGltfConverter.cpp | 314 ++++++++++++++++-- ...tUpgradeBatchTableToExtFeatureMetadata.cpp | 154 +++++++++ .../PointCloud/pointCloudDracoBatched.pnts | Bin 0 -> 1016 bytes .../PointCloud/pointCloudDracoPartial.pnts | Bin 0 -> 1024 bytes 5 files changed, 562 insertions(+), 83 deletions(-) create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudDracoBatched.pnts create mode 100644 Cesium3DTilesSelection/test/data/PointCloud/pointCloudDracoPartial.pnts diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index e1a605e94..e8831c7d9 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -92,7 +92,7 @@ struct MetadataProperty { enum Type { SCALAR, VEC2, VEC3, VEC4 }; static std::optional - componentTypeFromDracoDataType(const draco::DataType dataType) { + getComponentTypeFromDracoDataType(const draco::DataType dataType) { switch (dataType) { case draco::DT_INT8: return ComponentType::BYTE; @@ -115,7 +115,7 @@ struct MetadataProperty { } } - static size_t sizeOfComponentType(ComponentType componentType) { + static size_t getSizeOfComponentType(ComponentType componentType) { switch (componentType) { case ComponentType::BYTE: case ComponentType::UNSIGNED_BYTE: @@ -135,7 +135,8 @@ struct MetadataProperty { } }; - static std::optional typeFromNumberOfComponents(int8_t numComponents) { + static std::optional + getTypeFromNumberOfComponents(int8_t numComponents) { switch (numComponents) { case 1: return Type::SCALAR; @@ -205,6 +206,7 @@ struct PntsContent { std::optional batchId; std::optional batchIdComponentType; + std::optional batchLength; std::optional dracoByteOffset; std::optional dracoByteLength; @@ -429,42 +431,55 @@ void parseBatchIdsFromFeatureTableJson( const rapidjson::Document& featureTableJson, PntsContent& parsedContent) { const auto batchIdIt = featureTableJson.FindMember("BATCH_ID"); - if (batchIdIt != featureTableJson.MemberEnd() && - batchIdIt->value.IsObject()) { - const auto byteOffsetIt = batchIdIt->value.FindMember("byteOffset"); - if (byteOffsetIt == batchIdIt->value.MemberEnd() || - !byteOffsetIt->value.IsUint()) { - parsedContent.errors.emplaceWarning( - "Error parsing PNTS feature table, BATCH_ID semantic does not have " - "valid byteOffset. Skip parsing batch IDs."); - return; - } + if (batchIdIt == featureTableJson.MemberEnd() || + !batchIdIt->value.IsObject()) { + return; + } - parsedContent.batchId = std::make_optional(); - PntsSemantic& batchId = parsedContent.batchId.value(); - batchId.byteOffset = byteOffsetIt->value.GetUint(); + const auto byteOffsetIt = batchIdIt->value.FindMember("byteOffset"); + if (byteOffsetIt == batchIdIt->value.MemberEnd() || + !byteOffsetIt->value.IsUint()) { + parsedContent.errors.emplaceWarning( + "Error parsing PNTS feature table, BATCH_ID semantic does not have " + "valid byteOffset. Skip parsing batch IDs."); + return; + } - const auto componentTypeIt = batchIdIt->value.FindMember("componentType"); - if (componentTypeIt != featureTableJson.MemberEnd() && - componentTypeIt->value.IsString()) { - const std::string& componentTypeString = - componentTypeIt->value.GetString(); - - if (componentTypeString == "UNSIGNED_BYTE") { - parsedContent.batchIdComponentType = - Accessor::ComponentType::UNSIGNED_BYTE; - } else if (componentTypeString == "UNSIGNED_INT") { - parsedContent.batchIdComponentType = - Accessor::ComponentType::UNSIGNED_INT; - } else { - parsedContent.batchIdComponentType = - Accessor::ComponentType::UNSIGNED_SHORT; - } + parsedContent.batchId = std::make_optional(); + PntsSemantic& batchId = parsedContent.batchId.value(); + batchId.byteOffset = byteOffsetIt->value.GetUint(); + + const auto componentTypeIt = batchIdIt->value.FindMember("componentType"); + if (componentTypeIt != featureTableJson.MemberEnd() && + componentTypeIt->value.IsString()) { + const std::string& componentTypeString = componentTypeIt->value.GetString(); + + if (componentTypeString == "UNSIGNED_BYTE") { + parsedContent.batchIdComponentType = + Accessor::ComponentType::UNSIGNED_BYTE; + } else if (componentTypeString == "UNSIGNED_INT") { + parsedContent.batchIdComponentType = + Accessor::ComponentType::UNSIGNED_INT; } else { parsedContent.batchIdComponentType = Accessor::ComponentType::UNSIGNED_SHORT; } + } else { + parsedContent.batchIdComponentType = + Accessor::ComponentType::UNSIGNED_SHORT; } + + const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); + if (batchLengthIt == featureTableJson.MemberEnd() || + !batchLengthIt->value.IsUint()) { + parsedContent.errors.emplaceWarning( + "Error parsing PNTS feature table, BATCH_ID semantic is present but " + "no valid BATCH_LENGTH is defined. Skip parsing metadata."); + parsedContent.metadataHasErrors = true; + return; + } + + parsedContent.batchLength = batchLengthIt->value.GetUint(); } void parseSemanticsFromFeatureTableJson( @@ -659,9 +674,27 @@ void parseDracoExtensionFromBatchTableJson( return; } - const rapidjson::Value& dracoExtension = dracoExtensionIt->value; - for (auto dracoPropertyIt = dracoExtension.MemberBegin(); - dracoPropertyIt != dracoExtension.MemberEnd(); + if (parsedContent.batchLength) { + parsedContent.errors.emplaceWarning( + "Error parsing batch table, the 3DTILES_draco_point_compression " + "extension is present but BATCH_LENGTH is defined."); + parsedContent.metadataHasErrors = true; + return; + } + + const auto propertiesIt = dracoExtensionIt->value.FindMember("properties"); + if (propertiesIt == dracoExtensionIt->value.MemberEnd() || + !propertiesIt->value.IsObject()) { + parsedContent.errors.emplaceWarning( + "Error parsing 3DTILES_draco_point_compression extension, no " + "properties object was found."); + parsedContent.metadataHasErrors = true; + return; + } + + const rapidjson::Value& properties = propertiesIt->value.GetObject(); + for (auto dracoPropertyIt = properties.MemberBegin(); + dracoPropertyIt != properties.MemberEnd(); ++dracoPropertyIt) { const std::string name = dracoPropertyIt->name.GetString(); @@ -755,7 +788,9 @@ rapidjson::Document parseBatchTableJson( return document; } - parseDracoExtensionFromBatchTableJson(document, parsedContent); + if (!parsedContent.metadataHasErrors) { + parseDracoExtensionFromBatchTableJson(document, parsedContent); + } return document; } @@ -775,13 +810,15 @@ bool validateDracoMetadataAttribute( return false; } - if (MetadataProperty::componentTypeFromDracoDataType( - pAttribute->data_type()) != semantic.componentType) { + auto componentType = MetadataProperty::getComponentTypeFromDracoDataType( + pAttribute->data_type()); + if (!componentType || componentType.value() != semantic.componentType) { return false; } - return MetadataProperty::typeFromNumberOfComponents( - pAttribute->num_components()) != semantic.type; + auto type = MetadataProperty::getTypeFromNumberOfComponents( + pAttribute->num_components()); + return type && type.value() == semantic.type; } template @@ -789,17 +826,25 @@ void getDracoData( const draco::PointAttribute* pAttribute, std::vector& data, const uint32_t pointsLength) { - data.resize(pointsLength * sizeof(T)); - - gsl::span outData(reinterpret_cast(data.data()), pointsLength); + const size_t dataElementSize = sizeof(T); + const size_t databufferByteLength = pointsLength * dataElementSize; + data.resize(databufferByteLength); draco::DataBuffer* decodedBuffer = pAttribute->buffer(); int64_t decodedByteOffset = pAttribute->byte_offset(); int64_t decodedByteStride = pAttribute->byte_stride(); - for (uint32_t i = 0; i < pointsLength; ++i) { - outData[i] = *reinterpret_cast( - decodedBuffer->data() + decodedByteOffset + decodedByteStride * i); + if (dataElementSize != static_cast(decodedByteStride)) { + gsl::span outData(reinterpret_cast(data.data()), pointsLength); + for (uint32_t i = 0; i < pointsLength; ++i) { + outData[i] = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + decodedByteStride * i); + } + } else { + std::memcpy( + data.data(), + decodedBuffer->data() + decodedByteOffset, + databufferByteLength); } } @@ -838,7 +883,7 @@ void decodeDracoMetadata( byteOffsetIt->value.SetUint(static_cast(byteOffset)); const size_t metadataByteStride = - MetadataProperty::sizeOfComponentType(dracoSemantic.componentType) * + MetadataProperty::getSizeOfComponentType(dracoSemantic.componentType) * static_cast(pAttribute->num_components()); data.resize(byteOffset + pointsLength * metadataByteStride); @@ -846,11 +891,18 @@ void decodeDracoMetadata( int64_t decodedByteOffset = pAttribute->byte_offset(); int64_t decodedByteStride = pAttribute->byte_stride(); - for (uint32_t i = 0; i < pointsLength; ++i) { + if (metadataByteStride != static_cast(decodedByteStride)) { + for (uint32_t i = 0; i < pointsLength; ++i) { + std::memcpy( + data.data() + byteOffset + i * metadataByteStride, + decodedBuffer->data() + decodedByteOffset + decodedByteStride * i, + metadataByteStride); + } + } else { std::memcpy( - data.data() + byteOffset + i * metadataByteStride, - decodedBuffer->data() + decodedByteOffset + decodedByteStride * i, - metadataByteStride); + data.data() + byteOffset, + decodedBuffer->data() + decodedByteOffset, + metadataByteStride * pointsLength); } } } @@ -937,15 +989,17 @@ void decodeDraco( if (parsedContent.normal) { PntsSemantic& normal = parsedContent.normal.value(); - draco::PointAttribute* pNormalAttribute = - pPointCloud->attribute(normal.dracoId.value()); - if (validateDracoAttribute(pNormalAttribute, draco::DT_FLOAT32, 3)) { - getDracoData(pNormalAttribute, normal.data, pointsLength); - } else { - parsedContent.errors.emplaceWarning( - "Warning: decoded Draco point cloud did not contain valid normal " - "attribute. Skip parsing normals."); - parsedContent.normal = std::nullopt; + if (normal.dracoId) { + draco::PointAttribute* pNormalAttribute = + pPointCloud->attribute(normal.dracoId.value()); + if (validateDracoAttribute(pNormalAttribute, draco::DT_FLOAT32, 3)) { + getDracoData(pNormalAttribute, normal.data, pointsLength); + } else { + parsedContent.errors.emplaceWarning( + "Warning: decoded Draco point cloud did not contain valid normal " + "attribute. Skip parsing normals."); + parsedContent.normal = std::nullopt; + } } } @@ -1511,6 +1565,9 @@ void convertPntsContentToGltf( } if (!parsedContent.dracoBatchTableBinary.empty()) { + // If the point cloud has both compressed and uncompressed metadata + // values, then dracoBatchTableBinary will contain both the original batch + // table binary and the Draco decoded values. batchTableBinaryData = gsl::span(parsedContent.dracoBatchTableBinary); } diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index 9edabe207..a616f63eb 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -1,4 +1,3 @@ -#include "BatchTableToGltfFeatureMetadata.h" #include "ConvertTileToGltf.h" #include @@ -7,8 +6,6 @@ #include #include #include -#include -#include #include #include @@ -34,7 +31,7 @@ static void checkBufferContents( const glm::vec3& value = *reinterpret_cast(buffer.data() + i * byteStride); const glm::vec3& expectedValue = expected[i]; - CHECK(Math::equalsEpsilon( + REQUIRE(Math::equalsEpsilon( static_cast(value), static_cast(expectedValue), Math::Epsilon6)); @@ -44,7 +41,7 @@ static void checkBufferContents( const glm::vec4& value = *reinterpret_cast(buffer.data() + i * byteStride); const glm::vec4& expectedValue = expected[i]; - CHECK(Math::equalsEpsilon( + REQUIRE(Math::equalsEpsilon( static_cast(value), static_cast(expectedValue), Math::Epsilon6)); @@ -54,7 +51,7 @@ static void checkBufferContents( const Type& value = *reinterpret_cast(buffer.data() + i * byteStride); const Type& expectedValue = expected[i]; - CHECK(value == Approx(expectedValue)); + REQUIRE(value == Approx(expectedValue)); } } else if constexpr ( std::is_integral_v || std::is_same_v || @@ -63,13 +60,46 @@ static void checkBufferContents( const Type& value = *reinterpret_cast(buffer.data() + i * byteStride); const Type& expectedValue = expected[i]; - CHECK(value == expectedValue); + REQUIRE(value == expectedValue); } } else { FAIL("Buffer check has not been implemented for the given type."); } } +template +static void checkBufferContents( + const std::vector& buffer, + const std::vector& expected, + const double epsilon) { + REQUIRE(buffer.size() == expected.size() * sizeof(Type)); + const int32_t byteStride = sizeof(Type); + if constexpr (std::is_same_v) { + for (size_t i = 0; i < expected.size(); ++i) { + const glm::vec3& value = + *reinterpret_cast(buffer.data() + i * byteStride); + const glm::vec3& expectedValue = expected[i]; + REQUIRE(Math::equalsEpsilon( + static_cast(value), + static_cast(expectedValue), + epsilon)); + } + } else if constexpr (std::is_same_v) { + for (size_t i = 0; i < expected.size(); ++i) { + const glm::vec4& value = + *reinterpret_cast(buffer.data() + i * byteStride); + const glm::vec4& expectedValue = expected[i]; + REQUIRE(Math::equalsEpsilon( + static_cast(value), + static_cast(expectedValue), + epsilon)); + } + } else { + FAIL("Buffer check with epsilon has not been implemented for the given " + "type."); + } +} + template static void checkAttribute( const Model& gltf, @@ -243,6 +273,7 @@ TEST_CASE("Converts point cloud with RGBA to glTF") { Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; @@ -302,6 +333,7 @@ TEST_CASE("Converts point cloud with RGB to glTF") { Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; @@ -361,6 +393,7 @@ TEST_CASE("Converts point cloud with RGB565 to glTF") { Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; @@ -419,6 +452,7 @@ TEST_CASE("Converts point cloud with CONSTANT_RGBA") { Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); CHECK(primitive.material == 0); CHECK(gltf.buffers.size() == 1); @@ -464,6 +498,7 @@ TEST_CASE("Converts point cloud with quantized positions to glTF") { Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; @@ -534,6 +569,7 @@ TEST_CASE("Converts point cloud with normals to glTF") { Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; @@ -594,6 +630,7 @@ TEST_CASE("Converts point cloud with oct-encoded normals to glTF") { Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; @@ -666,6 +703,7 @@ TEST_CASE( Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); auto primitiveExtension = primitive.getExtension(); @@ -744,6 +782,7 @@ TEST_CASE("Converts point cloud with per-point properties to glTF with " Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); auto primitiveExtension = primitive.getExtension(); @@ -783,12 +822,11 @@ TEST_CASE("Converts point cloud with per-point properties to glTF with " checkAttribute(gltf, primitive, "POSITION", pointsLength); checkAttribute(gltf, primitive, "COLOR_0", pointsLength); } -/* + TEST_CASE("Converts point cloud with Draco compression to glTF") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = testFilePath / "PointCloud" / "pointCloudDraco.pnts"; const int32_t pointsLength = 8; - const int32_t expectedAttributeCount = 3; GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); @@ -796,23 +834,195 @@ TEST_CASE("Converts point cloud with Draco compression to glTF") { Model& gltf = *result.model; CHECK(gltf.hasExtension()); + // The correctness of the model extension is thoroughly tested in + // TestUpgradeBatchTableToExtFeatureMetadata + CHECK(gltf.hasExtension()); + CHECK(gltf.nodes.size() == 1); + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); + + auto primitiveExtension = + primitive.getExtension(); + REQUIRE(primitiveExtension); + REQUIRE(primitiveExtension->featureIdAttributes.size() == 1); + FeatureIDAttribute& attribute = primitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + // Check for implicit feature IDs + CHECK(!attribute.featureIds.attribute); + CHECK(attribute.featureIds.constant == 0); + CHECK(attribute.featureIds.divisor == 1); + + REQUIRE(gltf.materials.size() == 1); + Material& material = gltf.materials[0]; + CHECK(!material.hasExtension()); + + // The file has three binary metadata properties: + // - "temperature": float scalars + // - "secondaryColor": float vec3s + // - "id": unsigned short scalars + // There are three accessors (one per primitive attribute) + // and three additional buffer views: + // - temperature buffer view + // - secondary color buffer view + // - id buffer view + REQUIRE(gltf.accessors.size() == 3); + REQUIRE(gltf.bufferViews.size() == 6); + // There is only one added buffer containing all the binary values. + REQUIRE(gltf.buffers.size() == 4); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == 3); + + // Check that position, color, and normal attributes are present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "NORMAL", pointsLength); + + // Check each attribute more thoroughly + { + uint32_t accessorId = static_cast(attributes.at("POSITION")); + Accessor& accessor = gltf.accessors[accessorId]; + + const glm::vec3 expectedMin(-4.9270443f, -3.9144449f, -4.8131480f); + CHECK(accessor.min[0] == Approx(expectedMin.x)); + CHECK(accessor.min[1] == Approx(expectedMin.y)); + CHECK(accessor.min[2] == Approx(expectedMin.z)); + + const glm::vec3 expectedMax(3.7791683f, 4.8152132f, 3.2142156f); + CHECK(accessor.max[0] == Approx(expectedMax.x)); + CHECK(accessor.max[1] == Approx(expectedMax.y)); + CHECK(accessor.max[2] == Approx(expectedMax.z)); + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + std::vector expected = { + glm::vec3(-4.9270443f, 0.8337686f, 0.1705846f), + glm::vec3(-2.9789500f, 2.6891474f, 2.9824265f), + glm::vec3(-2.8329495f, -3.9144449f, -1.2851576f), + glm::vec3(-2.9022198f, -3.6128526f, 1.8772986f), + glm::vec3(-4.2673778f, -0.6459517f, -2.5240305f), + glm::vec3(3.7791683f, 0.6222278f, 3.2142156f), + glm::vec3(0.6870481f, -1.1670776f, -4.8131480f), + glm::vec3(-0.3168385f, 4.8152132f, 1.3087492f), + }; + checkBufferContents(buffer.cesium.data, expected); + } + + { + uint32_t accessorId = static_cast(attributes.at("COLOR_0")); + Accessor& accessor = gltf.accessors[accessorId]; + CHECK(accessor.normalized); + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + std::vector expected = { + glm::u8vec3(182, 215, 153), + glm::u8vec3(108, 159, 164), + glm::u8vec3(227, 14, 245), + glm::u8vec3(201, 207, 134), + glm::u8vec3(236, 213, 18), + glm::u8vec3(5, 93, 212), + glm::u8vec3(221, 221, 249), + glm::u8vec3(117, 132, 199), + }; + checkBufferContents(buffer.cesium.data, expected); + } + + { + uint32_t accessorId = static_cast(attributes.at("NORMAL")); + Accessor& accessor = gltf.accessors[accessorId]; + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + // The Draco-decoded normals are slightly different from the values + // derived by manually decoding the uncompressed oct-encoded normals, + // hence the less precise comparison. + std::vector expected{ + glm::vec3(-0.9824559f, 0.1803542f, 0.0474616f), + glm::vec3(-0.5766854f, 0.5427628f, 0.6106081f), + glm::vec3(-0.5725988f, -0.7802446f, -0.2516918f), + glm::vec3(-0.5705807f, -0.7345407f, 0.36727036f), + glm::vec3(-0.8560267f, -0.1281128f, -0.5008047f), + glm::vec3(0.7647877f, 0.11264316f, 0.63435888f), + glm::vec3(0.1301889f, -0.23434004f, -0.9633979f), + glm::vec3(-0.0450783f, 0.9616723f, 0.2704703f), + }; + checkBufferContents( + buffer.cesium.data, + expected, + Math::Epsilon1); + } +} + +TEST_CASE("Converts point cloud with partial Draco compression to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudDracoPartial.pnts"; + const int32_t pointsLength = 8; + + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + CHECK(gltf.hasExtension()); + CHECK(gltf.hasExtension()); + + CHECK(gltf.nodes.size() == 1); REQUIRE(gltf.meshes.size() == 1); Mesh& mesh = gltf.meshes[0]; REQUIRE(mesh.primitives.size() == 1); MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); + + auto primitiveExtension = + primitive.getExtension(); + REQUIRE(primitiveExtension); + REQUIRE(primitiveExtension->featureIdAttributes.size() == 1); + FeatureIDAttribute& attribute = primitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + // Check for implicit feature IDs + CHECK(!attribute.featureIds.attribute); + CHECK(attribute.featureIds.constant == 0); + CHECK(attribute.featureIds.divisor == 1); REQUIRE(gltf.materials.size() == 1); Material& material = gltf.materials[0]; CHECK(!material.hasExtension()); - REQUIRE(gltf.accessors.size() == expectedAttributeCount); - REQUIRE(gltf.bufferViews.size() == expectedAttributeCount); - REQUIRE(gltf.buffers.size() == expectedAttributeCount); + // The file has three binary metadata properties: + // - "temperature": float scalars + // - "secondaryColor": float vec3s + // - "id": unsigned short scalars + // There are three accessors (one per primitive attribute) + // and three additional buffer views: + // - temperature buffer view + // - secondary color buffer view + // - id buffer view + REQUIRE(gltf.accessors.size() == 3); + REQUIRE(gltf.bufferViews.size() == 6); + + // There is only one added buffer containing all the binary values. + REQUIRE(gltf.buffers.size() == 4); auto attributes = primitive.attributes; - REQUIRE(attributes.size() == expectedAttributeCount); + REQUIRE(attributes.size() == 3); // Check that position, color, and normal attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); @@ -821,7 +1031,64 @@ TEST_CASE("Converts point cloud with Draco compression to glTF") { // Check each attribute more thoroughly { - /* uint32_t accessorId = static_cast(attributes.at("POSITION")); + uint32_t accessorId = static_cast(attributes.at("POSITION")); + Accessor& accessor = gltf.accessors[accessorId]; + + const glm::vec3 expectedMin(-4.9270443f, -3.9144449f, -4.8131480f); + CHECK(accessor.min[0] == Approx(expectedMin.x)); + CHECK(accessor.min[1] == Approx(expectedMin.y)); + CHECK(accessor.min[2] == Approx(expectedMin.z)); + + const glm::vec3 expectedMax(3.7791683f, 4.8152132f, 3.2142156f); + CHECK(accessor.max[0] == Approx(expectedMax.x)); + CHECK(accessor.max[1] == Approx(expectedMax.y)); + CHECK(accessor.max[2] == Approx(expectedMax.z)); + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + std::vector expected = { + glm::vec3(-4.9270443f, 0.8337686f, 0.1705846f), + glm::vec3(-2.9789500f, 2.6891474f, 2.9824265f), + glm::vec3(-2.8329495f, -3.9144449f, -1.2851576f), + glm::vec3(-2.9022198f, -3.6128526f, 1.8772986f), + glm::vec3(-4.2673778f, -0.6459517f, -2.5240305f), + glm::vec3(3.7791683f, 0.6222278f, 3.2142156f), + glm::vec3(0.6870481f, -1.1670776f, -4.8131480f), + glm::vec3(-0.3168385f, 4.8152132f, 1.3087492f), + }; + checkBufferContents(buffer.cesium.data, expected); + } + + { + uint32_t accessorId = static_cast(attributes.at("COLOR_0")); + Accessor& accessor = gltf.accessors[accessorId]; + CHECK(accessor.normalized); + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + std::vector expected = { + glm::u8vec3(182, 215, 153), + glm::u8vec3(108, 159, 164), + glm::u8vec3(227, 14, 245), + glm::u8vec3(201, 207, 134), + glm::u8vec3(236, 213, 18), + glm::u8vec3(5, 93, 212), + glm::u8vec3(221, 221, 249), + glm::u8vec3(117, 132, 199), + }; + checkBufferContents(buffer.cesium.data, expected); + } + + { + uint32_t accessorId = static_cast(attributes.at("NORMAL")); Accessor& accessor = gltf.accessors[accessorId]; uint32_t bufferViewId = static_cast(accessor.bufferView); @@ -831,14 +1098,15 @@ TEST_CASE("Converts point cloud with Draco compression to glTF") { Buffer& buffer = gltf.buffers[bufferId]; const std::vector expected = { - glm::vec3(-0.9856477, 0.1634960, 0.0420418), - glm::vec3(-0.5901730, 0.5359042, 0.6037402), - glm::vec3(-0.5674310, -0.7817938, -0.2584963), - glm::vec3(-0.5861990, -0.7179291, 0.3754308), - glm::vec3(-0.8519385, -0.1283743, -0.5076620), - glm::vec3(0.7587127, 0.1254564, 0.6392304), - glm::vec3(0.1354662, -0.2292506, -0.9638947), - glm::vec3(-0.0656172, 0.9640687, 0.2574214)}; + glm::vec3(-0.9854088, 0.1667507, 0.0341110), + glm::vec3(-0.5957704, 0.5378777, 0.5964436), + glm::vec3(-0.5666092, -0.7828890, -0.2569800), + glm::vec3(-0.5804154, -0.7226123, 0.3754320), + glm::vec3(-0.8535281, -0.1291752, -0.5047805), + glm::vec3(0.7557975, 0.1243999, 0.6428800), + glm::vec3(0.1374090, -0.2333731, -0.9626296), + glm::vec3(-0.0633145, 0.9630424, 0.2618022)}; + checkBufferContents(buffer.cesium.data, expected); } -}*/ +} diff --git a/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp b/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp index c5b2fcfbc..5bfd9bd2a 100644 --- a/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp +++ b/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp @@ -900,6 +900,160 @@ TEST_CASE("Converts per-point PNTS batch table to EXT_feature_metadata") { } } +TEST_CASE("Converts Draco per-point PNTS batch table to " + "EXT_feature_metadata") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudDraco.pnts"; + + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + ExtensionModelExtFeatureMetadata* pExtension = + gltf.getExtension(); + REQUIRE(pExtension); + + // Check the schema + REQUIRE(pExtension->schema); + REQUIRE(pExtension->schema->classes.size() == 1); + + auto firstClassIt = pExtension->schema->classes.begin(); + CHECK(firstClassIt->first == "default"); + + CesiumGltf::Class& defaultClass = firstClassIt->second; + REQUIRE(defaultClass.properties.size() == 3); + + { + auto temperatureIt = defaultClass.properties.find("temperature"); + REQUIRE(temperatureIt != defaultClass.properties.end()); + auto secondaryColorIt = defaultClass.properties.find("secondaryColor"); + REQUIRE(secondaryColorIt != defaultClass.properties.end()); + auto idIt = defaultClass.properties.find("id"); + REQUIRE(idIt != defaultClass.properties.end()); + + CHECK(temperatureIt->second.type == "FLOAT32"); + CHECK(secondaryColorIt->second.type == "ARRAY"); + REQUIRE(secondaryColorIt->second.componentType); + CHECK(secondaryColorIt->second.componentType.value() == "FLOAT32"); + CHECK(idIt->second.type == "UINT16"); + } + + // Check the feature table + auto firstFeatureTableIt = pExtension->featureTables.begin(); + REQUIRE(firstFeatureTableIt != pExtension->featureTables.end()); + + FeatureTable& featureTable = firstFeatureTableIt->second; + CHECK(featureTable.classProperty == "default"); + REQUIRE(featureTable.properties.size() == 3); + + { + auto temperatureIt = featureTable.properties.find("temperature"); + REQUIRE(temperatureIt != featureTable.properties.end()); + auto secondaryColorIt = featureTable.properties.find("secondaryColor"); + REQUIRE(secondaryColorIt != featureTable.properties.end()); + auto idIt = featureTable.properties.find("id"); + REQUIRE(idIt != featureTable.properties.end()); + + CHECK(temperatureIt->second.bufferView >= 0); + CHECK( + temperatureIt->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(secondaryColorIt->second.bufferView >= 0); + CHECK( + secondaryColorIt->second.bufferView < + static_cast(gltf.bufferViews.size())); + CHECK(idIt->second.bufferView >= 0); + CHECK( + idIt->second.bufferView < + static_cast(gltf.bufferViews.size())); + } + + std::set bufferViewSet = + getUniqueBufferViewIds(gltf.accessors, featureTable); + CHECK(bufferViewSet.size() == gltf.bufferViews.size()); + + // Check the mesh primitives + CHECK(!gltf.meshes.empty()); + + for (Mesh& mesh : gltf.meshes) { + CHECK(!mesh.primitives.empty()); + for (MeshPrimitive& primitive : mesh.primitives) { + CHECK( + primitive.attributes.find("_FEATURE_ID_0") == + primitive.attributes.end()); + + ExtensionMeshPrimitiveExtFeatureMetadata* pPrimitiveExtension = + primitive.getExtension(); + REQUIRE(pPrimitiveExtension); + REQUIRE(pPrimitiveExtension->featureIdAttributes.size() == 1); + + FeatureIDAttribute& attribute = + pPrimitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + // Check for implicit feature IDs + CHECK(!attribute.featureIds.attribute); + CHECK(attribute.featureIds.constant == 0); + CHECK(attribute.featureIds.divisor == 1); + } + } + + // Check metadata values + { + std::vector expected = { + 0.2883025f, + 0.4338731f, + 0.1751145f, + 0.1430345f, + 0.1156959f, + 0.3274441f, + 0.1337535f, + 0.0207673f}; + checkScalarProperty( + *result.model, + featureTable, + defaultClass, + "temperature", + "FLOAT32", + expected, + expected.size()); + } + + { + std::vector> expected = { + {0.1182744f, 0, 0}, + {0.7206645f, 0, 0}, + {0.6399421f, 0, 0}, + {0.5820239f, 0, 0}, + {0.1432983f, 0, 0}, + {0.5374249f, 0, 0}, + {0.9446688f, 0, 0}, + {0.7586040f, 0, 0}}; + + checkArrayProperty( + *result.model, + featureTable, + defaultClass, + "secondaryColor", + 3, + "FLOAT32", + expected, + expected.size()); + } + + { + std::vector expected = {0, 1, 2, 3, 4, 5, 6, 7}; + checkScalarProperty( + *result.model, + featureTable, + defaultClass, + "id", + "UINT16", + expected, + expected.size()); + } +} + TEST_CASE("Upgrade json nested json metadata to string") { std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; testFilePath = diff --git a/Cesium3DTilesSelection/test/data/PointCloud/pointCloudDracoBatched.pnts b/Cesium3DTilesSelection/test/data/PointCloud/pointCloudDracoBatched.pnts new file mode 100644 index 0000000000000000000000000000000000000000..0aabaac25b9facf7c96fc444db042f458850ce59 GIT binary patch literal 1016 zcmaJ=TSyd97@pZ()9q>{X3%9h5%e(0&SiIJ2c6lwu5GimI|XJo>F%ie;Ow?Lgtjha zEJ@LeN@ztdLJ{4lw|Z)QlOn80h{yE|^z!5PT ztP}k{+0!WbLGeX`q7;=wez8uf2OJB_OJ3RQ7bSlLa7`pd+N>m1&9W5BI_wUbCH6B8 z+D?;nwbe$FPN&Vu5R8>2?N(bgMUj-<%23Tq4f$)Evsio)w>NBn!@;mJ2;793;lcoS za2zs&$$(=<18&6O8r$T+U2ul3;SZ`e&0H57z~i3&U#>!+>xr@A>!N^cam>6M-R<*_4kjyS#zyG&$-w$AJH9E->4xz~u@Wd+Kyp~fycI8l zPZeHe!p0t68MrME^_KR;5BHw0pIKOhnzKjkhdiAP1*vvAlQA*0yF@ zVe-`;!L?W+FweK}2l~yz>xoms2;&jn4V4I$pWoIFh;R7O>?}W#>Eowf@O(8B5boZp z5iVYy=1)Ft=0D{(3nQo3^5tKz){M{Hej`FX`kR@F+zE+L*iuEFssMTyDz z@df#rc_s15`MCu}sl^~QASDGw`30#(C7G#U4FUeao*|z8eo9sbwK_^kl_jbEX=%l& zB|tue>yw(7UXr0?Wn^Yh3siwnUyZ67C>P}J1hN3x5Rfh%B|rZlUq>GdISUi8fu4RL z!SOz>e(oV2N>&y?V?vzcon8GxT!WOXq797GBWV0GYT*>8Za}6%y7@%@@;o|`^^9E_k-+Y_|Fej z!pz7a&nUpi!KlE(V5cx4!xb2h{frC?+!+`YDu8MhvI7})K*5<{2g7wTG6HonGg>e+ z1hj$-1S(^QTs8NAOIy_ef7V$C)~(@ox4W_I^R;B$|Eb#1KI7;=+mn-3?7wZ{v5Whe zx9@3Q*?!U0jeEazmf3&gYO@15fa0`Sl9~%lsfi_}MX5?whB`{csmb|yDTzgu&iOg{ zMZhGfqm-GVWMy0nN|-2$Q4=9BC4=%neqL%`Nl0Y@&@d%8AAiRXC7><{H`v+H$1z9= pXbid?q?#1w>TC=$2jnnC6WUE+g1{DZJK2b78x6aY%uJj(z8 literal 0 HcmV?d00001 From c19db9f12ac7aad29856afc4e5007b88bc8f77c3 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 31 Jan 2023 16:43:08 -0500 Subject: [PATCH 13/20] Add test for Draco-compressed batched pnts --- .../test/TestPntsToGltfConverter.cpp | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index a616f63eb..792fbd4eb 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -1110,3 +1110,166 @@ TEST_CASE("Converts point cloud with partial Draco compression to glTF") { checkBufferContents(buffer.cesium.data, expected); } } + +TEST_CASE("Converts batched point cloud with Draco compression to glTF") { + std::filesystem::path testFilePath = Cesium3DTilesSelection_TEST_DATA_DIR; + testFilePath = testFilePath / "PointCloud" / "pointCloudDracoBatched.pnts"; + const int32_t pointsLength = 8; + + GltfConverterResult result = ConvertTileToGltf::fromPnts(testFilePath); + + REQUIRE(result.model); + Model& gltf = *result.model; + + // The correctness of the model extension is thoroughly tested in + // TestUpgradeBatchTableToExtFeatureMetadata + CHECK(gltf.hasExtension()); + + CHECK(gltf.nodes.size() == 1); + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); + MeshPrimitive& primitive = mesh.primitives[0]; + CHECK(primitive.mode == MeshPrimitive::Mode::POINTS); + + auto primitiveExtension = + primitive.getExtension(); + REQUIRE(primitiveExtension); + REQUIRE(primitiveExtension->featureIdAttributes.size() == 1); + FeatureIDAttribute& attribute = primitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + CHECK(attribute.featureIds.attribute == "_FEATURE_ID_0"); + + CHECK(gltf.materials.size() == 1); + + // The file has three metadata properties: + // - "name": string scalars in JSON + // - "dimensions": float vec3s in binary + // - "id": int scalars in binary + // There are four accessors (one per primitive attribute) + // and four additional buffer views: + // - "name" string data buffer view + // - "name" string offsets buffer view + // - "dimensions" buffer view + // - "id" buffer view + REQUIRE(gltf.accessors.size() == 4); + REQUIRE(gltf.bufferViews.size() == 8); + + // There are also three added buffers: + // - binary data in the batch table + // - string data of "name" + // - string offsets for the data for "name" + REQUIRE(gltf.buffers.size() == 7); + std::set bufferSet = getUniqueBufferIds(gltf.bufferViews); + CHECK(bufferSet.size() == 7); + + auto attributes = primitive.attributes; + REQUIRE(attributes.size() == 4); + + // Check that position, normal, and feature ID attributes are present + checkAttribute(gltf, primitive, "POSITION", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "NORMAL", pointsLength); + checkAttribute(gltf, primitive, "_FEATURE_ID_0", pointsLength); + + // Check each attribute more thoroughly + { + uint32_t accessorId = static_cast(attributes.at("POSITION")); + Accessor& accessor = gltf.accessors[accessorId]; + + const glm::vec3 expectedMin(-4.9270443f, -3.9144449f, -4.8131480f); + CHECK(accessor.min[0] == Approx(expectedMin.x)); + CHECK(accessor.min[1] == Approx(expectedMin.y)); + CHECK(accessor.min[2] == Approx(expectedMin.z)); + + const glm::vec3 expectedMax(3.7791683f, 4.8152132f, 3.2142156f); + CHECK(accessor.max[0] == Approx(expectedMax.x)); + CHECK(accessor.max[1] == Approx(expectedMax.y)); + CHECK(accessor.max[2] == Approx(expectedMax.z)); + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + std::vector expected = { + glm::vec3(-4.9270443f, 0.8337686f, 0.1705846f), + glm::vec3(-2.9789500f, 2.6891474f, 2.9824265f), + glm::vec3(-2.8329495f, -3.9144449f, -1.2851576f), + glm::vec3(-2.9022198f, -3.6128526f, 1.8772986f), + glm::vec3(-4.2673778f, -0.6459517f, -2.5240305f), + glm::vec3(3.7791683f, 0.6222278f, 3.2142156f), + glm::vec3(0.6870481f, -1.1670776f, -4.8131480f), + glm::vec3(-0.3168385f, 4.8152132f, 1.3087492f), + }; + checkBufferContents(buffer.cesium.data, expected); + } + + { + uint32_t accessorId = static_cast(attributes.at("COLOR_0")); + Accessor& accessor = gltf.accessors[accessorId]; + CHECK(accessor.normalized); + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + std::vector expected = { + glm::u8vec3(182, 215, 153), + glm::u8vec3(108, 159, 164), + glm::u8vec3(227, 14, 245), + glm::u8vec3(201, 207, 134), + glm::u8vec3(236, 213, 18), + glm::u8vec3(5, 93, 212), + glm::u8vec3(221, 221, 249), + glm::u8vec3(117, 132, 199), + }; + checkBufferContents(buffer.cesium.data, expected); + } + + { + uint32_t accessorId = static_cast(attributes.at("NORMAL")); + Accessor& accessor = gltf.accessors[accessorId]; + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + // The Draco-decoded normals are slightly different from the values + // derived by manually decoding the uncompressed oct-encoded normals, + // hence the less precise comparison. + std::vector expected{ + glm::vec3(-0.9824559f, 0.1803542f, 0.0474616f), + glm::vec3(-0.5766854f, 0.5427628f, 0.6106081f), + glm::vec3(-0.5725988f, -0.7802446f, -0.2516918f), + glm::vec3(-0.5705807f, -0.7345407f, 0.36727036f), + glm::vec3(-0.8560267f, -0.1281128f, -0.5008047f), + glm::vec3(0.7647877f, 0.11264316f, 0.63435888f), + glm::vec3(0.1301889f, -0.23434004f, -0.9633979f), + glm::vec3(-0.0450783f, 0.9616723f, 0.2704703f), + }; + checkBufferContents( + buffer.cesium.data, + expected, + Math::Epsilon1); + } + + { + uint32_t accessorId = static_cast(attributes.at("_FEATURE_ID_0")); + Accessor& accessor = gltf.accessors[accessorId]; + + uint32_t bufferViewId = static_cast(accessor.bufferView); + BufferView& bufferView = gltf.bufferViews[bufferViewId]; + + uint32_t bufferId = static_cast(bufferView.buffer); + Buffer& buffer = gltf.buffers[bufferId]; + + const std::vector expected = {5, 5, 6, 6, 7, 0, 3, 1}; + checkBufferContents(buffer.cesium.data, expected); + } +} From b91652e2f733270554e81660a578292caffffcea Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 31 Jan 2023 18:14:29 -0500 Subject: [PATCH 14/20] Code fixes and cleanup --- .../src/PntsToGltfConverter.cpp | 356 +++++++++--------- 1 file changed, 171 insertions(+), 185 deletions(-) diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index e8831c7d9..5a58819b5 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -1,7 +1,13 @@ #include "PntsToGltfConverter.h" #include "BatchTableToGltfFeatureMetadata.h" -#include "CesiumGeometry/AxisTransforms.h" + +#include +#include +#include +#include +#include +#include #ifdef _MSC_VER #pragma warning(push) @@ -17,16 +23,9 @@ #pragma warning(pop) #endif -#include -#include -#include -#include -#include - #include #include -#include #include using namespace CesiumGltf; @@ -190,14 +189,14 @@ struct PntsContent { std::optional quantizedVolumeOffset; std::optional quantizedVolumeScale; std::optional constantRgba; + std::optional batchLength; PntsSemantic position; + bool positionQuantized = false; // required by glTF spec glm::vec3 positionMin = glm::vec3(std::numeric_limits::max()); glm::vec3 positionMax = glm::vec3(std::numeric_limits::lowest()); - bool positionQuantized = false; - std::optional color; PntsColorType colorType = PntsColorType::CONSTANT; @@ -205,8 +204,7 @@ struct PntsContent { bool normalOctEncoded = false; std::optional batchId; - std::optional batchIdComponentType; - std::optional batchLength; + std::optional batchIdComponentType; std::optional dracoByteOffset; std::optional dracoByteLength; @@ -249,13 +247,11 @@ void parsePositionsFromFeatureTableJson( if (byteOffsetIt == positionIt->value.MemberEnd() || !byteOffsetIt->value.IsUint()) { parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, POSITION semantic does not have " + "Error parsing PNTS feature table, POSITION does not have " "valid byteOffset."); return; } - parsedContent.position.byteOffset = byteOffsetIt->value.GetUint(); - return; } @@ -276,7 +272,7 @@ void parsePositionsFromFeatureTableJson( !validateJsonArrayValues(quantizedVolumeOffsetIt->value, 3, isNumber)) { parsedContent.errors.emplaceError( "Error parsing PNTS feature table, POSITION_QUANTIZED is used but " - "no valid QUANTIZED_VOLUME_OFFSET semantic was found."); + "no valid QUANTIZED_VOLUME_OFFSET was found."); return; } @@ -284,7 +280,7 @@ void parsePositionsFromFeatureTableJson( !validateJsonArrayValues(quantizedVolumeScaleIt->value, 3, isNumber)) { parsedContent.errors.emplaceError( "Error parsing PNTS feature table, POSITION_QUANTIZED is used but " - "no valid QUANTIZED_VOLUME_SCALE semantic was found."); + "no valid QUANTIZED_VOLUME_SCALE was found."); return; } @@ -293,8 +289,8 @@ void parsePositionsFromFeatureTableJson( if (byteOffsetIt == positionQuantizedIt->value.MemberEnd() || !byteOffsetIt->value.IsUint()) { parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, POSITION_QUANTIZED semantic does " - "not have valid byteOffset."); + "Error parsing PNTS feature table, POSITION_QUANTIZED does not have " + "valid byteOffset."); return; } @@ -304,12 +300,12 @@ void parsePositionsFromFeatureTableJson( auto quantizedVolumeOffset = quantizedVolumeOffsetIt->value.GetArray(); auto quantizedVolumeScale = quantizedVolumeScaleIt->value.GetArray(); - parsedContent.quantizedVolumeOffset = std::make_optional( + parsedContent.quantizedVolumeOffset = glm::dvec3( quantizedVolumeOffset[0].GetDouble(), quantizedVolumeOffset[1].GetDouble(), quantizedVolumeOffset[2].GetDouble()); - parsedContent.quantizedVolumeScale = std::make_optional( + parsedContent.quantizedVolumeScale = glm::dvec3( quantizedVolumeScale[0].GetDouble(), quantizedVolumeScale[1].GetDouble(), quantizedVolumeScale[2].GetDouble()); @@ -318,8 +314,8 @@ void parsePositionsFromFeatureTableJson( } parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, no POSITION semantic was found. " - "One of POSITION or POSITION_QUANTIZED must be defined."); + "Error parsing PNTS feature table, one of POSITION or POSITION_QUANTIZED " + "must be defined."); return; } @@ -337,9 +333,8 @@ void parseColorsFromFeatureTableJson( parsedContent.colorType = PntsColorType::RGBA; return; } - parsedContent.errors.emplaceWarning( - "Error parsing PNTS feature table, RGBA semantic does not have valid " + "Error parsing PNTS feature table, RGBA does not have valid " "byteOffset. Skip parsing RGBA colors."); } @@ -353,10 +348,9 @@ void parseColorsFromFeatureTableJson( parsedContent.colorType = PntsColorType::RGB; return; } - parsedContent.errors.emplaceWarning( - "Error parsing PNTS feature table, RGB semantic does not have valid " - "byteOffset. Skip parsing RGB colors."); + "Error parsing PNTS feature table, RGB does not have valid byteOffset. " + "Skip parsing RGB colors."); } const auto rgb565It = featureTableJson.FindMember("RGB565"); @@ -371,8 +365,8 @@ void parseColorsFromFeatureTableJson( } parsedContent.errors.emplaceWarning( - "Error parsing PNTS feature table, RGB565 semantic does not have " - "valid byteOffset. Skip parsing RGB565 colors."); + "Error parsing PNTS feature table, RGB565 does not have valid " + "byteOffset. Skip parsing RGB565 colors."); } auto isUint = [](const rapidjson::Value& value) -> bool { @@ -383,7 +377,7 @@ void parseColorsFromFeatureTableJson( if (constantRgbaIt != featureTableJson.MemberEnd() && validateJsonArrayValues(constantRgbaIt->value, 4, isUint)) { const rapidjson::Value& arrayValue = constantRgbaIt->value; - parsedContent.constantRgba = std::make_optional( + parsedContent.constantRgba = glm::u8vec4( arrayValue[0].GetUint(), arrayValue[1].GetUint(), arrayValue[2].GetUint(), @@ -405,8 +399,8 @@ void parseNormalsFromFeatureTableJson( } parsedContent.errors.emplaceWarning( - "Error parsing PNTS feature table, NORMAL semantic does not have " - "valid byteOffset. Skip parsing normals."); + "Error parsing PNTS feature table, NORMAL does not have valid " + "byteOffset. Skip parsing normals."); } const auto normalOct16pIt = featureTableJson.FindMember("NORMAL_OCT16P"); @@ -422,8 +416,8 @@ void parseNormalsFromFeatureTableJson( } parsedContent.errors.emplaceWarning( - "Error parsing PNTS feature table, NORMAL_OCT16P semantic does not " - "have valid byteOffset. Skip parsing oct-encoded normals"); + "Error parsing PNTS feature table, NORMAL_OCT16P does not have valid " + "byteOffset. Skip parsing oct-encoded normals."); } } @@ -453,32 +447,40 @@ void parseBatchIdsFromFeatureTableJson( if (componentTypeIt != featureTableJson.MemberEnd() && componentTypeIt->value.IsString()) { const std::string& componentTypeString = componentTypeIt->value.GetString(); + if (stringToMetadataComponentType.find(componentTypeString) == + stringToMetadataComponentType.end()) { + parsedContent.errors.emplaceWarning( + "Error parsing PNTS feature table, BATCH_ID does not have " + "valid componentType. Skip parsing batch IDs."); + return; + } - if (componentTypeString == "UNSIGNED_BYTE") { - parsedContent.batchIdComponentType = - Accessor::ComponentType::UNSIGNED_BYTE; - } else if (componentTypeString == "UNSIGNED_INT") { - parsedContent.batchIdComponentType = - Accessor::ComponentType::UNSIGNED_INT; - } else { - parsedContent.batchIdComponentType = - Accessor::ComponentType::UNSIGNED_SHORT; + MetadataProperty::ComponentType componentType = + stringToMetadataComponentType.at(componentTypeString); + if (componentType != MetadataProperty::ComponentType::UNSIGNED_BYTE && + componentType != MetadataProperty::ComponentType::UNSIGNED_SHORT && + componentType != MetadataProperty::ComponentType::UNSIGNED_INT) { + parsedContent.errors.emplaceWarning( + "Error parsing PNTS feature table, BATCH_ID componentType is defined " + "but is not UNSIGNED_BYTE, UNSIGNED_SHORT, or UNSIGNED_INT. Skip " + "parsing batch IDs."); + return; } + parsedContent.batchIdComponentType = componentType; } else { parsedContent.batchIdComponentType = - Accessor::ComponentType::UNSIGNED_SHORT; + MetadataProperty::ComponentType::UNSIGNED_SHORT; } const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); if (batchLengthIt == featureTableJson.MemberEnd() || !batchLengthIt->value.IsUint()) { parsedContent.errors.emplaceWarning( - "Error parsing PNTS feature table, BATCH_ID semantic is present but " - "no valid BATCH_LENGTH is defined. Skip parsing metadata."); + "Error parsing PNTS feature table, BATCH_ID is defined but no valid " + "BATCH_LENGTH is defined. Skip parsing metadata."); parsedContent.metadataHasErrors = true; return; } - parsedContent.batchLength = batchLengthIt->value.GetUint(); } @@ -513,7 +515,7 @@ void parseSemanticsFromFeatureTableJson( if (rtcIt != featureTableJson.MemberEnd() && validateJsonArrayValues(rtcIt->value, 3, isNumber)) { const rapidjson::Value& rtcValue = rtcIt->value; - parsedContent.rtcCenter = std::make_optional( + parsedContent.rtcCenter = glm::vec3( rtcValue[0].GetDouble(), rtcValue[1].GetDouble(), rtcValue[2].GetDouble()); @@ -521,83 +523,74 @@ void parseSemanticsFromFeatureTableJson( } void parseDracoExtensionFromFeatureTableJson( - const rapidjson::Value& dracoExtensionValue, + const rapidjson::Value& dracoExtension, PntsContent& parsedContent) { - const auto propertiesIt = dracoExtensionValue.FindMember("properties"); - if (propertiesIt == dracoExtensionValue.MemberEnd() || + const auto propertiesIt = dracoExtension.FindMember("properties"); + if (propertiesIt == dracoExtension.MemberEnd() || !propertiesIt->value.IsObject()) { parsedContent.errors.emplaceError( - "Error parsing Draco compression extension, " + "Error parsing 3DTILES_draco_compression extension, " "no valid properties object found."); return; } - const auto byteOffsetIt = dracoExtensionValue.FindMember("byteOffset"); - if (byteOffsetIt == dracoExtensionValue.MemberEnd() || + const auto byteOffsetIt = dracoExtension.FindMember("byteOffset"); + if (byteOffsetIt == dracoExtension.MemberEnd() || !byteOffsetIt->value.IsUint64()) { parsedContent.errors.emplaceError( - "Error parsing Draco compression extension, " + "Error parsing 3DTILES_draco_compression extension, " "no valid byteOffset found."); return; } - const auto byteLengthIt = dracoExtensionValue.FindMember("byteLength"); - if (byteLengthIt == dracoExtensionValue.MemberEnd() || + const auto byteLengthIt = dracoExtension.FindMember("byteLength"); + if (byteLengthIt == dracoExtension.MemberEnd() || !byteLengthIt->value.IsUint64()) { parsedContent.errors.emplaceError( - "Error parsing Draco compression extension, " + "Error parsing 3DTILES_draco_compression extension, " "no valid byteLength found."); return; } - parsedContent.dracoByteOffset = - std::make_optional(byteOffsetIt->value.GetUint()); - parsedContent.dracoByteLength = - std::make_optional(byteLengthIt->value.GetUint()); + parsedContent.dracoByteOffset = byteOffsetIt->value.GetUint(); + parsedContent.dracoByteLength = byteLengthIt->value.GetUint(); - const rapidjson::Value& dracoPropertiesValue = propertiesIt->value; - - auto positionDracoIdIt = dracoPropertiesValue.FindMember("POSITION"); - if (positionDracoIdIt != dracoPropertiesValue.MemberEnd() && + const rapidjson::Value& dracoProperties = propertiesIt->value; + auto positionDracoIdIt = dracoProperties.FindMember("POSITION"); + if (positionDracoIdIt != dracoProperties.MemberEnd() && positionDracoIdIt->value.IsInt()) { - parsedContent.position.dracoId = - std::make_optional(positionDracoIdIt->value.GetInt()); + parsedContent.position.dracoId = positionDracoIdIt->value.GetInt(); } if (parsedContent.color) { - const PntsColorType& colorType = parsedContent.colorType; - if (colorType == PntsColorType::RGBA) { - auto rgbaDracoIdIt = dracoPropertiesValue.FindMember("RGBA"); - if (rgbaDracoIdIt != dracoPropertiesValue.MemberEnd() && + if (parsedContent.colorType == PntsColorType::RGBA) { + auto rgbaDracoIdIt = dracoProperties.FindMember("RGBA"); + if (rgbaDracoIdIt != dracoProperties.MemberEnd() && rgbaDracoIdIt->value.IsInt()) { - parsedContent.color.value().dracoId = - std::make_optional(rgbaDracoIdIt->value.GetInt()); + parsedContent.color.value().dracoId = rgbaDracoIdIt->value.GetInt(); } - } else if (colorType == PntsColorType::RGB) { - auto rgbDracoIdIt = dracoPropertiesValue.FindMember("RGB"); - if (rgbDracoIdIt != dracoPropertiesValue.MemberEnd() && + } else if (parsedContent.colorType == PntsColorType::RGB) { + auto rgbDracoIdIt = dracoProperties.FindMember("RGB"); + if (rgbDracoIdIt != dracoProperties.MemberEnd() && rgbDracoIdIt->value.IsInt()) { - parsedContent.color.value().dracoId = - std::make_optional(rgbDracoIdIt->value.GetInt()); + parsedContent.color.value().dracoId = rgbDracoIdIt->value.GetInt(); } } } if (parsedContent.normal) { - auto normalDracoIdIt = dracoPropertiesValue.FindMember("NORMAL"); - if (normalDracoIdIt != dracoPropertiesValue.MemberEnd() && + auto normalDracoIdIt = dracoProperties.FindMember("NORMAL"); + if (normalDracoIdIt != dracoProperties.MemberEnd() && normalDracoIdIt->value.IsInt()) { - parsedContent.normal.value().dracoId = - std::make_optional(normalDracoIdIt->value.GetInt()); + parsedContent.normal.value().dracoId = normalDracoIdIt->value.GetInt(); } } if (parsedContent.batchId) { - auto batchIdDracoIdIt = dracoPropertiesValue.FindMember("BATCH_ID"); - if (batchIdDracoIdIt != dracoPropertiesValue.MemberEnd() && + auto batchIdDracoIdIt = dracoProperties.FindMember("BATCH_ID"); + if (batchIdDracoIdIt != dracoProperties.MemberEnd() && batchIdDracoIdIt->value.IsInt()) { - parsedContent.batchId.value().dracoId = - std::make_optional(batchIdDracoIdIt->value.GetInt()); + parsedContent.batchId.value().dracoId = batchIdDracoIdIt->value.GetInt(); } } } @@ -622,18 +615,16 @@ rapidjson::Document parseFeatureTableJson( if (pointsLengthIt == document.MemberEnd() || !pointsLengthIt->value.IsUint()) { parsedContent.errors.emplaceError( - "Error parsing PNTS feature table, no " - "valid POINTS_LENGTH property was found."); + "Error parsing PNTS feature table, no valid POINTS_LENGTH was found."); return document; } - parsedContent.pointsLength = pointsLengthIt->value.GetUint(); if (parsedContent.pointsLength == 0) { // This *should* be disallowed by the spec, but it currently isn't. // In the future, this can be converted to an error. parsedContent.errors.emplaceWarning("The PNTS has a POINTS_LENGTH of zero. " - "Skip parsing feature table."); + "Skip parsing the PNTS feature table."); return document; } @@ -648,13 +639,10 @@ rapidjson::Document parseFeatureTableJson( extensionsIt->value.FindMember("3DTILES_draco_point_compression"); if (dracoExtensionIt != extensionsIt->value.MemberEnd() && dracoExtensionIt->value.IsObject()) { - const rapidjson::Value& dracoExtensionValue = dracoExtensionIt->value; - parseDracoExtensionFromFeatureTableJson( - dracoExtensionValue, - parsedContent); + const rapidjson::Value& dracoExtension = dracoExtensionIt->value; + parseDracoExtensionFromFeatureTableJson(dracoExtension, parsedContent); } } - return document; } @@ -676,8 +664,9 @@ void parseDracoExtensionFromBatchTableJson( if (parsedContent.batchLength) { parsedContent.errors.emplaceWarning( - "Error parsing batch table, the 3DTILES_draco_point_compression " - "extension is present but BATCH_LENGTH is defined."); + "Error parsing batch table, 3DTILES_draco_point_compression is present " + "but BATCH_LENGTH is defined. Only per-point properties can be " + "compressed by Draco."); parsedContent.metadataHasErrors = true; return; } @@ -704,18 +693,18 @@ void parseDracoExtensionFromBatchTableJson( if (batchTablePropertyIt == batchTableJson.MemberEnd() || !batchTablePropertyIt->value.IsObject()) { parsedContent.errors.emplaceWarning(fmt::format( - "Warning: the metadata property {} is in the " - "3DTILES_draco_point_compression extension but not in the batch " - "table itself.", + "The metadata property {} is in the 3DTILES_draco_point_compression " + "extension but not in the batch table itself.", name)); continue; } + const rapidjson::Value& batchTableProperty = batchTablePropertyIt->value; auto byteOffsetIt = batchTableProperty.FindMember("byteOffset"); if (byteOffsetIt == batchTableProperty.MemberEnd() || !byteOffsetIt->value.IsUint()) { parsedContent.errors.emplaceWarning(fmt::format( - "Error parsing batch table, the metadata property {} doesn't have a " + "Error parsing batch table, the metadata property {} does not have " "valid byteOffset. Skip parsing metadata.", name)); parsedContent.metadataHasErrors = true; @@ -731,7 +720,7 @@ void parseDracoExtensionFromBatchTableJson( if (stringToMetadataComponentType.find(componentType) == stringToMetadataComponentType.end()) { parsedContent.errors.emplaceWarning(fmt::format( - "Error parsing batch table, the metadata property {} doesn't have a " + "Error parsing batch table, the metadata property {} does not have " "valid componentType. Skip parsing metadata.", name)); parsedContent.metadataHasErrors = true; @@ -745,7 +734,7 @@ void parseDracoExtensionFromBatchTableJson( } if (stringToMetadataType.find(type) == stringToMetadataType.end()) { parsedContent.errors.emplaceWarning(fmt::format( - "Error parsing batch table, the metadata property {} doesn't have a " + "Error parsing batch table, the metadata property {} does not have " "valid type. Skip parsing metadata.", name)); parsedContent.metadataHasErrors = true; @@ -754,9 +743,8 @@ void parseDracoExtensionFromBatchTableJson( if (!dracoPropertyIt->value.IsInt()) { parsedContent.errors.emplaceWarning(fmt::format( - "Error parsing batch table with 3DTILES_draco_compression extension, " - "the metadata property {} doesn't have a valid draco ID. Skip " - "parsing metadata.", + "Error parsing 3DTILES_draco_compression extension, the metadata " + "property {} does not have valid draco ID. Skip parsing metadata.", name)); parsedContent.metadataHasErrors = true; return; @@ -855,7 +843,7 @@ void decodeDracoMetadata( const uint64_t pointsLength = parsedContent.pointsLength; std::vector& data = parsedContent.dracoBatchTableBinary; - auto& dracoMetadataSemantics = parsedContent.dracoMetadataSemantics; + const auto& dracoMetadataSemantics = parsedContent.dracoMetadataSemantics; for (auto dracoSemanticIt = dracoMetadataSemantics.begin(); dracoSemanticIt != dracoMetadataSemantics.end(); dracoSemanticIt++) { @@ -864,8 +852,8 @@ void decodeDracoMetadata( pPointCloud->attribute(dracoSemantic.dracoId); if (!validateDracoMetadataAttribute(pAttribute, dracoSemantic)) { parsedContent.errors.emplaceWarning(fmt::format( - "Error decoding the {} metadata property in the " - "3DTILES_draco_compression extension. Skip parsing metadata.", + "Error decoding {} property in the 3DTILES_draco_compression " + "extension. Skip parsing metadata.", dracoSemanticIt->first)); parsedContent.metadataHasErrors = true; return; @@ -873,8 +861,8 @@ void decodeDracoMetadata( const size_t byteOffset = data.size(); - // These checks do not test for validity since the batch table and extension - // were validated in parseDracoExtensionFromBatchTable. + // These do not test for validity since the batch table and extension + // were validated in parseDracoExtensionFromBatchTableJson. auto batchTableSemanticIt = batchTableJson.FindMember(dracoSemanticIt->first.c_str()); rapidjson::Value& batchTableSemantic = @@ -885,8 +873,8 @@ void decodeDracoMetadata( const size_t metadataByteStride = MetadataProperty::getSizeOfComponentType(dracoSemantic.componentType) * static_cast(pAttribute->num_components()); - data.resize(byteOffset + pointsLength * metadataByteStride); + data.resize(byteOffset + pointsLength * metadataByteStride); draco::DataBuffer* decodedBuffer = pAttribute->buffer(); int64_t decodedByteOffset = pAttribute->byte_offset(); int64_t decodedByteStride = pAttribute->byte_stride(); @@ -979,8 +967,8 @@ void decodeDraco( getDracoData(pColorAttribute, color.data, pointsLength); } else { parsedContent.errors.emplaceWarning( - "Warning: decoded Draco point cloud did not contain a valid " - "color attribute. Skip parsing colors."); + "Error parsing decoded Draco point cloud, invalid color attribute. " + "Skip parsing colors."); parsedContent.color = std::nullopt; parsedContent.colorType = PntsColorType::CONSTANT; } @@ -995,9 +983,9 @@ void decodeDraco( if (validateDracoAttribute(pNormalAttribute, draco::DT_FLOAT32, 3)) { getDracoData(pNormalAttribute, normal.data, pointsLength); } else { - parsedContent.errors.emplaceWarning( - "Warning: decoded Draco point cloud did not contain valid normal " - "attribute. Skip parsing normals."); + parsedContent.errors.emplaceWarning("Error parsing decoded Draco point " + "cloud, invalid normal attribute. " + "Skip parsing normals."); parsedContent.normal = std::nullopt; } } @@ -1014,22 +1002,23 @@ void decodeDraco( componentType = parsedContent.batchIdComponentType.value(); } - if (componentType == Accessor::ComponentType::UNSIGNED_BYTE && + if (componentType == MetadataProperty::ComponentType::UNSIGNED_BYTE && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT8, 1)) { getDracoData(pBatchIdAttribute, batchId.data, pointsLength); } else if ( - componentType == Accessor::ComponentType::UNSIGNED_INT && + componentType == MetadataProperty::ComponentType::UNSIGNED_INT && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT32, 1)) { getDracoData(pBatchIdAttribute, batchId.data, pointsLength); } else if ( (componentType == 0 || - componentType == Accessor::ComponentType::UNSIGNED_SHORT) && + componentType == MetadataProperty::ComponentType::UNSIGNED_SHORT) && validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT16, 1)) { getDracoData(pBatchIdAttribute, batchId.data, pointsLength); } else { parsedContent.errors.emplaceWarning( - "Warning: decoded Draco point cloud did not contain a valid " - "batch id attribute. Skip parsing batch IDs."); + "Error parsing decoded Draco point cloud, invalid batch ID " + "attribute. " + "Skip parsing batch IDs."); parsedContent.batchId = std::nullopt; } } @@ -1039,8 +1028,8 @@ void decodeDraco( return; } - // Not all metadata attributes may be compressed. Copy the binary of the - // uncompressed attributes first before appending the decoded data. + // Not all metadata attributes are necessarily compressed. Copy the binary of + // the uncompressed attributes first, before appending the decoded data. size_t batchTableBinaryByteLength = batchTableBinaryData.size(); if (batchTableBinaryByteLength > 0) { parsedContent.dracoBatchTableBinary.resize(batchTableBinaryByteLength); @@ -1073,18 +1062,17 @@ void parsePositionsFromFeatureTableBinary( if (parsedContent.positionQuantized) { // PERFORMANCE_IDEA: In the future, it might be more performant to detect - // if the recipient rendering engine can handle dequantization on its own - // and if so, use the KHR_mesh_quantization extension to avoid - // dequantizing here. + // if the recipient engine can handle dequantization itself, and if so, use + // the KHR_mesh_quantization extension to avoid dequantizing here. const gsl::span quantizedPositions( reinterpret_cast( featureTableBinaryData.data() + parsedContent.position.byteOffset), pointsLength); - const glm::vec3 quantizedVolumeScale = - glm::vec3(parsedContent.quantizedVolumeScale.value()); - const glm::vec3 quantizedVolumeOffset = - glm::vec3(parsedContent.quantizedVolumeOffset.value()); + const glm::vec3 quantizedVolumeScale( + parsedContent.quantizedVolumeScale.value()); + const glm::vec3 quantizedVolumeOffset( + parsedContent.quantizedVolumeOffset.value()); const glm::vec3 quantizedPositionScalar = quantizedVolumeScale / 65535.0f; @@ -1104,7 +1092,7 @@ void parsePositionsFromFeatureTableBinary( } } else { // The position accessor min / max is required by the glTF spec, so - // a for loop is used instead of std::memcpy. + // use a for loop instead of std::memcpy. const gsl::span positions( reinterpret_cast( featureTableBinaryData.data() + parsedContent.position.byteOffset), @@ -1129,30 +1117,7 @@ void parseColorsFromFeatureTableBinary( } const uint32_t pointsLength = parsedContent.pointsLength; - - if (parsedContent.colorType == PntsColorType::RGBA) { - const size_t colorsByteStride = sizeof(glm::u8vec4); - const size_t colorsByteLength = pointsLength * colorsByteStride; - colorData.resize(colorsByteLength); - - gsl::span outColors( - reinterpret_cast(colorData.data()), - pointsLength); - - std::memcpy( - colorData.data(), - featureTableBinaryData.data() + color.byteOffset, - colorsByteLength); - } else if (parsedContent.colorType == PntsColorType::RGB) { - const size_t colorsByteStride = sizeof(glm::u8vec3); - const size_t colorsByteLength = pointsLength * colorsByteStride; - colorData.resize(colorsByteLength); - - std::memcpy( - colorData.data(), - featureTableBinaryData.data() + color.byteOffset, - colorsByteLength); - } else if (parsedContent.colorType == PntsColorType::RGB565) { + if (parsedContent.colorType == PntsColorType::RGB565) { const size_t colorsByteStride = sizeof(glm::vec3); const size_t colorsByteLength = pointsLength * colorsByteStride; colorData.resize(colorsByteLength); @@ -1171,6 +1136,16 @@ void parseColorsFromFeatureTableBinary( outColors[i] = glm::vec3(AttributeCompression::decodeRGB565(compressedColor)); } + } else if (parsedContent.colorType != PntsColorType::CONSTANT) { + const size_t colorsByteStride = + parsedContent.colorType == PntsColorType::RGBA ? sizeof(glm::u8vec4) + : sizeof(glm::u8vec3); + const size_t colorsByteLength = pointsLength * colorsByteStride; + colorData.resize(colorsByteLength); + std::memcpy( + colorData.data(), + featureTableBinaryData.data() + color.byteOffset, + colorsByteLength); } } @@ -1226,9 +1201,8 @@ void parseBatchIdsFromFeatureTableBinary( const uint32_t pointsLength = parsedContent.pointsLength; size_t batchIdsByteStride = sizeof(uint16_t); if (parsedContent.batchIdComponentType) { - int8_t componentByteSize = Accessor::computeByteSizeOfComponent( + batchIdsByteStride = MetadataProperty::getSizeOfComponentType( parsedContent.batchIdComponentType.value()); - batchIdsByteStride = static_cast(componentByteSize); } const size_t batchIdsByteLength = pointsLength * batchIdsByteStride; batchIdData.resize(batchIdsByteLength); @@ -1412,25 +1386,36 @@ void addBatchIdsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { const int64_t count = static_cast(parsedContent.pointsLength); int32_t componentType = Accessor::ComponentType::UNSIGNED_SHORT; if (parsedContent.batchIdComponentType) { - componentType = parsedContent.batchIdComponentType.value(); + switch (parsedContent.batchIdComponentType.value()) { + case MetadataProperty::ComponentType::UNSIGNED_BYTE: + componentType = Accessor::ComponentType::UNSIGNED_BYTE; + break; + case MetadataProperty::ComponentType::UNSIGNED_INT: + componentType = Accessor::ComponentType::UNSIGNED_INT; + break; + case MetadataProperty::ComponentType::UNSIGNED_SHORT: + default: + componentType = Accessor::ComponentType::UNSIGNED_SHORT; + break; + } + const int64_t byteStride = + Accessor::computeByteSizeOfComponent(componentType); + const int64_t byteLength = static_cast(byteStride * count); + + int32_t bufferId = createBufferInGltf(gltf, batchId.data); + int32_t bufferViewId = + createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); + int32_t accessorId = createAccessorInGltf( + gltf, + bufferViewId, + componentType, + count, + Accessor::Type::SCALAR); + + MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; + // This will be renamed by BatchTableToGltfFeatureMetadata. + primitive.attributes.emplace("_BATCHID", accessorId); } - const int64_t byteStride = - Accessor::computeByteSizeOfComponent(componentType); - const int64_t byteLength = static_cast(byteStride * count); - - int32_t bufferId = createBufferInGltf(gltf, batchId.data); - int32_t bufferViewId = - createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); - int32_t accessorId = createAccessorInGltf( - gltf, - bufferViewId, - componentType, - count, - Accessor::Type::SCALAR); - - MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; - // This will be renamed by BatchTableToGltfFeatureMetadata. - primitive.attributes.emplace("_BATCHID", accessorId); } void createGltfFromParsedContent( @@ -1508,9 +1493,10 @@ void convertPntsContentToGltf( GltfConverterResult& result) { if (header.featureTableJsonByteLength > 0 && header.featureTableBinaryByteLength > 0) { + PntsContent parsedContent; + const gsl::span featureTableJsonData = pntsBinary.subspan(headerLength, header.featureTableJsonByteLength); - PntsContent parsedContent; rapidjson::Document featureTableJson = parseFeatureTableJson(featureTableJsonData, parsedContent); if (parsedContent.errors) { @@ -1518,9 +1504,10 @@ void convertPntsContentToGltf( return; } - // If the 3DTILES_draco_point_compression extension is present, - // the batch table's binary will be compressed with the feature - // table's binary. Parse both JSONs first in case the extension is there. + // If the batch table contains the 3DTILES_draco_point_compression + // extension, the compressed metdata properties will be included in the + // feature table binary. Parse both JSONs first in case the extension is + // there. const int64_t batchTableStart = headerLength + header.featureTableJsonByteLength + header.featureTableBinaryByteLength; @@ -1537,7 +1524,6 @@ void convertPntsContentToGltf( static_cast( headerLength + header.featureTableJsonByteLength), header.featureTableBinaryByteLength); - gsl::span batchTableBinaryData; if (header.batchTableBinaryByteLength > 0) { batchTableBinaryData = pntsBinary.subspan( @@ -1566,8 +1552,8 @@ void convertPntsContentToGltf( if (!parsedContent.dracoBatchTableBinary.empty()) { // If the point cloud has both compressed and uncompressed metadata - // values, then dracoBatchTableBinary will contain both the original batch - // table binary and the Draco decoded values. + // values, then dracoBatchTableBinary will contain both the original + // batch table binary and the Draco decoded values. batchTableBinaryData = gsl::span(parsedContent.dracoBatchTableBinary); } From 33b5fbd93163609a836bdf0e1000e1712140151c Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 1 Feb 2023 11:13:09 -0500 Subject: [PATCH 15/20] Metadata handling cleanup --- .../src/BatchTableToGltfFeatureMetadata.cpp | 5 +- .../src/PntsToGltfConverter.cpp | 75 ++++++------ ...tUpgradeBatchTableToExtFeatureMetadata.cpp | 112 ++++++++---------- 3 files changed, 87 insertions(+), 105 deletions(-) diff --git a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp index 556da0f79..4984e681e 100644 --- a/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp +++ b/Cesium3DTilesSelection/src/BatchTableToGltfFeatureMetadata.cpp @@ -1582,8 +1582,8 @@ ErrorList BatchTableToGltfFeatureMetadata::convertFromPnts( batchIdIt->value.IsObject()) { result.emplaceWarning( "The PNTS has a batch table, but it is being ignored because there " - "is no valid BATCH_LENGTH semantic in the feature table even though " - "the BATCH_ID semantic is defined."); + "is no valid BATCH_LENGTH in the feature table even though BATCH_ID is " + "defined."); return result; } else { featureCount = pointsLengthIt->value.GetInt64(); @@ -1613,7 +1613,6 @@ ErrorList BatchTableToGltfFeatureMetadata::convertFromPnts( // If _BATCHID is present, rename the _BATCHID attribute to _FEATURE_ID_0 primitive.attributes["_FEATURE_ID_0"] = primitiveBatchIdIt->second; primitive.attributes.erase("_BATCHID"); - attribute.featureIds.attribute = "_FEATURE_ID_0"; } else { // Otherwise, use implicit feature IDs to indicate the metadata is stored in diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index 5a58819b5..d43449635 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -213,7 +213,7 @@ struct PntsContent { std::vector dracoBatchTableBinary; Cesium3DTilesSelection::ErrorList errors; - bool metadataHasErrors = false; + bool dracoMetadataHasErrors = false; }; bool validateJsonArrayValues( @@ -473,15 +473,10 @@ void parseBatchIdsFromFeatureTableJson( } const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH"); - if (batchLengthIt == featureTableJson.MemberEnd() || - !batchLengthIt->value.IsUint()) { - parsedContent.errors.emplaceWarning( - "Error parsing PNTS feature table, BATCH_ID is defined but no valid " - "BATCH_LENGTH is defined. Skip parsing metadata."); - parsedContent.metadataHasErrors = true; - return; + if (batchLengthIt != featureTableJson.MemberEnd() && + batchLengthIt->value.IsUint()) { + parsedContent.batchLength = batchLengthIt->value.GetUint(); } - parsedContent.batchLength = batchLengthIt->value.GetUint(); } void parseSemanticsFromFeatureTableJson( @@ -667,7 +662,7 @@ void parseDracoExtensionFromBatchTableJson( "Error parsing batch table, 3DTILES_draco_point_compression is present " "but BATCH_LENGTH is defined. Only per-point properties can be " "compressed by Draco."); - parsedContent.metadataHasErrors = true; + parsedContent.dracoMetadataHasErrors = true; return; } @@ -677,7 +672,7 @@ void parseDracoExtensionFromBatchTableJson( parsedContent.errors.emplaceWarning( "Error parsing 3DTILES_draco_point_compression extension, no " "properties object was found."); - parsedContent.metadataHasErrors = true; + parsedContent.dracoMetadataHasErrors = true; return; } @@ -687,8 +682,9 @@ void parseDracoExtensionFromBatchTableJson( ++dracoPropertyIt) { const std::string name = dracoPropertyIt->name.GetString(); - // Validate the property against the batch table first. If there are - // any issues with the batch table, skip parsing metadata altogether. + // If there are issues with the extension, skip parsing metadata altogether. + // Otherwise, BatchTableToGltfFeatureMetadata will still try to parse the + // invalid Draco-compressed properties. auto batchTablePropertyIt = batchTableJson.FindMember(name.c_str()); if (batchTablePropertyIt == batchTableJson.MemberEnd() || !batchTablePropertyIt->value.IsObject()) { @@ -699,16 +695,27 @@ void parseDracoExtensionFromBatchTableJson( continue; } + if (!dracoPropertyIt->value.IsInt()) { + parsedContent.errors.emplaceWarning(fmt::format( + "Error parsing 3DTILES_draco_compression extension, the metadata " + "property {} does not have valid draco ID. Skip parsing metadata.", + name)); + parsedContent.dracoMetadataHasErrors = true; + return; + } + + // If the batch table binary property is invalid, it will also be ignored by + // BatchTableToGltfFeatureMetadata, so it's fine to continue parsing the + // other properties. const rapidjson::Value& batchTableProperty = batchTablePropertyIt->value; auto byteOffsetIt = batchTableProperty.FindMember("byteOffset"); if (byteOffsetIt == batchTableProperty.MemberEnd() || !byteOffsetIt->value.IsUint()) { parsedContent.errors.emplaceWarning(fmt::format( - "Error parsing batch table, the metadata property {} does not have " - "valid byteOffset. Skip parsing metadata.", + "Skip decoding Draco-compressed property {}. The binary property " + "doesn't have a valid byteOffset.", name)); - parsedContent.metadataHasErrors = true; - return; + continue; } std::string componentType; @@ -720,11 +727,10 @@ void parseDracoExtensionFromBatchTableJson( if (stringToMetadataComponentType.find(componentType) == stringToMetadataComponentType.end()) { parsedContent.errors.emplaceWarning(fmt::format( - "Error parsing batch table, the metadata property {} does not have " - "valid componentType. Skip parsing metadata.", + "Skip decoding Draco-compressed property {}. The binary property " + "doesn't have a valid componentType.", name)); - parsedContent.metadataHasErrors = true; - return; + continue; } std::string type; @@ -734,20 +740,10 @@ void parseDracoExtensionFromBatchTableJson( } if (stringToMetadataType.find(type) == stringToMetadataType.end()) { parsedContent.errors.emplaceWarning(fmt::format( - "Error parsing batch table, the metadata property {} does not have " - "valid type. Skip parsing metadata.", - name)); - parsedContent.metadataHasErrors = true; - return; - } - - if (!dracoPropertyIt->value.IsInt()) { - parsedContent.errors.emplaceWarning(fmt::format( - "Error parsing 3DTILES_draco_compression extension, the metadata " - "property {} does not have valid draco ID. Skip parsing metadata.", + "Skip decoding Draco-compressed property {}. The binary property " + "doesn't have a valid type.", name)); - parsedContent.metadataHasErrors = true; - return; + continue; } DracoMetadataSemantic semantic; @@ -776,9 +772,7 @@ rapidjson::Document parseBatchTableJson( return document; } - if (!parsedContent.metadataHasErrors) { - parseDracoExtensionFromBatchTableJson(document, parsedContent); - } + parseDracoExtensionFromBatchTableJson(document, parsedContent); return document; } @@ -855,7 +849,7 @@ void decodeDracoMetadata( "Error decoding {} property in the 3DTILES_draco_compression " "extension. Skip parsing metadata.", dracoSemanticIt->first)); - parsedContent.metadataHasErrors = true; + parsedContent.dracoMetadataHasErrors = true; return; } @@ -1024,7 +1018,7 @@ void decodeDraco( } } - if (batchTableJson.HasParseError() || parsedContent.metadataHasErrors) { + if (batchTableJson.HasParseError() || parsedContent.dracoMetadataHasErrors) { return; } @@ -1545,7 +1539,8 @@ void convertPntsContentToGltf( createGltfFromParsedContent(parsedContent, result); - if (batchTableJson.HasParseError() || parsedContent.metadataHasErrors) { + if (batchTableJson.HasParseError() || + parsedContent.dracoMetadataHasErrors) { result.errors.merge(parsedContent.errors); return; } diff --git a/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp b/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp index 5bfd9bd2a..3c2e62584 100644 --- a/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp +++ b/Cesium3DTilesSelection/test/TestUpgradeBatchTableToExtFeatureMetadata.cpp @@ -668,27 +668,23 @@ TEST_CASE("Converts batched PNTS batch table to EXT_feature_metadata") { getUniqueBufferViewIds(gltf.accessors, featureTable); CHECK(bufferViewSet.size() == gltf.bufferViews.size()); - // Check the mesh primitives - CHECK(!gltf.meshes.empty()); + // Check the mesh primitive + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); - for (Mesh& mesh : gltf.meshes) { - CHECK(!mesh.primitives.empty()); - for (MeshPrimitive& primitive : mesh.primitives) { - CHECK( - primitive.attributes.find("_FEATURE_ID_0") != - primitive.attributes.end()); + MeshPrimitive& primitive = mesh.primitives[0]; + CHECK( + primitive.attributes.find("_FEATURE_ID_0") != primitive.attributes.end()); - ExtensionMeshPrimitiveExtFeatureMetadata* pPrimitiveExtension = - primitive.getExtension(); - REQUIRE(pPrimitiveExtension); - REQUIRE(pPrimitiveExtension->featureIdAttributes.size() == 1); + ExtensionMeshPrimitiveExtFeatureMetadata* pPrimitiveExtension = + primitive.getExtension(); + REQUIRE(pPrimitiveExtension); + REQUIRE(pPrimitiveExtension->featureIdAttributes.size() == 1); - FeatureIDAttribute& attribute = - pPrimitiveExtension->featureIdAttributes[0]; - CHECK(attribute.featureIds.attribute == "_FEATURE_ID_0"); - CHECK(attribute.featureTable == "default"); - } - } + FeatureIDAttribute& attribute = pPrimitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + CHECK(attribute.featureIds.attribute == "_FEATURE_ID_0"); // Check metadata values { @@ -819,30 +815,26 @@ TEST_CASE("Converts per-point PNTS batch table to EXT_feature_metadata") { getUniqueBufferViewIds(gltf.accessors, featureTable); CHECK(bufferViewSet.size() == gltf.bufferViews.size()); - // Check the mesh primitives - CHECK(!gltf.meshes.empty()); + // Check the mesh primitive + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); - for (Mesh& mesh : gltf.meshes) { - CHECK(!mesh.primitives.empty()); - for (MeshPrimitive& primitive : mesh.primitives) { - CHECK( - primitive.attributes.find("_FEATURE_ID_0") == - primitive.attributes.end()); + MeshPrimitive& primitive = mesh.primitives[0]; + CHECK( + primitive.attributes.find("_FEATURE_ID_0") == primitive.attributes.end()); - ExtensionMeshPrimitiveExtFeatureMetadata* pPrimitiveExtension = - primitive.getExtension(); - REQUIRE(pPrimitiveExtension); - REQUIRE(pPrimitiveExtension->featureIdAttributes.size() == 1); + ExtensionMeshPrimitiveExtFeatureMetadata* pPrimitiveExtension = + primitive.getExtension(); + REQUIRE(pPrimitiveExtension); + REQUIRE(pPrimitiveExtension->featureIdAttributes.size() == 1); - FeatureIDAttribute& attribute = - pPrimitiveExtension->featureIdAttributes[0]; - CHECK(attribute.featureTable == "default"); - // Check for implicit feature IDs - CHECK(!attribute.featureIds.attribute); - CHECK(attribute.featureIds.constant == 0); - CHECK(attribute.featureIds.divisor == 1); - } - } + FeatureIDAttribute& attribute = pPrimitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + // Check for implicit feature IDs + CHECK(!attribute.featureIds.attribute); + CHECK(attribute.featureIds.constant == 0); + CHECK(attribute.featureIds.divisor == 1); // Check metadata values { @@ -973,30 +965,26 @@ TEST_CASE("Converts Draco per-point PNTS batch table to " getUniqueBufferViewIds(gltf.accessors, featureTable); CHECK(bufferViewSet.size() == gltf.bufferViews.size()); - // Check the mesh primitives - CHECK(!gltf.meshes.empty()); + // Check the mesh primitive + REQUIRE(gltf.meshes.size() == 1); + Mesh& mesh = gltf.meshes[0]; + REQUIRE(mesh.primitives.size() == 1); - for (Mesh& mesh : gltf.meshes) { - CHECK(!mesh.primitives.empty()); - for (MeshPrimitive& primitive : mesh.primitives) { - CHECK( - primitive.attributes.find("_FEATURE_ID_0") == - primitive.attributes.end()); - - ExtensionMeshPrimitiveExtFeatureMetadata* pPrimitiveExtension = - primitive.getExtension(); - REQUIRE(pPrimitiveExtension); - REQUIRE(pPrimitiveExtension->featureIdAttributes.size() == 1); - - FeatureIDAttribute& attribute = - pPrimitiveExtension->featureIdAttributes[0]; - CHECK(attribute.featureTable == "default"); - // Check for implicit feature IDs - CHECK(!attribute.featureIds.attribute); - CHECK(attribute.featureIds.constant == 0); - CHECK(attribute.featureIds.divisor == 1); - } - } + MeshPrimitive& primitive = mesh.primitives[0]; + CHECK( + primitive.attributes.find("_FEATURE_ID_0") == primitive.attributes.end()); + + ExtensionMeshPrimitiveExtFeatureMetadata* pPrimitiveExtension = + primitive.getExtension(); + REQUIRE(pPrimitiveExtension); + REQUIRE(pPrimitiveExtension->featureIdAttributes.size() == 1); + + FeatureIDAttribute& attribute = pPrimitiveExtension->featureIdAttributes[0]; + CHECK(attribute.featureTable == "default"); + // Check for implicit feature IDs + CHECK(!attribute.featureIds.attribute); + CHECK(attribute.featureIds.constant == 0); + CHECK(attribute.featureIds.divisor == 1); // Check metadata values { From cb77693b4de0815cd8bc3cd5ee6a4529094d4569 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 1 Feb 2023 11:23:37 -0500 Subject: [PATCH 16/20] Update changelog --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b50a002bc..a00cfba6a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log +### v0.?.? - 2023-?-? + +##### Additions :tada: + +- Added support for loading tilesets with `pnts` content. Point clouds are converted to `glTF`s with a single `POINTS` primitive, while batch tables are converted to `EXT_feature_metadata`. + ### v0.21.3 - 2023-02-01 ##### Fixes :wrench: From a0959d1242a60c92bf045b38faf7f3e6ad146ae7 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 2 Feb 2023 18:03:53 -0500 Subject: [PATCH 17/20] Add fix for empty batch table --- Cesium3DTilesSelection/src/PntsToGltfConverter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index d43449635..7f603935e 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -1539,7 +1539,7 @@ void convertPntsContentToGltf( createGltfFromParsedContent(parsedContent, result); - if (batchTableJson.HasParseError() || + if (batchTableJson.HasParseError() || batchTableJson.MemberCount() == 0 || parsedContent.dracoMetadataHasErrors) { result.errors.merge(parsedContent.errors); return; From 80c5584163496b44488803da9ee08baa6aa53da8 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 3 Feb 2023 10:39:53 -0500 Subject: [PATCH 18/20] Update for PR feedback --- .../src/PntsToGltfConverter.cpp | 21 +++++----- .../test/TestPntsToGltfConverter.cpp | 40 ++----------------- 2 files changed, 16 insertions(+), 45 deletions(-) diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index 7f603935e..d99ae31e3 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -216,10 +216,11 @@ struct PntsContent { bool dracoMetadataHasErrors = false; }; +template bool validateJsonArrayValues( const rapidjson::Value& arrayValue, uint32_t expectedLength, - std::function validate) { + TValidate validate) { if (!arrayValue.IsArray()) { return false; } @@ -816,11 +817,11 @@ void getDracoData( int64_t decodedByteOffset = pAttribute->byte_offset(); int64_t decodedByteStride = pAttribute->byte_stride(); + const uint8_t* pSource = decodedBuffer->data() + decodedByteOffset; if (dataElementSize != static_cast(decodedByteStride)) { gsl::span outData(reinterpret_cast(data.data()), pointsLength); for (uint32_t i = 0; i < pointsLength; ++i) { - outData[i] = *reinterpret_cast( - decodedBuffer->data() + decodedByteOffset + decodedByteStride * i); + outData[i] = *reinterpret_cast(pSource + decodedByteStride * i); } } else { std::memcpy( @@ -1234,7 +1235,7 @@ void parseFeatureTableBinary( } } -int32_t createBufferInGltf(Model& gltf, std::vector& buffer) { +int32_t createBufferInGltf(Model& gltf, std::vector&& buffer) { size_t bufferId = gltf.buffers.size(); Buffer& gltfBuffer = gltf.buffers.emplace_back(); gltfBuffer.byteLength = static_cast(buffer.size()); @@ -1280,7 +1281,8 @@ void addPositionsToGltf(PntsContent& parsedContent, Model& gltf) { const int64_t count = static_cast(parsedContent.pointsLength); const int64_t byteStride = static_cast(sizeof(glm ::vec3)); const int64_t byteLength = static_cast(byteStride * count); - int32_t bufferId = createBufferInGltf(gltf, parsedContent.position.data); + int32_t bufferId = + createBufferInGltf(gltf, std::move(parsedContent.position.data)); int32_t bufferViewId = createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); int32_t accessorId = createAccessorInGltf( @@ -1334,7 +1336,7 @@ void addColorsToGltf(PntsContent& parsedContent, Model& gltf) { } const int64_t byteLength = static_cast(byteStride * count); - int32_t bufferId = createBufferInGltf(gltf, color.data); + int32_t bufferId = createBufferInGltf(gltf, std::move(color.data)); int32_t bufferViewId = createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); int32_t accessorId = @@ -1360,7 +1362,7 @@ void addNormalsToGltf(PntsContent& parsedContent, Model& gltf) { const int64_t byteStride = static_cast(sizeof(glm ::vec3)); const int64_t byteLength = static_cast(byteStride * count); - int32_t bufferId = createBufferInGltf(gltf, normal.data); + int32_t bufferId = createBufferInGltf(gltf, std::move(normal.data)); int32_t bufferViewId = createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); int32_t accessorId = createAccessorInGltf( @@ -1396,7 +1398,7 @@ void addBatchIdsToGltf(PntsContent& parsedContent, CesiumGltf::Model& gltf) { Accessor::computeByteSizeOfComponent(componentType); const int64_t byteLength = static_cast(byteStride * count); - int32_t bufferId = createBufferInGltf(gltf, batchId.data); + int32_t bufferId = createBufferInGltf(gltf, std::move(batchId.data)); int32_t bufferViewId = createBufferViewInGltf(gltf, bufferId, byteLength, byteStride); int32_t accessorId = createAccessorInGltf( @@ -1518,6 +1520,7 @@ void convertPntsContentToGltf( static_cast( headerLength + header.featureTableJsonByteLength), header.featureTableBinaryByteLength); + gsl::span batchTableBinaryData; if (header.batchTableBinaryByteLength > 0) { batchTableBinaryData = pntsBinary.subspan( @@ -1539,7 +1542,7 @@ void convertPntsContentToGltf( createGltfFromParsedContent(parsedContent, result); - if (batchTableJson.HasParseError() || batchTableJson.MemberCount() == 0 || + if (!batchTableJson.IsObject() || batchTableJson.HasParseError() || parsedContent.dracoMetadataHasErrors) { result.errors.merge(parsedContent.errors); return; diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index 792fbd4eb..00ec2b211 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -23,7 +23,8 @@ using namespace CesiumUtility; template static void checkBufferContents( const std::vector& buffer, - const std::vector& expected) { + const std::vector& expected, + const double epsilon = Math::Epsilon6) { REQUIRE(buffer.size() == expected.size() * sizeof(Type)); const int32_t byteStride = sizeof(Type); if constexpr (std::is_same_v) { @@ -34,7 +35,7 @@ static void checkBufferContents( REQUIRE(Math::equalsEpsilon( static_cast(value), static_cast(expectedValue), - Math::Epsilon6)); + epsilon)); } } else if constexpr (std::is_same_v) { for (size_t i = 0; i < expected.size(); ++i) { @@ -44,7 +45,7 @@ static void checkBufferContents( REQUIRE(Math::equalsEpsilon( static_cast(value), static_cast(expectedValue), - Math::Epsilon6)); + epsilon)); } } else if constexpr (std::is_floating_point_v) { for (size_t i = 0; i < expected.size(); ++i) { @@ -67,39 +68,6 @@ static void checkBufferContents( } } -template -static void checkBufferContents( - const std::vector& buffer, - const std::vector& expected, - const double epsilon) { - REQUIRE(buffer.size() == expected.size() * sizeof(Type)); - const int32_t byteStride = sizeof(Type); - if constexpr (std::is_same_v) { - for (size_t i = 0; i < expected.size(); ++i) { - const glm::vec3& value = - *reinterpret_cast(buffer.data() + i * byteStride); - const glm::vec3& expectedValue = expected[i]; - REQUIRE(Math::equalsEpsilon( - static_cast(value), - static_cast(expectedValue), - epsilon)); - } - } else if constexpr (std::is_same_v) { - for (size_t i = 0; i < expected.size(); ++i) { - const glm::vec4& value = - *reinterpret_cast(buffer.data() + i * byteStride); - const glm::vec4& expectedValue = expected[i]; - REQUIRE(Math::equalsEpsilon( - static_cast(value), - static_cast(expectedValue), - epsilon)); - } - } else { - FAIL("Buffer check with epsilon has not been implemented for the given " - "type."); - } -} - template static void checkAttribute( const Model& gltf, From 34e209af460b55a41f884009032f6226b3655cc9 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 3 Feb 2023 16:03:15 -0500 Subject: [PATCH 19/20] Convert sRGB colors to linear RGB --- .../src/PntsToGltfConverter.cpp | 122 ++++++++++---- .../test/TestPntsToGltfConverter.cpp | 152 +++++++++--------- 2 files changed, 165 insertions(+), 109 deletions(-) diff --git a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp index d99ae31e3..58592a941 100644 --- a/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/src/PntsToGltfConverter.cpp @@ -183,6 +183,23 @@ struct DracoMetadataSemantic { enum PntsColorType { CONSTANT, RGBA, RGB, RGB565 }; +// Point cloud colors are stored in sRGB space, so they need to be converted to +// linear RGB for the glTF. +// This function assumes the sRGB values are normalized from [0, 255] to [0, 1] +template TColor srgbToLinear(const TColor srgb) { + static_assert( + std::is_same_v || std::is_same_v); + + glm::vec3 srgbInput = glm::vec3(srgb); + glm::vec3 linearOutput = glm::pow(srgbInput, glm::vec3(2.2f)); + + if constexpr (std::is_same_v) { + return glm::vec4(linearOutput, srgb.w); + } else if constexpr (std::is_same_v) { + return linearOutput; + } +} + struct PntsContent { uint32_t pointsLength = 0; std::optional rtcCenter; @@ -953,13 +970,44 @@ void decodeDraco( if (color.dracoId) { draco::PointAttribute* pColorAttribute = pPointCloud->attribute(color.dracoId.value()); + std::vector& colorData = parsedContent.color->data; if (parsedContent.colorType == PntsColorType::RGBA && validateDracoAttribute(pColorAttribute, draco::DT_UINT8, 4)) { - getDracoData(pColorAttribute, color.data, pointsLength); + colorData.resize(pointsLength * sizeof(glm::vec4)); + + gsl::span outColors( + reinterpret_cast(colorData.data()), + pointsLength); + + draco::DataBuffer* decodedBuffer = pColorAttribute->buffer(); + int64_t decodedByteOffset = pColorAttribute->byte_offset(); + int64_t decodedByteStride = pColorAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + const glm::u8vec4 rgbaColor = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + + decodedByteStride * i); + outColors[i] = srgbToLinear(glm::vec4(rgbaColor) / 255.0f); + } } else if ( parsedContent.colorType == PntsColorType::RGB && validateDracoAttribute(pColorAttribute, draco::DT_UINT8, 3)) { - getDracoData(pColorAttribute, color.data, pointsLength); + colorData.resize(pointsLength * sizeof(glm::vec3)); + + gsl::span outColors( + reinterpret_cast(colorData.data()), + pointsLength); + + draco::DataBuffer* decodedBuffer = pColorAttribute->buffer(); + int64_t decodedByteOffset = pColorAttribute->byte_offset(); + int64_t decodedByteStride = pColorAttribute->byte_stride(); + + for (uint32_t i = 0; i < pointsLength; ++i) { + const glm::u8vec3 rgbColor = *reinterpret_cast( + decodedBuffer->data() + decodedByteOffset + + decodedByteStride * i); + outColors[i] = srgbToLinear(glm::vec3(rgbColor) / 255.0f); + } } else { parsedContent.errors.emplaceWarning( "Error parsing decoded Draco point cloud, invalid color attribute. " @@ -1112,35 +1160,54 @@ void parseColorsFromFeatureTableBinary( } const uint32_t pointsLength = parsedContent.pointsLength; - if (parsedContent.colorType == PntsColorType::RGB565) { - const size_t colorsByteStride = sizeof(glm::vec3); - const size_t colorsByteLength = pointsLength * colorsByteStride; - colorData.resize(colorsByteLength); + const size_t colorsByteStride = parsedContent.colorType == PntsColorType::RGBA + ? sizeof(glm::vec4) + : sizeof(glm::vec3); + const size_t colorsByteLength = pointsLength * colorsByteStride; + colorData.resize(colorsByteLength); + if (parsedContent.colorType == PntsColorType::RGBA) { + const gsl::span rgbaColors( + reinterpret_cast( + featureTableBinaryData.data() + color.byteOffset), + pointsLength); + gsl::span outColors( + reinterpret_cast(colorData.data()), + pointsLength); + + for (size_t i = 0; i < pointsLength; i++) { + glm::vec4 normalizedColor = glm::vec4(rgbaColors[i]) / 255.0f; + outColors[i] = srgbToLinear(normalizedColor); + } + } else if (parsedContent.colorType == PntsColorType::RGB) { + const gsl::span rgbColors( + reinterpret_cast( + featureTableBinaryData.data() + color.byteOffset), + pointsLength); gsl::span outColors( reinterpret_cast(colorData.data()), pointsLength); + for (size_t i = 0; i < pointsLength; i++) { + glm::vec3 normalizedColor = glm::vec3(rgbColors[i]) / 255.0f; + outColors[i] = srgbToLinear(normalizedColor); + } + } else if (parsedContent.colorType == PntsColorType::RGB565) { + const gsl::span compressedColors( reinterpret_cast( featureTableBinaryData.data() + color.byteOffset), pointsLength); + gsl::span outColors( + reinterpret_cast(colorData.data()), + pointsLength); for (size_t i = 0; i < pointsLength; i++) { const uint16_t compressedColor = compressedColors[i]; - outColors[i] = + glm::vec3 decompressedColor = glm::vec3(AttributeCompression::decodeRGB565(compressedColor)); + outColors[i] = srgbToLinear(decompressedColor); } - } else if (parsedContent.colorType != PntsColorType::CONSTANT) { - const size_t colorsByteStride = - parsedContent.colorType == PntsColorType::RGBA ? sizeof(glm::u8vec4) - : sizeof(glm::u8vec3); - const size_t colorsByteLength = pointsLength * colorsByteStride; - colorData.resize(colorsByteLength); - std::memcpy( - colorData.data(), - featureTableBinaryData.data() + color.byteOffset, - colorsByteLength); } } @@ -1313,25 +1380,16 @@ void addColorsToGltf(PntsContent& parsedContent, Model& gltf) { const int64_t count = static_cast(parsedContent.pointsLength); int64_t byteStride = 0; - int32_t componentType = 0; + const int32_t componentType = Accessor::ComponentType::FLOAT; std::string type; bool isTranslucent = false; - bool isNormalized = false; if (parsedContent.colorType == PntsColorType::RGBA) { - byteStride = static_cast(sizeof(glm::u8vec4)); - componentType = Accessor::ComponentType::UNSIGNED_BYTE; + byteStride = static_cast(sizeof(glm::vec4)); type = Accessor::Type::VEC4; isTranslucent = true; - isNormalized = true; - } else if (parsedContent.colorType == PntsColorType::RGB) { - byteStride = static_cast(sizeof(glm::u8vec3)); - componentType = Accessor::ComponentType::UNSIGNED_BYTE; - isNormalized = true; - type = Accessor::Type::VEC3; - } else if (parsedContent.colorType == PntsColorType::RGB565) { + } else { byteStride = static_cast(sizeof(glm::vec3)); - componentType = Accessor::ComponentType::FLOAT; type = Accessor::Type::VEC3; } @@ -1342,9 +1400,6 @@ void addColorsToGltf(PntsContent& parsedContent, Model& gltf) { int32_t accessorId = createAccessorInGltf(gltf, bufferViewId, componentType, count, type); - Accessor& accessor = gltf.accessors[static_cast(accessorId)]; - accessor.normalized = isNormalized; - MeshPrimitive& primitive = gltf.meshes[0].primitives[0]; primitive.attributes.emplace("COLOR_0", accessorId); @@ -1449,9 +1504,8 @@ void createGltfFromParsedContent( if (parsedContent.color) { addColorsToGltf(parsedContent, gltf); } else if (parsedContent.constantRgba) { - // Map RGBA from [0, 255] to [0, 1] glm::vec4 materialColor(parsedContent.constantRgba.value()); - materialColor /= 255.0f; + materialColor = srgbToLinear(materialColor / 255.0f); material.pbrMetallicRoughness.value().baseColorFactor = {materialColor.x, materialColor.y, materialColor.z, materialColor.w}; diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index 00ec2b211..e979b760c 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -88,6 +88,9 @@ static void checkAttribute( if constexpr (std::is_same_v) { expectedComponentType = Accessor::ComponentType::FLOAT; expectedType = Accessor::Type::VEC3; + } else if constexpr (std::is_same_v) { + expectedComponentType = Accessor::ComponentType::FLOAT; + expectedType = Accessor::Type::VEC4; } else if constexpr (std::is_same_v) { expectedComponentType = Accessor::ComponentType::UNSIGNED_BYTE; expectedType = Accessor::Type::VEC3; @@ -257,12 +260,12 @@ TEST_CASE("Converts point cloud with RGBA to glTF") { // Check that position and color attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); // Check color attribute more thoroughly uint32_t colorAccessorId = static_cast(attributes.at("COLOR_0")); Accessor& colorAccessor = gltf.accessors[colorAccessorId]; - CHECK(colorAccessor.normalized); + CHECK(!colorAccessor.normalized); uint32_t colorBufferViewId = static_cast(colorAccessor.bufferView); BufferView& colorBufferView = gltf.bufferViews[colorBufferViewId]; @@ -270,17 +273,17 @@ TEST_CASE("Converts point cloud with RGBA to glTF") { uint32_t colorBufferId = static_cast(colorBufferView.buffer); Buffer& colorBuffer = gltf.buffers[colorBufferId]; - const std::vector expectedColors = { - glm::u8vec4(139, 151, 182, 108), - glm::u8vec4(153, 218, 138, 108), - glm::u8vec4(108, 159, 164, 49), - glm::u8vec4(111, 75, 227, 7), - glm::u8vec4(245, 69, 97, 61), - glm::u8vec4(201, 207, 134, 61), - glm::u8vec4(144, 100, 236, 107), - glm::u8vec4(18, 86, 22, 82)}; - - checkBufferContents(colorBuffer.cesium.data, expectedColors); + const std::vector expectedColors = { + glm::vec4(0.263174f, 0.315762f, 0.476177f, 0.423529f), + glm::vec4(0.325036f, 0.708297f, 0.259027f, 0.423529f), + glm::vec4(0.151058f, 0.353740f, 0.378676f, 0.192156f), + glm::vec4(0.160443f, 0.067724f, 0.774227f, 0.027450f), + glm::vec4(0.915750f, 0.056374f, 0.119264f, 0.239215f), + glm::vec4(0.592438f, 0.632042f, 0.242796f, 0.239215f), + glm::vec4(0.284452f, 0.127529f, 0.843369f, 0.419607f), + glm::vec4(0.002932f, 0.091518f, 0.004559f, 0.321568f)}; + + checkBufferContents(colorBuffer.cesium.data, expectedColors); } TEST_CASE("Converts point cloud with RGB to glTF") { @@ -317,12 +320,12 @@ TEST_CASE("Converts point cloud with RGB to glTF") { // Check that position and color attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); // Check color attribute more thoroughly uint32_t colorAccessorId = static_cast(attributes.at("COLOR_0")); Accessor& colorAccessor = gltf.accessors[colorAccessorId]; - CHECK(colorAccessor.normalized); + CHECK(!colorAccessor.normalized); uint32_t colorBufferViewId = static_cast(colorAccessor.bufferView); BufferView& colorBufferView = gltf.bufferViews[colorBufferViewId]; @@ -330,17 +333,17 @@ TEST_CASE("Converts point cloud with RGB to glTF") { uint32_t colorBufferId = static_cast(colorBufferView.buffer); Buffer& colorBuffer = gltf.buffers[colorBufferId]; - const std::vector expectedColors = { - glm::u8vec3(139, 151, 182), - glm::u8vec3(153, 218, 138), - glm::u8vec3(108, 159, 164), - glm::u8vec3(111, 75, 227), - glm::u8vec3(245, 69, 97), - glm::u8vec3(201, 207, 134), - glm::u8vec3(144, 100, 236), - glm::u8vec3(18, 86, 22)}; - - checkBufferContents(colorBuffer.cesium.data, expectedColors); + const std::vector expectedColors = { + glm::vec3(0.263174f, 0.315762f, 0.476177f), + glm::vec3(0.325036f, 0.708297f, 0.259027f), + glm::vec3(0.151058f, 0.353740f, 0.378676f), + glm::vec3(0.160443f, 0.067724f, 0.774227f), + glm::vec3(0.915750f, 0.056374f, 0.119264f), + glm::vec3(0.592438f, 0.632042f, 0.242796f), + glm::vec3(0.284452f, 0.127529f, 0.843369f), + glm::vec3(0.002932f, 0.091518f, 0.004559f)}; + + checkBufferContents(colorBuffer.cesium.data, expectedColors); } TEST_CASE("Converts point cloud with RGB565 to glTF") { @@ -379,7 +382,6 @@ TEST_CASE("Converts point cloud with RGB565 to glTF") { checkAttribute(gltf, primitive, "POSITION", pointsLength); checkAttribute(gltf, primitive, "COLOR_0", pointsLength); - // Check color attribute more thoroughly // Check color attribute more thoroughly uint32_t colorAccessorId = static_cast(attributes.at("COLOR_0")); Accessor& colorAccessor = gltf.accessors[colorAccessorId]; @@ -392,14 +394,14 @@ TEST_CASE("Converts point cloud with RGB565 to glTF") { Buffer& colorBuffer = gltf.buffers[colorBufferId]; const std::vector expectedColors = { - glm::vec3(0.5483871, 0.5873016, 0.7096773), - glm::vec3(0.5806451, 0.8571428, 0.5161290), - glm::vec3(0.4193548, 0.6190476, 0.6451612), - glm::vec3(0.4193548, 0.2857142, 0.8709677), - glm::vec3(0.9354838, 0.2698412, 0.3548386), - glm::vec3(0.7741935, 0.8095238, 0.5161290), - glm::vec3(0.5483871, 0.3809523, 0.9032257), - glm::vec3(0.0645161, 0.3333333, 0.0645161)}; + glm::vec3(0.2666808f, 0.3100948f, 0.4702556f), + glm::vec3(0.3024152f, 0.7123886f, 0.2333824f), + glm::vec3(0.1478017f, 0.3481712f, 0.3813029f), + glm::vec3(0.1478017f, 0.0635404f, 0.7379118f), + glm::vec3(0.8635347f, 0.0560322f, 0.1023452f), + glm::vec3(0.5694675f, 0.6282104f, 0.2333824f), + glm::vec3(0.2666808f, 0.1196507f, 0.7993773f), + glm::vec3(0.0024058f, 0.0891934f, 0.0024058f)}; checkBufferContents(colorBuffer.cesium.data, expectedColors); } @@ -481,7 +483,7 @@ TEST_CASE("Converts point cloud with quantized positions to glTF") { // Check that position and color attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); // Check position attribute more thoroughly uint32_t positionAccessorId = @@ -552,7 +554,7 @@ TEST_CASE("Converts point cloud with normals to glTF") { // Check that position, color, and normal attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); checkAttribute(gltf, primitive, "NORMAL", pointsLength); // Check normal attribute more thoroughly @@ -613,7 +615,7 @@ TEST_CASE("Converts point cloud with oct-encoded normals to glTF") { // Check that position, color, and normal attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); checkAttribute(gltf, primitive, "NORMAL", pointsLength); // Check normal attribute more thoroughly @@ -788,7 +790,7 @@ TEST_CASE("Converts point cloud with per-point properties to glTF with " // Check that position and color attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); } TEST_CASE("Converts point cloud with Draco compression to glTF") { @@ -848,7 +850,7 @@ TEST_CASE("Converts point cloud with Draco compression to glTF") { // Check that position, color, and normal attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); checkAttribute(gltf, primitive, "NORMAL", pointsLength); // Check each attribute more thoroughly @@ -888,7 +890,7 @@ TEST_CASE("Converts point cloud with Draco compression to glTF") { { uint32_t accessorId = static_cast(attributes.at("COLOR_0")); Accessor& accessor = gltf.accessors[accessorId]; - CHECK(accessor.normalized); + CHECK(!accessor.normalized); uint32_t bufferViewId = static_cast(accessor.bufferView); BufferView& bufferView = gltf.bufferViews[bufferViewId]; @@ -896,17 +898,17 @@ TEST_CASE("Converts point cloud with Draco compression to glTF") { uint32_t bufferId = static_cast(bufferView.buffer); Buffer& buffer = gltf.buffers[bufferId]; - std::vector expected = { - glm::u8vec3(182, 215, 153), - glm::u8vec3(108, 159, 164), - glm::u8vec3(227, 14, 245), - glm::u8vec3(201, 207, 134), - glm::u8vec3(236, 213, 18), - glm::u8vec3(5, 93, 212), - glm::u8vec3(221, 221, 249), - glm::u8vec3(117, 132, 199), + std::vector expected = { + glm::vec3(0.4761772f, 0.6870308f, 0.3250369f), + glm::vec3(0.1510580f, 0.3537409f, 0.3786762f), + glm::vec3(0.7742273f, 0.0016869f, 0.9157501f), + glm::vec3(0.5924380f, 0.6320426f, 0.2427963f), + glm::vec3(0.8433697f, 0.6730490f, 0.0029323f), + glm::vec3(0.0001751f, 0.1087111f, 0.6661169f), + glm::vec3(0.7299188f, 0.7299188f, 0.9489649f), + glm::vec3(0.1801442f, 0.2348952f, 0.5795466f), }; - checkBufferContents(buffer.cesium.data, expected); + checkBufferContents(buffer.cesium.data, expected); } { @@ -994,7 +996,7 @@ TEST_CASE("Converts point cloud with partial Draco compression to glTF") { // Check that position, color, and normal attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); checkAttribute(gltf, primitive, "NORMAL", pointsLength); // Check each attribute more thoroughly @@ -1034,7 +1036,7 @@ TEST_CASE("Converts point cloud with partial Draco compression to glTF") { { uint32_t accessorId = static_cast(attributes.at("COLOR_0")); Accessor& accessor = gltf.accessors[accessorId]; - CHECK(accessor.normalized); + CHECK(!accessor.normalized); uint32_t bufferViewId = static_cast(accessor.bufferView); BufferView& bufferView = gltf.bufferViews[bufferViewId]; @@ -1042,17 +1044,17 @@ TEST_CASE("Converts point cloud with partial Draco compression to glTF") { uint32_t bufferId = static_cast(bufferView.buffer); Buffer& buffer = gltf.buffers[bufferId]; - std::vector expected = { - glm::u8vec3(182, 215, 153), - glm::u8vec3(108, 159, 164), - glm::u8vec3(227, 14, 245), - glm::u8vec3(201, 207, 134), - glm::u8vec3(236, 213, 18), - glm::u8vec3(5, 93, 212), - glm::u8vec3(221, 221, 249), - glm::u8vec3(117, 132, 199), + std::vector expected = { + glm::vec3(0.4761772f, 0.6870308f, 0.3250369f), + glm::vec3(0.1510580f, 0.3537409f, 0.3786762f), + glm::vec3(0.7742273f, 0.0016869f, 0.9157501f), + glm::vec3(0.5924380f, 0.6320426f, 0.2427963f), + glm::vec3(0.8433697f, 0.6730490f, 0.0029323f), + glm::vec3(0.0001751f, 0.1087111f, 0.6661169f), + glm::vec3(0.7299188f, 0.7299188f, 0.9489649f), + glm::vec3(0.1801442f, 0.2348952f, 0.5795466f), }; - checkBufferContents(buffer.cesium.data, expected); + checkBufferContents(buffer.cesium.data, expected); } { @@ -1136,7 +1138,7 @@ TEST_CASE("Converts batched point cloud with Draco compression to glTF") { // Check that position, normal, and feature ID attributes are present checkAttribute(gltf, primitive, "POSITION", pointsLength); - checkAttribute(gltf, primitive, "COLOR_0", pointsLength); + checkAttribute(gltf, primitive, "COLOR_0", pointsLength); checkAttribute(gltf, primitive, "NORMAL", pointsLength); checkAttribute(gltf, primitive, "_FEATURE_ID_0", pointsLength); @@ -1177,7 +1179,7 @@ TEST_CASE("Converts batched point cloud with Draco compression to glTF") { { uint32_t accessorId = static_cast(attributes.at("COLOR_0")); Accessor& accessor = gltf.accessors[accessorId]; - CHECK(accessor.normalized); + CHECK(!accessor.normalized); uint32_t bufferViewId = static_cast(accessor.bufferView); BufferView& bufferView = gltf.bufferViews[bufferViewId]; @@ -1185,17 +1187,17 @@ TEST_CASE("Converts batched point cloud with Draco compression to glTF") { uint32_t bufferId = static_cast(bufferView.buffer); Buffer& buffer = gltf.buffers[bufferId]; - std::vector expected = { - glm::u8vec3(182, 215, 153), - glm::u8vec3(108, 159, 164), - glm::u8vec3(227, 14, 245), - glm::u8vec3(201, 207, 134), - glm::u8vec3(236, 213, 18), - glm::u8vec3(5, 93, 212), - glm::u8vec3(221, 221, 249), - glm::u8vec3(117, 132, 199), + std::vector expected = { + glm::vec3(0.4761772f, 0.6870308f, 0.3250369f), + glm::vec3(0.1510580f, 0.3537409f, 0.3786762f), + glm::vec3(0.7742273f, 0.0016869f, 0.9157501f), + glm::vec3(0.5924380f, 0.6320426f, 0.2427963f), + glm::vec3(0.8433697f, 0.6730490f, 0.0029323f), + glm::vec3(0.0001751f, 0.1087111f, 0.6661169f), + glm::vec3(0.7299188f, 0.7299188f, 0.9489649f), + glm::vec3(0.1801442f, 0.2348952f, 0.5795466f), }; - checkBufferContents(buffer.cesium.data, expected); + checkBufferContents(buffer.cesium.data, expected); } { From 547f92294c03fd09238edf0653b32800cca7a150 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 3 Feb 2023 17:38:30 -0500 Subject: [PATCH 20/20] Add maybe_unused attribute --- Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp index e979b760c..543397b89 100644 --- a/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp +++ b/Cesium3DTilesSelection/test/TestPntsToGltfConverter.cpp @@ -24,7 +24,7 @@ template static void checkBufferContents( const std::vector& buffer, const std::vector& expected, - const double epsilon = Math::Epsilon6) { + [[maybe_unused]] const double epsilon = Math::Epsilon6) { REQUIRE(buffer.size() == expected.size() * sizeof(Type)); const int32_t byteStride = sizeof(Type); if constexpr (std::is_same_v) {