Adds EVENGINE_ANDROID_TEST_APP build mode: the unit_test sources are compiled into libmain.so (SDL entry) and packaged by the existing Gradle APK along with the test assets, so the full zeroerr suite can run on a device via adb logcat. - test/main.cpp: Android runner with LogcatBuf (cout/cerr -> logcat), chdir to <files>/evengine_test, SDL_Init; keeps SDL_main symbol unmangled. - test/PathBesideSource.h: shared pathBesideThisSource helper that maps __FILE__ to the on-device unpacked tree on Android. - EVTestActivity: unpacks assets/test + assets/examples to internal storage and passes an optional --testcase filter from an intent extra. - test/CMakeLists.txt: builds `main` instead of unit_test on Android, excludes desktop-only devtools tests, links Android system libs. - Makefile: build/android-debug-test, sync/android-test-assets, run/android-test-debug, log/android-test targets. - Gradle wrapper bumped to 8.5 for Java 21 compatibility.
687 lines
24 KiB
C++
687 lines
24 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "model3d/Model3D.h"
|
|
#include "model3d/ModelData.h"
|
|
#include "data/ByteData.h"
|
|
#include "filesystem/Filesystem.h"
|
|
#include "common/Exception.h"
|
|
|
|
#include "graphics/Graphics.h"
|
|
#include "graphics/Mesh.h"
|
|
#include "graphics/RenderSystem.h"
|
|
#include "graphics/RenderSystem3D.h"
|
|
#include "image/Image.h"
|
|
#include "image/ImageData.h"
|
|
#include "window/Window.h"
|
|
|
|
#include <SDL2/SDL.h>
|
|
#include <assimp/mesh.h>
|
|
#include <assimp/scene.h>
|
|
#include <assimp/material.h>
|
|
#include <assimp/matrix4x4.h>
|
|
#include <assimp/texture.h>
|
|
#include <assimp/vector3.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include "graphics/shaders/mesh3d_toon_frag_spv.inc"
|
|
#include "graphics/shaders/mesh3d_toon_vert_spv.inc"
|
|
|
|
using namespace eve::graphics;
|
|
|
|
namespace {
|
|
|
|
#include "PathBesideSource.h"
|
|
EVE_DEFINE_PATH_BESIDE_SOURCE()
|
|
|
|
std::vector<char> readBinaryFile(const std::string &path) {
|
|
std::ifstream in(path, std::ios::binary);
|
|
if (!in)
|
|
return {};
|
|
return std::vector<char>((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
float luma(const Color &c) { return (c.r + c.g + c.b) / 3.f; }
|
|
|
|
void openGfxWindow(eve::window::Window *&win, Graphics *&gfx, int w = 640, int h = 480) {
|
|
win = eve::window::Window::create();
|
|
gfx = Graphics::create();
|
|
REQUIRE(win != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
win->setGraphics(gfx);
|
|
eve::window::WindowSettings s;
|
|
s.width = w;
|
|
s.height = h;
|
|
s.centered = true;
|
|
REQUIRE(win->setWindowSettings(s));
|
|
}
|
|
|
|
void resetScene3D() {
|
|
if (ecs::current()->getManager<Renderable3D>() != nullptr) {
|
|
auto view = ecs::View<Renderable3D, Renderable3D::MeshRenderer>();
|
|
for (auto it = view.begin(); it != view.end(); ++it) {
|
|
auto [mr] = *it;
|
|
mr->visible = false;
|
|
}
|
|
}
|
|
if (ecs::current()->getManager<Camera3D>() != nullptr) {
|
|
auto camView = ecs::View<Camera3D, Camera3D::Data>();
|
|
for (auto it = camView.begin(); it != camView.end(); ++it) {
|
|
auto [data] = *it;
|
|
data->active = false;
|
|
}
|
|
}
|
|
if (ecs::current()->getManager<Renderable2D>() != nullptr) {
|
|
auto view = ecs::View<Renderable2D, Renderable2D::Sprite>();
|
|
for (auto it = view.begin(); it != view.end(); ++it) {
|
|
auto [sp] = *it;
|
|
sp->visible = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Texture *makeSolidGray(Graphics *gfx, uint8_t g) {
|
|
uint8_t px[4] = {g, g, g, 255};
|
|
return gfx->newTexture(1, 1, px);
|
|
}
|
|
|
|
Texture *makeSolidRGB(Graphics *gfx, uint8_t r, uint8_t g, uint8_t b) {
|
|
uint8_t px[4] = {r, g, b, 255};
|
|
return gfx->newTexture(1, 1, px);
|
|
}
|
|
|
|
Texture *textureFromImageData(Graphics *gfx, eve::image::ImageData *img) {
|
|
if (!img)
|
|
return nullptr;
|
|
return gfx->newTexture(img);
|
|
}
|
|
|
|
Texture *loadAssimpDiffuseTexture(Graphics *gfx, const aiScene *scene, const aiMaterial *mat) {
|
|
if (!scene || !mat)
|
|
return nullptr;
|
|
|
|
aiString path;
|
|
if (mat->GetTexture(aiTextureType_DIFFUSE, 0, &path) != AI_SUCCESS)
|
|
return nullptr;
|
|
|
|
const char *p = path.C_Str();
|
|
if (!p || !p[0])
|
|
return nullptr;
|
|
|
|
eve::image::Image::create();
|
|
|
|
// Embedded: "*0", "*1", ...
|
|
if (p[0] == '*') {
|
|
int idx = std::atoi(p + 1);
|
|
if (idx < 0 || static_cast<unsigned>(idx) >= scene->mNumTextures)
|
|
return nullptr;
|
|
const aiTexture *tex = scene->mTextures[idx];
|
|
if (!tex || !tex->pcData)
|
|
return nullptr;
|
|
|
|
if (tex->mHeight == 0) {
|
|
// Compressed blob (png/jpg/...).
|
|
eve::data::ByteData bytes(tex->pcData, static_cast<size_t>(tex->mWidth));
|
|
try {
|
|
eve::image::ImageData *img = eve::image::Image::create()->newImageData(&bytes);
|
|
Texture *t = textureFromImageData(gfx, img);
|
|
delete img;
|
|
return t;
|
|
} catch (...) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
// Uncompressed BGRA8888.
|
|
const unsigned w = tex->mWidth;
|
|
const unsigned h = tex->mHeight;
|
|
std::vector<uint8_t> rgba(size_t(w) * size_t(h) * 4);
|
|
const aiTexel *src = tex->pcData;
|
|
for (unsigned i = 0; i < w * h; ++i) {
|
|
rgba[i * 4 + 0] = src[i].r;
|
|
rgba[i * 4 + 1] = src[i].g;
|
|
rgba[i * 4 + 2] = src[i].b;
|
|
rgba[i * 4 + 3] = src[i].a;
|
|
}
|
|
return gfx->newTexture(int(w), int(h), rgba.data());
|
|
}
|
|
|
|
// External file — try VFS then OS path beside test assets.
|
|
try {
|
|
auto *fs = eve::filesystem::Filesystem::create();
|
|
std::unique_ptr<eve::filesystem::FileData> fd(fs->read(p));
|
|
if (fd && fd->getSize() > 0) {
|
|
eve::image::ImageData *img = eve::image::Image::create()->newImageData(fd.get());
|
|
Texture *t = textureFromImageData(gfx, img);
|
|
delete img;
|
|
return t;
|
|
}
|
|
} catch (...) {
|
|
}
|
|
|
|
// Basename / common relative layouts under test/
|
|
std::string base = p;
|
|
auto slash = base.find_last_of("/\\");
|
|
if (slash != std::string::npos)
|
|
base = base.substr(slash + 1);
|
|
|
|
const char *candidates[] = {
|
|
base.c_str(),
|
|
nullptr, // filled below for Textures/
|
|
};
|
|
std::string underTextures = std::string("Textures/") + base;
|
|
candidates[1] = underTextures.c_str();
|
|
|
|
for (const char *rel : candidates) {
|
|
if (!rel || !rel[0])
|
|
continue;
|
|
auto fileBytes = readBinaryFile(pathBesideThisSource(rel));
|
|
if (fileBytes.empty())
|
|
continue;
|
|
try {
|
|
eve::data::ByteData bytes(fileBytes.data(), fileBytes.size());
|
|
eve::image::ImageData *img = eve::image::Image::create()->newImageData(&bytes);
|
|
Texture *t = textureFromImageData(gfx, img);
|
|
delete img;
|
|
return t;
|
|
} catch (...) {
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Texture *loadTextureFile(Graphics *gfx, const char *relPathBesideTest) {
|
|
if (!relPathBesideTest || !relPathBesideTest[0])
|
|
return nullptr;
|
|
auto fileBytes = readBinaryFile(pathBesideThisSource(relPathBesideTest));
|
|
if (fileBytes.empty())
|
|
return nullptr;
|
|
eve::image::Image::create();
|
|
try {
|
|
eve::data::ByteData bytes(fileBytes.data(), fileBytes.size());
|
|
eve::image::ImageData *img = eve::image::Image::create()->newImageData(&bytes);
|
|
Texture *t = textureFromImageData(gfx, img);
|
|
delete img;
|
|
return t;
|
|
} catch (...) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
struct Bounds {
|
|
float minX = 0, minY = 0, minZ = 0;
|
|
float maxX = 0, maxY = 0, maxZ = 0;
|
|
bool valid = false;
|
|
|
|
void expand(float x, float y, float z) {
|
|
if (!valid) {
|
|
minX = maxX = x;
|
|
minY = maxY = y;
|
|
minZ = maxZ = z;
|
|
valid = true;
|
|
return;
|
|
}
|
|
minX = std::min(minX, x);
|
|
minY = std::min(minY, y);
|
|
minZ = std::min(minZ, z);
|
|
maxX = std::max(maxX, x);
|
|
maxY = std::max(maxY, y);
|
|
maxZ = std::max(maxZ, z);
|
|
}
|
|
|
|
float centerX() const { return 0.5f * (minX + maxX); }
|
|
float centerY() const { return 0.5f * (minY + maxY); }
|
|
float centerZ() const { return 0.5f * (minZ + maxZ); }
|
|
float radius() const {
|
|
const float dx = maxX - minX;
|
|
const float dy = maxY - minY;
|
|
const float dz = maxZ - minZ;
|
|
return 0.5f * std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
}
|
|
};
|
|
|
|
Bounds boundsOf(eve::model3d::ModelData *md) {
|
|
Bounds b;
|
|
const aiScene *scene = md->getScene();
|
|
if (!scene || !scene->mRootNode) return b;
|
|
std::function<void(const aiNode *, const aiMatrix4x4 &)> walk =
|
|
[&](const aiNode *node, const aiMatrix4x4 &parent) {
|
|
const aiMatrix4x4 world = parent * node->mTransformation;
|
|
for (unsigned i = 0; i < node->mNumMeshes; ++i) {
|
|
const unsigned mi = node->mMeshes[i];
|
|
if (mi >= scene->mNumMeshes) continue;
|
|
const aiMesh *mesh = scene->mMeshes[mi];
|
|
if (!mesh) continue;
|
|
for (unsigned v = 0; v < mesh->mNumVertices; ++v) {
|
|
const aiVector3D p = world * mesh->mVertices[v];
|
|
b.expand(p.x, p.y, p.z);
|
|
}
|
|
}
|
|
for (unsigned c = 0; c < node->mNumChildren; ++c)
|
|
walk(node->mChildren[c], world);
|
|
};
|
|
walk(scene->mRootNode, aiMatrix4x4());
|
|
return b;
|
|
}
|
|
|
|
/** Upload every mesh with Assimp material tint/texture; frame camera; save PNG.
|
|
* @param fallbackDiffuse optional path relative to test/ when Assimp has no map.
|
|
* @param makeStyle optional factory: given Graphics*, return Mesh3D Shader for all entities. */
|
|
void renderModelSmoke(eve::model3d::ModelData *md, const char *pngName,
|
|
const char *fallbackDiffuse = nullptr,
|
|
std::function<Shader *(Graphics *)> makeStyle = {}) {
|
|
REQUIRE(md != nullptr);
|
|
REQUIRE(md->getMeshCount() >= 1);
|
|
REQUIRE(pngName != nullptr);
|
|
const aiScene *scene = md->getScene();
|
|
REQUIRE(scene != nullptr);
|
|
|
|
eve::window::Window *win = nullptr;
|
|
Graphics *gfx = nullptr;
|
|
openGfxWindow(win, gfx);
|
|
resetScene3D();
|
|
|
|
Shader *styleShader = makeStyle ? makeStyle(gfx) : nullptr;
|
|
|
|
Bounds b = boundsOf(md);
|
|
REQUIRE(b.valid);
|
|
const float cx = b.centerX();
|
|
const float cy = b.centerY();
|
|
const float cz = b.centerZ();
|
|
const float rad = std::max(0.5f, b.radius());
|
|
|
|
Texture *white = makeSolidGray(gfx, 255);
|
|
Texture *fallbackTex = loadTextureFile(gfx, fallbackDiffuse);
|
|
struct MatLook {
|
|
Texture *tex = nullptr;
|
|
float tr = 1.f, tg = 1.f, tb = 1.f;
|
|
};
|
|
std::unordered_map<unsigned, MatLook> matCache;
|
|
|
|
int spawned = 0;
|
|
std::function<void(const aiNode *, const aiMatrix4x4 &)> walk =
|
|
[&](const aiNode *node, const aiMatrix4x4 &parent) {
|
|
const aiMatrix4x4 world = parent * node->mTransformation;
|
|
for (unsigned i = 0; i < node->mNumMeshes; ++i) {
|
|
const unsigned mi = node->mMeshes[i];
|
|
if (mi >= scene->mNumMeshes) continue;
|
|
const aiMesh *ai = scene->mMeshes[mi];
|
|
if (!ai || ai->mNumFaces == 0) continue;
|
|
Mesh *mesh = gfx->newMeshFromAssimp(*ai, world);
|
|
REQUIRE(mesh != nullptr);
|
|
REQUIRE_GT(mesh->indexCount, 0);
|
|
|
|
MatLook look{white, 1.f, 1.f, 1.f};
|
|
const unsigned matIndex = ai->mMaterialIndex;
|
|
auto cached = matCache.find(matIndex);
|
|
if (cached != matCache.end()) {
|
|
look = cached->second;
|
|
} else if (scene->mMaterials && matIndex < scene->mNumMaterials) {
|
|
const aiMaterial *mat = scene->mMaterials[matIndex];
|
|
aiColor3D kd(1.f, 1.f, 1.f);
|
|
mat->Get(AI_MATKEY_COLOR_DIFFUSE, kd);
|
|
|
|
Texture *fromMat = loadAssimpDiffuseTexture(gfx, scene, mat);
|
|
if (fromMat) {
|
|
look.tex = fromMat;
|
|
look.tr = kd.r;
|
|
look.tg = kd.g;
|
|
look.tb = kd.b;
|
|
} else if (fallbackTex) {
|
|
look.tex = fallbackTex;
|
|
look.tr = look.tg = look.tb = 1.f;
|
|
} else {
|
|
look.tex = makeSolidRGB(gfx, uint8_t(std::clamp(kd.r, 0.f, 1.f) * 255.f + 0.5f),
|
|
uint8_t(std::clamp(kd.g, 0.f, 1.f) * 255.f + 0.5f),
|
|
uint8_t(std::clamp(kd.b, 0.f, 1.f) * 255.f + 0.5f));
|
|
look.tr = look.tg = look.tb = 1.f;
|
|
}
|
|
matCache[matIndex] = look;
|
|
} else if (fallbackTex) {
|
|
look.tex = fallbackTex;
|
|
matCache[matIndex] = look;
|
|
}
|
|
|
|
auto *ent = Renderable3D::create();
|
|
ent->meshRenderer()->mesh = mesh;
|
|
ent->meshRenderer()->texture = look.tex;
|
|
ent->meshRenderer()->shader = styleShader;
|
|
ent->meshRenderer()->visible = true;
|
|
ent->setTint(look.tr, look.tg, look.tb, 1.f);
|
|
++spawned;
|
|
}
|
|
for (unsigned c = 0; c < node->mNumChildren; ++c)
|
|
walk(node->mChildren[c], world);
|
|
};
|
|
REQUIRE(scene->mRootNode != nullptr);
|
|
walk(scene->mRootNode, aiMatrix4x4());
|
|
REQUIRE(spawned >= 1);
|
|
|
|
auto *cam = Camera3D::createCamera();
|
|
// 3/4 view: above + along +Z. Fit clip planes to asset scale (Rock1 is ~1e3 units).
|
|
cam->setTarget(cx, cy, cz);
|
|
cam->setEye(cx + rad * 0.9f, cy + rad * 0.55f, cz + rad * 1.8f);
|
|
cam->data()->nearZ = std::max(0.05f, rad * 0.01f);
|
|
cam->data()->farZ = std::max(100.f, rad * 20.f);
|
|
|
|
auto *hud = Renderable2D::create();
|
|
hud->transform()->x = 0;
|
|
hud->transform()->y = 0;
|
|
hud->sprite()->width = 2;
|
|
hud->sprite()->height = 2;
|
|
hud->sprite()->r = 0.f;
|
|
hud->sprite()->g = 0.f;
|
|
hud->sprite()->b = 0.f;
|
|
|
|
gfx->setScreenReadbackEnabled(true);
|
|
gfx->setBackgroundColor(Color(0.12f, 0.14f, 0.18f, 1.f));
|
|
|
|
// Light rays travel from camera toward the subject (matches RenderSystem3D lighting test).
|
|
const float lx = cx - (cx + rad * 0.9f);
|
|
const float ly = cy - (cy + rad * 0.55f);
|
|
const float lz = cz - (cz + rad * 1.8f);
|
|
RenderSystem3D::setDirectionalLight(lx, ly + rad * 1.2f, lz, 1.8f, 1.8f, 1.8f);
|
|
|
|
// ~1s live rotation so the asset is visibly on screen before PNG capture.
|
|
for (int i = 0; i < 60; ++i) {
|
|
if (ecs::current()->getManager<Renderable3D>() != nullptr) {
|
|
auto view = ecs::View<Renderable3D, Renderable3D::Transform3D, Renderable3D::MeshRenderer>();
|
|
for (auto it = view.begin(); it != view.end(); ++it) {
|
|
auto [xf, mr] = *it;
|
|
if (!mr->visible)
|
|
continue;
|
|
xf->yaw = float(i) * 0.05f;
|
|
}
|
|
}
|
|
RenderSystem3D::render(*gfx);
|
|
RenderSystem::render(*gfx);
|
|
SDL_Event e;
|
|
while (SDL_PollEvent(&e)) {
|
|
if (e.type == SDL_QUIT) break;
|
|
}
|
|
SDL_Delay(16);
|
|
}
|
|
|
|
Color mid = gfx->getPixel(gfx->getWidth() / 2, gfx->getHeight() / 2);
|
|
CHECK(luma(mid) > 0.05f);
|
|
|
|
eve::image::Image::create();
|
|
eve::image::ImageData *frame = gfx->newImageData();
|
|
REQUIRE(frame != nullptr);
|
|
eve::filesystem::FileData *png =
|
|
frame->encode(medialoader::FormatHandler::ENCODED_PNG, pngName, false);
|
|
REQUIRE(png != nullptr);
|
|
REQUIRE(png->getSize() > 0);
|
|
|
|
// Under the CMake test binary dir (build/.../test/out), already gitignored via build*/.
|
|
const std::string outDir = std::string(EVENGINE_TEST_BINARY_DIR) + "/out";
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(outDir, ec);
|
|
const std::string outPath = outDir + "/" + pngName;
|
|
{
|
|
std::ofstream out(outPath, std::ios::binary);
|
|
REQUIRE(out.good());
|
|
out.write(static_cast<const char *>(png->getData()),
|
|
static_cast<std::streamsize>(png->getSize()));
|
|
REQUIRE(out.good());
|
|
}
|
|
std::printf("model3d render saved: %s (materials=%u cached=%zu style=%s)\n", outPath.c_str(),
|
|
scene->mNumMaterials, matCache.size(), styleShader ? "yes" : "no");
|
|
delete png;
|
|
delete frame;
|
|
|
|
win->close();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Minimal unit cube as Wavefront OBJ (same shape as medialoader model tests).
|
|
static const char kCubeObj[] =
|
|
"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"
|
|
"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"
|
|
"f 5 6 7\n"
|
|
"f 5 7 8\n"
|
|
"f 1 2 6\n"
|
|
"f 1 6 5\n"
|
|
"f 2 3 7\n"
|
|
"f 2 7 6\n"
|
|
"f 3 4 8\n"
|
|
"f 3 8 7\n"
|
|
"f 4 1 5\n"
|
|
"f 4 5 8\n";
|
|
|
|
TEST_CASE("model3d.newModelData.objFromMemory") {
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
eve::data::ByteData data(kCubeObj, sizeof(kCubeObj) - 1);
|
|
auto *md = mod->newModelData(&data, ".obj");
|
|
REQUIRE(md != nullptr);
|
|
CHECK(!md->empty());
|
|
CHECK(md->getMeshCount() >= 1);
|
|
CHECK(md->getVertexCount(0) > 0);
|
|
CHECK(md->getFaceCount(0) > 0);
|
|
CHECK(md->getMesh(0) != nullptr);
|
|
CHECK(md->getScene() != nullptr);
|
|
CHECK(md->getMaterialCount() >= 0);
|
|
// Assimp's OBJ importer always computes per-face normals even without
|
|
// `vn` lines, but never synthesizes UVs without `vt` lines.
|
|
CHECK(md->hasNormals(0));
|
|
CHECK(!md->hasTexCoords(0));
|
|
delete md;
|
|
}
|
|
|
|
TEST_CASE("model3d.newModelData.emptyThrows") {
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
bool threw = false;
|
|
try {
|
|
mod->newModelData(nullptr, ".obj");
|
|
} catch (const eve::Exception &) {
|
|
threw = true;
|
|
}
|
|
CHECK(threw);
|
|
}
|
|
|
|
TEST_CASE("model3d.newModelData.invalidMeshIndexThrows") {
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
eve::data::ByteData data(kCubeObj, sizeof(kCubeObj) - 1);
|
|
auto *md = mod->newModelData(&data, ".obj");
|
|
REQUIRE(md != nullptr);
|
|
|
|
auto expectThrows = [](const std::function<void()> &fn) {
|
|
try {
|
|
fn();
|
|
} catch (const eve::Exception &) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
CHECK(expectThrows([&] { md->getVertexCount(999); }));
|
|
CHECK(expectThrows([&] { md->getFaceCount(999); }));
|
|
CHECK(expectThrows([&] { md->hasNormals(999); }));
|
|
CHECK(expectThrows([&] { md->hasTexCoords(999); }));
|
|
CHECK(md->getMesh(999) == nullptr);
|
|
delete md;
|
|
}
|
|
|
|
TEST_CASE("model3d.newModelDataFromFile.missingThrows") {
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
bool threw = false;
|
|
try {
|
|
mod->newModelDataFromFile("definitely_missing_eve_model3d.obj");
|
|
} catch (const eve::Exception &) {
|
|
threw = true;
|
|
}
|
|
CHECK(threw);
|
|
}
|
|
|
|
TEST_CASE("model3d.Model3D.getName") {
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
CHECK(mod->getName() == "Model3D");
|
|
}
|
|
|
|
TEST_CASE("model3d.newModelData.rock1Fbx") {
|
|
const std::string path = pathBesideThisSource("Rock1.fbx");
|
|
auto bytes = readBinaryFile(path);
|
|
REQUIRE(!bytes.empty());
|
|
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
eve::data::ByteData data(bytes.data(), bytes.size());
|
|
auto *md = mod->newModelData(&data, ".fbx");
|
|
REQUIRE(md != nullptr);
|
|
CHECK(!md->empty());
|
|
CHECK(md->getMeshCount() >= 1);
|
|
CHECK(md->getScene() != nullptr);
|
|
CHECK(md->hasTexCoords(0));
|
|
|
|
int totalVerts = 0;
|
|
int totalFaces = 0;
|
|
for (int i = 0; i < md->getMeshCount(); ++i) {
|
|
CHECK(md->getMesh(i) != nullptr);
|
|
int verts = md->getVertexCount(i);
|
|
int faces = md->getFaceCount(i);
|
|
CHECK(verts > 0);
|
|
CHECK(faces > 0);
|
|
totalVerts += verts;
|
|
totalFaces += faces;
|
|
}
|
|
CHECK(totalVerts > 0);
|
|
CHECK(totalFaces > 0);
|
|
delete md;
|
|
}
|
|
|
|
TEST_CASE("model3d.newModelDataFromFile.sofaObjMtl") {
|
|
const std::string objPath = pathBesideThisSource("sofa.obj");
|
|
const std::string mtlPath = pathBesideThisSource("sofa.mtl");
|
|
REQUIRE(!readBinaryFile(objPath).empty());
|
|
REQUIRE(!readBinaryFile(mtlPath).empty());
|
|
|
|
std::string testDir = pathBesideThisSource("");
|
|
if (!testDir.empty() && (testDir.back() == '/' || testDir.back() == '\\'))
|
|
testDir.pop_back();
|
|
|
|
auto *fs = eve::filesystem::Filesystem::create();
|
|
REQUIRE(fs->setIdentity("ev_ut_model3d_sofa", true));
|
|
REQUIRE(fs->setupWriteDirectory());
|
|
fs->allowMountingForPath(testDir);
|
|
REQUIRE(fs->mount(testDir, "", false));
|
|
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
auto *md = mod->newModelDataFromFile("sofa.obj");
|
|
REQUIRE(md != nullptr);
|
|
CHECK(!md->empty());
|
|
CHECK(md->getMeshCount() >= 1);
|
|
// Blender MTL lists 5 materials; Assimp may add a default.
|
|
CHECK(md->getMaterialCount() >= 5);
|
|
CHECK(md->getScene() != nullptr);
|
|
|
|
int totalVerts = 0;
|
|
int totalFaces = 0;
|
|
for (int i = 0; i < md->getMeshCount(); ++i) {
|
|
CHECK(md->getMesh(i) != nullptr);
|
|
int verts = md->getVertexCount(i);
|
|
int faces = md->getFaceCount(i);
|
|
CHECK(verts > 0);
|
|
CHECK(faces > 0);
|
|
totalVerts += verts;
|
|
totalFaces += faces;
|
|
}
|
|
CHECK(totalVerts > 0);
|
|
CHECK(totalFaces > 0);
|
|
delete md;
|
|
|
|
CHECK(fs->unmount(testDir));
|
|
}
|
|
|
|
TEST_CASE("model3d.render.rock1Fbx") {
|
|
const std::string path = pathBesideThisSource("Rock1.fbx");
|
|
auto bytes = readBinaryFile(path);
|
|
REQUIRE(!bytes.empty());
|
|
REQUIRE(!readBinaryFile(pathBesideThisSource("Textures/Rock1_Diffuse.png")).empty());
|
|
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
eve::data::ByteData data(bytes.data(), bytes.size());
|
|
auto *md = mod->newModelData(&data, ".fbx");
|
|
REQUIRE(md != nullptr);
|
|
// FBX material has no embedded map path; bind companion diffuse next to the asset.
|
|
renderModelSmoke(md, "model3d_rock1.png", "Textures/Rock1_Diffuse.png");
|
|
delete md;
|
|
}
|
|
|
|
TEST_CASE("model3d.render.rock1ToonShader") {
|
|
const std::string path = pathBesideThisSource("Rock1.fbx");
|
|
auto bytes = readBinaryFile(path);
|
|
REQUIRE(!bytes.empty());
|
|
REQUIRE(!readBinaryFile(pathBesideThisSource("Textures/Rock1_Diffuse.png")).empty());
|
|
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
eve::data::ByteData data(bytes.data(), bytes.size());
|
|
auto *md = mod->newModelData(&data, ".fbx");
|
|
REQUIRE(md != nullptr);
|
|
|
|
renderModelSmoke(md, "model3d_rock1_toon.png", "Textures/Rock1_Diffuse.png",
|
|
[](Graphics *gfx) -> Shader * {
|
|
std::vector<uint32_t> vert(mesh3d_toon_vert_spv,
|
|
mesh3d_toon_vert_spv + mesh3d_toon_vert_spv_count);
|
|
std::vector<uint32_t> frag(mesh3d_toon_frag_spv,
|
|
mesh3d_toon_frag_spv + mesh3d_toon_frag_spv_count);
|
|
Shader *toon = gfx->newMeshShaderFromSpv(vert, frag);
|
|
if (!toon || !toon->gpuHandle)
|
|
throw eve::Exception("failed to create toon mesh shader");
|
|
toon->declareFloat("bands");
|
|
toon->declareFloat("rimPower");
|
|
toon->declareFloat("rimStrength");
|
|
toon->declareFloat("posterize");
|
|
toon->sendFloat("bands", 4.f);
|
|
toon->sendFloat("rimPower", 2.8f);
|
|
toon->sendFloat("rimStrength", 0.55f);
|
|
toon->sendFloat("posterize", 6.f);
|
|
return toon;
|
|
});
|
|
delete md;
|
|
}
|
|
|
|
TEST_CASE("model3d.render.sofaObjMtl") {
|
|
const std::string objPath = pathBesideThisSource("sofa.obj");
|
|
const std::string mtlPath = pathBesideThisSource("sofa.mtl");
|
|
REQUIRE(!readBinaryFile(objPath).empty());
|
|
REQUIRE(!readBinaryFile(mtlPath).empty());
|
|
|
|
std::string testDir = pathBesideThisSource("");
|
|
if (!testDir.empty() && (testDir.back() == '/' || testDir.back() == '\\'))
|
|
testDir.pop_back();
|
|
|
|
auto *fs = eve::filesystem::Filesystem::create();
|
|
REQUIRE(fs->setIdentity("ev_ut_model3d_sofa_render", true));
|
|
REQUIRE(fs->setupWriteDirectory());
|
|
fs->allowMountingForPath(testDir);
|
|
REQUIRE(fs->mount(testDir, "", false));
|
|
|
|
auto *mod = eve::model3d::Model3D::create();
|
|
auto *md = mod->newModelDataFromFile("sofa.obj");
|
|
REQUIRE(md != nullptr);
|
|
renderModelSmoke(md, "model3d_sofa.png");
|
|
delete md;
|
|
|
|
CHECK(fs->unmount(testDir));
|
|
}
|