320 lines
12 KiB
C++
320 lines
12 KiB
C++
#include <CesiumAsync/AsyncSystem.h>
|
|
#include <CesiumGeospatial/Cartographic.h>
|
|
#include <CesiumGeospatial/Ellipsoid.h>
|
|
#include <CesiumGeospatial/GlobeTransforms.h>
|
|
#include <CesiumGltf/AccessorView.h>
|
|
#include <CesiumGltf/BufferView.h>
|
|
#include <CesiumGltf/ExtensionKhrTextureTransform.h>
|
|
#include <CesiumGltf/Image.h>
|
|
#include <CesiumGltf/Material.h>
|
|
#include <CesiumGltf/MaterialPBRMetallicRoughness.h>
|
|
#include <CesiumGltf/Mesh.h>
|
|
#include <CesiumGltf/MeshPrimitive.h>
|
|
#include <CesiumGltf/Sampler.h>
|
|
#include <CesiumGltf/Texture.h>
|
|
#include <CesiumGltf/TextureInfo.h>
|
|
#include <CesiumGltfContent/GltfUtilities.h>
|
|
#include <CesiumGltfContent/ImageManipulation.h>
|
|
#include <CesiumGltfReader/GltfReader.h>
|
|
#include <CesiumGltfWriter/GltfWriter.h>
|
|
#include <CesiumNativeTests/SimpleAssetAccessor.h>
|
|
#include <CesiumNativeTests/SimpleAssetRequest.h>
|
|
#include <CesiumNativeTests/SimpleAssetResponse.h>
|
|
#include <CesiumNativeTests/SimpleTaskProcessor.h>
|
|
#include <CesiumNativeTests/readFile.h>
|
|
#include <CesiumNativeTests/waitForFuture.h>
|
|
#include <CesiumRasterOverlays/RasterOverlayDetails.h>
|
|
#include <CesiumRasterOverlays/RasterOverlayTile.h>
|
|
#include <CesiumRasterOverlays/RasterOverlayTileProvider.h>
|
|
#include <CesiumRasterOverlays/RasterOverlayUtilities.h>
|
|
#include <CesiumRasterOverlays/TileMapServiceRasterOverlay.h>
|
|
#include <CesiumUtility/IntrusivePointer.h>
|
|
#include <CesiumUtility/StringHelpers.h>
|
|
|
|
#include <doctest/doctest.h>
|
|
#include <glm/ext/matrix_double4x4.hpp>
|
|
#include <glm/ext/matrix_transform.hpp>
|
|
#include <glm/ext/vector_double2.hpp>
|
|
#include <glm/ext/vector_double4.hpp>
|
|
#include <glm/ext/vector_float2.hpp>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using namespace CesiumAsync;
|
|
using namespace CesiumGeometry;
|
|
using namespace CesiumGeospatial;
|
|
using namespace CesiumGltf;
|
|
using namespace CesiumGltfContent;
|
|
using namespace CesiumGltfReader;
|
|
using namespace CesiumGltfWriter;
|
|
using namespace CesiumRasterOverlays;
|
|
using namespace CesiumUtility;
|
|
using namespace CesiumNativeTests;
|
|
|
|
TEST_CASE("Add raster overlay to glTF") {
|
|
std::filesystem::path dataDir(CesiumRasterOverlays_TEST_DATA_DIR);
|
|
std::vector<std::byte> bytes = readFile(dataDir / "Shadow_Tester.glb");
|
|
GltfReader reader;
|
|
GltfReaderResult result = reader.readGltf(bytes);
|
|
REQUIRE(result.model);
|
|
|
|
Model& gltf = *result.model;
|
|
|
|
// Place the glTF in Philly and make it huge.
|
|
glm::dmat4 enuToFixed = GlobeTransforms::eastNorthUpToFixedFrame(
|
|
Ellipsoid::WGS84.cartographicToCartesian(
|
|
Cartographic::fromDegrees(-75.14777, 39.95021, 200.0)),
|
|
Ellipsoid::WGS84);
|
|
glm::dmat4 scale =
|
|
glm::scale(glm::dmat4(1.0), glm::dvec3(100000.0, 100000.0, 100000.0));
|
|
glm::dmat4 modelToEcef = enuToFixed * scale;
|
|
|
|
// Find the first unused texture coordinate set across all
|
|
// primitives.
|
|
int32_t textureCoordinateIndex = 0;
|
|
for (const Mesh& mesh : gltf.meshes) {
|
|
for (const MeshPrimitive& primitive : mesh.primitives) {
|
|
while (primitive.attributes.find(
|
|
"TEXCOORD_" + std::to_string(textureCoordinateIndex)) !=
|
|
primitive.attributes.end()) {
|
|
++textureCoordinateIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set up some mock resources for the raster overlay.
|
|
auto pMockTaskProcessor = std::make_shared<SimpleTaskProcessor>();
|
|
CesiumAsync::AsyncSystem asyncSystem{pMockTaskProcessor};
|
|
|
|
std::map<std::string, std::shared_ptr<SimpleAssetRequest>> mapUrlToRequest;
|
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(
|
|
dataDir / "Cesium_Logo_Color")) {
|
|
if (!entry.is_regular_file())
|
|
continue;
|
|
auto pResponse = std::make_unique<SimpleAssetResponse>(
|
|
uint16_t(200),
|
|
"application/binary",
|
|
CesiumAsync::HttpHeaders{},
|
|
readFile(entry.path()));
|
|
std::string url = "file:///" + StringHelpers::toStringUtf8(
|
|
entry.path().generic_u8string());
|
|
auto pRequest = std::make_unique<SimpleAssetRequest>(
|
|
"GET",
|
|
url,
|
|
CesiumAsync::HttpHeaders{},
|
|
std::move(pResponse));
|
|
mapUrlToRequest[url] = std::move(pRequest);
|
|
}
|
|
|
|
auto pMockAssetAccessor =
|
|
std::make_shared<SimpleAssetAccessor>(std::move(mapUrlToRequest));
|
|
|
|
// Create the raster overlay to drape over the glTF.
|
|
std::string tmr =
|
|
"file:///" +
|
|
StringHelpers::toStringUtf8(
|
|
std::filesystem::directory_entry(
|
|
dataDir / "Cesium_Logo_Color" / "tilemapresource.xml")
|
|
.path()
|
|
.generic_u8string());
|
|
IntrusivePointer<TileMapServiceRasterOverlay> pRasterOverlay =
|
|
new TileMapServiceRasterOverlay("test", tmr);
|
|
|
|
auto future =
|
|
pRasterOverlay
|
|
->createTileProvider(
|
|
asyncSystem,
|
|
pMockAssetAccessor,
|
|
nullptr,
|
|
nullptr,
|
|
spdlog::default_logger(),
|
|
nullptr)
|
|
.thenInMainThread([&gltf, &modelToEcef, textureCoordinateIndex](
|
|
RasterOverlay::CreateTileProviderResult&&
|
|
tileProviderResult) {
|
|
REQUIRE(tileProviderResult);
|
|
|
|
IntrusivePointer<RasterOverlayTileProvider> pTileProvider =
|
|
*tileProviderResult;
|
|
|
|
std::optional<RasterOverlayDetails> details =
|
|
RasterOverlayUtilities::createRasterOverlayTextureCoordinates(
|
|
gltf,
|
|
modelToEcef,
|
|
std::nullopt,
|
|
{pTileProvider->getProjection()},
|
|
true,
|
|
"TEXCOORD_",
|
|
textureCoordinateIndex);
|
|
REQUIRE(details);
|
|
REQUIRE(details->rasterOverlayProjections.size() == 1);
|
|
REQUIRE(details->rasterOverlayRectangles.size() == 1);
|
|
|
|
// The geometric error will usually come from the tile, but here
|
|
// we're hard-coding it.
|
|
double geometricError = 100000.0;
|
|
|
|
// Determine the maximum number of screen pixels we expect to be
|
|
// covered by this raster overlay.
|
|
glm::dvec2 targetScreenPixels =
|
|
RasterOverlayUtilities::computeDesiredScreenPixels(
|
|
geometricError,
|
|
16.0, // the Max SSE used to render the geometry
|
|
details->rasterOverlayProjections[0],
|
|
details->rasterOverlayRectangles[0],
|
|
Ellipsoid::WGS84);
|
|
|
|
// Get a raster overlay texture of the proper dimensions.
|
|
IntrusivePointer<RasterOverlayTile> pRasterTile =
|
|
pTileProvider->getTile(
|
|
details->rasterOverlayRectangles[0],
|
|
targetScreenPixels);
|
|
|
|
glm::dvec4 textureTranslationAndScale =
|
|
RasterOverlayUtilities::computeTranslationAndScale(
|
|
details->rasterOverlayRectangles[0],
|
|
pRasterTile->getRectangle());
|
|
|
|
// Go load the texture.
|
|
return pTileProvider->loadTile(*pRasterTile)
|
|
.thenPassThrough(std::move(textureTranslationAndScale));
|
|
})
|
|
.thenInMainThread([&gltf, textureCoordinateIndex](
|
|
std::tuple<glm::dvec4, TileProviderAndTile>&&
|
|
tuple) {
|
|
auto& [textureTranslationAndScale, loadResult] = tuple;
|
|
|
|
// Create the image, sampler, and texture for the raster overlay
|
|
Image& image = gltf.images.emplace_back();
|
|
image.mimeType = Image::MimeType::image_png;
|
|
|
|
Sampler& sampler = gltf.samplers.emplace_back();
|
|
sampler.magFilter = Sampler::MagFilter::LINEAR;
|
|
sampler.minFilter = Sampler::MinFilter::LINEAR_MIPMAP_LINEAR;
|
|
sampler.wrapS = Sampler::WrapS::CLAMP_TO_EDGE;
|
|
sampler.wrapT = Sampler::WrapT::CLAMP_TO_EDGE;
|
|
|
|
Texture& texture = gltf.textures.emplace_back();
|
|
texture.sampler = int32_t(gltf.samplers.size() - 1);
|
|
texture.source = int32_t(gltf.images.size() - 1);
|
|
|
|
Buffer& buffer = !gltf.buffers.empty()
|
|
? gltf.buffers.front()
|
|
: gltf.buffers.emplace_back();
|
|
size_t imageStart = buffer.cesium.data.size();
|
|
|
|
// PNG-encode the raster overlay image and store it in the main
|
|
// buffer.
|
|
ImageManipulation::savePng(
|
|
*loadResult.pTile->getImage(),
|
|
buffer.cesium.data);
|
|
|
|
BufferView& bufferView = gltf.bufferViews.emplace_back();
|
|
bufferView.buffer = 0;
|
|
bufferView.byteOffset = int64_t(imageStart);
|
|
bufferView.byteLength =
|
|
int64_t(buffer.cesium.data.size() - imageStart);
|
|
|
|
buffer.byteLength = int64_t(buffer.cesium.data.size());
|
|
|
|
image.bufferView = int32_t(gltf.bufferViews.size() - 1);
|
|
|
|
// The below will replace any existing color texture with
|
|
// the raster overlay, because glTF only allows one color
|
|
// texture. However, it doesn't clean up previous textures or
|
|
// texture coordinates, leaving the model bigger than it needs
|
|
// to be. If this were production code (not just a test/demo), we
|
|
// would want to address this.
|
|
|
|
int32_t newMaterialIndex = -1;
|
|
|
|
for (Mesh& mesh : gltf.meshes) {
|
|
for (MeshPrimitive& primitive : mesh.primitives) {
|
|
if (primitive.material < 0 ||
|
|
size_t(primitive.material) >= gltf.materials.size()) {
|
|
// This primitive didn't previous have a material so
|
|
// assign one (creating it if needed).
|
|
if (newMaterialIndex < 0) {
|
|
newMaterialIndex = int32_t(gltf.materials.size());
|
|
Material& material = gltf.materials.emplace_back();
|
|
MaterialPBRMetallicRoughness& pbr =
|
|
material.pbrMetallicRoughness.emplace();
|
|
pbr.metallicFactor = 0.0;
|
|
pbr.roughnessFactor = 1.0;
|
|
}
|
|
primitive.material = newMaterialIndex;
|
|
}
|
|
|
|
Material& material = gltf.materials[size_t(primitive.material)];
|
|
if (!material.pbrMetallicRoughness)
|
|
material.pbrMetallicRoughness.emplace();
|
|
if (!material.pbrMetallicRoughness->baseColorTexture)
|
|
material.pbrMetallicRoughness->baseColorTexture.emplace();
|
|
|
|
TextureInfo& colorTexture =
|
|
*material.pbrMetallicRoughness->baseColorTexture;
|
|
colorTexture.index = int32_t(gltf.textures.size() - 1);
|
|
colorTexture.texCoord = textureCoordinateIndex;
|
|
|
|
ExtensionKhrTextureTransform& textureTransform =
|
|
colorTexture.addExtension<ExtensionKhrTextureTransform>();
|
|
textureTransform.offset = {
|
|
textureTranslationAndScale.x,
|
|
textureTranslationAndScale.y};
|
|
textureTransform.scale = {
|
|
textureTranslationAndScale.z,
|
|
textureTranslationAndScale.w};
|
|
}
|
|
}
|
|
});
|
|
|
|
waitForFuture(asyncSystem, std::move(future));
|
|
|
|
GltfUtilities::collapseToSingleBuffer(gltf);
|
|
|
|
GltfWriterOptions options;
|
|
options.prettyPrint = true;
|
|
|
|
GltfWriter writer;
|
|
GltfWriterResult writeResult =
|
|
writer.writeGlb(gltf, gltf.buffers[0].cesium.data, options);
|
|
|
|
// Read it back and verify everything still looks good.
|
|
GltfReaderResult resultBack = reader.readGltf(writeResult.gltfBytes);
|
|
REQUIRE(resultBack.model);
|
|
|
|
const Model& gltfBack = *resultBack.model;
|
|
|
|
REQUIRE(gltfBack.images.size() == 1);
|
|
REQUIRE(gltfBack.images[0].pAsset != nullptr);
|
|
CHECK(!gltfBack.images[0].pAsset->pixelData.empty());
|
|
|
|
REQUIRE(!gltfBack.meshes.empty());
|
|
REQUIRE(!gltfBack.meshes[0].primitives.empty());
|
|
|
|
const MeshPrimitive& primitive = gltfBack.meshes[0].primitives[0];
|
|
|
|
auto texCoordIt = primitive.attributes.find("TEXCOORD_0");
|
|
REQUIRE(texCoordIt != primitive.attributes.end());
|
|
|
|
AccessorView<glm::vec2> texCoordView(gltfBack, texCoordIt->second);
|
|
CHECK(texCoordView.size() > 0);
|
|
|
|
for (int64_t i = 0; i < texCoordView.size(); ++i) {
|
|
CHECK(texCoordView[i].x >= 0.0);
|
|
CHECK(texCoordView[i].x <= 1.0);
|
|
CHECK(texCoordView[i].y >= 0.0);
|
|
CHECK(texCoordView[i].y <= 1.0);
|
|
}
|
|
}
|