cesium-native/Cesium3DTilesContent/src/PntsToGltfConverter.cpp

1631 lines
58 KiB
C++

#include "BatchTableToGltfStructuralMetadata.h"
#include "MetadataProperty.h"
#include <Cesium3DTilesContent/GltfConverterResult.h>
#include <Cesium3DTilesContent/GltfConverters.h>
#include <Cesium3DTilesContent/PntsToGltfConverter.h>
#include <CesiumAsync/Future.h>
#include <CesiumGeometry/Transforms.h>
#include <CesiumGltf/Accessor.h>
#include <CesiumGltf/Buffer.h>
#include <CesiumGltf/BufferView.h>
#include <CesiumGltf/ExtensionCesiumRTC.h>
#include <CesiumGltf/ExtensionKhrMaterialsUnlit.h>
#include <CesiumGltf/Material.h>
#include <CesiumGltf/MaterialPBRMetallicRoughness.h>
#include <CesiumGltf/Mesh.h>
#include <CesiumGltf/MeshPrimitive.h>
#include <CesiumGltf/Model.h>
#include <CesiumGltf/Node.h>
#include <CesiumGltf/Scene.h>
#include <CesiumGltfReader/GltfReader.h>
#include <CesiumUtility/Assert.h>
#include <CesiumUtility/AttributeCompression.h>
#include <draco/core/data_buffer.h>
#include <draco/core/draco_types.h>
#include <draco/core/status_or.h>
#include <fmt/format.h>
#include <glm/common.hpp>
#include <glm/exponential.hpp>
#include <glm/ext/matrix_double4x4.hpp>
#include <glm/ext/vector_double3.hpp>
#include <glm/ext/vector_float3.hpp>
#include <glm/ext/vector_float4.hpp>
#include <glm/ext/vector_uint2_sized.hpp>
#include <glm/ext/vector_uint3_sized.hpp>
#include <glm/ext/vector_uint4_sized.hpp>
#include <rapidjson/rapidjson.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <map>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <utility>
#include <vector>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4127 4018 4804)
#endif
#include <draco/attributes/point_attribute.h>
#include <draco/compression/decode.h>
#include <draco/core/decoder_buffer.h>
#include <draco/point_cloud/point_cloud.h>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#include <rapidjson/document.h>
#include <limits>
using namespace CesiumGltf;
using namespace CesiumUtility;
namespace Cesium3DTilesContent {
namespace {
struct PntsHeader {
unsigned char magic[4] = {0, 0, 0, 0};
uint32_t version = 0;
uint32_t byteLength = 0;
uint32_t featureTableJsonByteLength = 0;
uint32_t featureTableBinaryByteLength = 0;
uint32_t batchTableJsonByteLength = 0;
uint32_t batchTableBinaryByteLength = 0;
};
void parsePntsHeader(
const std::span<const std::byte>& 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<const PntsHeader*>(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<uint32_t>(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;
}
}
struct PntsSemantic {
uint32_t byteOffset = 0;
std::optional<int32_t> dracoId;
std::vector<std::byte> data;
};
struct DracoMetadataSemantic {
int32_t dracoId;
MetadataProperty::ComponentType componentType;
MetadataProperty::Type type;
};
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 <typename TColor> TColor srgbToLinear(const TColor srgb) {
static_assert(
std::is_same_v<TColor, glm::vec3> || std::is_same_v<TColor, glm::vec4>);
glm::vec3 srgbInput = glm::vec3(srgb);
glm::vec3 linearOutput = glm::pow(srgbInput, glm::vec3(2.2f));
if constexpr (std::is_same_v<TColor, glm::vec4>) {
return glm::vec4(linearOutput, srgb.w);
} else if constexpr (std::is_same_v<TColor, glm::vec3>) {
return linearOutput;
}
}
struct PntsContent {
uint32_t pointsLength = 0;
std::optional<glm::dvec3> rtcCenter;
std::optional<glm::dvec3> quantizedVolumeOffset;
std::optional<glm::dvec3> quantizedVolumeScale;
std::optional<glm::u8vec4> constantRgba;
std::optional<uint32_t> batchLength;
PntsSemantic position;
bool positionQuantized = false;
// required by glTF spec
glm::vec3 positionMin = glm::vec3(std::numeric_limits<float>::max());
glm::vec3 positionMax = glm::vec3(std::numeric_limits<float>::lowest());
std::optional<PntsSemantic> color;
PntsColorType colorType = PntsColorType::CONSTANT;
std::optional<PntsSemantic> normal;
bool normalOctEncoded = false;
std::optional<PntsSemantic> batchId;
std::optional<MetadataProperty::ComponentType> batchIdComponentType;
std::optional<uint32_t> dracoByteOffset;
std::optional<uint32_t> dracoByteLength;
std::map<std::string, DracoMetadataSemantic> dracoMetadataSemantics;
std::vector<std::byte> dracoBatchTableBinary;
ErrorList errors;
bool dracoMetadataHasErrors = false;
};
template <typename TValidate>
bool validateJsonArrayValues(
const rapidjson::Value& arrayValue,
uint32_t expectedLength,
TValidate 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 parsePositionsFromFeatureTableJson(
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 does not have "
"valid byteOffset.");
return;
}
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 isNumber = [](const rapidjson::Value& value) -> bool {
return value.IsNumber();
};
if (quantizedVolumeOffsetIt == featureTableJson.MemberEnd() ||
!validateJsonArrayValues(quantizedVolumeOffsetIt->value, 3, isNumber)) {
parsedContent.errors.emplaceError(
"Error parsing PNTS feature table, POSITION_QUANTIZED is used but "
"no valid QUANTIZED_VOLUME_OFFSET was found.");
return;
}
if (quantizedVolumeScaleIt == featureTableJson.MemberEnd() ||
!validateJsonArrayValues(quantizedVolumeScaleIt->value, 3, isNumber)) {
parsedContent.errors.emplaceError(
"Error parsing PNTS feature table, POSITION_QUANTIZED is used but "
"no valid QUANTIZED_VOLUME_SCALE 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 does not have "
"valid byteOffset.");
return;
}
parsedContent.positionQuantized = true;
parsedContent.position.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, one of POSITION or POSITION_QUANTIZED "
"must be defined.");
return;
}
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.color = std::make_optional<PntsSemantic>();
parsedContent.color.value().byteOffset = byteOffsetIt->value.GetUint();
parsedContent.colorType = PntsColorType::RGBA;
return;
}
parsedContent.errors.emplaceWarning(
"Error parsing PNTS feature table, RGBA 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.color = std::make_optional<PntsSemantic>();
parsedContent.color.value().byteOffset = byteOffsetIt->value.GetUint();
parsedContent.colorType = PntsColorType::RGB;
return;
}
parsedContent.errors.emplaceWarning(
"Error parsing PNTS feature table, RGB 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.color = std::make_optional<PntsSemantic>();
parsedContent.color.value().byteOffset = byteOffsetIt->value.GetUint();
parsedContent.colorType = PntsColorType::RGB565;
return;
}
parsedContent.errors.emplaceWarning(
"Error parsing PNTS feature table, RGB565 does not have valid "
"byteOffset. Skip parsing RGB565 colors.");
}
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 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.normal = std::make_optional<PntsSemantic>();
parsedContent.normal.value().byteOffset = byteOffsetIt->value.GetUint();
return;
}
parsedContent.errors.emplaceWarning(
"Error parsing PNTS feature table, NORMAL 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.normal = std::make_optional<PntsSemantic>();
parsedContent.normal.value().byteOffset = byteOffsetIt->value.GetUint();
parsedContent.normalOctEncoded = true;
return;
}
parsedContent.errors.emplaceWarning(
"Error parsing PNTS feature table, NORMAL_OCT16P does not have valid "
"byteOffset. Skip parsing oct-encoded normals.");
}
}
void parseBatchIdsFromFeatureTableJson(
const rapidjson::Document& featureTableJson,
PntsContent& parsedContent) {
const auto batchIdIt = featureTableJson.FindMember("BATCH_ID");
if (batchIdIt == featureTableJson.MemberEnd() ||
!batchIdIt->value.IsObject()) {
return;
}
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;
}
parsedContent.batchId = std::make_optional<PntsSemantic>();
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 (MetadataProperty::stringToMetadataComponentType.find(
componentTypeString) ==
MetadataProperty::stringToMetadataComponentType.end()) {
parsedContent.errors.emplaceWarning(
"Error parsing PNTS feature table, BATCH_ID does not have "
"valid componentType. Skip parsing batch IDs.");
return;
}
MetadataProperty::ComponentType componentType =
MetadataProperty::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 =
MetadataProperty::ComponentType::UNSIGNED_SHORT;
}
const auto batchLengthIt = featureTableJson.FindMember("BATCH_LENGTH");
if (batchLengthIt != featureTableJson.MemberEnd() &&
batchLengthIt->value.IsUint()) {
parsedContent.batchLength = batchLengthIt->value.GetUint();
}
}
void parseSemanticsFromFeatureTableJson(
const rapidjson::Document& featureTableJson,
PntsContent& parsedContent) {
parsePositionsFromFeatureTableJson(featureTableJson, parsedContent);
if (parsedContent.errors) {
return;
}
parseColorsFromFeatureTableJson(featureTableJson, parsedContent);
if (parsedContent.errors) {
return;
}
parseNormalsFromFeatureTableJson(featureTableJson, parsedContent);
if (parsedContent.errors) {
return;
}
parseBatchIdsFromFeatureTableJson(featureTableJson, parsedContent);
if (parsedContent.errors) {
return;
}
auto isNumber = [](const rapidjson::Value& value) -> bool {
return value.IsNumber();
};
const auto rtcIt = featureTableJson.FindMember("RTC_CENTER");
if (rtcIt != featureTableJson.MemberEnd() &&
validateJsonArrayValues(rtcIt->value, 3, isNumber)) {
const rapidjson::Value& rtcValue = rtcIt->value;
parsedContent.rtcCenter = glm::vec3(
rtcValue[0].GetDouble(),
rtcValue[1].GetDouble(),
rtcValue[2].GetDouble());
}
}
void parseDracoExtensionFromFeatureTableJson(
const rapidjson::Value& dracoExtension,
PntsContent& parsedContent) {
const auto propertiesIt = dracoExtension.FindMember("properties");
if (propertiesIt == dracoExtension.MemberEnd() ||
!propertiesIt->value.IsObject()) {
parsedContent.errors.emplaceError(
"Error parsing 3DTILES_draco_compression extension, "
"no valid properties object found.");
return;
}
const auto byteOffsetIt = dracoExtension.FindMember("byteOffset");
if (byteOffsetIt == dracoExtension.MemberEnd() ||
!byteOffsetIt->value.IsUint64()) {
parsedContent.errors.emplaceError(
"Error parsing 3DTILES_draco_compression extension, "
"no valid byteOffset found.");
return;
}
const auto byteLengthIt = dracoExtension.FindMember("byteLength");
if (byteLengthIt == dracoExtension.MemberEnd() ||
!byteLengthIt->value.IsUint64()) {
parsedContent.errors.emplaceError(
"Error parsing 3DTILES_draco_compression extension, "
"no valid byteLength found.");
return;
}
parsedContent.dracoByteOffset = byteOffsetIt->value.GetUint();
parsedContent.dracoByteLength = byteLengthIt->value.GetUint();
const rapidjson::Value& dracoProperties = propertiesIt->value;
auto positionDracoIdIt = dracoProperties.FindMember("POSITION");
if (positionDracoIdIt != dracoProperties.MemberEnd() &&
positionDracoIdIt->value.IsInt()) {
parsedContent.position.dracoId = positionDracoIdIt->value.GetInt();
}
if (parsedContent.color) {
if (parsedContent.colorType == PntsColorType::RGBA) {
auto rgbaDracoIdIt = dracoProperties.FindMember("RGBA");
if (rgbaDracoIdIt != dracoProperties.MemberEnd() &&
rgbaDracoIdIt->value.IsInt()) {
parsedContent.color.value().dracoId = rgbaDracoIdIt->value.GetInt();
}
} else if (parsedContent.colorType == PntsColorType::RGB) {
auto rgbDracoIdIt = dracoProperties.FindMember("RGB");
if (rgbDracoIdIt != dracoProperties.MemberEnd() &&
rgbDracoIdIt->value.IsInt()) {
parsedContent.color.value().dracoId = rgbDracoIdIt->value.GetInt();
}
}
}
if (parsedContent.normal) {
auto normalDracoIdIt = dracoProperties.FindMember("NORMAL");
if (normalDracoIdIt != dracoProperties.MemberEnd() &&
normalDracoIdIt->value.IsInt()) {
parsedContent.normal.value().dracoId = normalDracoIdIt->value.GetInt();
}
}
if (parsedContent.batchId) {
auto batchIdDracoIdIt = dracoProperties.FindMember("BATCH_ID");
if (batchIdDracoIdIt != dracoProperties.MemberEnd() &&
batchIdDracoIdIt->value.IsInt()) {
parsedContent.batchId.value().dracoId = batchIdDracoIdIt->value.GetInt();
}
}
}
rapidjson::Document parseFeatureTableJson(
const std::span<const std::byte>& featureTableJsonData,
PntsContent& parsedContent) {
rapidjson::Document document;
document.Parse(
reinterpret_cast<const char*>(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 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 the PNTS feature table.");
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& dracoExtension = dracoExtensionIt->value;
parseDracoExtensionFromFeatureTableJson(dracoExtension, parsedContent);
}
}
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;
}
if (parsedContent.batchLength) {
parsedContent.errors.emplaceWarning(
"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.dracoMetadataHasErrors = 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.dracoMetadataHasErrors = 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();
// If there are issues with the extension, skip parsing metadata altogether.
// Otherwise, BatchTableToGltfStructuralMetadata will still try to parse the
// invalid Draco-compressed properties.
auto batchTablePropertyIt = batchTableJson.FindMember(name.c_str());
if (batchTablePropertyIt == batchTableJson.MemberEnd() ||
!batchTablePropertyIt->value.IsObject()) {
parsedContent.errors.emplaceWarning(fmt::format(
"The metadata property {} is in the 3DTILES_draco_point_compression "
"extension but not in the batch table itself.",
name));
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
// BatchTableToGltfStructuralMetadata, 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(
"Skip decoding Draco-compressed property {}. The binary property "
"doesn't have a valid byteOffset.",
name));
continue;
}
std::string componentType;
auto componentTypeIt = batchTableProperty.FindMember("componentType");
if (componentTypeIt != batchTableProperty.MemberEnd() &&
componentTypeIt->value.IsString()) {
componentType = componentTypeIt->value.GetString();
}
if (MetadataProperty::stringToMetadataComponentType.find(componentType) ==
MetadataProperty::stringToMetadataComponentType.end()) {
parsedContent.errors.emplaceWarning(fmt::format(
"Skip decoding Draco-compressed property {}. The binary property "
"doesn't have a valid componentType.",
name));
continue;
}
std::string type;
auto typeIt = batchTableProperty.FindMember("type");
if (typeIt != batchTableProperty.MemberEnd() && typeIt->value.IsString()) {
type = typeIt->value.GetString();
}
if (MetadataProperty::stringToMetadataType.find(type) ==
MetadataProperty::stringToMetadataType.end()) {
parsedContent.errors.emplaceWarning(fmt::format(
"Skip decoding Draco-compressed property {}. The binary property "
"doesn't have a valid type.",
name));
continue;
}
DracoMetadataSemantic semantic;
semantic.dracoId = dracoPropertyIt->value.GetInt();
semantic.componentType =
MetadataProperty::stringToMetadataComponentType.at(componentType);
semantic.type = MetadataProperty::stringToMetadataType.at(type);
parsedContent.dracoMetadataSemantics.insert({name, semantic});
}
}
rapidjson::Document parseBatchTableJson(
const std::span<const std::byte>& batchTableJsonData,
PntsContent& parsedContent) {
rapidjson::Document document;
document.Parse(
reinterpret_cast<const char*>(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;
}
parseDracoExtensionFromBatchTableJson(document, parsedContent);
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;
}
std::optional<MetadataProperty::ComponentType>
getComponentTypeFromDracoDataType(const draco::DataType dataType) {
switch (dataType) {
case draco::DT_INT8:
return MetadataProperty::ComponentType::BYTE;
case draco::DT_UINT8:
return MetadataProperty::ComponentType::UNSIGNED_BYTE;
case draco::DT_INT16:
return MetadataProperty::ComponentType::SHORT;
case draco::DT_UINT16:
return MetadataProperty::ComponentType::UNSIGNED_SHORT;
case draco::DT_INT32:
return MetadataProperty::ComponentType::INT;
case draco::DT_UINT32:
return MetadataProperty::ComponentType::UNSIGNED_INT;
case draco::DT_FLOAT32:
return MetadataProperty::ComponentType::FLOAT;
case draco::DT_FLOAT64:
return MetadataProperty::ComponentType::DOUBLE;
default:
return std::nullopt;
}
}
bool validateDracoMetadataAttribute(
const draco::PointAttribute* const pAttribute,
const DracoMetadataSemantic semantic) {
if (!pAttribute) {
return false;
}
auto componentType =
getComponentTypeFromDracoDataType(pAttribute->data_type());
if (!componentType || componentType.value() != semantic.componentType) {
return false;
}
auto type = MetadataProperty::getTypeFromNumberOfComponents(
static_cast<int8_t>(pAttribute->num_components()));
return type && type.value() == semantic.type;
}
template <typename T>
void getDracoData(
const draco::PointAttribute* pAttribute,
std::vector<std::byte>& data,
const uint32_t 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();
const uint8_t* pSource = decodedBuffer->data() + decodedByteOffset;
if (dataElementSize != static_cast<size_t>(decodedByteStride)) {
std::span<T> outData(reinterpret_cast<T*>(data.data()), pointsLength);
for (uint32_t i = 0; i < pointsLength; ++i) {
outData[i] = *reinterpret_cast<const T*>(pSource + decodedByteStride * i);
}
} else {
std::memcpy(
data.data(),
decodedBuffer->data() + decodedByteOffset,
databufferByteLength);
}
}
void decodeDracoMetadata(
const std::unique_ptr<draco::PointCloud>& pPointCloud,
rapidjson::Document& batchTableJson,
PntsContent& parsedContent) {
const uint64_t pointsLength = parsedContent.pointsLength;
std::vector<std::byte>& data = parsedContent.dracoBatchTableBinary;
const auto& dracoMetadataSemantics = parsedContent.dracoMetadataSemantics;
for (const auto& dracoMetadataSemantic : dracoMetadataSemantics) {
const DracoMetadataSemantic& dracoSemantic = dracoMetadataSemantic.second;
draco::PointAttribute* pAttribute =
pPointCloud->attribute(dracoSemantic.dracoId);
if (!validateDracoMetadataAttribute(pAttribute, dracoSemantic)) {
parsedContent.errors.emplaceWarning(fmt::format(
"Error decoding {} property in the 3DTILES_draco_compression "
"extension. Skip parsing metadata.",
dracoMetadataSemantic.first));
parsedContent.dracoMetadataHasErrors = true;
return;
}
const size_t byteOffset = data.size();
// These do not test for validity since the batch table and extension
// were validated in parseDracoExtensionFromBatchTableJson.
auto batchTableSemanticIt =
batchTableJson.FindMember(dracoMetadataSemantic.first.c_str());
rapidjson::Value& batchTableSemantic =
batchTableSemanticIt->value.GetObject();
auto byteOffsetIt = batchTableSemantic.FindMember("byteOffset");
byteOffsetIt->value.SetUint(static_cast<uint32_t>(byteOffset));
const size_t metadataByteStride =
MetadataProperty::getSizeOfComponentType(dracoSemantic.componentType) *
static_cast<size_t>(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();
if (metadataByteStride != static_cast<size_t>(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,
decodedBuffer->data() + decodedByteOffset,
metadataByteStride * pointsLength);
}
}
}
void decodeDraco(
const std::span<const std::byte>& featureTableBinaryData,
rapidjson::Document& batchTableJson,
const std::span<const std::byte>& batchTableBinaryData,
PntsContent& parsedContent) {
if (!parsedContent.dracoByteOffset || !parsedContent.dracoByteLength) {
return;
}
draco::Decoder decoder;
draco::DecoderBuffer buffer;
buffer.Init(
reinterpret_cast<const char*>(featureTableBinaryData.data()) +
parsedContent.dracoByteOffset.value(),
parsedContent.dracoByteLength.value());
draco::StatusOr<std::unique_ptr<draco::PointCloud>> dracoResult =
decoder.DecodePointCloudFromBuffer(&buffer);
if (!dracoResult.ok()) {
parsedContent.errors.emplaceError("Error decoding Draco point cloud.");
return;
}
const std::unique_ptr<draco::PointCloud>& 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<std::byte>& positionData = parsedContent.position.data;
positionData.resize(pointsLength * sizeof(glm::vec3));
std::span<glm::vec3> outPositions(
reinterpret_cast<glm::vec3*>(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) {
const glm::vec3 position = *reinterpret_cast<const glm::vec3*>(
decodedBuffer->data() + decodedByteOffset + decodedByteStride * i);
outPositions[i] = position;
parsedContent.positionMin = glm::min(position, parsedContent.positionMin);
parsedContent.positionMax = glm::max(position, parsedContent.positionMax);
}
}
if (parsedContent.color) {
PntsSemantic& color = parsedContent.color.value();
if (color.dracoId) {
draco::PointAttribute* pColorAttribute =
pPointCloud->attribute(color.dracoId.value());
std::vector<std::byte>& colorData = parsedContent.color->data;
if (parsedContent.colorType == PntsColorType::RGBA &&
validateDracoAttribute(pColorAttribute, draco::DT_UINT8, 4)) {
colorData.resize(pointsLength * sizeof(glm::vec4));
std::span<glm::vec4> outColors(
reinterpret_cast<glm::vec4*>(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<const glm::u8vec4*>(
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)) {
colorData.resize(pointsLength * sizeof(glm::vec3));
std::span<glm::vec3> outColors(
reinterpret_cast<glm::vec3*>(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<const glm::u8vec3*>(
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. "
"Skip parsing colors.");
parsedContent.color = std::nullopt;
parsedContent.colorType = PntsColorType::CONSTANT;
}
}
}
if (parsedContent.normal) {
PntsSemantic& normal = parsedContent.normal.value();
if (normal.dracoId) {
draco::PointAttribute* pNormalAttribute =
pPointCloud->attribute(normal.dracoId.value());
if (validateDracoAttribute(pNormalAttribute, draco::DT_FLOAT32, 3)) {
getDracoData<glm::vec3>(pNormalAttribute, normal.data, pointsLength);
} else {
parsedContent.errors.emplaceWarning("Error parsing decoded Draco point "
"cloud, invalid 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());
int32_t componentType = 0;
if (parsedContent.batchIdComponentType) {
componentType = parsedContent.batchIdComponentType.value();
}
if (componentType == MetadataProperty::ComponentType::UNSIGNED_BYTE &&
validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT8, 1)) {
getDracoData<uint8_t>(pBatchIdAttribute, batchId.data, pointsLength);
} else if (
componentType == MetadataProperty::ComponentType::UNSIGNED_INT &&
validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT32, 1)) {
getDracoData<uint32_t>(pBatchIdAttribute, batchId.data, pointsLength);
} else if (
(componentType == 0 ||
componentType == MetadataProperty::ComponentType::UNSIGNED_SHORT) &&
validateDracoAttribute(pBatchIdAttribute, draco::DT_UINT16, 1)) {
getDracoData<uint16_t>(pBatchIdAttribute, batchId.data, pointsLength);
} else {
parsedContent.errors.emplaceWarning(
"Error parsing decoded Draco point cloud, invalid batch ID "
"attribute. "
"Skip parsing batch IDs.");
parsedContent.batchId = std::nullopt;
}
}
}
if (batchTableJson.HasParseError() || parsedContent.dracoMetadataHasErrors) {
return;
}
// 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);
std::memcpy(
parsedContent.dracoBatchTableBinary.data(),
batchTableBinaryData.data(),
batchTableBinaryByteLength);
}
decodeDracoMetadata(pPointCloud, batchTableJson, parsedContent);
}
void parsePositionsFromFeatureTableBinary(
const std::span<const std::byte>& featureTableBinaryData,
PntsContent& parsedContent) {
std::vector<std::byte>& positionData = parsedContent.position.data;
if (positionData.size() > 0) {
// If data isn't empty, it must have been decoded from Draco.
return;
}
const uint32_t pointsLength = parsedContent.pointsLength;
const size_t positionsByteStride = sizeof(glm::vec3);
const size_t positionsByteLength = pointsLength * positionsByteStride;
positionData.resize(positionsByteLength);
std::span<glm::vec3> outPositions(
reinterpret_cast<glm::vec3*>(positionData.data()),
pointsLength);
if (parsedContent.positionQuantized && parsedContent.quantizedVolumeScale &&
parsedContent.quantizedVolumeOffset) {
// PERFORMANCE_IDEA: In the future, it might be more performant to detect
// if the recipient engine can handle dequantization itself, and if so, use
// the KHR_mesh_quantization extension to avoid dequantizing here.
const std::span<const glm::u16vec3> quantizedPositions(
reinterpret_cast<const glm::u16vec3*>(
featureTableBinaryData.data() + parsedContent.position.byteOffset),
pointsLength);
const glm::vec3 quantizedVolumeScale(
parsedContent.quantizedVolumeScale.value());
const glm::vec3 quantizedVolumeOffset(
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);
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 if (parsedContent.positionQuantized) {
parsedContent.errors.emplaceError(
"Missing quantizedVolumeScale or quantizedVolumeOffset in parsed "
"content");
} else {
// The position accessor min / max is required by the glTF spec, so
// use a for loop instead of std::memcpy.
const std::span<const glm::vec3> positions(
reinterpret_cast<const glm::vec3*>(
featureTableBinaryData.data() + parsedContent.position.byteOffset),
pointsLength);
for (size_t i = 0; i < pointsLength; i++) {
const glm::vec3 position = positions[i];
outPositions[i] = position;
parsedContent.positionMin = glm::min(parsedContent.positionMin, position);
parsedContent.positionMax = glm::max(parsedContent.positionMax, position);
}
}
}
void parseColorsFromFeatureTableBinary(
const std::span<const std::byte>& featureTableBinaryData,
PntsContent& parsedContent) {
CESIUM_ASSERT(parsedContent.color.has_value());
PntsSemantic& color = parsedContent.color.value();
std::vector<std::byte>& colorData = color.data;
if (colorData.size() > 0) {
// If data isn't empty, it must have been decoded from Draco.
return;
}
const uint32_t pointsLength = parsedContent.pointsLength;
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 std::span<const glm::u8vec4> rgbaColors(
reinterpret_cast<const glm::u8vec4*>(
featureTableBinaryData.data() + color.byteOffset),
pointsLength);
std::span<glm::vec4> outColors(
reinterpret_cast<glm::vec4*>(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 std::span<const glm::u8vec3> rgbColors(
reinterpret_cast<const glm::u8vec3*>(
featureTableBinaryData.data() + color.byteOffset),
pointsLength);
std::span<glm::vec3> outColors(
reinterpret_cast<glm::vec3*>(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 std::span<const uint16_t> compressedColors(
reinterpret_cast<const uint16_t*>(
featureTableBinaryData.data() + color.byteOffset),
pointsLength);
std::span<glm::vec3> outColors(
reinterpret_cast<glm::vec3*>(colorData.data()),
pointsLength);
for (size_t i = 0; i < pointsLength; i++) {
const uint16_t compressedColor = compressedColors[i];
glm::vec3 decompressedColor =
glm::vec3(AttributeCompression::decodeRGB565(compressedColor));
outColors[i] = srgbToLinear(decompressedColor);
}
}
}
void parseNormalsFromFeatureTableBinary(
const std::span<const std::byte>& featureTableBinaryData,
PntsContent& parsedContent) {
CESIUM_ASSERT(parsedContent.normal.has_value());
PntsSemantic& normal = parsedContent.normal.value();
std::vector<std::byte>& 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 std::span<const glm::u8vec2> encodedNormals(
reinterpret_cast<const glm::u8vec2*>(
featureTableBinaryData.data() + normal.byteOffset),
pointsLength);
std::span<glm::vec3> outNormals(
reinterpret_cast<glm::vec3*>(normalData.data()),
pointsLength);
for (size_t i = 0; i < pointsLength; i++) {
const glm::u8vec2 encodedNormal = encodedNormals[i];
outNormals[i] = glm::vec3(CesiumUtility::AttributeCompression::octDecode(
encodedNormal.x,
encodedNormal.y));
}
} else {
std::memcpy(
normalData.data(),
featureTableBinaryData.data() + normal.byteOffset,
normalsByteLength);
}
}
void parseBatchIdsFromFeatureTableBinary(
const std::span<const std::byte>& featureTableBinaryData,
PntsContent& parsedContent) {
CESIUM_ASSERT(parsedContent.batchId.has_value());
PntsSemantic& batchId = parsedContent.batchId.value();
std::vector<std::byte>& 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 = MetadataProperty::getSizeOfComponentType(
parsedContent.batchIdComponentType.value());
}
const size_t batchIdsByteLength = pointsLength * batchIdsByteStride;
batchIdData.resize(batchIdsByteLength);
std::memcpy(
batchIdData.data(),
featureTableBinaryData.data() + batchId.byteOffset,
batchIdsByteLength);
}
void parseFeatureTableBinary(
const std::span<const std::byte>& featureTableBinaryData,
rapidjson::Document& batchTableJson,
const std::span<const std::byte>& batchTableBinaryData,
PntsContent& parsedContent) {
decodeDraco(
featureTableBinaryData,
batchTableJson,
batchTableBinaryData,
parsedContent);
if (parsedContent.errors) {
return;
}
parsePositionsFromFeatureTableBinary(featureTableBinaryData, parsedContent);
if (parsedContent.color) {
parseColorsFromFeatureTableBinary(featureTableBinaryData, parsedContent);
}
if (parsedContent.normal) {
parseNormalsFromFeatureTableBinary(featureTableBinaryData, parsedContent);
}
if (parsedContent.batchId) {
parseBatchIdsFromFeatureTableBinary(featureTableBinaryData, parsedContent);
}
}
int32_t createBufferInGltf(Model& gltf, std::vector<std::byte>&& buffer) {
size_t bufferId = gltf.buffers.size();
Buffer& gltfBuffer = gltf.buffers.emplace_back();
gltfBuffer.byteLength = static_cast<int32_t>(buffer.size());
gltfBuffer.cesium.data = std::move(buffer);
return static_cast<int32_t>(bufferId);
}
int32_t createBufferViewInGltf(
Model& gltf,
const int32_t bufferId,
const int64_t byteLength,
const int64_t byteStride) {
size_t bufferViewId = gltf.bufferViews.size();
BufferView& bufferView = gltf.bufferViews.emplace_back();
bufferView.buffer = bufferId;
bufferView.byteLength = byteLength;
bufferView.byteOffset = 0;
bufferView.byteStride = byteStride;
bufferView.target = BufferView::Target::ARRAY_BUFFER;
return static_cast<int32_t>(bufferViewId);
}
int32_t createAccessorInGltf(
Model& gltf,
const int32_t bufferViewId,
const int32_t componentType,
const int64_t count,
const std::string& type) {
size_t accessorId = gltf.accessors.size();
Accessor& accessor = gltf.accessors.emplace_back();
accessor.bufferView = bufferViewId;
accessor.byteOffset = 0;
accessor.componentType = componentType;
accessor.count = count;
accessor.type = type;
return static_cast<int32_t>(accessorId);
}
void addPositionsToGltf(PntsContent& parsedContent, Model& gltf) {
const int64_t count = static_cast<int64_t>(parsedContent.pointsLength);
const int64_t byteStride = static_cast<int64_t>(sizeof(glm ::vec3));
const int64_t byteLength = static_cast<int64_t>(byteStride * count);
int32_t bufferId =
createBufferInGltf(gltf, std::move(parsedContent.position.data));
int32_t bufferViewId =
createBufferViewInGltf(gltf, bufferId, byteLength, byteStride);
int32_t accessorId = createAccessorInGltf(
gltf,
bufferViewId,
Accessor::ComponentType::FLOAT,
count,
Accessor::Type::VEC3);
Accessor& accessor = gltf.accessors[static_cast<uint32_t>(accessorId)];
accessor.min = {
parsedContent.positionMin.x,
parsedContent.positionMin.y,
parsedContent.positionMin.z,
};
accessor.max = {
parsedContent.positionMax.x,
parsedContent.positionMax.y,
parsedContent.positionMax.z,
};
MeshPrimitive& primitive = gltf.meshes[0].primitives[0];
primitive.attributes.emplace("POSITION", accessorId);
}
void addColorsToGltf(PntsContent& parsedContent, Model& gltf) {
CESIUM_ASSERT(parsedContent.color.has_value());
PntsSemantic& color = parsedContent.color.value();
const int64_t count = static_cast<int64_t>(parsedContent.pointsLength);
int64_t byteStride = 0;
const int32_t componentType = Accessor::ComponentType::FLOAT;
std::string type;
bool isTranslucent = false;
if (parsedContent.colorType == PntsColorType::RGBA) {
byteStride = static_cast<int64_t>(sizeof(glm::vec4));
type = Accessor::Type::VEC4;
isTranslucent = true;
} else {
byteStride = static_cast<int64_t>(sizeof(glm::vec3));
type = Accessor::Type::VEC3;
}
const int64_t byteLength = static_cast<int64_t>(byteStride * count);
int32_t bufferId = createBufferInGltf(gltf, std::move(color.data));
int32_t bufferViewId =
createBufferViewInGltf(gltf, bufferId, byteLength, byteStride);
int32_t accessorId =
createAccessorInGltf(gltf, bufferViewId, componentType, count, type);
MeshPrimitive& primitive = gltf.meshes[0].primitives[0];
primitive.attributes.emplace("COLOR_0", accessorId);
if (isTranslucent) {
Material& material =
gltf.materials[static_cast<uint32_t>(primitive.material)];
material.alphaMode = Material::AlphaMode::BLEND;
}
}
void addNormalsToGltf(PntsContent& parsedContent, Model& gltf) {
CESIUM_ASSERT(parsedContent.normal.has_value());
PntsSemantic& normal = parsedContent.normal.value();
const int64_t count = static_cast<int64_t>(parsedContent.pointsLength);
const int64_t byteStride = static_cast<int64_t>(sizeof(glm ::vec3));
const int64_t byteLength = static_cast<int64_t>(byteStride * count);
int32_t bufferId = createBufferInGltf(gltf, std::move(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) {
CESIUM_ASSERT(parsedContent.batchId.has_value());
PntsSemantic& batchId = parsedContent.batchId.value();
const int64_t count = static_cast<int64_t>(parsedContent.pointsLength);
int32_t componentType = Accessor::ComponentType::UNSIGNED_SHORT;
if (parsedContent.batchIdComponentType) {
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<int64_t>(byteStride * count);
int32_t bufferId = createBufferInGltf(gltf, std::move(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 BatchTableToGltfStructuralMetadata.
primitive.attributes.emplace("_BATCHID", accessorId);
}
}
void createGltfFromParsedContent(
PntsContent& parsedContent,
GltfConverterResult& result) {
result.model.reset();
Model& gltf = result.model.emplace();
gltf.asset.version = "2.0";
// Create a single node with a single mesh, with a single primitive.
Node& node = gltf.nodes.emplace_back();
std::memcpy(
node.matrix.data(),
&CesiumGeometry::Transforms::Z_UP_TO_Y_UP,
sizeof(glm::dmat4));
// Create a scene containing the node, and make it the default scene.
Scene& scene = gltf.scenes.emplace_back();
scene.nodes = {0};
gltf.scene = 0;
size_t meshId = gltf.meshes.size();
Mesh& mesh = gltf.meshes.emplace_back();
node.mesh = static_cast<int32_t>(meshId);
MeshPrimitive& primitive = mesh.primitives.emplace_back();
primitive.mode = MeshPrimitive::Mode::POINTS;
size_t materialId = gltf.materials.size();
Material& material = gltf.materials.emplace_back();
material.pbrMetallicRoughness =
std::make_optional<CesiumGltf::MaterialPBRMetallicRoughness>();
// These values are borrowed from CesiumJS.
material.pbrMetallicRoughness.value().metallicFactor = 0;
material.pbrMetallicRoughness.value().roughnessFactor = 0.9;
primitive.material = static_cast<int32_t>(materialId);
addPositionsToGltf(parsedContent, gltf);
if (parsedContent.color) {
addColorsToGltf(parsedContent, gltf);
} else if (parsedContent.constantRgba) {
glm::vec4 materialColor(parsedContent.constantRgba.value());
materialColor = srgbToLinear(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<CesiumGltf::ExtensionKhrMaterialsUnlit>();
gltf.addExtensionUsed(
CesiumGltf::ExtensionKhrMaterialsUnlit::ExtensionName);
}
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,
// 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<CesiumGltf::ExtensionCesiumRTC>();
result.model->addExtensionRequired(
CesiumGltf::ExtensionCesiumRTC::ExtensionName);
glm::dvec3 rtcCenter = parsedContent.rtcCenter.value();
cesiumRTC.center = {rtcCenter.x, rtcCenter.y, rtcCenter.z};
}
}
void convertPntsContentToGltf(
const std::span<const std::byte>& pntsBinary,
const PntsHeader& header,
uint32_t headerLength,
GltfConverterResult& result) {
if (header.featureTableJsonByteLength > 0 &&
header.featureTableBinaryByteLength > 0) {
PntsContent parsedContent;
const std::span<const std::byte> featureTableJsonData =
pntsBinary.subspan(headerLength, header.featureTableJsonByteLength);
rapidjson::Document featureTableJson =
parseFeatureTableJson(featureTableJsonData, parsedContent);
if (parsedContent.errors) {
result.errors.merge(parsedContent.errors);
return;
}
// 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;
rapidjson::Document batchTableJson;
if (header.batchTableJsonByteLength > 0) {
const std::span<const std::byte> batchTableJsonData = pntsBinary.subspan(
static_cast<size_t>(batchTableStart),
header.batchTableJsonByteLength);
batchTableJson = parseBatchTableJson(batchTableJsonData, parsedContent);
}
const std::span<const std::byte> featureTableBinaryData =
pntsBinary.subspan(
static_cast<size_t>(
headerLength + header.featureTableJsonByteLength),
header.featureTableBinaryByteLength);
std::span<const std::byte> batchTableBinaryData;
if (header.batchTableBinaryByteLength > 0) {
batchTableBinaryData = pntsBinary.subspan(
static_cast<size_t>(
batchTableStart + header.batchTableJsonByteLength),
header.batchTableBinaryByteLength);
}
parseFeatureTableBinary(
featureTableBinaryData,
batchTableJson,
batchTableBinaryData,
parsedContent);
if (parsedContent.errors) {
result.errors.merge(parsedContent.errors);
return;
}
createGltfFromParsedContent(parsedContent, result);
if (!batchTableJson.IsObject() || batchTableJson.HasParseError() ||
parsedContent.dracoMetadataHasErrors) {
result.errors.merge(parsedContent.errors);
return;
}
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 =
std::span<const std::byte>(parsedContent.dracoBatchTableBinary);
}
if (result.model) {
result.errors.merge(BatchTableToGltfStructuralMetadata::convertFromPnts(
featureTableJson,
batchTableJson,
batchTableBinaryData,
result.model.value()));
}
}
}
} // namespace
CesiumAsync::Future<GltfConverterResult> PntsToGltfConverter::convert(
const std::span<const std::byte>& pntsBinary,
const CesiumGltfReader::GltfReaderOptions& /*options*/,
const AssetFetcher& assetFetcher) {
GltfConverterResult result;
PntsHeader header;
uint32_t headerLength = 0;
parsePntsHeader(pntsBinary, header, headerLength, result);
if (!result.errors) {
convertPntsContentToGltf(pntsBinary, header, headerLength, result);
}
return assetFetcher.asyncSystem.createResolvedFuture(std::move(result));
}
} // namespace Cesium3DTilesContent