Add unit tests for normals, oct encoded normals, and positions

This commit is contained in:
Janine Liu 2023-01-26 14:55:42 -05:00
parent fc9f633791
commit cfb7ec7258
5 changed files with 405 additions and 153 deletions

View File

@ -18,6 +18,8 @@
#endif
#include <CesiumGltf/ExtensionCesiumRTC.h>
#include <CesiumGltf/ExtensionKhrMaterialsUnlit.h>
#include <CesiumUtility/Math.h>
#include <rapidjson/document.h>
#include <spdlog/fmt/fmt.h>
@ -93,7 +95,7 @@ struct PntsContent {
PntsSemantic position;
// required by glTF spec
glm::vec3 positionMin = glm::vec3(std::numeric_limits<float>::max());
glm::vec3 positionMax = glm::vec3(std::numeric_limits<float>::min());
glm::vec3 positionMax = glm::vec3(std::numeric_limits<float>::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<const glm::vec3> positions(
reinterpret_cast<const glm::vec3*>(binaryByteOffset),
reinterpret_cast<const glm::vec3*>(
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<const std::byte>& featureTableBinaryData,
PntsContent& parsedContent) {
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 gsl::span<const glm::u8vec2> encodedNormals(
reinterpret_cast<const glm::u8vec2*>(
featureTableBinaryData.data() + normal.byteOffset),
pointsLength);
gsl::span<glm::vec3> outNormals(
reinterpret_cast<glm::vec3*>(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<const std::byte>& 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<int64_t>(parsedContent.pointsLength);
const int64_t positionsByteStride = static_cast<int64_t>(sizeof(glm ::vec3));
const int64_t positionsByteLength =
static_cast<int64_t>(positionsByteStride * count);
int32_t positionsBufferId =
createBufferInGltf(gltf, parsedContent.position.data);
int32_t positionsBufferViewId = createBufferViewInGltf(
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, parsedContent.position.data);
int32_t bufferViewId =
createBufferViewInGltf(gltf, bufferId, byteLength, byteStride);
int32_t accessorId = createAccessorInGltf(
gltf,
positionsBufferId,
positionsByteLength,
positionsByteStride);
int32_t positionAccessorId = createAccessorInGltf(
gltf,
positionsBufferViewId,
bufferViewId,
CesiumGltf::Accessor::ComponentType::FLOAT,
count,
CesiumGltf::Accessor::Type::VEC3);
CesiumGltf::Accessor& accessor =
gltf.accessors[static_cast<uint32_t>(positionAccessorId)];
gltf.accessors[static_cast<uint32_t>(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<int64_t>(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<int64_t>(sizeof(glm::u8vec4));
byteStride = static_cast<int64_t>(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<int64_t>(sizeof(glm::u8vec3));
byteStride = static_cast<int64_t>(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<int64_t>(sizeof(glm::vec3));
byteStride = static_cast<int64_t>(sizeof(glm::vec3));
componentType = CesiumGltf::Accessor::ComponentType::FLOAT;
type = CesiumGltf::Accessor::Type::VEC3;
}
const int64_t colorsByteLength =
static_cast<int64_t>(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<int64_t>(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<uint32_t>(colorsAccessorId)];
gltf.accessors[static_cast<uint32_t>(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<uint32_t>(primitive.material)];
CesiumGltf::Material& material =
gltf.materials[static_cast<uint32_t>(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<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, 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<uint32_t>(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<int32_t>(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<CesiumGltf::ExtensionKhrMaterialsUnlit>();
}
if (parsedContent.rtcCenter) {
// Add the RTC_CENTER value to the glTF as a CESIUM_RTC extension.

View File

@ -5,10 +5,12 @@
#include <CesiumAsync/AsyncSystem.h>
#include <CesiumAsync/HttpHeaders.h>
#include <CesiumGltf/ExtensionCesiumRTC.h>
#include <CesiumGltf/ExtensionKhrMaterialsUnlit.h>
#include <CesiumGltf/ExtensionMeshPrimitiveExtFeatureMetadata.h>
#include <CesiumGltf/ExtensionModelExtFeatureMetadata.h>
#include <CesiumGltf/MetadataFeatureTableView.h>
#include <CesiumGltf/MetadataPropertyView.h>
#include <CesiumUtility/Math.h>
#include <catch2/catch.hpp>
#include <rapidjson/document.h>
@ -20,6 +22,7 @@
using namespace CesiumGltf;
using namespace Cesium3DTilesSelection;
using namespace CesiumUtility;
template <typename Type>
static void checkBufferContents(
@ -32,19 +35,20 @@ static void checkBufferContents(
const glm::vec3& value =
*reinterpret_cast<const glm::vec3*>(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<glm::dvec3>(value),
static_cast<glm::dvec3>(expectedValue),
Math::Epsilon6));
}
} else if constexpr (std::is_same_v<Type, glm::vec4>) {
for (size_t i = 0; i < expected.size(); ++i) {
const glm::vec4& value =
*reinterpret_cast<const glm::vec4*>(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<glm::dvec4>(value),
static_cast<glm::dvec4>(expectedValue),
Math::Epsilon6));
}
} else if constexpr (
std::is_same_v<Type, glm::u8vec3> || std::is_same_v<Type, glm::u8vec4>) {
@ -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<uint32_t>(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<Type, glm::vec3>) {
expectedComponentType = Accessor::ComponentType::FLOAT;
expectedType = Accessor::Type::VEC3;
} else if constexpr (std::is_same_v<Type, glm::u8vec3>) {
expectedComponentType = Accessor::ComponentType::UNSIGNED_BYTE;
expectedType = Accessor::Type::VEC3;
} else if constexpr (std::is_same_v<Type, glm::u8vec4>) {
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<uint32_t>(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<uint32_t>(bufferId);
REQUIRE(bufferIdUint < gltf.buffers.size());
const Buffer& buffer = gltf.buffers[bufferId];
const Buffer& buffer = gltf.buffers[static_cast<uint32_t>(bufferIdUint)];
CHECK(buffer.byteLength == expectedByteLength);
CHECK(static_cast<int64_t>(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<double> 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<ExtensionKhrMaterialsUnlit>());
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<ExtensionKhrMaterialsUnlit>());
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<glm::vec3>(gltf, primitive, "POSITION", pointsLength);
checkAttribute<glm::u8vec4>(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<uint32_t>(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<uint32_t>(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<uint32_t>(colorBufferView.buffer);
Buffer& colorBuffer = gltf.buffers[colorBufferId];
CHECK(colorBuffer.byteLength == pointsLength * sizeof(glm::u8vec4));
CHECK(
static_cast<int64_t>(colorBuffer.cesium.data.size()) ==
colorBuffer.byteLength);
const std::vector<glm::u8vec4> 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<ExtensionKhrMaterialsUnlit>());
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<glm::vec3>(gltf, primitive, "POSITION", pointsLength);
checkAttribute<glm::u8vec3>(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<uint32_t>(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<uint32_t>(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<uint32_t>(colorBufferView.buffer);
Buffer& colorBuffer = gltf.buffers[colorBufferId];
CHECK(colorBuffer.byteLength == pointsLength * sizeof(glm::u8vec3));
CHECK(
static_cast<int64_t>(colorBuffer.cesium.data.size()) ==
colorBuffer.byteLength);
const std::vector<glm::u8vec3> 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<ExtensionKhrMaterialsUnlit>());
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<glm::vec3>(gltf, primitive, "POSITION", pointsLength);
checkAttribute<glm::vec3>(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<uint32_t>(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<uint32_t>(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<uint32_t>(colorBufferView.buffer);
Buffer& colorBuffer = gltf.buffers[colorBufferId];
CHECK(colorBuffer.byteLength == pointsLength * sizeof(glm::vec3));
CHECK(
static_cast<int64_t>(colorBuffer.cesium.data.size()) ==
colorBuffer.byteLength);
const std::vector<glm::vec3> 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<ExtensionKhrMaterialsUnlit>());
}
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<CesiumGltf::ExtensionCesiumRTC>());
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<ExtensionKhrMaterialsUnlit>());
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<glm::vec3>(gltf, primitive, "POSITION", pointsLength);
checkAttribute<glm::u8vec3>(gltf, primitive, "COLOR_0", pointsLength);
checkAttribute<glm::vec3>(gltf, primitive, "NORMAL", pointsLength);
// Check normal attribute more thoroughly
uint32_t normalAccessorId = static_cast<uint32_t>(attributes.at("NORMAL"));
Accessor& normalAccessor = gltf.accessors[normalAccessorId];
uint32_t normalBufferViewId =
static_cast<uint32_t>(normalAccessor.bufferView);
BufferView& normalBufferView = gltf.bufferViews[normalBufferViewId];
uint32_t normalBufferId = static_cast<uint32_t>(normalBufferView.buffer);
Buffer& normalBuffer = gltf.buffers[normalBufferId];
const std::vector<glm::vec3> 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<glm::vec3>(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<CesiumGltf::ExtensionCesiumRTC>());
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<ExtensionKhrMaterialsUnlit>());
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<glm::vec3>(gltf, primitive, "POSITION", pointsLength);
checkAttribute<glm::u8vec3>(gltf, primitive, "COLOR_0", pointsLength);
checkAttribute<glm::vec3>(gltf, primitive, "NORMAL", pointsLength);
// Check normal attribute more thoroughly
uint32_t normalAccessorId = static_cast<uint32_t>(attributes.at("NORMAL"));
Accessor& normalAccessor = gltf.accessors[normalAccessorId];
CHECK(!normalAccessor.normalized);
uint32_t normalBufferViewId =
static_cast<uint32_t>(normalAccessor.bufferView);
BufferView& normalBufferView = gltf.bufferViews[normalBufferViewId];
uint32_t normalBufferId = static_cast<uint32_t>(normalBufferView.buffer);
Buffer& normalBuffer = gltf.buffers[normalBufferId];
const std::vector<glm::vec3> 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<glm::vec3>(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<CesiumGltf::ExtensionCesiumRTC>());
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<ExtensionKhrMaterialsUnlit>());
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<glm::vec3>(gltf, primitive, "POSITION", pointsLength);
checkAttribute<glm::u8vec3>(gltf, primitive, "COLOR_0", pointsLength);
// Check position attribute more thoroughly
uint32_t positionAccessorId =
static_cast<uint32_t>(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<uint32_t>(positionAccessor.bufferView);
BufferView& positionBufferView = gltf.bufferViews[positionBufferViewId];
uint32_t positionBufferId = static_cast<uint32_t>(positionBufferView.buffer);
Buffer& positionBuffer = gltf.buffers[positionBufferId];
const std::vector<glm::vec3> 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<glm::vec3>(positionBuffer.cesium.data, expectedPositions);
}