Files
EVEngine/test/model3d_normals.cpp
T

133 lines
4.4 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "common/Diagnostic.h"
#include "common/Exception.h"
#include "common/Status.h"
#include "data/ByteData.h"
#include "image/ImageData.h"
#include "model3d/Model3D.h"
#include "model3d/ModelData.h"
#include <cmath>
#include <memory>
#include <string>
using eve::image::ImageData;
namespace {
// One +Z quad (z=0.5) covering UV 0..1, two triangles. Center of the mesh AABB is origin.
static const char kUvQuadObj[] =
"v -0.5 -0.5 0.5\n"
"v 0.5 -0.5 0.5\n"
"v 0.5 0.5 0.5\n"
"v -0.5 0.5 0.5\n"
"vt 0 0\n"
"vt 1 0\n"
"vt 1 1\n"
"vt 0 1\n"
"f 1/1 2/2 3/3\n"
"f 1/1 3/3 4/4\n";
static const char kNoUvCubeObj[] =
"v -0.5 -0.5 -0.5\n"
"v 0.5 -0.5 -0.5\n"
"v 0.5 0.5 -0.5\n"
"v -0.5 0.5 -0.5\n"
"f 1 2 3\n"
"f 1 3 4\n";
eve::model3d::ModelData *loadObj(const char *text, bool flipUVs = true) {
auto *mod = eve::model3d::Model3D::create();
eve::data::ByteData data(text, std::char_traits<char>::length(text));
eve::model3d::ModelLoadOptions options;
options.flipUVs = flipUVs;
return mod->newModelData(&data, ".obj", options);
}
float length3(float x, float y, float z) { return std::sqrt(x * x + y * y + z * z); }
} // namespace
TEST_CASE("model3d.normals.applyRadialFromOrigin") {
std::unique_ptr<eve::model3d::ModelData> md(loadObj(kUvQuadObj));
REQUIRE(md.get() != nullptr);
REQUIRE(md->getMeshCount() >= 1);
auto applied = md->applyVertexNormalsFrom(0, "radial", 0.f, 0.f, 0.f);
CHECK(applied.ok());
CHECK_EQ(applied.code(), eve::StatusCode::Applied);
const int n = md->getVertexCount(0);
REQUIRE(n >= 4);
for (int i = 0; i < n; ++i) {
const float px = md->getVertexPosition(0, i, 0);
const float py = md->getVertexPosition(0, i, 1);
const float pz = md->getVertexPosition(0, i, 2);
const float nx = md->getVertexNormal(0, i, 0);
const float ny = md->getVertexNormal(0, i, 1);
const float nz = md->getVertexNormal(0, i, 2);
const float len = length3(px, py, pz);
REQUIRE(len > 1e-6f);
CHECK(std::fabs(nx - px / len) < 1e-5f);
CHECK(std::fabs(ny - py / len) < 1e-5f);
CHECK(std::fabs(nz - pz / len) < 1e-5f);
CHECK(std::fabs(length3(nx, ny, nz) - 1.f) < 1e-5f);
}
}
TEST_CASE("model3d.normals.setVertexNormal") {
std::unique_ptr<eve::model3d::ModelData> md(loadObj(kUvQuadObj));
REQUIRE(md.get() != nullptr);
auto set = md->setVertexNormal(0, 0, 0.f, 1.f, 0.f);
CHECK(set.ok());
CHECK_EQ(md->getVertexNormal(0, 0, 0), 0.f);
CHECK_EQ(md->getVertexNormal(0, 0, 1), 1.f);
CHECK_EQ(md->getVertexNormal(0, 0, 2), 0.f);
}
TEST_CASE("model3d.normals.applyUnknownKindFails") {
std::unique_ptr<eve::model3d::ModelData> md(loadObj(kUvQuadObj));
REQUIRE(md.get() != nullptr);
auto failed = md->applyVertexNormals(0, "smooth");
CHECK(!failed.ok());
REQUIRE(failed.status().primaryDiagnostic() != nullptr);
CHECK_EQ(failed.status().primaryDiagnostic()->code(), eve::DiagnosticCode::Unsupported);
}
TEST_CASE("model3d.normals.bakeObjectSpaceCorner") {
// This oracle uses the source OBJ UVs; default import flips V.
std::unique_ptr<eve::model3d::ModelData> md(loadObj(kUvQuadObj, false));
REQUIRE(md.get() != nullptr);
REQUIRE(md->applyVertexNormalsFrom(0, "radial", 0.f, 0.f, 0.f).ok());
auto baked = md->bakeNormalMap(0, 32, 32, 0, "object");
REQUIRE(baked.ok());
std::unique_ptr<ImageData> map = std::move(baked).takeValue();
REQUIRE(map.get() != nullptr);
CHECK_EQ(map->getWidth(), 32);
CHECK_EQ(map->getHeight(), 32);
// UV (0.9, 0.9) is near vertex (0.5, 0.5, 0.5); image Y = (1-v)*height.
const auto pixel = map->getPixel(28, 3);
const float nx = pixel.r * 2.f - 1.f;
const float ny = pixel.g * 2.f - 1.f;
const float nz = pixel.b * 2.f - 1.f;
const float len = length3(nx, ny, nz);
REQUIRE(len > 0.5f);
CHECK(nx > 0.2f);
CHECK(ny > 0.2f);
CHECK(nz > 0.2f);
}
TEST_CASE("model3d.normals.bakeRequiresUv") {
std::unique_ptr<eve::model3d::ModelData> md(loadObj(kNoUvCubeObj));
REQUIRE(md.get() != nullptr);
REQUIRE(md->applyVertexNormals(0, "radial").ok());
auto baked = md->bakeNormalMap(0, 8, 8, 0, "tangent");
CHECK(!baked.ok());
REQUIRE(baked.status().primaryDiagnostic() != nullptr);
CHECK_EQ(baked.status().primaryDiagnostic()->code(), eve::DiagnosticCode::NotFound);
}