326 lines
18 KiB
C++
326 lines
18 KiB
C++
#ifdef EVE_TEST_MESH_UPLOAD
|
|
#include "asset/graphics/CookedMaterial.h"
|
|
#endif
|
|
#include "asset/AssetCooker.h"
|
|
#include "asset/EvpackResourceReader.h"
|
|
#include "asset/RuntimeDefinition.h"
|
|
#include "asset/import/AssetImporter.h"
|
|
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
using namespace eve;
|
|
using namespace eve::asset_import;
|
|
namespace {
|
|
GltfImportRequest materialTriangle() {
|
|
GltfImportRequest r;
|
|
r.package = {*PersistentId::parse("11111111-2222-4333-8444-555555555555"), "material.test", "1.0.0", {}};
|
|
r.sourceName = "material.gltf";
|
|
r.externalResources["vertices.bin"].resize(36);
|
|
r.externalResources["uv.bin"].resize(24);
|
|
r.externalResources["pixel.png"] = {
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0x0d, 0x49, 0x48, 0x44, 0x52, 0, 0,
|
|
0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 0x1f, 0x15, 0xc4, 0x89, 0, 0, 0,
|
|
0x0d, 0x49, 0x44, 0x41, 0x54, 8, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0xf0, 0x1f, 0, 5, 0, 1, 0xff,
|
|
0x72, 0x9c, 0x52, 0x67, 0, 0, 0, 0, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82};
|
|
std::string json =
|
|
R"({"asset":{"version":"2.0"},"buffers":[{"uri":"vertices.bin","byteLength":36},{"uri":"uv.bin","byteLength":24}],"bufferViews":[{"buffer":0,"byteLength":36},{"buffer":1,"byteLength":24}],"accessors":[{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":3,"type":"VEC2"}],"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0}]}],"materials":[{"pbrMetallicRoughness":{"baseColorFactor":[0.2,0.3,0.4,0.5],"metallicFactor":0.7,"roughnessFactor":0.8,"baseColorTexture":{"index":0},"metallicRoughnessTexture":{"index":0}},"normalTexture":{"index":0,"scale":0.6},"occlusionTexture":{"index":0,"strength":0.4},"emissiveTexture":{"index":0},"emissiveFactor":[0.1,0.2,0.3],"alphaMode":"MASK","alphaCutoff":0.3,"doubleSided":true}],"textures":[{"source":0}],"images":[{"uri":"pixel.png"}]})";
|
|
r.documentBytes.assign(json.begin(), json.end());
|
|
return r;
|
|
}
|
|
} // namespace
|
|
TEST_CASE("asset.import.gltfMaterialAndImagesSurviveCook") {
|
|
auto result = prepareGltfImport(materialTriangle());
|
|
REQUIRE(result.ok());
|
|
REQUIRE_EQ(result.value().manifest.assets.size(), size_t(7));
|
|
REQUIRE_EQ(result.value().manifest.dependencies.size(), size_t(6));
|
|
bool found = false;
|
|
for (const auto& a : result.value().manifest.assets)
|
|
if (a.type == "eve.material") {
|
|
found = true;
|
|
for (const auto& e : result.value().entries)
|
|
if (e.path == a.definition) {
|
|
std::string text(e.bytes.begin(), e.bytes.end());
|
|
REQUIRE(text.find("metallicRoughnessTexture") != std::string::npos);
|
|
REQUIRE(text.find("normalTexture") != std::string::npos);
|
|
REQUIRE(text.find("masked") != std::string::npos);
|
|
auto decoded = Value::fromJson(text);
|
|
REQUIRE(decoded.ok());
|
|
const auto* object = decoded.value().getIf<Value::Object>();
|
|
REQUIRE(object != nullptr);
|
|
REQUIRE_EQ(object->at("metallic").asDouble(), 0.7);
|
|
REQUIRE_EQ(object->at("roughness").asDouble(), 0.8);
|
|
REQUIRE_EQ(object->at("normalScale").asDouble(), 0.6);
|
|
REQUIRE_EQ(object->at("occlusionStrength").asDouble(), 0.4);
|
|
REQUIRE_EQ(object->at("alphaCutoff").asDouble(), 0.3);
|
|
}
|
|
}
|
|
REQUIRE(found);
|
|
auto bytes = asset::buildEvaArchive(result.value().manifest, result.value().entries);
|
|
REQUIRE(bytes.ok());
|
|
auto archive = asset::parseEvaArchive(bytes.value());
|
|
REQUIRE(archive.ok());
|
|
auto profile = asset::assetCookProfileForTarget("windows-x86_64-vulkan");
|
|
REQUIRE(profile.ok());
|
|
auto pack = asset::cookEvaToEvpack(archive.value(), profile.value());
|
|
REQUIRE(pack.ok());
|
|
}
|
|
TEST_CASE("asset.import.gltfMissingTextureFailsAtomically") {
|
|
auto request = materialTriangle();
|
|
request.externalResources.erase("pixel.png");
|
|
REQUIRE(!prepareGltfImport(request).ok());
|
|
}
|
|
|
|
TEST_CASE("asset.import.gltfMaterialSupportsIdentityTransformAndRejectsBudget") {
|
|
auto request = materialTriangle();
|
|
std::string text(request.documentBytes.begin(), request.documentBytes.end());
|
|
auto at = text.find("\"baseColorTexture\":{\"index\":0}");
|
|
REQUIRE(at != std::string::npos);
|
|
text.replace(at, std::string("\"baseColorTexture\":{\"index\":0}").size(),
|
|
"\"baseColorTexture\":{\"index\":0,\"extensions\":{\"KHR_texture_transform\":{}}}");
|
|
request.documentBytes.assign(text.begin(), text.end());
|
|
REQUIRE(prepareGltfImport(request).ok());
|
|
request = materialTriangle();
|
|
request.limits.maximumAssets = 2;
|
|
REQUIRE(!prepareGltfImport(request).ok());
|
|
}
|
|
|
|
TEST_CASE("asset.import.gltfMaterialExtensionsRetainParametersAndTextures") {
|
|
auto request = materialTriangle();
|
|
auto parsed = Value::fromJson(std::string(request.documentBytes.begin(), request.documentBytes.end()));
|
|
REQUIRE(parsed.ok());
|
|
auto& root = *parsed.value().getIf<Value::Object>();
|
|
auto& mat = *root.at("materials").getIf<Value::Array>()->at(0).getIf<Value::Object>();
|
|
auto ext = Value::fromJson(
|
|
R"({"KHR_materials_specular":{"specularFactor":0.4,"specularColorFactor":[2,1,0.5],"specularTexture":{"index":0},"specularColorTexture":{"index":0}},"KHR_materials_anisotropy":{"anisotropyStrength":0.7,"anisotropyRotation":-1.5,"anisotropyTexture":{"index":0}},"KHR_materials_clearcoat":{"clearcoatFactor":0.8,"clearcoatRoughnessFactor":0.2,"clearcoatTexture":{"index":0},"clearcoatRoughnessTexture":{"index":0},"clearcoatNormalTexture":{"index":0,"scale":0.3}},"KHR_materials_emissive_strength":{"emissiveStrength":3},"KHR_materials_ior":{"ior":1.7}})");
|
|
REQUIRE(ext.ok());
|
|
mat["extensions"] = std::move(ext).takeValue();
|
|
root["extensionsRequired"] = Value(Value::Array{Value("KHR_materials_specular"), Value("KHR_materials_ior")});
|
|
auto text = parsed.value().toJson();
|
|
REQUIRE(text.ok());
|
|
request.documentBytes.assign(text.value().begin(), text.value().end());
|
|
auto imported = prepareGltfImport(request);
|
|
REQUIRE(imported.ok());
|
|
bool checked = false;
|
|
for (const auto& entry : imported.value().entries) {
|
|
if (!entry.path.ends_with("/asset.json")) continue;
|
|
auto value = Value::fromJson(std::string(entry.bytes.begin(), entry.bytes.end()));
|
|
REQUIRE(value.ok());
|
|
auto* o = value.value().getIf<Value::Object>();
|
|
if (o->at("schema").asString() != "eve.material") continue;
|
|
REQUIRE_EQ(o->at("schemaVersion").asInt(), int64_t(2));
|
|
REQUIRE_EQ(o->at("specularFactor").asDouble(), 0.4);
|
|
REQUIRE_EQ(o->at("anisotropyRotation").asDouble(), -1.5);
|
|
REQUIRE_EQ(o->at("emissiveStrength").asDouble(), 3.0);
|
|
REQUIRE_EQ(o->at("ior").asDouble(), 1.7);
|
|
REQUIRE(o->contains("clearcoatNormalTexture"));
|
|
REQUIRE_EQ(o->at("clearcoatNormalScale").asDouble(), 0.3);
|
|
checked = true;
|
|
}
|
|
REQUIRE(checked);
|
|
auto archive = asset::buildEvaArchive(imported.value().manifest, imported.value().entries);
|
|
REQUIRE(archive.ok());
|
|
auto decoded = asset::parseEvaArchive(archive.value());
|
|
REQUIRE(decoded.ok());
|
|
auto profile = asset::assetCookProfileForTarget("windows-x86_64-vulkan");
|
|
REQUIRE(profile.ok());
|
|
auto cooked = asset::cookEvaToEvpack(decoded.value(), profile.value());
|
|
REQUIRE(cooked.ok());
|
|
auto pack = asset::parseEvpack(cooked.value().bytes);
|
|
REQUIRE(pack.ok());
|
|
asset::EvpackResourceReader reader(std::make_shared<const asset::Evpack>(std::move(pack).takeValue()));
|
|
asset::EvpackCapabilities caps{"windows", "x86_64", "vulkan", {"rgba8"}, {"spirv-1.6"}, {"high"}, {}};
|
|
auto payload =
|
|
reader.read(imported.value().manifest.entrypoints.at("material:0"), "eve.material/2", caps, 1024 * 1024);
|
|
REQUIRE(payload.ok());
|
|
#ifdef EVE_TEST_MESH_UPLOAD
|
|
auto runtime = eve::asset_graphics::detail::readCookedMaterial(
|
|
reader, imported.value().manifest.entrypoints.at("material:0"), caps);
|
|
REQUIRE(runtime.ok());
|
|
REQUIRE_EQ(runtime.value().surface.specularFactor, .4f);
|
|
REQUIRE_EQ(runtime.value().surface.specularColor[0], 2.f);
|
|
REQUIRE_EQ(runtime.value().surface.anisotropyStrength, .7f);
|
|
REQUIRE_EQ(runtime.value().surface.clearcoatNormalScale, .3f);
|
|
REQUIRE(runtime.value().images[10].has_value());
|
|
REQUIRE(!runtime.value().surface.textures[6].srgbDecode);
|
|
#endif
|
|
bool retained = false;
|
|
for (const auto& chunk : payload.value().chunks) {
|
|
if (chunk.kind != asset::EvpackChunkKind::Definition) continue;
|
|
auto value = asset::decodeRuntimeDefinition(chunk.bytes);
|
|
REQUIRE(value.ok());
|
|
auto& material = *value.value().getIf<Value::Object>();
|
|
REQUIRE_EQ(material.at("specularColorFactor").getIf<Value::Array>()->at(0).asDouble(), 2.0);
|
|
REQUIRE_EQ(material.at("clearcoatNormalScale").asDouble(), 0.3);
|
|
auto imageRef = AssetRef::parse(material.at("specularColorTexture").asString());
|
|
REQUIRE(imageRef.ok());
|
|
auto image = reader.read(imageRef.value(), "eve.image/2", caps, 1024 * 1024);
|
|
REQUIRE(image.ok());
|
|
retained = true;
|
|
}
|
|
REQUIRE(retained);
|
|
}
|
|
|
|
TEST_CASE("asset.import.gltfExtensionsRejectInvalidInputs") {
|
|
for (const char* ext :
|
|
{R"({"KHR_materials_ior":{"ior":0.5}})", R"({"KHR_materials_specular":{"specularFactor":2}})",
|
|
R"({"KHR_materials_anisotropy":{"anisotropyStrength":-1}})",
|
|
R"({"KHR_materials_clearcoat":{"clearcoatTexture":{"index":99}}})",
|
|
R"({"KHR_materials_emissive_strength":{"emissiveStrength":-1}})",
|
|
R"({"KHR_materials_unlit":[],"KHR_materials_specular":{}})",
|
|
R"({"KHR_materials_unlit":{},"KHR_materials_specular":{}})", R"({"EXT_unknown":{}})"}) {
|
|
auto request = materialTriangle();
|
|
request.mode = GltfImportMode::Strict;
|
|
auto document = Value::fromJson(std::string(request.documentBytes.begin(), request.documentBytes.end()));
|
|
REQUIRE(document.ok());
|
|
auto extension = Value::fromJson(ext);
|
|
REQUIRE(extension.ok());
|
|
auto& mat = *document.value()
|
|
.getIf<Value::Object>()
|
|
->at("materials")
|
|
.getIf<Value::Array>()
|
|
->at(0)
|
|
.getIf<Value::Object>();
|
|
mat["extensions"] = std::move(extension).takeValue();
|
|
auto text = document.value().toJson();
|
|
REQUIRE(text.ok());
|
|
request.documentBytes.assign(text.value().begin(), text.value().end());
|
|
REQUIRE(!prepareGltfImport(request).ok());
|
|
}
|
|
}
|
|
TEST_CASE("asset.import.gltfUnlitAndTextureTransformSurvive") {
|
|
auto request = materialTriangle();
|
|
auto document = Value::fromJson(std::string(request.documentBytes.begin(), request.documentBytes.end()));
|
|
REQUIRE(document.ok());
|
|
auto& mat =
|
|
*document.value().getIf<Value::Object>()->at("materials").getIf<Value::Array>()->at(0).getIf<Value::Object>();
|
|
mat["extensions"] = Value(Value::Object{{"KHR_materials_unlit", Value(Value::Object{})}});
|
|
auto textureDefinition = Value::fromJson(
|
|
R"({"index":0,"texCoord":1,"extensions":{"KHR_texture_transform":{"offset":[-2,3],"scale":[2,-4],"rotation":-1.2,"texCoord":0}}})");
|
|
REQUIRE(textureDefinition.ok());
|
|
mat.at("pbrMetallicRoughness").getIf<Value::Object>()->at("baseColorTexture") =
|
|
std::move(textureDefinition).takeValue();
|
|
auto text = document.value().toJson();
|
|
REQUIRE(text.ok());
|
|
request.documentBytes.assign(text.value().begin(), text.value().end());
|
|
auto imported = prepareGltfImport(request);
|
|
REQUIRE(imported.ok());
|
|
bool found = false;
|
|
for (const auto& a : imported.value().manifest.assets)
|
|
if (a.type == "eve.material") {
|
|
for (const auto& entry : imported.value().entries)
|
|
if (entry.path == a.definition) {
|
|
auto value = Value::fromJson(std::string(entry.bytes.begin(), entry.bytes.end()));
|
|
REQUIRE(value.ok());
|
|
auto& o = *value.value().getIf<Value::Object>();
|
|
REQUIRE_EQ(o.at("shadingModel").asString(), std::string("unlit"));
|
|
auto& transform = *o.at("baseColorTextureTransform").getIf<Value::Object>();
|
|
REQUIRE_EQ(transform.at("rotation").asDouble(), -1.2);
|
|
REQUIRE_EQ(transform.at("offset").getIf<Value::Array>()->at(0).asDouble(), -2.0);
|
|
REQUIRE_EQ(transform.at("scale").getIf<Value::Array>()->at(1).asDouble(), -4.0);
|
|
found = true;
|
|
}
|
|
}
|
|
REQUIRE(found);
|
|
}
|
|
|
|
TEST_CASE("asset.import.gltfTextureTransformRejectsUnavailableUvSet") {
|
|
auto request = materialTriangle();
|
|
auto document = Value::fromJson(std::string(request.documentBytes.begin(), request.documentBytes.end()));
|
|
REQUIRE(document.ok());
|
|
auto& root = *document.value().getIf<Value::Object>();
|
|
auto& mat = *root.at("materials").getIf<Value::Array>()->at(0).getIf<Value::Object>();
|
|
auto texture = Value::fromJson(R"({"index":0,"extensions":{"KHR_texture_transform":{"texCoord":1}}})");
|
|
REQUIRE(texture.ok());
|
|
mat.at("pbrMetallicRoughness").getIf<Value::Object>()->at("baseColorTexture") = std::move(texture).takeValue();
|
|
auto json = document.value().toJson();
|
|
REQUIRE(json.ok());
|
|
request.documentBytes.assign(json.value().begin(), json.value().end());
|
|
auto rejected = prepareGltfImport(request);
|
|
REQUIRE(!rejected.ok());
|
|
REQUIRE(rejected.error()->message().find("UV set") != std::string::npos);
|
|
}
|
|
TEST_CASE("asset.import.gltfMaterialSelectsNonzeroUvWithoutTransform") {
|
|
auto request = materialTriangle();
|
|
auto document = Value::fromJson(std::string(request.documentBytes.begin(), request.documentBytes.end()));
|
|
REQUIRE(document.ok());
|
|
auto& root = *document.value().getIf<Value::Object>();
|
|
auto& attrs = *root.at("meshes")
|
|
.getIf<Value::Array>()
|
|
->at(0)
|
|
.getIf<Value::Object>()
|
|
->at("primitives")
|
|
.getIf<Value::Array>()
|
|
->at(0)
|
|
.getIf<Value::Object>()
|
|
->at("attributes")
|
|
.getIf<Value::Object>();
|
|
attrs["TEXCOORD_2"] = Value(int64_t(1));
|
|
auto& material = *root.at("materials").getIf<Value::Array>()->at(0).getIf<Value::Object>();
|
|
material.at("pbrMetallicRoughness")
|
|
.getIf<Value::Object>()
|
|
->at("baseColorTexture")
|
|
.getIf<Value::Object>()
|
|
->emplace("texCoord", Value(int64_t(2)));
|
|
auto json = document.value().toJson();
|
|
REQUIRE(json.ok());
|
|
request.documentBytes.assign(json.value().begin(), json.value().end());
|
|
auto imported = prepareGltfImport(request);
|
|
REQUIRE(imported.ok());
|
|
bool found = false;
|
|
for (const auto& a : imported.value().manifest.assets)
|
|
if (a.type == "eve.material")
|
|
for (const auto& e : imported.value().entries)
|
|
if (e.path == a.definition) {
|
|
auto d = Value::fromJson(std::string(e.bytes.begin(), e.bytes.end()));
|
|
REQUIRE(d.ok());
|
|
REQUIRE_EQ(d.value().getIf<Value::Object>()->at("baseColorTextureTexCoord").asInt(), int64_t(2));
|
|
found = true;
|
|
}
|
|
REQUIRE(found);
|
|
}
|
|
|
|
TEST_CASE("asset.import.gltfPermissiveClampsSpecularAndWarns") {
|
|
auto request = materialTriangle();
|
|
auto doc = Value::fromJson(std::string(request.documentBytes.begin(), request.documentBytes.end()));
|
|
REQUIRE(doc.ok());
|
|
auto& mat =
|
|
*doc.value().getIf<Value::Object>()->at("materials").getIf<Value::Array>()->at(0).getIf<Value::Object>();
|
|
mat["extensions"] =
|
|
Value(Value::Object{{"KHR_materials_specular", Value(Value::Object{{"specularFactor", Value(38406.7422)}})}});
|
|
auto json = doc.value().toJson();
|
|
REQUIRE(json.ok());
|
|
request.documentBytes.assign(json.value().begin(), json.value().end());
|
|
auto imported = prepareGltfImport(request);
|
|
REQUIRE(imported.ok());
|
|
bool warned = false, clamped = false;
|
|
for (const auto& finding : imported.value().findings)
|
|
if (finding.severity == ImportSeverity::Warning) {
|
|
REQUIRE(finding.feature.find("specularFactor") != std::string::npos);
|
|
warned = true;
|
|
}
|
|
for (const auto& a : imported.value().manifest.assets)
|
|
if (a.type == "eve.material")
|
|
for (const auto& e : imported.value().entries)
|
|
if (e.path == a.definition) {
|
|
auto d = Value::fromJson(std::string(e.bytes.begin(), e.bytes.end()));
|
|
REQUIRE(d.ok());
|
|
REQUIRE_EQ(d.value().getIf<Value::Object>()->at("specularFactor").asDouble(), 1.0);
|
|
clamped = true;
|
|
}
|
|
REQUIRE(warned);
|
|
REQUIRE(clamped);
|
|
auto archive = asset::buildEvaArchive(imported.value().manifest, imported.value().entries);
|
|
REQUIRE(archive.ok());
|
|
auto decoded = asset::parseEvaArchive(archive.value());
|
|
REQUIRE(decoded.ok());
|
|
bool persisted = false;
|
|
for (const auto& entry : imported.value().entries)
|
|
if (entry.path.find("report") != std::string::npos) {
|
|
std::string report(entry.bytes.begin(), entry.bytes.end());
|
|
if (report.find("warning") != std::string::npos && report.find("specularFactor") != std::string::npos)
|
|
persisted = true;
|
|
}
|
|
REQUIRE(persisted);
|
|
request.mode = GltfImportMode::Strict;
|
|
REQUIRE(!prepareGltfImport(request).ok());
|
|
}
|