#include "RenderImageAudit.h" #include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include #include "filesystem/FileData.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "animation/AnimClip.h" #include "animation/AnimImporter.h" #include "animation/AnimPose.h" #include "animation/AnimSkeleton.h" #include "animation/AnimSkin.h" #include "avatar/Avatar.h" #include "data/ByteData.h" #include "filesystem/Filesystem.h" #include "font/Font.h" #include "font/FontData.h" #include "graphics/AmbientOcclusion.h" #include "graphics/AntiAliasing.h" #include "graphics/Canvas.h" #include "graphics/ClipSpace.h" #include "graphics/DrawItem2D.h" #include "graphics/Font.h" #include "graphics/GBuffer.h" #include "graphics/GlobalIllumination.h" #include "graphics/Graphics.h" #include "graphics/Grass.h" #include "graphics/Outline.h" #include "graphics/ScreenSpaceReflection.h" #include "graphics/Shader.h" #include "graphics/Texture.h" #include "graphics/Water.h" #include "graphics/Waterfall.h" // Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form. using eve::graphics::Color; #include "graphics/Light.h" #include "graphics/Material.h" #include "graphics/Mesh.h" #include "graphics/Quad.h" #include "graphics/RenderControl.h" #include "graphics/RenderSystem.h" #include "graphics/RenderSystem3D.h" #include "graphics/TextureSampler.h" #include "graphics/Volumetric.h" #include "graphics/shaders/custom2d_frag_spv.inc" #include "image/Image.h" #include "image/ImageData.h" #include "map/DualGrid.h" #include "map/Fov.h" #include "map/Map.h" #include "map/TileLayer.h" #include "map/TileOrientation.h" #include "model3d/Model3D.h" #include "model3d/ModelData.h" #include "particles/ParticleEmitter.h" #include "particles/ParticleSystem.h" #include "particles/Particles.h" #include "physics/Body.h" #include "physics/Cloth.h" #include "physics/Fluid2D.h" #include "physics/Physics.h" #include "physics/World.h" #include "procgen/Params.h" #include "procgen/Procgen.h" #include "scene/NodeDesc.h" #include "scene/SceneHost.h" #include "scene/TransformSystem.h" #include "stylize/Stylize.h" #include "ui/UI.h" #include "voxel/VoxelWorld.h" #include "window/Window.h" using eve::image::ImageData; using namespace eve::graphics; namespace { constexpr int kTile = 16; constexpr int kMaxDefects = 32; constexpr int kEmptyDist2 = 14 * 14; // ~0.055 in 8-bit constexpr int kDeadLumaSum = 15; // (r+2g+b) < 15 ≈ luma 0.02 constexpr float kOccTile = 0.40f; constexpr float kEmptyTile = 0.12f; constexpr float kFlickerTile = 0.07f; constexpr float kFlickerSevere = 0.15f; int lumaSumU8(const uint8_t *p) { return int(p[0]) + 2 * int(p[1]) + int(p[2]); } float lumaU8(const uint8_t *p) { return (0.2126f * p[0] + 0.7152f * p[1] + 0.0722f * p[2]) / 255.f; } struct BgBytes { int r, g, b; }; BgBytes toBytes(const RenderAuditBg &bg) { return BgBytes{int(bg.r * 255.f + 0.5f), int(bg.g * 255.f + 0.5f), int(bg.b * 255.f + 0.5f)}; } bool pixelEmpty(const uint8_t *p, const BgBytes &bg) { const int dr = int(p[0]) - bg.r; const int dg = int(p[1]) - bg.g; const int db = int(p[2]) - bg.b; return dr * dr + dg * dg + db * db < kEmptyDist2 || lumaSumU8(p) < kDeadLumaSum; } bool pixelMagenta(const uint8_t *p) { return p[0] > 200 && p[1] < 40 && p[2] > 200; } bool pixelCyan(const uint8_t *p) { return p[0] < 40 && p[1] > 200 && p[2] > 200; } const uint8_t *pxAt(const uint8_t *rgba, int w, int x, int y) { return rgba + (size_t(y) * size_t(w) + size_t(x)) * 4u; } std::string jsonEscape(const std::string &s) { std::string o; o.reserve(s.size()); for (char c : s) { if (c == '"' || c == '\\') { o.push_back('\\'); o.push_back(c); } else if (c == '\n') { o += "\\n"; } else { o.push_back(c); } } return o; } void addDefect(RenderAuditResult &r, RenderDefect::Kind kind, int x, int y, int w, int h, float score, const std::string &detail) { if (int(r.defects.size()) >= kMaxDefects) return; x = std::max(0, x); y = std::max(0, y); RenderDefect d; d.kind = kind; d.x = x; d.y = y; d.w = std::max(1, w); d.h = std::max(1, h); d.score = score; d.detail = detail; r.defects.push_back(std::move(d)); } struct TileStat { float occ = 0.f; float meanL = 0.f; }; int occupiedNeighbors(const std::vector &tiles, int tilesX, int tilesY, int tx, int ty) { int n = 0; for (int dy = -1; dy <= 1; ++dy) { for (int dx = -1; dx <= 1; ++dx) { if (dx == 0 && dy == 0) continue; const int nx = tx + dx; const int ny = ty + dy; if (nx < 0 || ny < 0 || nx >= tilesX || ny >= tilesY) continue; if (tiles[size_t(ny * tilesX + nx)].occ > kOccTile) ++n; } } return n; } void detectHoles(const std::vector &tiles, int tilesX, int tilesY, int w, int h, RenderAuditResult &r) { std::vector hole(size_t(tilesX * tilesY), 0); for (int ty = 1; ty < tilesY - 1; ++ty) { for (int tx = 1; tx < tilesX - 1; ++tx) { const TileStat &t = tiles[size_t(ty * tilesX + tx)]; if (t.occ > kEmptyTile) continue; if (occupiedNeighbors(tiles, tilesX, tilesY, tx, ty) >= 5) hole[size_t(ty * tilesX + tx)] = 1; } } std::vector seen(size_t(tilesX * tilesY), 0); for (int ty = 1; ty < tilesY - 1; ++ty) { for (int tx = 1; tx < tilesX - 1; ++tx) { const int seed = ty * tilesX + tx; if (!hole[size_t(seed)] || seen[size_t(seed)]) continue; int minX = tx, maxX = tx, minY = ty, maxY = ty, count = 0; std::vector stack{seed}; seen[size_t(seed)] = 1; while (!stack.empty()) { const int i = stack.back(); stack.pop_back(); ++count; const int cx = i % tilesX; const int cy = i / tilesX; minX = std::min(minX, cx); maxX = std::max(maxX, cx); minY = std::min(minY, cy); maxY = std::max(maxY, cy); const int nb[4] = {i - 1, i + 1, i - tilesX, i + tilesX}; for (int n : nb) { if (n < 0 || n >= tilesX * tilesY) continue; if (!hole[size_t(n)] || seen[size_t(n)]) continue; seen[size_t(n)] = 1; stack.push_back(n); } } const int px = minX * kTile; const int py = minY * kTile; const int pw = std::min(w, (maxX + 1) * kTile) - px; const int ph = std::min(h, (maxY + 1) * kTile) - py; char buf[96]; std::snprintf(buf, sizeof(buf), "interior hole tiles=%d occ=%.2f", count, tiles[size_t(ty * tilesX + tx)].occ); addDefect(r, RenderDefect::Kind::Incomplete, px, py, pw, ph, float(count), buf); } } } void detectCoarseIncomplete(const std::vector &tiles, int tilesX, int tilesY, int w, int h, float occupancy, RenderAuditResult &r) { // Sparse frames (small object + lots of sky) have empty quadrants by design. // Only look for missing chunks when the frame is mostly filled. if (occupancy < 0.40f) return; const int gx = 4, gy = 4; for (int gyi = 0; gyi < gy; ++gyi) { for (int gxi = 0; gxi < gx; ++gxi) { const int tx0 = (tilesX * gxi) / gx; const int tx1 = (tilesX * (gxi + 1)) / gx; const int ty0 = (tilesY * gyi) / gy; const int ty1 = (tilesY * (gyi + 1)) / gy; if (tx1 <= tx0 || ty1 <= ty0) continue; double occSum = 0.0; int n = 0; for (int ty = ty0; ty < ty1; ++ty) { for (int tx = tx0; tx < tx1; ++tx) { occSum += tiles[size_t(ty * tilesX + tx)].occ; ++n; } } const float cellOcc = n ? float(occSum / n) : 0.f; if (cellOcc > 0.08f) continue; int richN = 0; const int nbs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; for (auto &d : nbs) { const int nx = gxi + d[0]; const int ny = gyi + d[1]; if (nx < 0 || ny < 0 || nx >= gx || ny >= gy) continue; const int ntx0 = (tilesX * nx) / gx; const int ntx1 = (tilesX * (nx + 1)) / gx; const int nty0 = (tilesY * ny) / gy; const int nty1 = (tilesY * (ny + 1)) / gy; double nOcc = 0.0; int nn = 0; for (int ty = nty0; ty < nty1; ++ty) { for (int tx = ntx0; tx < ntx1; ++tx) { nOcc += tiles[size_t(ty * tilesX + tx)].occ; ++nn; } } if (nn && float(nOcc / nn) > 0.30f) ++richN; } if (richN < 2) continue; const int px = (w * gxi) / gx; const int py = (h * gyi) / gy; const int pw = (w * (gxi + 1)) / gx - px; const int ph = (h * (gyi + 1)) / gy - py; char buf[80]; std::snprintf(buf, sizeof(buf), "coarse empty cell occ=%.2f neighbors=%d", cellOcc, richN); addDefect(r, RenderDefect::Kind::Incomplete, px, py, pw, ph, 1.f - cellOcc, buf); } } } struct ScanAccum { std::vector tiles; int tilesX = 0; int tilesY = 0; double sumL = 0.0; int n = 0; int occ = 0; int interiorOcc = 0; int interiorN = 0; int magN = 0, cyanN = 0; int magX0 = 0, magY0 = 0, magX1 = 0, magY1 = 0; int cyanX0 = 0, cyanY0 = 0, cyanX1 = 0, cyanY1 = 0; std::vector rowLit; std::vector rowN; std::vector colLit; std::vector colN; }; ScanAccum scanFrame(const uint8_t *rgba, int w, int h, const BgBytes &bg, int step) { ScanAccum a; a.tilesX = (w + kTile - 1) / kTile; a.tilesY = (h + kTile - 1) / kTile; a.tiles.assign(size_t(a.tilesX * a.tilesY), {}); a.rowLit.assign(size_t(h), 0); a.rowN.assign(size_t(h), 0); a.colLit.assign(size_t(w), 0); a.colN.assign(size_t(w), 0); a.magX0 = w; a.magY0 = h; a.cyanX0 = w; a.cyanY0 = h; const int ix0 = w / 10, ix1 = w - w / 10; const int iy0 = h / 10, iy1 = h - h / 10; std::vector tileOcc(size_t(a.tilesX * a.tilesY), 0); std::vector tileN(size_t(a.tilesX * a.tilesY), 0); std::vector tileL(size_t(a.tilesX * a.tilesY), 0.0); for (int y = 0; y < h; y += step) { for (int x = 0; x < w; x += step) { const uint8_t *p = pxAt(rgba, w, x, y); const float L = lumaU8(p); const bool emptyPx = pixelEmpty(p, bg); a.sumL += L; ++a.n; if (!emptyPx) ++a.occ; if (x >= ix0 && x < ix1 && y >= iy0 && y < iy1) { ++a.interiorN; if (!emptyPx) ++a.interiorOcc; } const int tx = x / kTile; const int ty = y / kTile; const int ti = ty * a.tilesX + tx; ++tileN[size_t(ti)]; tileL[size_t(ti)] += L; if (!emptyPx) ++tileOcc[size_t(ti)]; ++a.rowN[size_t(y)]; ++a.colN[size_t(x)]; if (!emptyPx) { ++a.rowLit[size_t(y)]; ++a.colLit[size_t(x)]; } if (pixelMagenta(p)) { ++a.magN; a.magX0 = std::min(a.magX0, x); a.magY0 = std::min(a.magY0, y); a.magX1 = std::max(a.magX1, x); a.magY1 = std::max(a.magY1, y); } if (pixelCyan(p)) { ++a.cyanN; a.cyanX0 = std::min(a.cyanX0, x); a.cyanY0 = std::min(a.cyanY0, y); a.cyanX1 = std::max(a.cyanX1, x); a.cyanY1 = std::max(a.cyanY1, y); } } } for (int i = 0; i < a.tilesX * a.tilesY; ++i) { const int n = tileN[size_t(i)]; a.tiles[size_t(i)].occ = n ? float(tileOcc[size_t(i)]) / float(n) : 0.f; a.tiles[size_t(i)].meanL = n ? float(tileL[size_t(i)] / n) : 0.f; } return a; } void emitDamage(const ScanAccum &a, int w, int h, int step, RenderAuditResult &r) { const int magMin = std::max(2, 8 / (step * step)); if (a.magN >= magMin) { char buf[64]; std::snprintf(buf, sizeof(buf), "debug magenta samples=%d", a.magN); addDefect(r, RenderDefect::Kind::Damage, a.magX0, a.magY0, a.magX1 - a.magX0 + 1, a.magY1 - a.magY0 + 1, float(a.magN), buf); } if (a.cyanN >= magMin) { char buf[64]; std::snprintf(buf, sizeof(buf), "debug cyan samples=%d", a.cyanN); addDefect(r, RenderDefect::Kind::Damage, a.cyanX0, a.cyanY0, a.cyanX1 - a.cyanX0 + 1, a.cyanY1 - a.cyanY0 + 1, float(a.cyanN), buf); } auto tornRun = [&](const std::vector &lit, const std::vector &nn, int n, bool rows) { auto occAt = [&](int i) -> float { return nn[size_t(i)] ? float(lit[size_t(i)]) / float(nn[size_t(i)]) : -1.f; }; int y = step; while (y < n - step) { const float o = occAt(y); const float prev = occAt(y - step); const float next = occAt(y + step); if (!(o >= 0.f && o < 0.05f && prev > 0.35f && next > 0.35f)) { y += step; continue; } const int start = y; y += step; while (y < n - step) { const float cur = occAt(y); if (!(cur >= 0.f && cur < 0.05f)) break; y += step; } const int len = std::max(step, y - start); char buf[72]; if (rows) { std::snprintf(buf, sizeof(buf), "torn scanlines y=%d..%d", start, start + len - 1); addDefect(r, RenderDefect::Kind::Damage, 0, start, w, len, float(len), buf); } else { std::snprintf(buf, sizeof(buf), "torn columns x=%d..%d", start, start + len - 1); addDefect(r, RenderDefect::Kind::Damage, start, 0, len, h, float(len), buf); } } }; tornRun(a.rowLit, a.rowN, h, true); tornRun(a.colLit, a.colN, w, false); } } // namespace bool RenderAuditResult::hasSevere() const { if (empty) return true; int holeArea = 0; int imgArea = std::max(1, cfg.width * cfg.height); for (const auto &d : defects) { if (d.kind == RenderDefect::Kind::Damage) return true; if (d.kind == RenderDefect::Kind::Flicker && d.score >= kFlickerSevere) return true; if (d.kind == RenderDefect::Kind::Incomplete) holeArea += d.w * d.h; } if (flickerMad >= 0.12f) return true; return holeArea > imgArea / 16; } RenderAuditResult auditRgba8(const uint8_t *rgba, int w, int h, const RenderAuditConfig &cfg, const RenderAuditBg &bg, int step) { RenderAuditResult r; r.cfg = cfg; r.cfg.width = w; r.cfg.height = h; if (!rgba || w <= 0 || h <= 0) { r.empty = true; return r; } step = std::max(1, step); const ScanAccum a = scanFrame(rgba, w, h, toBytes(bg), step); r.meanLuma = a.n ? float(a.sumL / a.n) : 0.f; r.occupancy = a.n ? float(a.occ) / float(a.n) : 0.f; r.interiorOccupancy = a.interiorN ? float(a.interiorOcc) / float(a.interiorN) : 0.f; r.empty = r.interiorOccupancy < 0.02f; detectHoles(a.tiles, a.tilesX, a.tilesY, w, h, r); detectCoarseIncomplete(a.tiles, a.tilesX, a.tilesY, w, h, r.occupancy, r); emitDamage(a, w, h, step, r); if (r.empty) { addDefect(r, RenderDefect::Kind::Incomplete, 0, 0, w, h, 1.f - r.interiorOccupancy, "empty interior (occupancy < 2%)"); } return r; } RenderAuditResult auditImage(const ImageData &img, const RenderAuditConfig &cfg, const RenderAuditBg &bg, int step) { return auditRgba8(static_cast(img.getData()), img.getWidth(), img.getHeight(), cfg, bg, step); } RenderAuditResult auditFlickerRgba8(const uint8_t *a, const uint8_t *b, int w, int h, const RenderAuditConfig &cfg, const RenderAuditBg &bg, int step) { RenderAuditResult r; r.cfg = cfg; r.cfg.width = w; r.cfg.height = h; r.cfg.extra = r.cfg.extra.empty() ? "flicker" : r.cfg.extra + "+flicker"; if (!a || !b || w <= 0 || h <= 0) return r; step = std::max(1, step); double sumMad = 0.0; int nAll = 0; const int tilesX = (w + kTile - 1) / kTile; const int tilesY = (h + kTile - 1) / kTile; for (int ty = 0; ty < tilesY; ++ty) { for (int tx = 0; tx < tilesX; ++tx) { const int x0 = tx * kTile; const int y0 = ty * kTile; const int x1 = std::min(w, x0 + kTile); const int y1 = std::min(h, y0 + kTile); double tileMad = 0.0; int n = 0; for (int y = y0; y < y1; y += step) { for (int x = x0; x < x1; x += step) { const float d = std::fabs(lumaU8(pxAt(a, w, x, y)) - lumaU8(pxAt(b, w, x, y))); tileMad += d; sumMad += d; ++n; } } nAll += n; const float mad = n ? float(tileMad / n) : 0.f; if (mad < kFlickerTile) continue; char buf[80]; std::snprintf(buf, sizeof(buf), "static-camera tile MAD=%.3f", mad); addDefect(r, RenderDefect::Kind::Flicker, x0, y0, x1 - x0, y1 - y0, mad, buf); } } r.flickerMad = nAll ? float(sumMad / double(nAll)) : 0.f; (void)bg; return r; } RenderAuditResult auditFlicker(const ImageData &a, const ImageData &b, const RenderAuditConfig &cfg, const RenderAuditBg &bg, int step) { if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight()) { RenderAuditResult r; r.cfg = cfg; addDefect(r, RenderDefect::Kind::Flicker, 0, 0, a.getWidth(), a.getHeight(), 1.f, "flicker compare size mismatch"); return r; } return auditFlickerRgba8(static_cast(a.getData()), static_cast(b.getData()), a.getWidth(), a.getHeight(), cfg, bg, step); } void mergeAudit(RenderAuditResult &dst, const RenderAuditResult &src) { dst.flickerMad = std::max(dst.flickerMad, src.flickerMad); for (const auto &d : src.defects) { if (int(dst.defects.size()) >= kMaxDefects) break; dst.defects.push_back(d); } } void paintDefectOverlay(ImageData &img, const std::vector &defs) { const int w = img.getWidth(); const int h = img.getHeight(); auto *data = static_cast(img.getData()); auto plot = [&](int x, int y, uint8_t r, uint8_t g, uint8_t b) { if (x < 0 || y < 0 || x >= w || y >= h) return; uint8_t *p = data + (size_t(y) * size_t(w) + size_t(x)) * 4u; p[0] = r; p[1] = g; p[2] = b; p[3] = 255; }; for (const auto &d : defs) { uint8_t cr = 255, cg = 40, cb = 40; if (d.kind == RenderDefect::Kind::Damage) { cr = 255; cg = 220; cb = 0; } else if (d.kind == RenderDefect::Kind::Flicker) { cr = 40; cg = 180; cb = 255; } const int x1 = std::min(w - 1, d.x + d.w - 1); const int y1 = std::min(h - 1, d.y + d.h - 1); for (int t = 0; t < 2; ++t) { for (int x = d.x; x <= x1; ++x) { plot(x, d.y + t, cr, cg, cb); plot(x, y1 - t, cr, cg, cb); } for (int y = d.y; y <= y1; ++y) { plot(d.x + t, y, cr, cg, cb); plot(x1 - t, y, cr, cg, cb); } } } } bool saveImagePng(const ImageData &img, const std::string &path) { [[maybe_unused]] auto *const imageModule = eve::image::Image::create(); eve::filesystem::FileData *png = img.encode(medialoader::FormatHandler::ENCODED_PNG, path.c_str(), false); if (!png) return false; std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); std::ofstream out(path, std::ios::binary); const bool ok = out.good(); if (ok) { out.write(static_cast(png->getData()), static_cast(png->getSize())); } delete png; return ok && out.good(); } void appendAuditReport(const std::string &outDir, const RenderAuditResult &r) { std::error_code ec; std::filesystem::create_directories(outDir, ec); const std::string mdPath = outDir + "/image_audit.md"; const std::string jsPath = outDir + "/image_audit.jsonl"; { std::ofstream md(mdPath, std::ios::app); if (md.tellp() == 0) { md << "# Render image audit\n\n" << "Generated by unit_test. Boxes are pixel coordinates `(x,y,w,h)`.\n" << "Kinds: `incomplete` (holes / missing tiles), `damage` (magenta/cyan / tearing), " "`flicker` (static-camera MAD).\n\n"; } md << "## " << r.cfg.scene << " / " << r.cfg.phase; if (!r.cfg.extra.empty()) md << " / " << r.cfg.extra; md << "\n\n"; md << "- size=" << r.cfg.width << "x" << r.cfg.height; if (r.cfg.frame >= 0) md << " frame=" << r.cfg.frame; md << "\n- meanLuma=" << r.meanLuma << " occupancy=" << r.occupancy << " interiorOcc=" << r.interiorOccupancy << " flickerMad=" << r.flickerMad << "\n"; if (r.empty) md << "- **empty interior**\n"; if (r.defects.empty()) { md << "- no defects\n\n"; } else { for (const auto &d : r.defects) { md << "- DEFECT `" << RenderDefect::kindName(d.kind) << "` bbox=(" << d.x << "," << d.y << "," << d.w << "," << d.h << ") score=" << d.score << " " << d.detail << "\n"; } md << "\n"; } } { std::ofstream js(jsPath, std::ios::app); js << "{\"scene\":\"" << jsonEscape(r.cfg.scene) << "\",\"phase\":\"" << jsonEscape(r.cfg.phase) << "\",\"extra\":\"" << jsonEscape(r.cfg.extra) << "\",\"width\":" << r.cfg.width << ",\"height\":" << r.cfg.height << ",\"frame\":" << r.cfg.frame << ",\"meanLuma\":" << r.meanLuma << ",\"occupancy\":" << r.occupancy << ",\"interiorOccupancy\":" << r.interiorOccupancy << ",\"flickerMad\":" << r.flickerMad << ",\"empty\":" << (r.empty ? "true" : "false") << ",\"severe\":" << (r.hasSevere() ? "true" : "false") << ",\"defects\":["; for (size_t i = 0; i < r.defects.size(); ++i) { const auto &d = r.defects[i]; if (i) js << ","; js << "{\"kind\":\"" << RenderDefect::kindName(d.kind) << "\",\"x\":" << d.x << ",\"y\":" << d.y << ",\"w\":" << d.w << ",\"h\":" << d.h << ",\"score\":" << d.score << ",\"detail\":\"" << jsonEscape(d.detail) << "\"}"; } js << "]}\n"; } std::printf("RenderImageAudit[%s/%s] defects=%zu severe=%d meanLuma=%.4f occ=%.3f flickerMad=%.4f\n", r.cfg.scene.c_str(), r.cfg.phase.c_str(), r.defects.size(), int(r.hasSevere()), r.meanLuma, r.occupancy, r.flickerMad); for (const auto &d : r.defects) { std::printf(" %s bbox=(%d,%d,%d,%d) score=%.3f %s\n", RenderDefect::kindName(d.kind), d.x, d.y, d.w, d.h, d.score, d.detail.c_str()); } } std::string saveAuditOverlay(ImageData &img, const RenderAuditResult &r, const std::string &outDir) { if (!r.hasDefects()) return {}; paintDefectOverlay(img, r.defects); std::string name = r.cfg.scene + "_" + r.cfg.phase; if (!r.cfg.extra.empty()) name += "_" + r.cfg.extra; name += "_audit.png"; const std::string path = outDir + "/" + name; if (!saveImagePng(img, path)) return {}; std::printf("RenderImageAudit overlay: %s\n", path.c_str()); return path; } namespace { ImageData *makeRgba(int w, int h, uint8_t r, uint8_t g, uint8_t b) { auto *img = new ImageData(w, h, "RGBA8"); auto *p = static_cast(img->getData()); for (int i = 0; i < w * h; ++i) { p[i * 4 + 0] = r; p[i * 4 + 1] = g; p[i * 4 + 2] = b; p[i * 4 + 3] = 255; } return img; } void fillRect(ImageData *img, int x0, int y0, int rw, int rh, uint8_t r, uint8_t g, uint8_t b) { const int w = img->getWidth(); const int h = img->getHeight(); auto *p = static_cast(img->getData()); for (int y = y0; y < y0 + rh && y < h; ++y) { for (int x = x0; x < x0 + rw && x < w; ++x) { uint8_t *q = p + (y * w + x) * 4; q[0] = r; q[1] = g; q[2] = b; q[3] = 255; } } } bool hasKind(const RenderAuditResult &r, RenderDefect::Kind k) { for (const auto &d : r.defects) if (d.kind == k) return true; return false; } std::string auditOutDir() { return std::string(EVENGINE_TEST_BINARY_DIR) + "/out/classic_scenes"; } Texture *makeSolid(Graphics *gfx, uint8_t r, uint8_t g, uint8_t b) { const uint8_t px[4] = {r, g, b, 255}; return gfx->newTexture(1, 1, px); } Texture *makeStudioCubemap(Graphics *gfx, int face = 8) { const uint8_t faceRgb[6][3] = { {220, 180, 140}, {120, 150, 210}, {245, 245, 250}, {40, 40, 45}, {200, 210, 230}, {180, 160, 140}, }; const size_t faceBytes = size_t(face) * size_t(face) * 4u; std::vector faces(faceBytes * 6u); for (int f = 0; f < 6; ++f) { uint8_t *dst = faces.data() + size_t(f) * faceBytes; for (size_t i = 0; i < faceBytes; i += 4) { dst[i + 0] = faceRgb[f][0]; dst[i + 1] = faceRgb[f][1]; dst[i + 2] = faceRgb[f][2]; dst[i + 3] = 255; } } return gfx->newCubemap(face, faces.data()); } void resetScene3D() { if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [mr] = *it; mr->visible = false; } } if (ecs::current()->getManager() != nullptr) { auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [data] = *it; data->active = false; data->envMap = nullptr; data->envIntensity = 1.f; } } if (ecs::current()->getManager() != nullptr) { auto lightView = ecs::View(); for (auto it = lightView.begin(); it != lightView.end(); ++it) { auto [d] = *it; d->enabled = false; } } if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [sp] = *it; sp->visible = false; } } if (ecs::current()->getManager() != nullptr) { auto lightView = ecs::View(); for (auto it = lightView.begin(); it != lightView.end(); ++it) { auto [d] = *it; d->enabled = false; } } if (ecs::current()->getManager() != nullptr) { auto camView = ecs::View(); for (auto it = camView.begin(); it != camView.end(); ++it) { auto [d] = *it; d->active = false; } } if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [d] = *it; d->visible = false; } } if (ecs::current()->getManager() != nullptr) { auto view = ecs::View(); for (auto it = view.begin(); it != view.end(); ++it) { auto [d] = *it; d->visible = false; } } } void auditGpuFrame(Graphics *gfx, const char *scene, const char *phase, const RenderAuditBg &bg, bool flicker = false, const std::function &after3D = {}, const char *rawCaptureName = nullptr) { auto drawOnce = [&]() { RenderSystem3D::render(*gfx); if (after3D) after3D(); RenderSystem::render(*gfx); }; gfx->setScreenReadbackEnabled(false); for (int i = 0; i < 2; ++i) drawOnce(); gfx->setScreenReadbackEnabled(true); drawOnce(); std::unique_ptr a(gfx->newImageData()); REQUIRE(a.get() != nullptr); if (rawCaptureName) REQUIRE(saveImagePng(*a, auditOutDir() + "/" + rawCaptureName)); RenderAuditConfig cfg{scene, phase, "", 0}; auto result = auditImage(*a, cfg, bg, /*step=*/2); if (flicker) { drawOnce(); std::unique_ptr b(gfx->newImageData()); REQUIRE(b.get() != nullptr); mergeAudit(result, auditFlicker(*a, *b, cfg, bg, /*step=*/2)); } gfx->setScreenReadbackEnabled(false); if (result.hasDefects()) { appendAuditReport(auditOutDir(), result); saveAuditOverlay(*a, result, auditOutDir()); } REQUIRE(result.interiorOccupancy > 0.03f); REQUIRE(!result.hasSevere()); } void auditGpu2D(Graphics *gfx, const char *scene, const char *phase, const RenderAuditBg &bg) { gfx->setScreenReadbackEnabled(false); for (int i = 0; i < 2; ++i) RenderSystem::render(*gfx); gfx->setScreenReadbackEnabled(true); RenderSystem::render(*gfx); std::unique_ptr a(gfx->newImageData()); REQUIRE(a.get() != nullptr); RenderAuditConfig cfg{scene, phase, "", 0}; auto result = auditImage(*a, cfg, bg, /*step=*/2); gfx->setScreenReadbackEnabled(false); if (result.hasDefects()) { appendAuditReport(auditOutDir(), result); saveAuditOverlay(*a, result, auditOutDir()); } REQUIRE(result.interiorOccupancy > 0.03f); REQUIRE(!result.hasSevere()); } void blitAndPresent(Graphics *gfx, Texture *tex) { gfx->setScreenReadbackEnabled(true); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); gfx->drawTexturedRectRGBA(tex, 0, 0, float(gfx->getWidth()), float(gfx->getHeight()), 1, 1, 1, 1); gfx->present(); } void auditSwapchain(Graphics *gfx, const char *scene, const char *phase, const RenderAuditBg &bg) { std::unique_ptr a(gfx->newImageData()); REQUIRE(a.get() != nullptr); RenderAuditConfig cfg{scene, phase, "", 0}; auto result = auditImage(*a, cfg, bg, /*step=*/2); gfx->setScreenReadbackEnabled(false); if (result.hasDefects()) { appendAuditReport(auditOutDir(), result); saveAuditOverlay(*a, result, auditOutDir()); } REQUIRE(result.interiorOccupancy > 0.03f); REQUIRE(!result.hasSevere()); } Texture *makeChecker(Graphics *gfx, int w, int h, bool repeat = false) { std::vector px(size_t(w * h * 4)); for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { const bool on = ((x / 4) + (y / 4)) & 1; const size_t i = size_t((y * w + x) * 4); px[i + 0] = on ? 210 : 50; px[i + 1] = on ? 210 : 50; px[i + 2] = on ? 215 : 55; px[i + 3] = 255; } } // Studio / classic-style 3D audits: mipmaps + anisotropy (engine default stays off). TextureCreateInfo info = TextureCreateInfo::withMipmaps(true); info.sampler.repeatU = repeat; info.sampler.repeatV = repeat; return gfx->newTexture(w, h, px.data(), info); } struct CloseWin { eve::window::Window *w = nullptr; ~CloseWin() { if (w) w->close(); } }; #include "PathBesideSource.h" EVE_DEFINE_PATH_BESIDE_SOURCE() Mesh *makeFloor(Graphics *gfx, float half = 4.5f) { const float pos[] = { -half, 0.f, -half, -half, 0.f, half, half, 0.f, half, half, 0.f, -half, }; const float nrm[] = { 0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f, 0.f, }; const float uv[] = {0.f, 0.f, 0.f, 4.f, 4.f, 4.f, 4.f, 0.f}; const uint32_t idx[] = {0, 1, 2, 0, 2, 3}; return gfx->newMeshFromArrays(pos, nrm, uv, 4, idx, 6); } Texture *makeAtlas4(Graphics *gfx, int cell = 16) { const int w = cell * 2; const int h = cell * 2; std::vector px(size_t(w * h * 4)); const uint8_t cols[4][3] = {{220, 70, 60}, {60, 180, 80}, {60, 90, 220}, {230, 200, 50}}; for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { const int ci = (y < cell ? 0 : 2) + (x < cell ? 0 : 1); const size_t i = size_t((y * w + x) * 4); px[i + 0] = cols[ci][0]; px[i + 1] = cols[ci][1]; px[i + 2] = cols[ci][2]; px[i + 3] = 255; } } return gfx->newTexture(w, h, px.data()); } Texture *makeFlatNormal(Graphics *gfx) { const uint8_t px[4] = {128, 128, 255, 255}; return gfx->newTexture(1, 1, px); } struct Studio3D { Camera3D *cam = nullptr; Renderable3D *ground = nullptr; Renderable3D *subject = nullptr; Light3D *sun = nullptr; Renderable2D *hud = nullptr; Mesh *sphere = nullptr; Mesh *floor = nullptr; }; Studio3D makeStudio3D(Graphics *gfx) { Studio3D s; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); s.cam = Camera3D::createCamera(); s.cam->setEye(0.f, 1.5f, 4.0f); s.cam->setTarget(0.f, 0.15f, 0.f); s.cam->data()->nearZ = 0.05f; s.cam->data()->farZ = 40.f; s.cam->setAmbient(0.06f, 0.06f, 0.07f); s.cam->setEnvMap(makeStudioCubemap(gfx)); s.cam->setEnvIntensity(0.f); s.floor = makeFloor(gfx, 4.5f); s.sphere = gfx->newMeshSphere(18, 12); s.ground = Renderable3D::create(); s.ground->setMesh(s.floor); s.ground->setTexture(makeSolid(gfx, 70, 72, 78)); s.ground->setPosition(0.f, -0.7f, 0.f); s.ground->setMetallic(0.f); s.ground->setRoughness(0.92f); s.ground->setCastShadow(false); s.ground->setReceiveShadow(true); s.subject = Renderable3D::create(); s.subject->setMesh(s.sphere); s.subject->setTexture(makeSolid(gfx, 210, 210, 215)); s.subject->setPosition(0.f, 0.15f, 0.f); s.subject->setScale(0.55f, 0.55f, 0.55f); s.subject->setMetallic(0.15f); s.subject->setRoughness(0.45f); s.subject->setCastShadow(true); s.subject->setReceiveShadow(true); s.sun = Light3D::createLight("dir"); s.sun->setDirection(0.45f, 1.f, 0.35f); s.sun->setColor(1.f, 0.97f, 0.92f, 2.4f); s.sun->setCastShadow(true); s.sun->setEnabled(true); s.hud = Renderable2D::create(); s.hud->transform()->x = 0; s.hud->transform()->y = 0; s.hud->sprite()->width = 2; s.hud->sprite()->height = 2; s.hud->sprite()->receiveLight = false; s.hud->sprite()->castOcclusion = false; return s; } Texture *capturePresented(Graphics *gfx) { gfx->setScreenReadbackEnabled(false); for (int i = 0; i < 2; ++i) { RenderSystem3D::render(*gfx); RenderSystem::render(*gfx); } gfx->setScreenReadbackEnabled(true); RenderSystem3D::render(*gfx); RenderSystem::render(*gfx); std::unique_ptr img(gfx->newImageData()); REQUIRE(img.get() != nullptr); Texture *tex = gfx->newTexture(img.get()); gfx->setScreenReadbackEnabled(false); return tex; } Renderable2D *makeSprite(Texture *tex, float x, float y, float w, float h, bool receiveLight = false) { auto *sp = Renderable2D::create(); sp->transform()->x = x; sp->transform()->y = y; sp->sprite()->width = w; sp->sprite()->height = h; sp->sprite()->texture = tex; sp->sprite()->receiveLight = receiveLight; sp->sprite()->castOcclusion = false; return sp; } Texture *makeAtlasGrid(Graphics *gfx, int cells, int cell = 8) { const int w = cell * cells; const int h = cell * cells; std::vector px(size_t(w * h * 4)); for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { const int cx = x / cell; const int cy = y / cell; const int id = cy * cells + cx; const size_t i = size_t((y * w + x) * 4); px[i + 0] = uint8_t(40 + (id * 37) % 200); px[i + 1] = uint8_t(50 + (id * 53) % 190); px[i + 2] = uint8_t(70 + (id * 19) % 180); px[i + 3] = 255; } } return gfx->newTexture(w, h, px.data()); } Mesh *makeHairCard(Graphics *gfx) { const float pos[] = {-0.45f, 0.f, 0.f, 0.45f, 0.f, 0.f, 0.45f, 1.3f, 0.f, -0.45f, 1.3f, 0.f}; const float nrm[] = {0.f, 0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f}; const float uv[] = {0.f, 1.f, 1.f, 1.f, 1.f, 0.f, 0.f, 0.f}; const uint32_t idx[] = {0, 1, 2, 0, 2, 3}; return gfx->newMeshFromArrays(pos, nrm, uv, 4, idx, 6); } Mesh *uploadSkinnedAiMesh(Graphics *gfx, const aiMesh *src, const std::vector &posXYZ) { REQUIRE(src != nullptr); const int vc = int(src->mNumVertices); REQUIRE(vc > 0); REQUIRE(int(posXYZ.size()) == vc * 3); std::vector nrm(size_t(vc) * 3u, 0.f); std::vector uv(size_t(vc) * 2u, 0.f); for (int i = 0; i < vc; ++i) { if (src->mNormals) { nrm[size_t(i) * 3u + 0] = src->mNormals[i].x; nrm[size_t(i) * 3u + 1] = src->mNormals[i].y; nrm[size_t(i) * 3u + 2] = src->mNormals[i].z; } else { nrm[size_t(i) * 3u + 1] = 1.f; } if (src->mTextureCoords[0]) { uv[size_t(i) * 2u + 0] = src->mTextureCoords[0][i].x; uv[size_t(i) * 2u + 1] = src->mTextureCoords[0][i].y; } } std::vector idx; idx.reserve(size_t(src->mNumFaces) * 3u); for (unsigned f = 0; f < src->mNumFaces; ++f) { const aiFace &face = src->mFaces[f]; if (face.mNumIndices < 3) continue; for (unsigned k = 1; k + 1 < face.mNumIndices; ++k) { idx.push_back(face.mIndices[0]); idx.push_back(face.mIndices[k]); idx.push_back(face.mIndices[k + 1]); } } REQUIRE(!idx.empty()); return gfx->newMeshFromArrays(posXYZ.data(), nrm.data(), uv.data(), vc, idx.data(), int(idx.size())); } eve::model3d::ModelData *loadCesiumMan(const char *fsIdentity) { const std::string dir = pathBesideThisSource("assets/skinned/cesium_man/glTF"); REQUIRE(std::filesystem::is_regular_file(dir + "/CesiumMan.gltf")); auto *fs = eve::filesystem::Filesystem::create(); REQUIRE(fs != nullptr); REQUIRE(fs->setIdentity(fsIdentity, true)); REQUIRE(fs->setupWriteDirectory()); fs->allowMountingForPath(dir); REQUIRE(fs->mount(dir, "", false)); auto *mod = eve::model3d::Model3D::create(); return mod->newModelDataFromFile("CesiumMan.gltf"); } int findFirstSkinnedMesh(const eve::model3d::ModelData *model) { for (int i = 0; i < model->getMeshCount(); ++i) { if (model->hasBones(i)) return i; } return -1; } bool printOverlayThunk(void *userdata, void *commandBuffer) { auto *gfx = static_cast(userdata); if (!gfx || !gfx->getFont()) return false; if (!commandBuffer) return true; Font *font = gfx->getFont(); const char *icon = "\xEF\x80\x80"; std::string row; for (int i = 0; i < 8; ++i) row += icon; gfx->drawText(font, row, 8.f, 8.f, Color(1.f, 0.82f, 0.2f, 1.f), 2.4f); return true; } } // namespace TEST_CASE("graphics.imageAudit.syntheticDefects") { RenderAuditBg bg{0.08f, 0.09f, 0.11f}; const uint8_t geoR = 180, geoG = 180, geoB = 185; { std::unique_ptr img(makeRgba(64, 64, geoR, geoG, geoB)); RenderAuditConfig cfg{"synthetic", "clean", "", 0}; auto r = auditImage(*img, cfg, bg); CHECK(!r.empty); CHECK(r.defects.empty()); CHECK(r.occupancy > 0.9f); } { std::unique_ptr img(makeRgba(64, 64, geoR, geoG, geoB)); // One full interior 16x16 tile; unaligned holes split across tiles and // stay above kEmptyTile so detectHoles never fires. fillRect(img.get(), 16, 16, 16, 16, 0, 0, 0); RenderAuditConfig cfg{"synthetic", "hole", "", 0}; auto r = auditImage(*img, cfg, bg); REQUIRE(hasKind(r, RenderDefect::Kind::Incomplete)); bool locOk = false; for (const auto &d : r.defects) { if (d.kind != RenderDefect::Kind::Incomplete) continue; const int cx = d.x + d.w / 2; const int cy = d.y + d.h / 2; if (cx >= 16 && cx < 48 && cy >= 16 && cy < 48) locOk = true; } CHECK(locOk); appendAuditReport(auditOutDir(), r); saveAuditOverlay(*img, r, auditOutDir()); } { std::unique_ptr img(makeRgba(64, 64, geoR, geoG, geoB)); fillRect(img.get(), 8, 8, 12, 12, 255, 0, 255); RenderAuditConfig cfg{"synthetic", "magenta", "", 0}; auto r = auditImage(*img, cfg, bg); REQUIRE(hasKind(r, RenderDefect::Kind::Damage)); REQUIRE(r.hasSevere()); appendAuditReport(auditOutDir(), r); } { std::unique_ptr img(makeRgba(64, 64, geoR, geoG, geoB)); fillRect(img.get(), 0, 30, 64, 1, 20, 23, 28); RenderAuditConfig cfg{"synthetic", "tearing", "", 0}; auto r = auditImage(*img, cfg, bg); REQUIRE(hasKind(r, RenderDefect::Kind::Damage)); appendAuditReport(auditOutDir(), r); } { std::unique_ptr a(makeRgba(64, 64, geoR, geoG, geoB)); std::unique_ptr b(makeRgba(64, 64, geoR, geoG, geoB)); fillRect(b.get(), 16, 16, 24, 24, 40, 40, 40); RenderAuditConfig cfg{"synthetic", "flicker", "", 0}; auto r = auditFlicker(*a, *b, cfg, bg); REQUIRE(hasKind(r, RenderDefect::Kind::Flicker)); bool locOk = false; for (const auto &d : r.defects) { if (d.kind != RenderDefect::Kind::Flicker) continue; const int cx = d.x + d.w / 2; const int cy = d.y + d.h / 2; if (cx >= 12 && cx < 44 && cy >= 12 && cy < 44) locOk = true; } CHECK(locOk); appendAuditReport(auditOutDir(), r); } { std::unique_ptr img(makeRgba(64, 64, 20, 23, 28)); RenderAuditConfig cfg{"synthetic", "empty", "", 0}; auto r = auditImage(*img, cfg, bg); REQUIRE(r.empty); REQUIRE(r.hasSevere()); appendAuditReport(auditOutDir(), r); } } TEST_CASE("graphics.imageAudit.pipelineConfigs") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 480; s.height = 360; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto *cam = Camera3D::createCamera(); cam->setEye(0.f, 1.6f, 4.2f); cam->setTarget(0.f, 0.2f, 0.f); cam->data()->nearZ = 0.05f; cam->data()->farZ = 40.f; cam->setAmbient(0.06f, 0.06f, 0.07f); cam->setEnvMap(makeStudioCubemap(gfx)); cam->setEnvIntensity(0.f); auto *ground = Renderable3D::create(); ground->setMesh(gfx->newMeshSphere(16, 8)); ground->setTexture(makeSolid(gfx, 70, 72, 78)); ground->setPosition(0.f, -0.7f, 0.f); ground->setScale(3.2f, 0.08f, 3.2f); ground->setMetallic(0.f); ground->setRoughness(0.92f); ground->setCastShadow(false); ground->setReceiveShadow(true); auto *box = Renderable3D::create(); box->setMesh(gfx->newMeshSphere(20, 12)); box->setTexture(makeSolid(gfx, 210, 210, 215)); box->setPosition(0.f, 0.15f, 0.f); box->setScale(0.55f, 0.55f, 0.55f); box->setMetallic(0.15f); box->setRoughness(0.45f); box->setCastShadow(true); box->setReceiveShadow(true); 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; auto *sun = Light3D::createLight("dir"); sun->setDirection(0.45f, 1.f, 0.35f); sun->setColor(1.f, 0.97f, 0.92f, 2.4f); sun->setCastShadow(false); sun->setEnabled(false); auto *point = Light3D::createLight("point"); point->setPosition(-1.2f, 1.1f, 1.0f); point->setColor(1.f, 0.6f, 0.4f, 5.f); point->setRadius(6.f); point->setEnabled(false); auto *pointB = Light3D::createLight("point"); pointB->setPosition(1.1f, 0.9f, -0.8f); pointB->setColor(0.4f, 0.55f, 1.f, 4.5f); pointB->setRadius(6.f); pointB->setEnabled(false); std::vector clusterLights; clusterLights.reserve(10); for (int i = 0; i < 10; ++i) { auto *l = Light3D::createLight("point"); const float a = float(i) / 10.f * 6.2831853f; l->setPosition(std::cos(a) * 1.7f, 0.55f + 0.15f * std::sin(a * 2.f), std::sin(a) * 1.7f); l->setColor(0.85f, 0.9f, 1.f, 2.2f); l->setRadius(3.2f); l->setEnabled(false); clusterLights.push_back(l); } RenderControl *rc = gfx->getRenderControl(); REQUIRE(rc != nullptr); auto resetRc = [&]() { rc->enable("depthTest"); rc->enable("shadow"); rc->enable("forward"); rc->enable("hair"); rc->enable("clustered"); rc->disable("gbuffer"); rc->compile(); }; resetRc(); struct Phase { const char *name; int light; // 0 amb 1 dir 2 shadows 3 point 4 ibl 5 dir+point 6 shadows+ibl 7 full const char *enableFeat; const char *disableFeat; bool manyPoints; bool flicker; }; const Phase phases[] = { {"ambient", 0, nullptr, nullptr, false, true}, {"dir_lit", 1, nullptr, nullptr, false, true}, {"dir_shadows", 2, nullptr, nullptr, false, true}, {"point_lit", 3, nullptr, nullptr, false, false}, {"ibl_reflect", 4, nullptr, nullptr, false, true}, {"dir_point", 5, nullptr, nullptr, false, false}, {"shadows_ibl", 6, nullptr, nullptr, false, false}, {"full_lit", 7, nullptr, nullptr, false, true}, {"gbuffer", 1, "gbuffer", nullptr, false, false}, {"gbuffer_albedo", 1, "gbufferAlbedo", nullptr, false, false}, {"no_shadow_pass", 2, nullptr, "shadow", false, false}, {"clustered_many", 1, "clustered", nullptr, true, false}, {"packed_only", 1, nullptr, "clustered", true, false}, }; const std::string outDir = auditOutDir(); for (const auto &ph : phases) { sun->setEnabled(false); sun->setCastShadow(false); point->setEnabled(false); pointB->setEnabled(false); for (auto *l : clusterLights) l->setEnabled(false); cam->setEnvIntensity(0.f); cam->setAmbient(0.18f, 0.18f, 0.2f); resetRc(); if (ph.enableFeat) { rc->enable(ph.enableFeat); rc->compile(); } if (ph.disableFeat) { rc->disable(ph.disableFeat); rc->compile(); } auto enableSun = [&](bool shadows) { cam->setAmbient(0.05f, 0.05f, 0.06f); sun->setEnabled(true); sun->setCastShadow(shadows); sun->setShadowStrength(1.f); sun->setColor(1.f, 0.97f, 0.92f, 2.4f); }; switch (ph.light) { case 1: enableSun(false); break; case 2: enableSun(true); break; case 3: cam->setAmbient(0.04f, 0.04f, 0.05f); point->setEnabled(true); break; case 4: cam->setAmbient(0.03f, 0.03f, 0.04f); sun->setEnabled(true); cam->setEnvIntensity(1.2f); break; case 5: enableSun(false); point->setEnabled(true); pointB->setEnabled(true); break; case 6: enableSun(true); cam->setEnvIntensity(1.1f); break; case 7: enableSun(true); point->setEnabled(true); pointB->setEnabled(true); cam->setEnvIntensity(1.0f); break; default: break; } if (ph.manyPoints) { for (auto *l : clusterLights) l->setEnabled(true); } gfx->setScreenReadbackEnabled(false); for (int i = 0; i < 2; ++i) { RenderSystem3D::render(*gfx); RenderSystem::render(*gfx); } gfx->setScreenReadbackEnabled(true); RenderSystem3D::render(*gfx); RenderSystem::render(*gfx); std::unique_ptr a(gfx->newImageData()); REQUIRE(a.get() != nullptr); RenderAuditConfig cfg{"pipeline_cube", ph.name, "", 0}; auto result = auditImage(*a, cfg, bg, /*step=*/2); if (ph.flicker) { RenderSystem3D::render(*gfx); RenderSystem::render(*gfx); std::unique_ptr b(gfx->newImageData()); REQUIRE(b.get() != nullptr); mergeAudit(result, auditFlicker(*a, *b, cfg, bg, /*step=*/2)); } gfx->setScreenReadbackEnabled(false); if (result.hasDefects()) { appendAuditReport(outDir, result); saveAuditOverlay(*a, result, outDir); } else { saveImagePng(*a, outDir + "/pipeline_cube_" + std::string(ph.name) + ".png"); } REQUIRE(result.interiorOccupancy > 0.04f); REQUIRE(!result.hasSevere()); } resetRc(); win->close(); } TEST_CASE("graphics.imageAudit.materialsAndCamera") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto *cam = Camera3D::createCamera(); cam->setEye(0.f, 1.5f, 4.0f); cam->setTarget(0.f, 0.15f, 0.f); cam->data()->nearZ = 0.05f; cam->data()->farZ = 40.f; cam->setAmbient(0.06f, 0.06f, 0.07f); cam->setEnvMap(makeStudioCubemap(gfx)); cam->setEnvIntensity(0.f); Mesh *sphere = gfx->newMeshSphere(18, 12); Mesh *cyl = gfx->newMeshCylinder(16, 1, true); Texture *checker = makeChecker(gfx, 32, 32); Texture *height = makeChecker(gfx, 32, 32); auto *ground = Renderable3D::create(); ground->setMesh(sphere); ground->setTexture(makeSolid(gfx, 70, 72, 78)); ground->setPosition(0.f, -0.7f, 0.f); ground->setScale(3.0f, 0.08f, 3.0f); ground->setMetallic(0.f); ground->setRoughness(0.92f); ground->setCastShadow(false); ground->setReceiveShadow(true); auto *box = Renderable3D::create(); box->setMesh(sphere); box->setTexture(makeSolid(gfx, 210, 210, 215)); box->setPosition(0.f, 0.15f, 0.f); box->setScale(0.55f, 0.55f, 0.55f); box->setMetallic(0.15f); box->setRoughness(0.45f); box->setCastShadow(true); box->setReceiveShadow(true); auto *hud = Renderable2D::create(); hud->transform()->x = 0; hud->transform()->y = 0; hud->sprite()->width = 2; hud->sprite()->height = 2; auto *sun = Light3D::createLight("dir"); sun->setDirection(0.45f, 1.f, 0.35f); sun->setColor(1.f, 0.97f, 0.92f, 2.4f); sun->setCastShadow(true); sun->setShadowStrength(1.f); sun->setEnabled(true); Material *unlit = gfx->newMaterial(); unlit->setShadingModel("unlit"); unlit->setAlbedoTexture(makeSolid(gfx, 200, 50, 50)); unlit->setReceiveLight(false); auditGpuFrame(gfx, "materials", "pbr_default", bg, true); box->setMaterial(unlit); box->setReceiveLight(false); auditGpuFrame(gfx, "materials", "unlit", bg); box->setMaterial(nullptr); box->setReceiveLight(true); box->setTexture(makeSolid(gfx, 210, 210, 215)); box->setMetallic(1.f); box->setRoughness(0.08f); cam->setEnvIntensity(1.3f); auditGpuFrame(gfx, "materials", "metal_ibl", bg); cam->setEnvIntensity(0.f); box->setMetallic(0.f); box->setRoughness(0.95f); auditGpuFrame(gfx, "materials", "dielectric_rough", bg); box->setMetallic(0.15f); box->setRoughness(0.45f); box->setTexture(checker); box->setTexCellBomb(4.f, 0.85f, 1.f); auditGpuFrame(gfx, "materials", "tex_cell_bomb", bg); box->setTexCellBomb(4.f, 0.f, 1.f); box->setHeightTexture(height); box->setParallax(0.06f, 8.f, 24.f); auditGpuFrame(gfx, "materials", "parallax", bg); box->setParallax(0.f); box->setHeightTexture(nullptr); box->setTexture(makeSolid(gfx, 210, 210, 215)); box->setHair(true); auditGpuFrame(gfx, "materials", "hair", bg); box->setHair(false); box->setMesh(cyl); auditGpuFrame(gfx, "materials", "cylinder", bg); box->setMesh(sphere); ground->setReceiveShadow(false); auditGpuFrame(gfx, "materials", "receive_shadow_off", bg); ground->setReceiveShadow(true); sun->setShadowBias(0.02f); auditGpuFrame(gfx, "materials", "shadow_bias_high", bg); sun->setShadowBias(0.002f); cam->setFov(28.f); auditGpuFrame(gfx, "materials", "fov_narrow", bg); cam->setFov(75.f); auditGpuFrame(gfx, "materials", "fov_wide", bg); cam->setFov(60.f); cam->data()->nearZ = 0.8f; cam->data()->farZ = 6.f; auditGpuFrame(gfx, "materials", "near_far_tight", bg); cam->data()->nearZ = 0.05f; cam->data()->farZ = 40.f; delete unlit; win->close(); } TEST_CASE("graphics.imageAudit.postFx") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; const int w = 400; const int h = 300; auto *cam = Camera3D::createCamera(); cam->setEye(0.f, 1.5f, 4.0f); cam->setTarget(0.f, 0.15f, 0.f); cam->data()->nearZ = 0.05f; cam->data()->farZ = 40.f; cam->setAmbient(0.05f, 0.05f, 0.06f); cam->setEnvMap(makeStudioCubemap(gfx)); cam->setEnvIntensity(0.f); Mesh *sphere = gfx->newMeshSphere(18, 12); auto *ground = Renderable3D::create(); ground->setMesh(sphere); ground->setTexture(makeSolid(gfx, 70, 72, 78)); ground->setPosition(0.f, -0.7f, 0.f); ground->setScale(3.0f, 0.08f, 3.0f); ground->setRoughness(0.92f); ground->setCastShadow(false); ground->setReceiveShadow(true); auto *box = Renderable3D::create(); box->setMesh(sphere); box->setTexture(makeSolid(gfx, 210, 210, 215)); box->setPosition(0.f, 0.15f, 0.f); box->setScale(0.55f, 0.55f, 0.55f); box->setCastShadow(true); auto *hud = Renderable2D::create(); hud->transform()->x = 0; hud->transform()->y = 0; hud->sprite()->width = 2; hud->sprite()->height = 2; auto *sun = Light3D::createLight("dir"); sun->setDirection(0.45f, 1.f, 0.35f); sun->setColor(1.f, 0.97f, 0.92f, 2.4f); sun->setCastShadow(true); sun->setEnabled(true); RenderControl *rc = gfx->getRenderControl(); REQUIRE(rc != nullptr); rc->enable("gbuffer"); rc->enable("gbufferAlbedo"); rc->compile(); gfx->setScreenReadbackEnabled(false); for (int i = 0; i < 2; ++i) { RenderSystem3D::render(*gfx); RenderSystem::render(*gfx); } gfx->setScreenReadbackEnabled(true); RenderSystem3D::render(*gfx); RenderSystem::render(*gfx); std::unique_ptr sceneImg(gfx->newImageData()); REQUIRE(sceneImg.get() != nullptr); Texture *sceneTex = gfx->newTexture(sceneImg.get()); REQUIRE(sceneTex != nullptr); GBuffer *gb = rc->getGBuffer(); REQUIRE(gb != nullptr); Texture *depth = gb->getDepthTexture(); REQUIRE(depth != nullptr); std::unique_ptr aa(gfx->newAntiAliasing()); aa->setQuality("low"); Canvas *aaDest = gfx->newCanvas(w, h); REQUIRE(aaDest != nullptr); const char *aaModes[] = {"fxaa", "smaa", "nfaa"}; for (const char *mode : aaModes) { aa->setMode(mode); aa->applyTo(gfx, sceneTex, aaDest); blitAndPresent(gfx, aaDest->getTexture()); auditSwapchain(gfx, "postfx", (std::string("aa_") + mode).c_str(), bg); } std::unique_ptr ao(gfx->newAmbientOcclusion()); ao->setQuality("low"); ao->setCamera(0.f, 1.5f, 4.0f, 0.f, 0.15f, 0.f, 0.f, 1.f, 0.f, 60.f, float(w) / float(h), 0.05f, 40.f); Canvas *aoMap = gfx->newCanvas(ao->resolutionFor(w), ao->resolutionFor(h)); Canvas *composed = gfx->newCanvas(w, h); REQUIRE(aoMap != nullptr); REQUIRE(composed != nullptr); const char *aoModes[] = {"ssao", "hbao", "gtao"}; for (const char *mode : aoModes) { ao->setMode(mode); ao->computeTo(gfx, depth, aoMap); gfx->setCanvas(composed); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); gfx->drawTexturedRectRGBA(sceneTex, 0, 0, float(w), float(h), 1, 1, 1, 1); ao->applyOverlay(gfx, aoMap->getTexture()); gfx->setCanvas(); blitAndPresent(gfx, composed->getTexture()); auditSwapchain(gfx, "postfx", (std::string("ao_") + mode).c_str(), bg); } std::unique_ptr vol(gfx->newVolumetric()); vol->setQuality("low"); vol->setCamera(0.f, 1.5f, 4.0f, 0.f, 0.15f, 0.f, 0.f, 1.f, 0.f, 60.f, float(w) / float(h), 0.05f, 40.f); vol->setFogColor(0.45f, 0.5f, 0.55f); vol->setFogHeight(-0.7f); vol->setFogHeightFalloff(0.8f); vol->setFogStart(1.2f); vol->setFogEnd(9.f); vol->setDensity(0.35f); vol->setMode("fog"); gfx->setCanvas(composed); gfx->clearScreen(); gfx->drawTexturedRectRGBA(sceneTex, 0, 0, float(w), float(h), 1, 1, 1, 1); vol->applyFog(gfx, depth); gfx->setCanvas(); blitAndPresent(gfx, composed->getTexture()); auditSwapchain(gfx, "postfx", "vol_fog", bg); vol->setMode("raymarch"); vol->setLightDirection(0.45f, 1.f, 0.35f); vol->setLightScreenUV(0.62f, 0.28f); vol->setIntensity(0.8f); gfx->setCanvas(composed); gfx->clearScreen(); gfx->drawTexturedRectRGBA(sceneTex, 0, 0, float(w), float(h), 1, 1, 1, 1); vol->rayMarch(gfx, depth); gfx->setCanvas(); blitAndPresent(gfx, composed->getTexture()); auditSwapchain(gfx, "postfx", "vol_raymarch", bg); vol->setMode("screenspace"); vol->applyFromSceneTo(gfx, sceneTex, composed); blitAndPresent(gfx, composed->getTexture()); auditSwapchain(gfx, "postfx", "vol_screenspace", bg); rc->disable("gbuffer"); rc->compile(); win->close(); } TEST_CASE("graphics.imageAudit.composite2d3d") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); studio.hud->sprite()->visible = false; Texture *cells[4] = { makeSolid(gfx, 220, 80, 70), makeSolid(gfx, 70, 190, 90), makeSolid(gfx, 70, 110, 220), makeSolid(gfx, 230, 200, 60), }; Texture *flatN = makeFlatNormal(gfx); Renderable2D *packed[4]; const float ox = 80.f, oy = 60.f, cw = 120.f, ch = 90.f; for (int i = 0; i < 4; ++i) { packed[i] = makeSprite(cells[i], ox + float(i % 2) * cw, oy + float(i / 2) * ch, cw, ch, false); packed[i]->sprite()->normalTexture = flatN; } auditGpuFrame(gfx, "composite", "sprites_over_3d", bg); auto *pt = Light2D::createLight("point"); pt->setPosition(200.f, 150.f); pt->setColor(1.f, 0.95f, 0.85f, 2.2f); pt->setRadius(280.f); pt->setEnabled(true); for (auto *sp : packed) sp->sprite()->receiveLight = true; auditGpuFrame(gfx, "composite", "lit2d_normal", bg); pt->setEnabled(false); auto *dir2 = Light2D::createLight("dir"); dir2->setDirection(0.6f, -0.4f); dir2->setColor(0.95f, 0.9f, 1.f, 1.6f); dir2->setEnabled(true); auditGpuFrame(gfx, "composite", "dir2d", bg); dir2->setEnabled(false); for (auto *sp : packed) sp->sprite()->visible = false; Texture *atlas = makeAtlas4(gfx, 16); auto *atlasSp = makeSprite(atlas, 80.f, 60.f, 240.f, 180.f, false); atlasSp->sprite()->quad = gfx->newQuad(0, 0, 16, 16); auditGpuFrame(gfx, "composite", "atlas_quad", bg); } TEST_CASE("graphics.imageAudit.stylizeAndSsaa") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); Mesh *cyl = gfx->newMeshCylinder(24, 1, true); studio.subject->setMesh(cyl); studio.subject->setTexture(makeChecker(gfx, 32, 32)); studio.subject->setScale(0.45f, 0.7f, 0.45f); auto *stylize = eve::stylize::Stylize::create(); Shader *toon = stylize->newMeshShader(gfx, "cartoon"); REQUIRE(toon != nullptr); if (toon->hasUniform("bands")) toon->sendFloat("bands", 3.f); if (toon->hasUniform("rimPower")) toon->sendFloat("rimPower", 2.6f); if (toon->hasUniform("rimStrength")) toon->sendFloat("rimStrength", 0.45f); studio.subject->setShader(toon); auditGpuFrame(gfx, "stylize", "mesh_cartoon", bg); Shader *ink = stylize->newMeshShader(gfx, "ink"); REQUIRE(ink != nullptr); studio.subject->setShader(ink); auditGpuFrame(gfx, "stylize", "mesh_ink", bg); studio.subject->setShader(nullptr); Texture *sceneTex = capturePresented(gfx); REQUIRE(sceneTex != nullptr); const int w = 400, h = 300; Canvas *dest = gfx->newCanvas(w, h); REQUIRE(dest != nullptr); const char *posts[] = {"cartoon", "watercolor", "ink", "pixel"}; for (const char *id : posts) { eve::stylize::StylePass *pass = stylize->newPass(gfx, id); REQUIRE(pass != nullptr); pass->applyTo(gfx, sceneTex, dest); blitAndPresent(gfx, dest->getTexture()); auditSwapchain(gfx, "stylize", (std::string("post_") + id).c_str(), bg); delete pass; } std::unique_ptr aa(gfx->newAntiAliasing()); aa->setQuality("low"); aa->setMode("ssaa"); aa->applyTo(gfx, sceneTex, dest); blitAndPresent(gfx, dest->getTexture()); auditSwapchain(gfx, "stylize", "aa_ssaa", bg); } TEST_CASE("graphics.imageAudit.multiObjectLod") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); Mesh *cyl = gfx->newMeshCylinder(12, 1, true); Mesh *hi = gfx->newMeshSphere(20, 14); Mesh *lo = gfx->newMeshSphere(8, 6); auto *left = Renderable3D::create(); left->setMesh(studio.sphere); left->setTexture(makeSolid(gfx, 210, 90, 80)); left->setPosition(-1.1f, 0.2f, 0.f); left->setScale(0.4f, 0.4f, 0.4f); left->setCastShadow(true); auto *right = Renderable3D::create(); right->setMesh(cyl); right->setTexture(makeSolid(gfx, 80, 160, 210)); right->setPosition(1.1f, 0.25f, 0.f); right->setScale(0.35f, 0.55f, 0.35f); right->setCastShadow(true); auditGpuFrame(gfx, "multi", "gallery", bg); studio.subject->setTint(1.f, 0.25f, 0.2f, 1.f); auditGpuFrame(gfx, "multi", "tint_red", bg); studio.subject->setTint(1.f, 1.f, 1.f, 0.72f); auditGpuFrame(gfx, "multi", "tint_alpha", bg); studio.subject->setTint(1.f, 1.f, 1.f, 1.f); left->setVisible(false); right->setVisible(false); studio.subject->setMesh(nullptr); auto *matA = gfx->newMaterial(); matA->setShadingModel("pbr"); matA->setAlbedoTexture(makeSolid(gfx, 220, 180, 70)); auto *matB = gfx->newMaterial(); matB->setShadingModel("pbr"); matB->setAlbedoTexture(makeSolid(gfx, 70, 140, 210)); studio.subject->setPart(0, "body", hi, matA); studio.subject->setPart(1, "cap", cyl, matB); studio.subject->setScale(0.5f, 0.5f, 0.5f); auditGpuFrame(gfx, "multi", "mesh_parts", bg); studio.subject->clearParts(); studio.subject->setMesh(hi); left->setVisible(true); right->setVisible(true); studio.subject->setMeshLod(0, hi); studio.subject->setMeshLod(1, lo, 3.2f); studio.cam->setEye(0.f, 1.2f, 2.2f); auditGpuFrame(gfx, "multi", "lod_near", bg); studio.cam->setEye(0.f, 2.2f, 7.5f); auditGpuFrame(gfx, "multi", "lod_far", bg); studio.cam->setEye(4.2f, 1.5f, 0.1f); studio.cam->setTarget(0.f, 0.15f, 0.f); auditGpuFrame(gfx, "multi", "camera_side", bg); studio.cam->setEye(0.f, 1.5f, 4.0f); Canvas *cv = gfx->newCanvas(400, 300); REQUIRE(cv != nullptr); Texture *cellA = makeSolid(gfx, 200, 90, 70); Texture *cellB = makeSolid(gfx, 70, 180, 110); auto *c0 = makeSprite(cellA, 40.f, 30.f, 160.f, 120.f); auto *c1 = makeSprite(cellB, 200.f, 30.f, 160.f, 120.f); auto *c2 = makeSprite(cellB, 40.f, 150.f, 160.f, 120.f); auto *c3 = makeSprite(cellA, 200.f, 150.f, 160.f, 120.f); c0->sprite()->canvas = cv; c1->sprite()->canvas = cv; c2->sprite()->canvas = cv; c3->sprite()->canvas = cv; RenderSystem::render(*gfx); blitAndPresent(gfx, cv->getTexture()); auditSwapchain(gfx, "multi", "canvas_2d", bg); } TEST_CASE("graphics.imageAudit.lightsAndXform") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); auto *pR = Light3D::createLight("point"); pR->setPosition(-1.3f, 1.0f, 0.8f); pR->setColor(1.f, 0.25f, 0.2f, 5.f); pR->setRadius(5.f); pR->setEnabled(true); auto *pG = Light3D::createLight("point"); pG->setPosition(1.2f, 0.9f, 0.6f); pG->setColor(0.2f, 1.f, 0.35f, 4.5f); pG->setRadius(5.f); pG->setEnabled(true); auto *pB = Light3D::createLight("point"); pB->setPosition(0.1f, 1.2f, -1.0f); pB->setColor(0.25f, 0.4f, 1.f, 5.f); pB->setRadius(5.f); pB->setEnabled(true); auditGpuFrame(gfx, "lights", "dir_rgb_points", bg); studio.sun->setShadowStrength(0.35f); auditGpuFrame(gfx, "lights", "shadow_soft", bg); studio.sun->setShadowStrength(1.f); studio.subject->setCastShadow(false); studio.subject->setReceiveShadow(false); studio.ground->setReceiveShadow(false); auditGpuFrame(gfx, "lights", "cast_receive_off", bg); studio.subject->setCastShadow(true); studio.subject->setReceiveShadow(true); studio.ground->setReceiveShadow(true); studio.subject->setRotation(0.9f, 0.4f, 0.15f); auditGpuFrame(gfx, "lights", "yaw_pitch_roll", bg); studio.subject->setRotation(0.f, 0.f, 0.f); studio.cam->setUp(0.25f, 0.97f, 0.f); auditGpuFrame(gfx, "lights", "camera_roll", bg); studio.cam->setUp(0.f, 1.f, 0.f); pR->setEnabled(false); pG->setEnabled(false); pB->setEnabled(false); studio.sun->setEnabled(false); studio.subject->setMetallic(1.f); studio.subject->setRoughness(0.12f); studio.cam->setEnvIntensity(1.8f); auditGpuFrame(gfx, "lights", "ibl_metal", bg); } TEST_CASE("graphics.imageAudit.samplerMorph") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); Texture *checker = makeChecker(gfx, 32, 32); studio.subject->setTexture(checker); gfx->setTextureSampler(checker, "nearest", "none", 1.f, 0.f); auditGpuFrame(gfx, "sampler", "nearest", bg); gfx->setTextureSampler(checker, "linear", "none", 1.f, 0.f); auditGpuFrame(gfx, "sampler", "linear", bg); TextureCreateInfo mip = TextureCreateInfo::withMipmaps(true, 8.f); std::vector mipPx(32 * 32 * 4); for (int y = 0; y < 32; ++y) { for (int x = 0; x < 32; ++x) { const bool on = ((x / 4) + (y / 4)) & 1; const size_t i = size_t((y * 32 + x) * 4); mipPx[i + 0] = on ? 210 : 50; mipPx[i + 1] = on ? 210 : 50; mipPx[i + 2] = on ? 215 : 55; mipPx[i + 3] = 255; } } Texture *mipTex = gfx->newTexture(32, 32, mipPx.data(), mip); studio.subject->setTexture(mipTex); gfx->setTextureSampler(mipTex, "linear", "linear", 8.f, 2.5f); auditGpuFrame(gfx, "sampler", "mip_lod_bias", bg); Texture *rep = makeChecker(gfx, 16, 16, true); studio.ground->setTexture(rep); auditGpuFrame(gfx, "sampler", "repeat_uv", bg); const float hs = 0.45f; const float pos[] = { -hs, -hs, -hs, hs, -hs, -hs, hs, hs, -hs, -hs, hs, -hs, -hs, -hs, hs, hs, -hs, hs, hs, hs, hs, -hs, hs, hs, }; float nrm[24]; float uv[16]; for (int i = 0; i < 8; ++i) { float x = pos[i * 3 + 0], y = pos[i * 3 + 1], z = pos[i * 3 + 2]; float len = std::sqrt(x * x + y * y + z * z); if (len < 1e-5f) len = 1.f; nrm[i * 3 + 0] = x / len; nrm[i * 3 + 1] = y / len; nrm[i * 3 + 2] = z / len; uv[i * 2 + 0] = (i & 1) ? 1.f : 0.f; uv[i * 2 + 1] = (i & 2) ? 1.f : 0.f; } const uint32_t idx[] = {0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5, 6, 1, 6, 2}; Mesh *morphMesh = gfx->newMeshFromArrays(pos, nrm, uv, 8, idx, 36); morphMesh->initMorphBase(8, pos, nrm, uv); float delta[24]; for (int i = 0; i < 8; ++i) { delta[i * 3 + 0] = nrm[i * 3 + 0] * 0.28f; delta[i * 3 + 1] = nrm[i * 3 + 1] * 0.28f; delta[i * 3 + 2] = nrm[i * 3 + 2] * 0.28f; } REQUIRE(morphMesh->addMorphTarget("inflate", delta)); studio.subject->setMesh(morphMesh); studio.subject->setTexture(makeSolid(gfx, 200, 160, 90)); gfx->bakeMeshMorph(morphMesh); auditGpuFrame(gfx, "sampler", "morph_base", bg); morphMesh->setMorphWeight("inflate", 1.f); gfx->bakeMeshMorph(morphMesh); } TEST_CASE("graphics.imageAudit.gbufferViews") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); (void)studio; RenderControl *rc = gfx->getRenderControl(); REQUIRE(rc != nullptr); rc->enable("gbuffer"); rc->enable("gbufferAlbedo"); rc->compile(); gfx->setScreenReadbackEnabled(false); for (int i = 0; i < 2; ++i) { RenderSystem3D::render(*gfx); RenderSystem::render(*gfx); } GBuffer *gb = rc->getGBuffer(); REQUIRE(gb != nullptr); Texture *albedo = gb->getAlbedoTexture(); Texture *normal = gb->getNormalTexture(); Texture *depth = gb->getDepthTexture(); REQUIRE(albedo != nullptr); REQUIRE(normal != nullptr); REQUIRE(depth != nullptr); blitAndPresent(gfx, albedo); auditSwapchain(gfx, "gbuffer", "albedo", bg); blitAndPresent(gfx, normal); auditSwapchain(gfx, "gbuffer", "normal", bg); rc->disable("gbuffer"); rc->compile(); } TEST_CASE("graphics.imageAudit.occlusionScatter") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); const int w = 400, h = 300; auto *plate = makeSprite(makeSolid(gfx, 190, 175, 150), 0.f, 0.f, float(w), float(h), false); plate->setCastOcclusion(false); auto *occA = makeSprite(makeSolid(gfx, 40, 40, 48), 70.f, 80.f, 90.f, 110.f, false); occA->setCastOcclusion(true); auto *occB = makeSprite(makeSolid(gfx, 48, 40, 40), 230.f, 90.f, 80.f, 100.f, false); occB->setCastOcclusion(true); gfx->setScreenReadbackEnabled(true); RenderSystem::render(*gfx); std::unique_ptr sceneImg(gfx->newImageData()); REQUIRE(sceneImg.get() != nullptr); Texture *sceneTex = gfx->newTexture(sceneImg.get()); gfx->setScreenReadbackEnabled(false); std::unique_ptr vol(gfx->newVolumetric()); vol->setQuality("low"); vol->setMode("screenspace"); vol->setLightScreenPos(320.f, 40.f, float(w), float(h)); vol->setIntensity(0.9f); Canvas *occMap = gfx->newCanvas(w, h); Canvas *composed = gfx->newCanvas(w, h); REQUIRE(occMap != nullptr); REQUIRE(composed != nullptr); gfx->setCanvas(occMap); vol->beginOcclusionMap(gfx, 320.f, 40.f, 22.f); vol->drawOccluders2D(gfx); gfx->setCanvas(); gfx->setCanvas(composed); gfx->clearScreen(); gfx->drawTexturedRectRGBA(sceneTex, 0, 0, float(w), float(h), 1, 1, 1, 1); vol->scatter(gfx, occMap->getTexture()); gfx->setCanvas(); blitAndPresent(gfx, composed->getTexture()); auditSwapchain(gfx, "scatter", "scatter_on", bg); occA->setCastOcclusion(false); occB->setCastOcclusion(false); gfx->setCanvas(occMap); vol->beginOcclusionMap(gfx, 320.f, 40.f, 22.f); vol->drawOccluders2D(gfx); gfx->setCanvas(); gfx->setCanvas(composed); gfx->clearScreen(); gfx->drawTexturedRectRGBA(sceneTex, 0, 0, float(w), float(h), 1, 1, 1, 1); vol->scatter(gfx, occMap->getTexture()); gfx->setCanvas(); blitAndPresent(gfx, composed->getTexture()); auditSwapchain(gfx, "scatter", "scatter_off", bg); } TEST_CASE("graphics.imageAudit.textureSources") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); std::unique_ptr dst(makeRgba(32, 32, 50, 90, 160)); std::unique_ptr src(makeRgba(16, 16, 220, 70, 40)); dst->paste(src.get(), 8, 8, 0, 0, 16, 16); Texture *pasted = gfx->newTexture(dst.get()); REQUIRE(pasted != nullptr); studio.subject->setTexture(pasted); auditGpuFrame(gfx, "texsrc", "imagedata_paste", bg); auto *fs = eve::filesystem::Filesystem::create(); REQUIRE(fs != nullptr); REQUIRE(fs->setIdentity("ev_ut_image_audit_texfile", true)); REQUIRE(fs->setupWriteDirectory()); const std::string texDir = pathBesideThisSource("Textures"); fs->allowMountingForPath(texDir); REQUIRE(fs->mount(texDir, "", false)); std::unique_ptr diffFile(fs->read("Rock1_Diffuse.png")); std::unique_ptr nrmFile(fs->read("Rock_Normal.png")); REQUIRE(diffFile.get() != nullptr); REQUIRE(nrmFile.get() != nullptr); auto *imgMod = eve::image::Image::create(); std::unique_ptr diffImg(imgMod->newImageData(diffFile.get())); std::unique_ptr nrmImg(imgMod->newImageData(nrmFile.get())); REQUIRE(diffImg.get() != nullptr); REQUIRE(nrmImg.get() != nullptr); Texture *diff = gfx->newTexture(diffImg.get(), TextureCreateInfo::withMipmaps(true)); Texture *nrm = gfx->newTexture(nrmImg.get(), TextureCreateInfo::withMipmaps(true)); REQUIRE(diff != nullptr); REQUIRE(nrm != nullptr); REQUIRE(diff->getMipmapCount() > 1); REQUIRE(diff->getSampler().maxAnisotropy > 1.f); studio.subject->setTexture(diff); studio.subject->setNormalTexture(nrm); auditGpuFrame(gfx, "texsrc", "file_rock", bg); } TEST_CASE("graphics.imageAudit.renderControlToggles") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); RenderControl *rc = gfx->getRenderControl(); REQUIRE(rc != nullptr); rc->disable("depthTest"); rc->compile(); auditGpuFrame(gfx, "rctrl", "depth_test_off", bg); rc->enable("depthTest"); rc->disable("hair"); rc->compile(); auditGpuFrame(gfx, "rctrl", "hair_off", bg); rc->enable("hair"); Shader *hairSh = gfx->newHairShader(); REQUIRE(hairSh != nullptr); auto *card = Renderable3D::create(); card->setMesh(makeHairCard(gfx)); card->setTexture(makeSolid(gfx, 210, 160, 70)); card->setShader(hairSh); card->setHair(true); card->setPosition(0.f, 0.05f, 0.35f); card->setCastShadow(false); studio.subject->setVisible(false); rc->disable("forward"); rc->compile(); makeSprite(makeSolid(gfx, 90, 140, 200), 0.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 200, 90, 70), 200.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 70, 180, 110), 0.f, 150.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 220, 200, 60), 200.f, 150.f, 200.f, 150.f, false); auditGpuFrame(gfx, "rctrl", "forward_off_hair", bg); rc->enable("forward"); rc->compile(); studio.subject->setVisible(true); card->setVisible(false); TextureCreateInfo mip = TextureCreateInfo::withMipmaps(true, 16.f); std::vector px(32 * 32 * 4); for (int y = 0; y < 32; ++y) { for (int x = 0; x < 32; ++x) { const bool on = ((x / 4) + (y / 4)) & 1; const size_t i = size_t((y * 32 + x) * 4); px[i + 0] = on ? 210 : 50; px[i + 1] = on ? 180 : 70; px[i + 2] = on ? 90 : 160; px[i + 3] = 255; } } Texture *aniso = gfx->newTexture(32, 32, px.data(), mip); gfx->setTextureSampler(aniso, "linear", "linear", 16.f, 0.f); studio.ground->setTexture(aniso); studio.subject->setTexture(aniso); auditGpuFrame(gfx, "rctrl", "aniso_mip", bg); } TEST_CASE("graphics.imageAudit.sprite2dCamera") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); Texture *cells[4] = { makeSolid(gfx, 220, 80, 70), makeSolid(gfx, 70, 190, 90), makeSolid(gfx, 70, 110, 220), makeSolid(gfx, 230, 200, 60), }; Renderable2D *packed[4]; for (int i = 0; i < 4; ++i) { packed[i] = makeSprite(cells[i], float((i % 2) * 200), float((i / 2) * 150), 200.f, 150.f); } auditGpu2D(gfx, "sprite2d", "identity", bg); auto *cam = Camera2D::createCamera(); cam->setPosition(200.f, 150.f); cam->setZoom(1.3f); const float visW = 400.f / 1.3f; const float visH = 300.f / 1.3f; const float x0 = 200.f - visW * 0.5f; const float y0 = 150.f - visH * 0.5f; const float cw = visW * 0.5f; const float ch = visH * 0.5f; for (int i = 0; i < 4; ++i) { packed[i]->transform()->x = x0 + float(i % 2) * cw; packed[i]->transform()->y = y0 + float(i / 2) * ch; packed[i]->sprite()->width = cw; packed[i]->sprite()->height = ch; packed[i]->transform()->sx = 1.f; packed[i]->transform()->sy = 1.f; } auditGpu2D(gfx, "sprite2d", "zoom_1_3", bg); cam->setZoom(1.f); for (int i = 0; i < 4; ++i) { packed[i]->transform()->x = float((i % 2) * 200); packed[i]->transform()->y = float((i / 2) * 150); packed[i]->sprite()->width = 100.f; packed[i]->sprite()->height = 75.f; packed[i]->transform()->sx = 2.f; packed[i]->transform()->sy = 2.f; } auditGpu2D(gfx, "sprite2d", "scale_sx_sy", bg); cam->data()->active = false; std::vector frag(custom2d_frag_spv, custom2d_frag_spv + custom2d_frag_spv_count); Shader *sh = gfx->newShaderFromSpv({}, frag); REQUIRE(sh != nullptr); sh->declareFloat("factor"); sh->sendFloat("factor", 0.55f); for (int i = 0; i < 4; ++i) { packed[i]->transform()->x = float((i % 2) * 200); packed[i]->transform()->y = float((i / 2) * 150); packed[i]->sprite()->width = 200.f; packed[i]->sprite()->height = 150.f; packed[i]->transform()->sx = 1.f; packed[i]->transform()->sy = 1.f; packed[i]->sprite()->shader = sh; } auditGpu2D(gfx, "sprite2d", "custom_spv", bg); } TEST_CASE("graphics.imageAudit.overlayFx") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); (void)studio; Texture *sceneTex = capturePresented(gfx); REQUIRE(sceneTex != nullptr); auto *stylize = eve::stylize::Stylize::create(); eve::stylize::StyleChain *chain = stylize->newChain(); eve::stylize::StylePass *cartoon = stylize->newPass(gfx, "cartoon"); eve::stylize::StylePass *pixel = stylize->newPass(gfx, "pixel"); REQUIRE(chain != nullptr); REQUIRE(cartoon != nullptr); REQUIRE(pixel != nullptr); chain->add(cartoon); chain->add(pixel); Canvas *dest = gfx->newCanvas(400, 300); Canvas *temp = gfx->newCanvas(400, 300); chain->apply(gfx, sceneTex, dest, temp); blitAndPresent(gfx, dest->getTexture()); auditSwapchain(gfx, "overlay", "stylechain_cartoon_pixel", bg); delete pixel; delete cartoon; delete chain; auto raw = [&]() { std::ifstream in(pathBesideThisSource("fonts/FontAwesome.ttf"), std::ios::binary); return std::vector((std::istreambuf_iterator(in)), std::istreambuf_iterator()); }(); REQUIRE(!raw.empty()); eve::data::ByteData bytes(raw.data(), raw.size()); std::unique_ptr fd(eve::font::Font::create()->newFontData(&bytes, 32)); REQUIRE(fd.get() != nullptr); Font *gpuFont = gfx->newFont(fd.get(), std::string("\xEF\x80\x80")); REQUIRE(gpuFont != nullptr); gfx->setFont(gpuFont); gfx->setPresentOverlay(&printOverlayThunk, gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); auditGpuFrame(gfx, "overlay", "font_draw_text", bg); gfx->setPresentOverlay(nullptr, nullptr); auto *parts = eve::particles::Particles::create(); eve::particles::ParticleEmitter *em = parts->newEmitter(64); em->setPosition(200.f, 140.f); em->setEmissionArea("rect", 120.f, 70.f); em->setParticleLifetime(8.f, 8.f); em->setSpeed(0.f, 0.f); em->setParticleSize(36.f, 36.f); em->setColorStart(1.f, 0.55f, 0.15f, 1.f); em->setColorEnd(1.f, 0.55f, 0.15f, 1.f); em->setTexture(makeSolid(gfx, 230, 140, 50)); em->emit(48); auditGpuFrame(gfx, "overlay", "particles", bg, false, [&]() { eve::particles::ParticleRenderSystem::render(gfx); }); } TEST_CASE("graphics.imageAudit.voxelAndHair") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); studio.subject->setVisible(false); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); eve::voxel::VoxelWorld world; for (int z = 0; z < 14; ++z) { for (int x = 0; x < 14; ++x) world.setVoxel(x, 0, z, uint8_t(1 + (x + z) % 4)); } for (int y = 1; y < 8; ++y) { for (int z = 5; z < 8; ++z) for (int x = 5; x < 8; ++x) world.setVoxel(x, y, z, 3); } REQUIRE(world.remeshDirty() > 0); Texture *atlas = makeAtlasGrid(gfx, 2, 16); studio.cam->setEye(7.f, 9.f, 18.f); studio.cam->setTarget(7.f, 2.f, 7.f); auto drawVoxels = [&]() { auto cd = studio.cam->data(); const glm::vec3 eye(cd->eyeX, cd->eyeY, cd->eyeZ); const glm::vec3 target(cd->targetX, cd->targetY, cd->targetZ); const glm::vec3 up(cd->upX, cd->upY, cd->upZ); const glm::mat4 viewM = glm::lookAtRH(eye, target, up); const float aspect = float(gfx->getWidth()) / float(std::max(gfx->getHeight(), 1)); const glm::mat4 projM = perspectiveVulkanRH_ZO(cd->fovYDeg * 0.017453292519943295f, aspect, cd->nearZ, cd->farZ); const glm::mat4 vp = projM * viewM; gfx->setMesh3DViewProj(vp); world.selectVisible(&vp[0][0], cd->eyeX, cd->eyeY, cd->eyeZ, 40.f, true); world.drawVisible(gfx, atlas, 2); }; auditGpuFrame(gfx, "voxel_hair", "voxels", bg, false, drawVoxels); studio.cam->setEye(0.f, 1.5f, 4.0f); studio.cam->setTarget(0.f, 0.15f, 0.f); studio.ground->setVisible(true); Shader *hairSh = gfx->newHairShader(); REQUIRE(hairSh != nullptr); auto *card = Renderable3D::create(); card->setMesh(makeHairCard(gfx)); card->setTexture(makeSolid(gfx, 200, 150, 80)); card->setShader(hairSh); card->setHair(true); card->setPosition(0.f, 0.05f, 0.2f); card->setCastShadow(false); auditGpuFrame(gfx, "voxel_hair", "hair_cards", bg); } TEST_CASE("graphics.imageAudit.uiAndMap") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); auto *map = eve::map::Map::create(); Texture *tiles = makeAtlasGrid(gfx, 4, 8); eve::map::TileLayer *layer = map->newLayer(25, 19, 16.f, 16.f); REQUIRE(layer != nullptr); layer->setTileset(tiles, 1, 4); layer->setTilesetTileSize(8, 8); for (int y = 0; y < 19; ++y) for (int x = 0; x < 25; ++x) layer->setTile(x, y, 1 + (x + y) % 15); auto *cam = Camera2D::createCamera(); cam->setPosition(200.f, 150.f); cam->setZoom(1.f); layer->setCamera(cam); auto presentMap = [&]() { gfx->setScreenReadbackEnabled(true); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); map->render(gfx); gfx->present(); }; presentMap(); auditSwapchain(gfx, "uimap", "ortho_fill", bg); cam->setZoom(1.25f); presentMap(); auditSwapchain(gfx, "uimap", "ortho_zoom", bg); cam->setZoom(1.f); auto *ui = eve::ui::UI::create(); REQUIRE(ui->initBackend()); ui->beginFrameAndRender(); presentMap(); auditSwapchain(gfx, "uimap", "imgui_hook", bg); ui->dispatchEvents(); layer->setVisible(false); eve::map::TileLayer *logic = map->newLayer(16, 12, 16.f, 16.f); eve::map::TileLayer *display = map->newLayer(1, 1, 16.f, 16.f); logic->setTileset(tiles, 1, 4); display->setTileset(tiles, 1, 4); display->setTilesetTileSize(8, 8); for (int y = 0; y < 12; ++y) for (int x = 0; x < 16; ++x) logic->setTile(x, y, ((x + y) % 3 == 0) ? 1 : 0); eve::map::DualGridOptions opts; opts.filledGid = 1; opts.hideLogic = true; REQUIRE(eve::map::resolveDualGrid(logic, display, opts, nullptr)); display->setCamera(cam); makeSprite(makeSolid(gfx, 70, 90, 130), 0.f, 0.f, 400.f, 300.f, false)->sprite()->layer = -1; presentMap(); auditSwapchain(gfx, "uimap", "dual_grid", bg); display->setVisible(false); eve::map::TileLayer *iso = map->newLayer(18, 18, 32.f, 16.f); iso->config()->orientation = eve::map::MapOrientation::Isometric; iso->config()->originX = 200.f; iso->config()->originY = 16.f; iso->setTileset(tiles, 1, 4); iso->setTilesetTileSize(8, 8); iso->setCamera(cam); for (int y = 0; y < 18; ++y) for (int x = 0; x < 18; ++x) iso->setTile(x, y, 1 + (x * 3 + y) % 15); presentMap(); auditSwapchain(gfx, "uimap", "isometric", bg); } TEST_CASE("graphics.imageAudit.skinnedStill") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); eve::ref model(loadCesiumMan("ev_ut_image_audit_skin_still")); REQUIRE(model.get() != nullptr); const int mi = findFirstSkinnedMesh(model.get()); REQUIRE(mi >= 0); const aiMesh *src = model->getMesh(mi); REQUIRE(src != nullptr); Mesh *gpu = gfx->newMeshFromAssimp(*src); REQUIRE(gpu != nullptr); studio.subject->setMesh(gpu); studio.subject->setTexture(makeSolid(gfx, 200, 170, 120)); studio.subject->setScale(1.15f, 1.15f, 1.15f); studio.subject->setPosition(0.f, 0.f, 0.f); studio.subject->setYaw(1.2f); studio.cam->setEye(0.f, 1.3f, 3.2f); studio.cam->setTarget(0.f, 0.7f, 0.f); studio.ground->setPosition(0.f, 0.f, 0.f); auditGpuFrame(gfx, "skinned", "bind_pose", bg); } TEST_CASE("graphics.imageAudit.skinnedPose") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); eve::ref model(loadCesiumMan("ev_ut_image_audit_skin_pose")); REQUIRE(model.get() != nullptr); const int mi = findFirstSkinnedMesh(model.get()); REQUIRE(mi >= 0); const aiMesh *src = model->getMesh(mi); REQUIRE(src != nullptr); using eve::animation::AnimClip; using eve::animation::AnimImporter; using eve::animation::AnimPose; using eve::animation::AnimSkeleton; using eve::animation::AnimSkin; std::unique_ptr sk(AnimImporter::loadSkeletonFromModel(model.get())); REQUIRE(sk.get() != nullptr); std::unique_ptr clip(AnimImporter::loadClipFromModel(model.get(), sk.get(), 0)); REQUIRE(clip.get() != nullptr); std::unique_ptr skin(AnimSkin::fromModel(model.get(), mi, sk.get())); REQUIRE(skin.get() != nullptr); auto uploadAt = [&](float t) { AnimPose pose; clip->sample(t, &pose, sk.get()); pose.computeWorld(sk.get()); std::vector pos; REQUIRE(skin->skinPositionsTo(&pose, pos)); return uploadSkinnedAiMesh(gfx, src, pos); }; studio.subject->setMesh(uploadAt(0.f)); studio.subject->setTexture(makeSolid(gfx, 200, 170, 120)); studio.subject->setScale(1.15f, 1.15f, 1.15f); studio.subject->setPosition(0.f, 0.f, 0.f); studio.subject->setYaw(1.2f); studio.cam->setEye(0.f, 1.3f, 3.2f); studio.cam->setTarget(0.f, 0.7f, 0.f); studio.ground->setPosition(0.f, 0.f, 0.f); auditGpuFrame(gfx, "skinned", "pose_t0", bg); studio.subject->setMesh(uploadAt(clip->getDuration() * 0.5f)); auditGpuFrame(gfx, "skinned", "pose_tmid", bg); } TEST_CASE("graphics.imageAudit.reloadTex") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); auto *fs = eve::filesystem::Filesystem::create(); REQUIRE(fs->setIdentity("ev_ut_image_audit_texreload", true)); REQUIRE(fs->setupWriteDirectory()); const std::string texDir = pathBesideThisSource("Textures"); fs->allowMountingForPath(texDir); REQUIRE(fs->mount(texDir, "", false)); Texture *diff = gfx->newTextureFromFile("Rock1_Diffuse.png"); REQUIRE(diff != nullptr); studio.subject->setTexture(diff); auditGpuFrame(gfx, "reload", "before", bg); REQUIRE(gfx->reloadTextureFromFile("Rock1_Diffuse.png")); auditGpuFrame(gfx, "reload", "after", bg); } TEST_CASE("graphics.imageAudit.morphInflate") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); const float hs = 0.45f; const float pos[] = { -hs, -hs, -hs, hs, -hs, -hs, hs, hs, -hs, -hs, hs, -hs, -hs, -hs, hs, hs, -hs, hs, hs, hs, hs, -hs, hs, hs, }; float nrm[24]; float uv[16]; for (int i = 0; i < 8; ++i) { float x = pos[i * 3 + 0], y = pos[i * 3 + 1], z = pos[i * 3 + 2]; float len = std::sqrt(x * x + y * y + z * z); if (len < 1e-5f) len = 1.f; nrm[i * 3 + 0] = x / len; nrm[i * 3 + 1] = y / len; nrm[i * 3 + 2] = z / len; uv[i * 2 + 0] = (i & 1) ? 1.f : 0.f; uv[i * 2 + 1] = (i & 2) ? 1.f : 0.f; } const uint32_t idx[] = {0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5, 6, 1, 6, 2}; Mesh *morphMesh = gfx->newMeshFromArrays(pos, nrm, uv, 8, idx, 36); morphMesh->initMorphBase(8, pos, nrm, uv); float delta[24]; for (int i = 0; i < 8; ++i) { delta[i * 3 + 0] = nrm[i * 3 + 0] * 0.28f; delta[i * 3 + 1] = nrm[i * 3 + 1] * 0.28f; delta[i * 3 + 2] = nrm[i * 3 + 2] * 0.28f; } REQUIRE(morphMesh->addMorphTarget("inflate", delta)); studio.subject->setMesh(morphMesh); studio.subject->setTexture(makeSolid(gfx, 200, 160, 90)); gfx->bakeMeshMorph(morphMesh); auditGpuFrame(gfx, "morph", "base", bg); morphMesh->setMorphWeight("inflate", 1.f); gfx->bakeMeshMorph(morphMesh); auditGpuFrame(gfx, "morph", "inflate", bg); morphMesh->setMorphWeight("inflate", 0.45f); gfx->bakeMeshMorph(morphMesh); auditGpuFrame(gfx, "morph", "inflate_mid", bg); } TEST_CASE("graphics.imageAudit.procgenAssets") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); auto *pg = eve::procgen::Procgen::create(); REQUIRE(pg != nullptr); auto paramsResult = eve::procgen::Procgen::newParamsHandle(); REQUIRE(paramsResult.ok()); auto paramsHandle = std::move(paramsResult).takeValue(); auto paramsView = eve::procgen::Procgen::resolve(paramsHandle); REQUIRE(paramsView.isBound()); eve::procgen::Params ¶ms = *paramsView; params.setSeed(11); params.setSize(64, 64); params.setInt("colors", 6); params.setInt("pixelSize", 2); params.setInt("seamless", 1); const char *recipes[] = {"tex.soil", "tex.stone", "tex.marble"}; for (const char *id : recipes) { auto textureView = pg->generateTextureBorrowed(id, paramsHandle, gfx); REQUIRE(textureView.isBound()); studio.subject->setTexture(textureView.get()); auditGpuFrame(gfx, "procgen", id, bg); } auto normalResult = pg->generateNormalImageHandle("tex.stone", paramsHandle); REQUIRE(normalResult.ok()); auto normalHandle = std::move(normalResult).takeValue(); CHECK(!eve::procgen::Procgen::isStale(normalHandle)); auto normalView = eve::procgen::Procgen::resolve(normalHandle); REQUIRE(normalView.isBound()); Texture *nrm = gfx->newTextureFromImageData(normalView.get(), true, true); REQUIRE(nrm != nullptr); studio.subject->setNormalTexture(nrm); auto stoneTextureView = pg->generateTextureBorrowed("tex.stone", paramsHandle, gfx); REQUIRE(stoneTextureView.isBound()); studio.subject->setTexture(stoneTextureView.get()); auditGpuFrame(gfx, "procgen", "stone_normal", bg); studio.subject->setNormalTexture(nullptr); auto releaseNormalResult = eve::procgen::Procgen::release(normalHandle); REQUIRE(releaseNormalResult.ok()); CHECK(eve::procgen::Procgen::isStale(normalHandle)); params.setSeed(1); params.setInt("resolution", 16); params.setString("field", "sphere"); params.setFloat("radius", 0.7f); params.setFloat("isolevel", 0.f); auto meshView = pg->generateMeshBorrowed("mesh.marchingcubes", paramsHandle, gfx); REQUIRE(meshView.isBound()); studio.subject->setMesh(meshView.get()); auto marbleTextureView = pg->generateTextureBorrowed("tex.marble", paramsHandle, gfx); REQUIRE(marbleTextureView.isBound()); studio.subject->setTexture(marbleTextureView.get()); studio.subject->setScale(0.7f, 0.7f, 0.7f); auditGpuFrame(gfx, "procgen", "marching_cubes", bg); auto releaseParamsResult = eve::procgen::Procgen::release(paramsHandle); REQUIRE(releaseParamsResult.ok()); Texture *sceneTex = capturePresented(gfx); REQUIRE(sceneTex != nullptr); Canvas *dest = gfx->newCanvas(400, 300); REQUIRE(dest != nullptr); std::unique_ptr aa(gfx->newAntiAliasing()); aa->setMode("ssaa"); aa->setQuality("medium"); aa->applyTo(gfx, sceneTex, dest); blitAndPresent(gfx, dest->getTexture()); auditSwapchain(gfx, "procgen", "ssaa_medium", bg); aa->setQuality("high"); aa->applyTo(gfx, sceneTex, dest); blitAndPresent(gfx, dest->getTexture()); auditSwapchain(gfx, "procgen", "ssaa_high", bg); } TEST_CASE("graphics.imageAudit.mapFovHex") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); auto *map = eve::map::Map::create(); Texture *tiles = makeAtlasGrid(gfx, 4, 8); eve::map::TileLayer *layer = map->newLayer(25, 19, 16.f, 16.f); REQUIRE(layer != nullptr); layer->setTileset(tiles, 1, 4); layer->setTilesetTileSize(8, 8); for (int y = 0; y < 19; ++y) for (int x = 0; x < 25; ++x) layer->setTile(x, y, 1 + (x + y) % 15); auto *cam = Camera2D::createCamera(); cam->setPosition(200.f, 150.f); cam->setZoom(1.f); layer->setCamera(cam); auto presentMap = [&]() { gfx->setScreenReadbackEnabled(true); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); map->render(gfx); gfx->present(); }; std::unique_ptr fov(map->newFov(layer)); REQUIRE(fov.get() != nullptr); fov->setAlgorithm("shadowcast"); fov->setRadiusMetric("chebyshev"); for (int y = 0; y < 19; ++y) { if (y == 9) continue; fov->setOpaque(8, y, true); layer->setTile(8, y, 2); } fov->addRevealer(4, 9, 10); fov->compute(); for (int y = 0; y < 19; ++y) { for (int x = 0; x < 25; ++x) { const bool vis = fov->isVisible(x, y); layer->setTile(x, y, vis ? (1 + (x + y) % 8) : (9 + (x * 3 + y) % 7)); } } presentMap(); auditSwapchain(gfx, "mapfov", "shadowcast", bg); fov->setAlgorithm("raycast"); fov->compute(); for (int y = 0; y < 19; ++y) { for (int x = 0; x < 25; ++x) { const bool vis = fov->isVisible(x, y); layer->setTile(x, y, vis ? (1 + (x + y) % 8) : (9 + (x * 3 + y) % 7)); } } presentMap(); auditSwapchain(gfx, "mapfov", "raycast", bg); std::vector r8; REQUIRE(fov->fillMaskR8(r8)); const int mw = fov->getWidth(); const int mh = fov->getHeight(); REQUIRE(mw == 25); REQUIRE(mh == 19); std::vector rgba(size_t(mw * mh * 4)); for (int i = 0; i < mw * mh; ++i) { const uint8_t v = r8[size_t(i)]; const size_t o = size_t(i) * 4u; rgba[o + 0] = uint8_t(50 + int(v) * 180 / 255); rgba[o + 1] = uint8_t(90 + int(v) * 110 / 255); rgba[o + 2] = uint8_t(200 - int(v) * 70 / 255); rgba[o + 3] = 255; } Texture *maskVis = gfx->newTexture(mw, mh, rgba.data()); REQUIRE(maskVis != nullptr); REQUIRE(fov->buildMaskTexture(gfx) != nullptr); layer->setVisible(false); auto *maskSp = makeSprite(maskVis, 0.f, 0.f, 400.f, 300.f, false); presentMap(); auditSwapchain(gfx, "mapfov", "mask_vis", bg); maskSp->sprite()->visible = false; auto *pg = eve::procgen::Procgen::create(); auto paramsResult = eve::procgen::Procgen::newParamsHandle(); REQUIRE(paramsResult.ok()); auto paramsHandle = std::move(paramsResult).takeValue(); auto paramsView = eve::procgen::Procgen::resolve(paramsHandle); REQUIRE(paramsView.isBound()); paramsView->setSeed(1); paramsView->setSize(25, 19); auto gridResult = pg->generateHandle("dungeon.bsp", paramsHandle); REQUIRE(gridResult.ok()); auto gridHandle = std::move(gridResult).takeValue(); auto gridView = eve::procgen::Procgen::resolve(gridHandle); REQUIRE(gridView.isBound()); pg->setPaletteGid("audit", "wall", 2); pg->setPaletteGid("audit", "floor", 5); pg->setPaletteGid("audit", "corridor", 6); layer->setVisible(true); auto applyResult = pg->applyToLayer(gridHandle, "audit", *layer); REQUIRE(applyResult.ok()); for (int y = 0; y < 19; ++y) for (int x = 0; x < 25; ++x) if (layer->getTile(x, y) == 0) layer->setTile(x, y, 5); auto releaseGridResult = eve::procgen::Procgen::release(gridHandle); REQUIRE(releaseGridResult.ok()); presentMap(); auditSwapchain(gfx, "mapfov", "dungeon_bsp", bg); layer->setVisible(false); auto releaseParamsResult = eve::procgen::Procgen::release(paramsHandle); REQUIRE(releaseParamsResult.ok()); eve::map::TileLayer *hex = map->newLayer(18, 16, 32.f, 28.f); hex->config()->orientation = eve::map::MapOrientation::Hexagonal; hex->config()->hexSideLength = 16.f; hex->config()->originX = 16.f; hex->config()->originY = 8.f; hex->setTileset(tiles, 1, 4); hex->setTilesetTileSize(8, 8); hex->setCamera(cam); for (int y = 0; y < 16; ++y) for (int x = 0; x < 18; ++x) hex->setTile(x, y, 1 + (x * 3 + y) % 15); makeSprite(makeSolid(gfx, 70, 90, 130), 0.f, 0.f, 400.f, 300.f, false)->sprite()->layer = -1; presentMap(); auditSwapchain(gfx, "mapfov", "hexagonal", bg); hex->setVisible(false); eve::map::TileLayer *stag = map->newLayer(18, 20, 32.f, 16.f); stag->config()->orientation = eve::map::MapOrientation::Staggered; stag->config()->staggerAxis = eve::map::StaggerAxis::Y; stag->config()->staggerIndex = eve::map::StaggerIndex::Odd; stag->config()->originX = 0.f; stag->config()->originY = 0.f; stag->setTileset(tiles, 1, 4); stag->setTilesetTileSize(8, 8); stag->setCamera(cam); for (int y = 0; y < 20; ++y) for (int x = 0; x < 18; ++x) stag->setTile(x, y, 1 + (x + y * 2) % 15); presentMap(); auditSwapchain(gfx, "mapfov", "staggered", bg); } TEST_CASE("graphics.imageAudit.particleSkin") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); eve::ref model(loadCesiumMan("ev_ut_image_audit_skin_part")); REQUIRE(model.get() != nullptr); const int mi = findFirstSkinnedMesh(model.get()); REQUIRE(mi >= 0); const aiMesh *src = model->getMesh(mi); REQUIRE(src != nullptr); Mesh *gpu = gfx->newMeshFromAssimp(*src); REQUIRE(gpu != nullptr); studio.subject->setMesh(gpu); studio.subject->setTexture(makeSolid(gfx, 200, 170, 120)); studio.subject->setScale(1.15f, 1.15f, 1.15f); studio.subject->setPosition(0.f, 0.f, 0.f); studio.subject->setYaw(1.2f); studio.cam->setEye(0.f, 1.3f, 3.2f); studio.cam->setTarget(0.f, 0.7f, 0.f); studio.ground->setPosition(0.f, 0.f, 0.f); using eve::animation::AnimClip; using eve::animation::AnimImporter; using eve::animation::AnimPose; using eve::animation::AnimSkeleton; using eve::animation::AnimSkin; std::unique_ptr sk(AnimImporter::loadSkeletonFromModel(model.get())); REQUIRE(sk.get() != nullptr); std::unique_ptr clip(AnimImporter::loadClipFromModel(model.get(), sk.get(), 0)); REQUIRE(clip.get() != nullptr); std::unique_ptr skin(AnimSkin::fromModel(model.get(), mi, sk.get())); REQUIRE(skin.get() != nullptr); std::unique_ptr pose(new AnimPose()); clip->sample(0.f, pose.get(), sk.get()); pose->computeWorld(sk.get()); REQUIRE(skin->updateSkinnedPositions(pose.get())); auto *parts = eve::particles::Particles::create(); eve::particles::ParticleEmitter *em = parts->newEmitter(128); em->setParticleLifetime(8.f, 8.f); em->setSpeed(0.f, 0.f); em->setParticleSize(28.f, 28.f); em->setColorStart(1.f, 0.45f, 0.15f, 1.f); em->setColorEnd(1.f, 0.45f, 0.15f, 1.f); em->setTexture(makeSolid(gfx, 230, 120, 40)); em->setEmissionArea("none", 0.f, 0.f); em->setSkinScale(90.f); em->setSkinPlane("xy"); em->setSkinSource(skin.get(), pose.get()); REQUIRE(em->hasSkinSource()); em->emitFromSkin(64); REQUIRE(em->getCount() >= 16); auditGpuFrame(gfx, "partskin", "emit_from_skin", bg, false, [&]() { eve::particles::ParticleRenderSystem::render(gfx); }); em->clearSkinSource(); std::unique_ptr attachSk(new AnimSkeleton()); const int root = attachSk->addBone("root", -1); std::unique_ptr bonePose(new AnimPose()); attachSk->applyBindPose(bonePose.get()); bonePose->setLocalPosition(root, 200.f, 160.f, 0.f); bonePose->computeWorld(attachSk.get()); em->setParticleSize(40.f, 40.f); em->attachToBone(bonePose.get(), root); em->syncAttach(); em->emit(24); auditGpuFrame(gfx, "partskin", "attach_bone", bg, false, [&]() { eve::particles::ParticleRenderSystem::render(gfx); }); } TEST_CASE("graphics.imageAudit.materialHair") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); auto *auditBanner = makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); studio.subject->setReceiveLight(false); studio.subject->setTexture(makeSolid(gfx, 220, 90, 70)); auditGpuFrame(gfx, "mathair", "receive_off", bg); studio.subject->setReceiveLight(true); Material *hairMat = gfx->newMaterial(); hairMat->setShadingModel("hair"); hairMat->setAlbedoTexture(makeSolid(gfx, 210, 160, 70)); Shader *hairSh = gfx->newHairShader(); REQUIRE(hairSh != nullptr); hairMat->setShader(hairSh); auto *card = Renderable3D::create(); card->setMesh(makeHairCard(gfx)); card->setMaterial(hairMat); card->setPosition(0.f, 0.05f, 0.35f); card->setCastShadow(false); auditGpuFrame(gfx, "mathair", "hair_material", bg); // General-purpose transparent PBR materials must use the transparent // forward queue without pretending to be hair. Isolate this frame from the // preceding hair audit, then place one overlapping card behind the opaque // sphere and one in front to make depth testing and compositing unambiguous. card->setPosition(100.f, 100.f, 100.f); auditBanner->sprite()->visible = false; studio.subject->setTexture(makeSolid(gfx, 205, 210, 220)); Material *glassBack = gfx->newMaterial(); glassBack->setSurfaceMode("transparent"); glassBack->setTint(0.08f, 0.35f, 1.f, 0.48f); glassBack->setSortPriority(0); auto *backCard = Renderable3D::create(); backCard->setMesh(makeHairCard(gfx)); backCard->setMaterial(glassBack); backCard->setPosition(-0.22f, -0.05f, -0.3f); backCard->setCastShadow(false); Material *glassFront = gfx->newMaterial(); glassFront->setSurfaceMode("transparent"); glassFront->setTint(1.f, 0.12f, 0.04f, 0.42f); glassFront->setSortPriority(0); auto *frontCard = Renderable3D::create(); frontCard->setMesh(makeHairCard(gfx)); frontCard->setMaterial(glassFront); frontCard->setPosition(0.22f, -0.05f, 0.65f); frontCard->setCastShadow(false); auditGpuFrame(gfx, "mathair", "transparent_pbr_overlap", bg, false, {}, "mathair_transparent_pbr_overlap.png"); studio.subject->setVisible(false); makeSprite(makeSolid(gfx, 90, 140, 200), 0.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 200, 90, 70), 200.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 70, 180, 110), 0.f, 150.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 220, 200, 60), 200.f, 150.f, 200.f, 150.f, false); auditGpuFrame(gfx, "mathair", "hair_only_packed", bg); } TEST_CASE("graphics.imageAudit.clusteredHair") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); studio.sun->setEnabled(false); studio.cam->setAmbient(0.08f, 0.08f, 0.09f); RenderControl *rc = gfx->getRenderControl(); REQUIRE(rc != nullptr); rc->enable("clustered"); rc->enable("hair"); rc->enable("forward"); rc->compile(); const float cols[12][3] = { {1.f, 0.25f, 0.2f}, {0.2f, 1.f, 0.3f}, {0.25f, 0.4f, 1.f}, {1.f, 0.9f, 0.2f}, {1.f, 0.4f, 0.9f}, {0.2f, 0.95f, 0.9f}, {1.f, 0.55f, 0.15f}, {0.6f, 0.3f, 1.f}, {0.4f, 1.f, 0.55f}, {1.f, 0.2f, 0.45f}, {0.55f, 0.75f, 1.f}, {0.95f, 0.95f, 0.7f}, }; for (int i = 0; i < 12; ++i) { auto *l = Light3D::createLight("point"); const float a = float(i) / 12.f * 6.2831853f; l->setPosition(std::cos(a) * 1.6f, 0.5f + 0.2f * std::sin(a * 2.f), std::sin(a) * 1.6f); l->setColor(cols[i][0], cols[i][1], cols[i][2], 3.2f); l->setRadius(3.5f); l->setEnabled(true); } auditGpuFrame(gfx, "cluster", "twelve_points", bg); Shader *hairSh = gfx->newHairShader(); REQUIRE(hairSh != nullptr); auto *card = Renderable3D::create(); card->setMesh(makeHairCard(gfx)); card->setTexture(makeSolid(gfx, 210, 160, 70)); card->setShader(hairSh); card->setHair(true); card->setPosition(0.f, 0.05f, 0.4f); card->setCastShadow(false); auditGpuFrame(gfx, "cluster", "hair_plus_clustered", bg); rc->disable("clustered"); rc->compile(); auditGpuFrame(gfx, "cluster", "packed_fallback", bg); rc->enable("clustered"); rc->compile(); } TEST_CASE("graphics.imageAudit.clothFluid2D") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); makeSprite(makeSolid(gfx, 90, 140, 200), 0.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 200, 90, 70), 200.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 70, 180, 110), 0.f, 150.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 220, 200, 60), 200.f, 150.f, 200.f, 150.f, false); auto presentExtra = [&](const std::function &extra) { gfx->setScreenReadbackEnabled(true); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); std::vector items; RenderSystem::collectSprites(items); RenderSystem::drawItems(*gfx, items, false); extra(); gfx->present(); }; eve::physics::Cloth cloth(20, 16, 20.f, 8.f, 8.f); cloth.pinTopRow(); cloth.setGravity(0.f, 0.f); cloth.setColor(0.95f, 0.35f, 0.2f, 1.f); cloth.update(0.016f); presentExtra([&]() { cloth.draw(gfx); }); auditSwapchain(gfx, "phys2d", "cloth", bg); cloth.setGravity(0.f, 400.f); cloth.applyForce(80.f, 0.f); for (int i = 0; i < 8; ++i) cloth.update(0.016f); presentExtra([&]() { cloth.draw(gfx); }); auditSwapchain(gfx, "phys2d", "cloth_wind", bg); eve::physics::Fluid2D fluid(512); fluid.setGravity(0.f, 0.f); fluid.setParticleSize(18.f); fluid.setColor(0.25f, 0.6f, 0.95f, 1.f); fluid.setBounds(0.f, 0.f, 400.f, 300.f); for (int y = 30; y < 280; y += 16) for (int x = 30; x < 380; x += 16) fluid.emit(float(x), float(y), 1); REQUIRE(fluid.getParticleCount() > 100); presentExtra([&]() { fluid.draw(gfx); }); auditSwapchain(gfx, "phys2d", "fluid", bg); } TEST_CASE("graphics.imageAudit.particlePresets") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); (void)studio; makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); auto *parts = eve::particles::Particles::create(); eve::particles::ParticleEmitter *em = parts->newEmitter(128); em->setTexture(makeSolid(gfx, 230, 160, 50)); em->setPosition(200.f, 160.f); auto burst = [&](const char *preset, const char *phase, float r, float g, float b) { em->reset(); em->applyPreset(preset); em->setParticleLifetime(8.f, 8.f); em->setSpeed(0.f, 0.f); em->setParticleSize(32.f, 32.f); em->setColorStart(r, g, b, 1.f); em->setColorEnd(r, g, b, 1.f); em->setEmissionArea("rect", 140.f, 80.f); em->emit(48); REQUIRE(em->getCount() >= 16); auditGpuFrame(gfx, "presets", phase, bg, false, [&]() { eve::particles::ParticleRenderSystem::render(gfx); }); }; burst("spark", "spark", 1.f, 0.85f, 0.25f); burst("smoke", "smoke", 0.75f, 0.78f, 0.82f); burst("fire", "fire", 1.f, 0.4f, 0.12f); em->reset(); REQUIRE(em->applyConfig("{\"preset\":\"fire\",\"x\":200,\"y\":160,\"particleSize\":[36,36]}")); em->setParticleLifetime(8.f, 8.f); em->setSpeed(0.f, 0.f); em->setColorStart(1.f, 0.45f, 0.15f, 1.f); em->setColorEnd(1.f, 0.45f, 0.15f, 1.f); em->setEmissionArea("rect", 140.f, 80.f); em->emit(40); auditGpuFrame(gfx, "presets", "json_fire", bg, false, [&]() { eve::particles::ParticleRenderSystem::render(gfx); }); } TEST_CASE("graphics.imageAudit.mapPath") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); auto *map = eve::map::Map::create(); Texture *tiles = makeAtlasGrid(gfx, 4, 8); eve::map::TileLayer *layer = map->newLayer(25, 19, 16.f, 16.f); REQUIRE(layer != nullptr); layer->setTileset(tiles, 1, 4); layer->setTilesetTileSize(8, 8); for (int y = 0; y < 19; ++y) for (int x = 0; x < 25; ++x) layer->setTile(x, y, 5); auto *cam = Camera2D::createCamera(); cam->setPosition(200.f, 150.f); cam->setZoom(1.f); layer->setCamera(cam); auto presentMap = [&]() { gfx->setScreenReadbackEnabled(true); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); map->render(gfx); gfx->present(); }; std::unique_ptr pf(map->newPathfinder(layer)); REQUIRE(pf.get() != nullptr); pf->setDiagonal(true); for (int y = 0; y < 19; ++y) { if (y == 9) continue; pf->setBlocked(12, y, true); layer->setTile(12, y, 2); } std::unique_ptr path(pf->findPath(1, 9, 23, 9)); REQUIRE(path.get() != nullptr); REQUIRE(path->getLength() > 4); for (int i = 0; i < path->getLength(); ++i) layer->setTile(path->getX(i), path->getY(i), 4); layer->setTile(1, 9, 8); layer->setTile(23, 9, 12); presentMap(); auditSwapchain(gfx, "mappath", "astar", bg); std::unique_ptr field(pf->buildFlowField(23, 9)); REQUIRE(field.get() != nullptr); for (int y = 0; y < 19; ++y) { for (int x = 0; x < 25; ++x) { if (layer->getTile(x, y) == 2) continue; layer->setTile(x, y, field->isReachable(x, y) ? (6 + (x + y) % 3) : 10); } } presentMap(); auditSwapchain(gfx, "mappath", "flow_field", bg); std::unique_ptr group(pf->findGroupPath(2, 2, 23, 9)); REQUIRE(group.get() != nullptr); for (int y = 0; y < 19; ++y) for (int x = 0; x < 25; ++x) if (layer->getTile(x, y) != 2) layer->setTile(x, y, 5); for (int i = 0; i < group->getLength(); ++i) layer->setTile(group->getX(i), group->getY(i), 14); presentMap(); auditSwapchain(gfx, "mappath", "group_flow", bg); } TEST_CASE("graphics.imageAudit.reloadTexBytes") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); auto *fs = eve::filesystem::Filesystem::create(); REQUIRE(fs->setIdentity("ev_ut_image_audit_texbytes", true)); REQUIRE(fs->setupWriteDirectory()); [[maybe_unused]] auto *const imageModule = eve::image::Image::create(); auto writeSolidPng = [&](uint8_t r, uint8_t g, uint8_t b) { eve::image::ImageData img(32, 32, "RGBA8"); auto *px = static_cast(img.getData()); for (int i = 0; i < 32 * 32; ++i) { px[i * 4 + 0] = r; px[i * 4 + 1] = g; px[i * 4 + 2] = b; px[i * 4 + 3] = 255; } std::unique_ptr encoded( img.encode(medialoader::FormatHandler::ENCODED_PNG, "audit_reload_swap.png", true)); REQUIRE(encoded.get() != nullptr); }; writeSolidPng(220, 60, 50); Texture *tex = gfx->newTextureFromFile("audit_reload_swap.png"); REQUIRE(tex != nullptr); studio.subject->setTexture(tex); auditGpuFrame(gfx, "texbytes", "red", bg); writeSolidPng(50, 200, 80); REQUIRE(gfx->reloadTextureFromFile("audit_reload_swap.png")); auditGpuFrame(gfx, "texbytes", "green", bg); writeSolidPng(60, 90, 220); REQUIRE(gfx->reloadTextureFromFile("audit_reload_swap.png")); auditGpuFrame(gfx, "texbytes", "blue", bg); } TEST_CASE("graphics.imageAudit.postQuality") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); (void)studio; makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); const int w = 400, h = 300; RenderControl *rc = gfx->getRenderControl(); REQUIRE(rc != nullptr); rc->enable("gbuffer"); rc->enable("gbufferAlbedo"); rc->compile(); Texture *sceneTex = capturePresented(gfx); REQUIRE(sceneTex != nullptr); GBuffer *gb = rc->getGBuffer(); REQUIRE(gb != nullptr); Texture *depth = gb->getDepthTexture(); REQUIRE(depth != nullptr); Canvas *composed = gfx->newCanvas(w, h); REQUIRE(composed != nullptr); std::unique_ptr ao(gfx->newAmbientOcclusion()); ao->setCamera(0.f, 1.5f, 4.0f, 0.f, 0.15f, 0.f, 0.f, 1.f, 0.f, 60.f, float(w) / float(h), 0.05f, 40.f); ao->setMode("ssao"); const char *qualities[] = {"low", "medium", "high"}; for (const char *q : qualities) { ao->setQuality(q); Canvas *aoMap = gfx->newCanvas(ao->resolutionFor(w), ao->resolutionFor(h)); REQUIRE(aoMap != nullptr); ao->computeTo(gfx, depth, aoMap); gfx->setCanvas(composed); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); gfx->drawTexturedRectRGBA(sceneTex, 0, 0, float(w), float(h), 1, 1, 1, 1); ao->applyOverlay(gfx, aoMap->getTexture()); gfx->setCanvas(); blitAndPresent(gfx, composed->getTexture()); auditSwapchain(gfx, "postq", (std::string("ssao_") + q).c_str(), bg); } std::unique_ptr vol(gfx->newVolumetric()); vol->setCamera(0.f, 1.5f, 4.0f, 0.f, 0.15f, 0.f, 0.f, 1.f, 0.f, 60.f, float(w) / float(h), 0.05f, 40.f); vol->setFogColor(0.45f, 0.5f, 0.55f); vol->setFogHeight(-0.7f); vol->setFogHeightFalloff(0.8f); vol->setFogStart(1.2f); vol->setFogEnd(9.f); vol->setDensity(0.35f); vol->setMode("fog"); for (const char *q : qualities) { vol->setQuality(q); gfx->setCanvas(composed); gfx->clearScreen(); gfx->drawTexturedRectRGBA(sceneTex, 0, 0, float(w), float(h), 1, 1, 1, 1); vol->applyFog(gfx, depth); gfx->setCanvas(); blitAndPresent(gfx, composed->getTexture()); auditSwapchain(gfx, "postq", (std::string("fog_") + q).c_str(), bg); } vol->setMode("raymarch"); vol->setLightDirection(0.45f, 1.f, 0.35f); vol->setLightScreenUV(0.62f, 0.28f); vol->setIntensity(0.8f); vol->setQuality("high"); gfx->setCanvas(composed); gfx->clearScreen(); gfx->drawTexturedRectRGBA(sceneTex, 0, 0, float(w), float(h), 1, 1, 1, 1); vol->rayMarch(gfx, depth); gfx->setCanvas(); blitAndPresent(gfx, composed->getTexture()); auditSwapchain(gfx, "postq", "raymarch_high", bg); rc->disable("gbuffer"); rc->compile(); } TEST_CASE("graphics.imageAudit.dualIsoFov") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); auto *map = eve::map::Map::create(); Texture *tiles = makeAtlasGrid(gfx, 4, 8); auto *cam = Camera2D::createCamera(); cam->setPosition(200.f, 150.f); cam->setZoom(1.f); auto presentMap = [&]() { gfx->setScreenReadbackEnabled(true); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); map->render(gfx); gfx->present(); }; eve::map::TileLayer *ortho = map->newLayer(25, 19, 16.f, 16.f); ortho->setTileset(tiles, 1, 4); ortho->setTilesetTileSize(8, 8); ortho->setCamera(cam); for (int y = 0; y < 19; ++y) for (int x = 0; x < 25; ++x) ortho->setTile(x, y, 1 + (x + y) % 15); std::unique_ptr fov(map->newFov(ortho)); REQUIRE(fov.get() != nullptr); fov->setRadiusMetric("chebyshev"); for (int y = 0; y < 19; ++y) { if (y == 9) continue; fov->setOpaque(8, y, true); ortho->setTile(8, y, 2); } fov->addRevealer(4, 9, 10); auto paintFov = [&]() { for (int y = 0; y < 19; ++y) { for (int x = 0; x < 25; ++x) { const bool vis = fov->isVisible(x, y); ortho->setTile(x, y, vis ? (1 + (x + y) % 8) : (9 + (x * 3 + y) % 7)); } } }; fov->setAlgorithm("permissive"); fov->compute(); paintFov(); presentMap(); auditSwapchain(gfx, "dualiso", "fov_permissive", bg); fov->setAlgorithm("rectangle"); fov->compute(); paintFov(); presentMap(); auditSwapchain(gfx, "dualiso", "fov_rectangle", bg); ortho->setVisible(false); eve::map::TileLayer *logic = map->newLayer(16, 16, 32.f, 16.f); eve::map::TileLayer *display = map->newLayer(1, 1, 32.f, 16.f); logic->config()->orientation = eve::map::MapOrientation::Isometric; logic->config()->originX = 200.f; logic->config()->originY = 16.f; logic->setTileset(tiles, 1, 4); display->setTileset(tiles, 1, 4); display->setTilesetTileSize(8, 8); for (int y = 0; y < 16; ++y) for (int x = 0; x < 16; ++x) logic->setTile(x, y, ((x + y) % 3 == 0) ? 1 : 2); eve::map::DualGridOptions opts; opts.filledGid = 1; opts.hideLogic = true; REQUIRE(eve::map::resolveDualGrid(logic, display, opts, nullptr)); display->setCamera(cam); makeSprite(makeSolid(gfx, 70, 90, 130), 0.f, 0.f, 400.f, 300.f, false)->sprite()->layer = -1; presentMap(); auditSwapchain(gfx, "dualiso", "dual_isometric", bg); } TEST_CASE("graphics.imageAudit.avatarImage") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); auto *mod = eve::avatar::Avatar::create(); REQUIRE(mod != nullptr); eve::avatar::AvatarInstance *av = mod->newImageAvatar(); REQUIRE(av != nullptr); av->setPosition(0.f, 0.f); REQUIRE(av->addLayer("tl", makeSolid(gfx, 90, 140, 200), 0)); REQUIRE(av->addLayer("tr", makeSolid(gfx, 200, 90, 70), 1)); REQUIRE(av->addLayer("bl", makeSolid(gfx, 70, 180, 110), 2)); REQUIRE(av->addLayer("br", makeSolid(gfx, 220, 200, 60), 3)); av->setLayerSize("tl", 200.f, 150.f); av->setLayerSize("tr", 200.f, 150.f); av->setLayerSize("bl", 200.f, 150.f); av->setLayerSize("br", 200.f, 150.f); av->setLayerOffset("tr", 200.f, 0.f); av->setLayerOffset("bl", 0.f, 150.f); av->setLayerOffset("br", 200.f, 150.f); av->sync(); auditGpu2D(gfx, "avatar", "layers_packed", bg); av->setLayerColor("tl", 0.95f, 0.35f, 0.25f, 1.f); av->setLayerColor("tr", 0.25f, 0.55f, 0.95f, 1.f); av->sync(); auditGpu2D(gfx, "avatar", "recolor", bg); REQUIRE(av->defineExpression("flash", "tl=1;tr=1;bl=1;br=1")); REQUIRE(av->applyExpression("flash")); av->setLayerColor("bl", 0.95f, 0.85f, 0.2f, 1.f); av->setLayerColor("br", 0.7f, 0.3f, 0.9f, 1.f); av->sync(); auditGpu2D(gfx, "avatar", "expression", bg); } TEST_CASE("graphics.imageAudit.avatarVroid") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); studio.subject->setVisible(false); eve::ref model(loadCesiumMan("ev_ut_image_audit_vroid")); REQUIRE(model.get() != nullptr); const int mi = findFirstSkinnedMesh(model.get()); REQUIRE(mi >= 0); const aiMesh *src = model->getMesh(mi); REQUIRE(src != nullptr); Mesh *gpu = gfx->newMeshFromAssimp(*src); REQUIRE(gpu != nullptr); auto *mod = eve::avatar::Avatar::create(); eve::avatar::AvatarInstance *av = mod->newVroidAvatar(); REQUIRE(av != nullptr); REQUIRE(av->bindVroidModelData(model.get())); av->setMesh(gpu); av->setTexture(makeSolid(gfx, 200, 170, 120)); av->setPosition3D(0.f, 0.f, 0.f); av->setRotation3D(1.2f, 0.f, 0.f); av->setScale3D(1.15f, 1.15f, 1.15f); studio.cam->setEye(0.f, 1.3f, 3.2f); studio.cam->setTarget(0.f, 0.7f, 0.f); studio.ground->setPosition(0.f, 0.f, 0.f); av->sync(); auditGpuFrame(gfx, "vroid", "bind_pose", bg); av->setRotation3D(0.4f, 0.15f, 0.f); av->sync(); auditGpuFrame(gfx, "vroid", "yaw_pitch", bg); } TEST_CASE("graphics.imageAudit.box2dDebug") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); makeSprite(makeSolid(gfx, 90, 140, 200), 0.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 200, 90, 70), 200.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 70, 180, 110), 0.f, 150.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 220, 200, 60), 200.f, 150.f, 200.f, 150.f, false); auto *phys = eve::physics::Physics::create(); std::unique_ptr world(phys->newWorld(0.f, 0.f, true)); REQUIRE(world.get() != nullptr); eve::physics::Body *ground = world->newBody("static", 200.f, 260.f); ground->newRectangleFixture(360.f, 40.f, 0.f, 0.4f, 0.f); eve::physics::Body *wallL = world->newBody("static", 30.f, 150.f); wallL->newRectangleFixture(40.f, 220.f, 0.f, 0.4f, 0.f); eve::physics::Body *wallR = world->newBody("static", 370.f, 150.f); wallR->newRectangleFixture(40.f, 220.f, 0.f, 0.4f, 0.f); eve::physics::Body *box = world->newBody("dynamic", 200.f, 80.f); box->newRectangleFixture(70.f, 70.f, 1.f, 0.3f, 0.1f); eve::physics::Body *ball = world->newBody("dynamic", 120.f, 60.f); ball->newCircleFixture(28.f, 1.f, 0.2f, 0.4f); world->setGravity(0.f, 600.f); for (int i = 0; i < 20; ++i) world->update(1.f / 60.f); auto presentDebug = [&]() { gfx->setScreenReadbackEnabled(true); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); std::vector items; RenderSystem::collectSprites(items); RenderSystem::drawItems(*gfx, items, false); world->drawDebug(gfx); gfx->present(); }; presentDebug(); auditSwapchain(gfx, "box2d", "debug_draw", bg); } TEST_CASE("graphics.imageAudit.imageRotate") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); [[maybe_unused]] auto *const imageModule = eve::image::Image::create(); ImageData src(64, 64, "RGBA8"); auto *px = static_cast(src.getData()); for (int y = 0; y < 64; ++y) { for (int x = 0; x < 64; ++x) { const bool on = ((x / 8) + (y / 8)) & 1; const size_t i = size_t((y * 64 + x) * 4); px[i + 0] = on ? 220 : 50; px[i + 1] = on ? 90 : 180; px[i + 2] = on ? 70 : 210; px[i + 3] = 255; } } std::unique_ptr rot90(src.rotate(1.5707963f, "nearest", true)); REQUIRE(rot90.get() != nullptr); Texture *t90 = gfx->newTexture(rot90.get()); auto *full = makeSprite(t90, 0.f, 0.f, 400.f, 300.f, false); auditGpu2D(gfx, "imrotate", "nearest_90", bg); std::unique_ptr rotSprite(src.rotate(1.5707963f, "rotsprite", true)); REQUIRE(rotSprite.get() != nullptr); full->sprite()->texture = gfx->newTexture(rotSprite.get()); auditGpu2D(gfx, "imrotate", "rotsprite_90", bg); makeSprite(makeSolid(gfx, 90, 140, 200), 0.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 200, 90, 70), 200.f, 0.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 70, 180, 110), 0.f, 150.f, 200.f, 150.f, false); makeSprite(makeSolid(gfx, 220, 200, 60), 200.f, 150.f, 200.f, 150.f, false); full->sprite()->visible = false; std::unique_ptr rot45(src.rotate(0.78539816f, "linear", true)); REQUIRE(rot45.get() != nullptr); makeSprite(gfx->newTexture(rot45.get()), 80.f, 50.f, 240.f, 200.f, false); auditGpu2D(gfx, "imrotate", "linear_45", bg); auto *cam = Camera2D::createCamera(); cam->setPosition(200.f, 150.f); cam->setZoom(1.f); Texture *bigTex = gfx->newTexture(&src); auto *big = makeSprite(bigTex, 0.f, 0.f, 800.f, 600.f, false); big->sprite()->camera = cam; big->sprite()->layer = 50; auditGpu2D(gfx, "imrotate", "cam_pan_tl", bg); cam->setPosition(400.f, 250.f); auditGpu2D(gfx, "imrotate", "cam_pan_mid", bg); } TEST_CASE("graphics.imageAudit.sceneGraph") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; auto studio = makeStudio3D(gfx); makeSprite(makeSolid(gfx, 180, 140, 70), 0.f, 0.f, 400.f, 84.f, false); auto hostResult = eve::scene::SceneHost::createHost("audit3d"); REQUIRE(hostResult.ok()); eve::scene::SceneHost *host = hostResult.value(); REQUIRE(host != nullptr); host->setTree(eve::scene::node( "root", {eve::scene::node("subject").withPosition(0.f, 0.15f, 0.f).withScale(0.55f)})); REQUIRE(host->linkRenderable3D("subject", studio.subject)); eve::scene::TransformSystem::updateHost(host); auditGpuFrame(gfx, "scene", "linked_rest", bg); host->setTreeReconcile(eve::scene::node( "root", {eve::scene::node("subject") .withPosition(0.7f, 0.2f, 0.f) .withRotation(0.8f, 0.15f, 0.f) .withScale(0.6f)})); eve::scene::TransformSystem::updateHost(host); auditGpuFrame(gfx, "scene", "linked_moved", bg); auto *left = Renderable3D::create(); left->setMesh(studio.sphere); left->setTexture(makeSolid(gfx, 210, 90, 80)); left->setCastShadow(true); host->setTree(eve::scene::node("root", {eve::scene::node("subject").withPosition(0.5f, 0.15f, 0.f).withScale(0.45f), eve::scene::node("left").withPosition(-1.0f, 0.2f, 0.f).withScale(0.4f)})); REQUIRE(host->linkRenderable3D("subject", studio.subject)); REQUIRE(host->linkRenderable3D("left", left)); eve::scene::TransformSystem::updateHost(host); auditGpuFrame(gfx, "scene", "two_nodes", bg); } TEST_CASE("graphics.imageAudit.fovHeightHex") { auto *win = eve::window::Window::create(); auto *gfx = Graphics::create(); REQUIRE(win != nullptr); REQUIRE(gfx != nullptr); eve::window::WindowSettings s; s.width = 400; s.height = 300; s.centered = true; REQUIRE(win->setWindowSettings(s)); CloseWin closer{win}; resetScene3D(); RenderAuditBg bg{0.08f, 0.09f, 0.11f}; gfx->setBackgroundColor(Color(0.08f, 0.09f, 0.11f, 1.f)); auto *map = eve::map::Map::create(); Texture *tiles = makeAtlasGrid(gfx, 4, 8); auto *cam = Camera2D::createCamera(); cam->setPosition(200.f, 150.f); cam->setZoom(1.f); auto presentMap = [&]() { gfx->setScreenReadbackEnabled(true); gfx->setBackgroundColorRGBA(0.08f, 0.09f, 0.11f, 1.f); gfx->clearScreen(); map->render(gfx); gfx->present(); }; eve::map::TileLayer *ortho = map->newLayer(25, 19, 16.f, 16.f); ortho->setTileset(tiles, 1, 4); ortho->setTilesetTileSize(8, 8); ortho->setCamera(cam); for (int y = 0; y < 19; ++y) for (int x = 0; x < 25; ++x) ortho->setTile(x, y, 1 + (x + y) % 15); std::unique_ptr fov(map->newFov(ortho)); REQUIRE(fov.get() != nullptr); fov->setMode("heightmap"); fov->setRadiusMetric("chebyshev"); fov->setCliffBlock(1.f); fov->setEyeOffset(0.f); for (int y = 0; y < 19; ++y) fov->setElevation(10, y, 2.f); fov->addRevealer(3, 9, 12); fov->compute(); for (int y = 0; y < 19; ++y) { for (int x = 0; x < 25; ++x) { const bool vis = fov->isVisible(x, y); ortho->setTile(x, y, vis ? (1 + (x + y) % 8) : (9 + (x * 3 + y) % 7)); } } presentMap(); auditSwapchain(gfx, "fovmore", "heightmap", bg); ortho->setVisible(false); eve::map::TileLayer *hex = map->newLayer(18, 16, 32.f, 28.f); hex->config()->orientation = eve::map::MapOrientation::Hexagonal; hex->config()->hexSideLength = 16.f; hex->config()->originX = 16.f; hex->config()->originY = 8.f; hex->setTileset(tiles, 1, 4); hex->setTilesetTileSize(8, 8); hex->setCamera(cam); for (int y = 0; y < 16; ++y) for (int x = 0; x < 18; ++x) hex->setTile(x, y, 1 + (x * 3 + y) % 15); fov->bindLayer(hex); fov->setMode("grid2d"); fov->setTopology("hex"); fov->setAlgorithm("shadowcast"); fov->clearRevealers(); fov->addRevealer(4, 6, 8); fov->compute(); for (int y = 0; y < 16; ++y) { for (int x = 0; x < 18; ++x) { const bool vis = fov->isVisible(x, y); hex->setTile(x, y, vis ? (1 + (x + y) % 8) : (9 + (x * 3 + y) % 7)); } } makeSprite(makeSolid(gfx, 70, 90, 130), 0.f, 0.f, 400.f, 300.f, false)->sprite()->layer = -1; presentMap(); auditSwapchain(gfx, "fovmore", "hex_topology", bg); }