617 lines
22 KiB
C++
617 lines
22 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include <SDL2/SDL.h>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "common/Exception.h"
|
|
#include "filesystem/FileData.h"
|
|
#include "graphics/AmbientOcclusion.h"
|
|
#include "graphics/AntiAliasing.h"
|
|
#include "graphics/Canvas.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/Light.h"
|
|
#include "graphics/Material.h"
|
|
#include "graphics/Mesh.h"
|
|
#include "graphics/Outline.h"
|
|
#include "graphics/Quad.h"
|
|
#include "graphics/RenderControl.h"
|
|
#include "graphics/RenderSystem.h"
|
|
#include "graphics/RenderSystem3D.h"
|
|
#include "graphics/ScreenSpaceReflection.h"
|
|
#include "graphics/Shader.h"
|
|
#include "graphics/Texture.h"
|
|
#include "graphics/Volumetric.h"
|
|
#include "graphics/Water.h"
|
|
#include "graphics/Waterfall.h"
|
|
#include "image/Image.h"
|
|
#include "image/ImageData.h"
|
|
#include "stylize/Stylize.h"
|
|
#include "stylize/StyleInstance.h"
|
|
#include "stylize/StyleRecipe.h"
|
|
#include "window/Window.h"
|
|
// Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form.
|
|
using eve::graphics::Color;
|
|
|
|
using eve::graphics::Canvas;
|
|
using eve::graphics::Graphics;
|
|
using eve::graphics::Mesh;
|
|
using eve::graphics::Renderable2D;
|
|
using eve::graphics::Renderable3D;
|
|
using eve::graphics::Camera3D;
|
|
using eve::graphics::RenderSystem;
|
|
using eve::graphics::RenderSystem3D;
|
|
using eve::graphics::Shader;
|
|
using eve::graphics::Texture;
|
|
using eve::image::ImageData;
|
|
using eve::stylize::StyleChain;
|
|
using eve::stylize::StyleInstance;
|
|
using eve::stylize::StylePass;
|
|
using eve::stylize::StyleRecipe;
|
|
using eve::stylize::Stylize;
|
|
using Colorf = ImageData::Colorf;
|
|
|
|
namespace {
|
|
|
|
bool near(float a, float b, float eps = 0.08f) { return std::fabs(a - b) <= eps; }
|
|
|
|
float luma(const ::Color &c) { return (c.r + c.g + c.b) / 3.f; }
|
|
|
|
ImageData *makeGradient(int w, int h) {
|
|
auto *img = new ImageData(w, h, "RGBA8");
|
|
for (int y = 0; y < h; ++y) {
|
|
for (int x = 0; x < w; ++x) {
|
|
float u = float(x) / float(std::max(w - 1, 1));
|
|
float v = float(y) / float(std::max(h - 1, 1));
|
|
Colorf c{u, 0.35f + 0.4f * v, 1.f - u, 1.f};
|
|
// Hard edge for Sobel / outline styles.
|
|
if (x > w / 2 && y > h / 3 && y < 2 * h / 3) c = Colorf{0.95f, 0.85f, 0.2f, 1.f};
|
|
img->setPixel(x, y, c);
|
|
}
|
|
}
|
|
return img;
|
|
}
|
|
|
|
Texture *makeCylinderAlbedo(Graphics *gfx, int size = 256) {
|
|
// Bold horizontal stripes only (no vertical seams — seams become false ink/toon outlines).
|
|
// Avoid near-white bands so PBR/toon highlights keep readable form.
|
|
std::vector<uint8_t> px(size_t(size) * size_t(size) * 4);
|
|
const int bandH = size / 5;
|
|
const uint8_t palette[3][3] = {
|
|
{210, 92, 58}, // terracotta
|
|
{232, 186, 110}, // warm sand
|
|
{48, 148, 158}, // teal
|
|
};
|
|
for (int y = 0; y < size; ++y) {
|
|
const int band = (y / std::max(bandH, 1)) % 3;
|
|
for (int x = 0; x < size; ++x) {
|
|
const size_t i = size_t(y * size + x) * 4;
|
|
px[i + 0] = palette[band][0];
|
|
px[i + 1] = palette[band][1];
|
|
px[i + 2] = palette[band][2];
|
|
px[i + 3] = 255;
|
|
}
|
|
}
|
|
return gfx->newTexture(size, size, px.data());
|
|
}
|
|
|
|
void resetScene3D() {
|
|
if (ecs::current()->getManager<Renderable3D>() != nullptr) {
|
|
auto view = ecs::View<Renderable3D, Renderable3D::MeshRenderer>();
|
|
for (auto it = view.begin(); it != view.end(); ++it) {
|
|
auto [mr] = *it;
|
|
mr->visible = false;
|
|
}
|
|
}
|
|
if (ecs::current()->getManager<Camera3D>() != nullptr) {
|
|
auto camView = ecs::View<Camera3D, Camera3D::Data>();
|
|
for (auto it = camView.begin(); it != camView.end(); ++it) {
|
|
auto [data] = *it;
|
|
data->active = false;
|
|
}
|
|
}
|
|
if (ecs::current()->getManager<Renderable2D>() != nullptr) {
|
|
auto view = ecs::View<Renderable2D, Renderable2D::Sprite>();
|
|
for (auto it = view.begin(); it != view.end(); ++it) {
|
|
auto [sp] = *it;
|
|
sp->visible = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void saveImageDataPng(ImageData *frame, const std::string &path) {
|
|
REQUIRE(frame != nullptr);
|
|
[[maybe_unused]] auto *const imageModule = eve::image::Image::create();
|
|
eve::filesystem::FileData *png =
|
|
frame->encode(medialoader::FormatHandler::ENCODED_PNG, "stylize.png", false);
|
|
REQUIRE(png != nullptr);
|
|
REQUIRE(png->getSize() > 0);
|
|
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
|
|
{
|
|
std::ofstream out(path, std::ios::binary);
|
|
REQUIRE(out.good());
|
|
out.write(static_cast<const char *>(png->getData()),
|
|
static_cast<std::streamsize>(png->getSize()));
|
|
REQUIRE(out.good());
|
|
}
|
|
delete png;
|
|
std::printf("stylize render saved: %s\n", path.c_str());
|
|
}
|
|
|
|
std::string testOutDir() {
|
|
return std::string(EVENGINE_TEST_BINARY_DIR) + "/out";
|
|
}
|
|
|
|
/** Render cylinder with optional mesh shader; return screen ImageData (caller owns). */
|
|
ImageData *renderCylinderFrame(Graphics *gfx, Mesh *mesh, Texture *albedo, Shader *meshShader) {
|
|
resetScene3D();
|
|
|
|
auto *ent = Renderable3D::create();
|
|
ent->meshRenderer()->mesh = mesh;
|
|
ent->meshRenderer()->texture = albedo;
|
|
ent->meshRenderer()->shader = meshShader;
|
|
ent->meshRenderer()->visible = true;
|
|
ent->setTint(1.f, 1.f, 1.f, 1.f);
|
|
ent->transform()->yaw = 0.55f;
|
|
ent->transform()->pitch = -0.15f;
|
|
|
|
auto *cam = Camera3D::createCamera();
|
|
cam->setTarget(0.f, 0.f, 0.f);
|
|
cam->setEye(2.4f, 1.5f, 3.2f);
|
|
cam->data()->nearZ = 0.1f;
|
|
cam->data()->farZ = 50.f;
|
|
// Balanced fill so form reads without crushing shadows to black.
|
|
cam->data()->ambientR = 0.28f;
|
|
cam->data()->ambientG = 0.30f;
|
|
cam->data()->ambientB = 0.34f;
|
|
|
|
// Tiny 2D sprite keeps RenderSystem happy / present path consistent with model3d tests.
|
|
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;
|
|
hud->sprite()->a = 0.f;
|
|
hud->sprite()->visible = true;
|
|
|
|
gfx->setScreenReadbackEnabled(true);
|
|
// Lifted slate so object silhouettes separate cleanly for outline/ink.
|
|
gfx->setBackgroundColor(::Color(0.14f, 0.16f, 0.20f, 1.f));
|
|
// Empirically +Z / camera-side lights the visible face (see RenderSystem3D.lightingAffectsPixels).
|
|
// Keep intensity near 1 so highlights don't blow cel/watercolor pigment to pure white.
|
|
RenderSystem3D::setDirectionalLight(0.55f, 0.95f, 1.15f, 1.05f, 1.0f, 0.95f);
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
ent->transform()->yaw = 0.45f + float(i) * 0.04f;
|
|
RenderSystem3D::render(*gfx);
|
|
RenderSystem::render(*gfx);
|
|
}
|
|
|
|
::Color mid = gfx->getPixel(gfx->getWidth() / 2, gfx->getHeight() / 2);
|
|
CHECK(luma(mid) > 0.12f);
|
|
CHECK(luma(mid) < 0.92f);
|
|
|
|
[[maybe_unused]] auto *const imageModule = eve::image::Image::create();
|
|
ImageData *frame = gfx->newImageData();
|
|
REQUIRE(frame != nullptr);
|
|
return frame;
|
|
}
|
|
|
|
ImageData *applyPostToFrame(Graphics *gfx, ImageData *frame, StylePass *pass) {
|
|
REQUIRE(frame != nullptr);
|
|
REQUIRE(pass != nullptr);
|
|
Texture *tex = gfx->newTexture(frame);
|
|
REQUIRE(tex != nullptr);
|
|
|
|
Canvas *rt = gfx->newCanvas(frame->getWidth(), frame->getHeight());
|
|
REQUIRE(rt != nullptr);
|
|
gfx->setCanvas(rt);
|
|
gfx->clear(::Color(0.14f, 0.16f, 0.20f, 1.f), std::nullopt, std::nullopt);
|
|
if (pass->hasParam("time") && pass->getFloat("time") <= 0.f) pass->setTime(0.35f);
|
|
pass->apply(gfx, tex);
|
|
gfx->setCanvas();
|
|
|
|
ImageData *out = rt->newImageData();
|
|
REQUIRE(out != nullptr);
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("stylize.styles.registry") {
|
|
auto *mod = Stylize::create();
|
|
REQUIRE(mod != nullptr);
|
|
CHECK_EQ(mod->getName(), std::string("Stylize"));
|
|
CHECK_EQ(mod->getStyleCount(), 12);
|
|
CHECK(mod->hasStyle("cartoon"));
|
|
CHECK(mod->hasStyle("watercolor"));
|
|
CHECK(mod->hasStyle("ink"));
|
|
CHECK(mod->hasStyle("pixel"));
|
|
CHECK(mod->hasStyle("xray"));
|
|
CHECK(mod->hasStyle("rim"));
|
|
CHECK(mod->hasStyle("dissolve"));
|
|
CHECK(mod->hasStyle("hologram"));
|
|
CHECK(mod->hasStyle("snow"));
|
|
CHECK(mod->hasStyle("vignette"));
|
|
CHECK(mod->hasStyle("chromatic"));
|
|
CHECK(mod->hasStyle("grain"));
|
|
CHECK(!mod->hasStyle("oil"));
|
|
CHECK(mod->hasMeshStyle("cartoon"));
|
|
CHECK(mod->hasMeshStyle("ink"));
|
|
CHECK(!mod->hasMeshStyle("watercolor"));
|
|
CHECK(!mod->hasMeshStyle("pixel"));
|
|
CHECK(mod->hasMeshStyle("xray"));
|
|
CHECK(mod->hasMeshStyle("rim"));
|
|
CHECK(mod->hasMeshStyle("dissolve"));
|
|
CHECK(mod->hasMeshStyle("hologram"));
|
|
CHECK(mod->hasMeshStyle("snow"));
|
|
CHECK(!mod->hasMeshStyle("vignette"));
|
|
CHECK(!mod->hasMeshStyle("chromatic"));
|
|
CHECK(!mod->hasMeshStyle("grain"));
|
|
|
|
CHECK(mod->supports("cartoon", "post"));
|
|
CHECK(mod->supports("cartoon", "mesh"));
|
|
CHECK(mod->supports("watercolor", "cpu"));
|
|
CHECK(!mod->supports("watercolor", "mesh"));
|
|
CHECK(!mod->supports("ink", "gbuffer")); // current shader is color/Sobel only
|
|
CHECK(mod->supports("xray", "depth"));
|
|
CHECK(!mod->supports("xray", "post"));
|
|
CHECK(!mod->supports("xray", "cpu"));
|
|
CHECK(!mod->supports("oil", "post"));
|
|
|
|
CHECK(mod->supports("rim", "mesh"));
|
|
CHECK(!mod->supports("rim", "post"));
|
|
CHECK(mod->supports("vignette", "post"));
|
|
CHECK(!mod->supports("vignette", "mesh"));
|
|
CHECK(!mod->supports("vignette", "cpu"));
|
|
|
|
CHECK_GT(mod->getStyleParamCount("pixel"), 0);
|
|
CHECK_EQ(mod->getStyleParamName("pixel", 0), std::string("pixelSize"));
|
|
CHECK(mod->getStyleParamName("pixel", 999).empty());
|
|
|
|
bool sawCartoon = false;
|
|
for (int i = 0; i < mod->getStyleCount(); ++i) {
|
|
if (mod->getStyleId(i) == "cartoon") sawCartoon = true;
|
|
}
|
|
CHECK(sawCartoon);
|
|
}
|
|
|
|
TEST_CASE("stylize.processImage.cpuAllStyles") {
|
|
auto *mod = Stylize::create();
|
|
std::unique_ptr<ImageData> src(makeGradient(32, 24));
|
|
|
|
for (const char *style : {"cartoon", "watercolor", "ink", "pixel"}) {
|
|
std::unique_ptr<ImageData> out(mod->processImage(src.get(), style));
|
|
REQUIRE(out.get() != nullptr);
|
|
CHECK_EQ(out->getWidth(), 32);
|
|
CHECK_EQ(out->getHeight(), 24);
|
|
CHECK_EQ(out->getFormat(), std::string("RGBA8"));
|
|
Colorf p = out->getPixel(8, 8);
|
|
CHECK(p.a > 0.9f);
|
|
// Must differ from a flat identity copy somewhere in the image.
|
|
bool changed = false;
|
|
for (int y = 0; y < 24 && !changed; ++y) {
|
|
for (int x = 0; x < 32 && !changed; ++x) {
|
|
Colorf a = src->getPixel(x, y);
|
|
Colorf b = out->getPixel(x, y);
|
|
if (!near(a.r, b.r, 0.02f) || !near(a.g, b.g, 0.02f) || !near(a.b, b.b, 0.02f))
|
|
changed = true;
|
|
}
|
|
}
|
|
CHECK(changed);
|
|
}
|
|
|
|
bool threw = false;
|
|
try {
|
|
mod->processImage(src.get(), "unknown");
|
|
} catch (const eve::Exception &) {
|
|
threw = true;
|
|
}
|
|
CHECK(threw);
|
|
}
|
|
|
|
TEST_CASE("stylize.instance.parameterSchema") {
|
|
auto *mod = Stylize::create();
|
|
std::unique_ptr<StyleInstance> pixel(mod->newInstance("pixel"));
|
|
REQUIRE(pixel.get() != nullptr);
|
|
CHECK_EQ(pixel->getStyle(), std::string("pixel"));
|
|
CHECK_EQ(pixel->getStage(), std::string("afterTonemap"));
|
|
CHECK(pixel->requiresInput("color"));
|
|
CHECK(!pixel->requiresInput("depth"));
|
|
CHECK(pixel->hasParam("pixelSize"));
|
|
CHECK(!pixel->hasParam("texelW"));
|
|
CHECK_EQ(pixel->getFloat("pixelSize"), 5.f);
|
|
|
|
pixel->setFloat("pixelSize", 999.f);
|
|
CHECK_EQ(pixel->getFloat("pixelSize"), pixel->getParamMax("pixelSize"));
|
|
CHECK(pixel->isOverridden("pixelSize"));
|
|
pixel->reset("pixelSize");
|
|
CHECK(!pixel->isOverridden("pixelSize"));
|
|
CHECK_EQ(pixel->getFloat("pixelSize"), pixel->getParamDefault("pixelSize"));
|
|
}
|
|
|
|
TEST_CASE("stylize.processImage.inkIsDesaturated") {
|
|
auto *mod = Stylize::create();
|
|
std::unique_ptr<ImageData> src(makeGradient(48, 32));
|
|
std::unique_ptr<ImageData> ink(mod->processImage(src.get(), "ink"));
|
|
REQUIRE(ink.get() != nullptr);
|
|
// Ink wash should collapse toward gray/sepia paper — chroma reduced.
|
|
int nearGray = 0;
|
|
for (int y = 0; y < 32; ++y) {
|
|
for (int x = 0; x < 48; ++x) {
|
|
Colorf c = ink->getPixel(x, y);
|
|
float mx = std::max(c.r, std::max(c.g, c.b));
|
|
float mn = std::min(c.r, std::min(c.g, c.b));
|
|
if (mx - mn < 0.25f) ++nearGray;
|
|
}
|
|
}
|
|
CHECK_GT(nearGray, 48 * 32 / 2);
|
|
}
|
|
|
|
TEST_CASE("stylize.processImage.pixelQuantizes") {
|
|
auto *mod = Stylize::create();
|
|
std::unique_ptr<ImageData> src(makeGradient(64, 32));
|
|
std::unique_ptr<ImageData> pix(mod->processImage(src.get(), "pixel"));
|
|
REQUIRE(pix.get() != nullptr);
|
|
// Neighboring pixels inside a 4x4 block should match (UV snap).
|
|
Colorf a = pix->getPixel(0, 0);
|
|
Colorf b = pix->getPixel(1, 1);
|
|
CHECK(near(a.r, b.r, 0.001f));
|
|
CHECK(near(a.g, b.g, 0.001f));
|
|
CHECK(near(a.b, b.b, 0.001f));
|
|
}
|
|
|
|
TEST_CASE("stylize.api.passFromShaderAndChain") {
|
|
auto *win = eve::window::Window::create();
|
|
auto *gfx = Graphics::create();
|
|
REQUIRE(win != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
eve::window::WindowSettings s;
|
|
s.width = 128;
|
|
s.height = 96;
|
|
s.centered = true;
|
|
REQUIRE(win->setWindowSettings(s));
|
|
|
|
auto *mod = Stylize::create();
|
|
Shader *raw = mod->newPostShader(gfx, "pixel");
|
|
REQUIRE(raw != nullptr);
|
|
StylePass *custom = mod->newPassFromShader("custom_pixel", raw);
|
|
REQUIRE(custom != nullptr);
|
|
CHECK_EQ(custom->getStyle(), std::string("custom_pixel"));
|
|
|
|
StylePass *a = mod->newPass(gfx, "cartoon");
|
|
StylePass *b = mod->newPass(gfx, "pixel");
|
|
REQUIRE(a != nullptr);
|
|
REQUIRE(b != nullptr);
|
|
StyleChain *chain = mod->newChain();
|
|
REQUIRE(chain != nullptr);
|
|
chain->add(a);
|
|
chain->add(b);
|
|
CHECK_EQ(chain->getPassCount(), 2);
|
|
CHECK(chain->getPass(0) == a);
|
|
|
|
std::unique_ptr<ImageData> img(makeGradient(64, 48));
|
|
Texture *tex = gfx->newTexture(img.get());
|
|
Canvas *dest = gfx->newCanvas(64, 48);
|
|
Canvas *temp = gfx->newCanvas(64, 48);
|
|
REQUIRE(tex != nullptr);
|
|
REQUIRE(dest != nullptr);
|
|
REQUIRE(temp != nullptr);
|
|
|
|
custom->applyTo(gfx, tex, dest);
|
|
::Color p0 = dest->getPixel(32, 24);
|
|
CHECK_GT(p0.a, 0.5f);
|
|
|
|
chain->apply(gfx, tex, dest, temp);
|
|
::Color p1 = dest->getPixel(32, 24);
|
|
CHECK_GT(p1.a, 0.5f);
|
|
|
|
std::unique_ptr<StyleInstance> watercolor(mod->newInstance("watercolor"));
|
|
std::unique_ptr<StyleInstance> pixel(mod->newInstance("pixel"));
|
|
std::unique_ptr<StyleRecipe> recipe(mod->newRecipe());
|
|
recipe->add(pixel.get());
|
|
recipe->add(watercolor.get());
|
|
recipe->compile(gfx);
|
|
CHECK(recipe->isCompiled());
|
|
CHECK_EQ(recipe->getStage(), std::string("afterTonemap"));
|
|
CHECK_EQ(recipe->getStyle(0)->getStyle(), std::string("watercolor"));
|
|
recipe->apply(gfx, tex, dest);
|
|
::Color p2 = dest->getPixel(32, 24);
|
|
CHECK_GT(p2.a, 0.5f);
|
|
|
|
delete chain;
|
|
delete custom;
|
|
delete a;
|
|
delete b;
|
|
win->close();
|
|
}
|
|
|
|
TEST_CASE("stylize.gpu.postPassAndMeshShader") {
|
|
auto *win = eve::window::Window::create();
|
|
auto *gfx = Graphics::create();
|
|
REQUIRE(win != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
eve::window::WindowSettings s;
|
|
s.width = 256;
|
|
s.height = 192;
|
|
s.centered = true;
|
|
REQUIRE(win->setWindowSettings(s));
|
|
|
|
auto *mod = Stylize::create();
|
|
|
|
for (const char *style : {"cartoon", "watercolor", "ink", "pixel"}) {
|
|
StylePass *pass = mod->newPass(gfx, style);
|
|
REQUIRE(pass != nullptr);
|
|
CHECK_EQ(pass->getStyle(), std::string(style));
|
|
REQUIRE(pass->getShader() != nullptr);
|
|
REQUIRE(pass->getShader()->gpuHandle != nullptr);
|
|
CHECK(pass->hasParam("texelW"));
|
|
if (pass->hasParam("time")) pass->setTime(0.25f);
|
|
|
|
std::unique_ptr<ImageData> img(makeGradient(64, 48));
|
|
auto *tex = gfx->newTexture(img.get());
|
|
REQUIRE(tex != nullptr);
|
|
|
|
Canvas *rt = gfx->newCanvas(128, 96);
|
|
REQUIRE(rt != nullptr);
|
|
gfx->setCanvas(rt);
|
|
gfx->clear(::Color(0.1f, 0.1f, 0.1f, 1.f), std::nullopt, std::nullopt);
|
|
pass->apply(gfx, tex);
|
|
gfx->setCanvas();
|
|
|
|
::Color p = rt->getPixel(64, 48);
|
|
CHECK_GT(p.a, 0.5f);
|
|
delete pass;
|
|
}
|
|
|
|
Shader *toonMesh = mod->newMeshShader(gfx, "cartoon");
|
|
REQUIRE(toonMesh != nullptr);
|
|
REQUIRE(toonMesh->gpuHandle != nullptr);
|
|
CHECK(toonMesh->hasUniform("bands"));
|
|
|
|
Shader *inkMesh = mod->newMeshShader(gfx, "ink");
|
|
REQUIRE(inkMesh != nullptr);
|
|
REQUIRE(inkMesh->gpuHandle != nullptr);
|
|
CHECK(inkMesh->hasUniform("washLevels"));
|
|
|
|
bool threw = false;
|
|
try {
|
|
mod->newMeshShader(gfx, "watercolor");
|
|
} catch (const eve::Exception &) {
|
|
threw = true;
|
|
}
|
|
CHECK(threw);
|
|
}
|
|
|
|
TEST_CASE("stylize.render.cylinderStyleGallery") {
|
|
auto *win = eve::window::Window::create();
|
|
auto *gfx = Graphics::create();
|
|
REQUIRE(win != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
eve::window::WindowSettings s;
|
|
s.width = 512;
|
|
s.height = 384;
|
|
s.centered = true;
|
|
REQUIRE(win->setWindowSettings(s));
|
|
|
|
Mesh *cylinder = gfx->newMeshCylinder(64, 1, true);
|
|
REQUIRE(cylinder != nullptr);
|
|
CHECK_GT(cylinder->indexCount, 0);
|
|
|
|
Texture *albedo = makeCylinderAlbedo(gfx, 128);
|
|
REQUIRE(albedo != nullptr);
|
|
|
|
auto *mod = Stylize::create();
|
|
const std::string outDir = testOutDir();
|
|
|
|
std::unique_ptr<ImageData> baseFrame(renderCylinderFrame(gfx, cylinder, albedo, nullptr));
|
|
REQUIRE(baseFrame.get() != nullptr);
|
|
saveImageDataPng(baseFrame.get(), outDir + "/cylinder_base.png");
|
|
|
|
// Cartoon uses object-space cel mesh first, then outline post (UTS-like).
|
|
Shader *toonMesh = mod->newMeshShader(gfx, "cartoon");
|
|
REQUIRE(toonMesh != nullptr);
|
|
toonMesh->sendFloat("bands", 3.f);
|
|
toonMesh->sendFloat("rimPower", 2.6f);
|
|
toonMesh->sendFloat("rimStrength", 0.45f);
|
|
toonMesh->sendFloat("posterize", 5.f);
|
|
std::unique_ptr<ImageData> cartoonBase(renderCylinderFrame(gfx, cylinder, albedo, toonMesh));
|
|
|
|
const char *styles[] = {"cartoon", "ink", "watercolor", "pixel"};
|
|
for (const char *id : styles) {
|
|
StylePass *pass = mod->newPass(gfx, id);
|
|
REQUIRE(pass != nullptr);
|
|
ImageData *srcFrame = baseFrame.get();
|
|
if (std::string(id) == "cartoon") {
|
|
srcFrame = cartoonBase.get();
|
|
// Mesh already did cel; post lightly reinforces bands + silhouette ink.
|
|
pass->setFloat("bands", 3.f);
|
|
pass->setFloat("outlineStrength", 1.35f);
|
|
pass->setFloat("outlineThreshold", 0.08f);
|
|
pass->setFloat("posterize", 5.f);
|
|
pass->setFloat("outlineWidth", 2.0f);
|
|
pass->setFloat("shadowLift", 0.18f);
|
|
pass->setFloat("softEdge", 0.12f);
|
|
} else if (std::string(id) == "watercolor") {
|
|
pass->setFloat("blurAmount", 2.2f);
|
|
pass->setFloat("edgeDarken", 1.2f);
|
|
pass->setFloat("paperStrength", 0.6f);
|
|
pass->setFloat("distortion", 0.55f);
|
|
pass->setFloat("bleed", 0.58f);
|
|
pass->setFloat("saturation", 0.95f);
|
|
pass->setFloat("granulation", 0.45f);
|
|
} else if (std::string(id) == "ink") {
|
|
pass->setFloat("inkContrast", 1.35f);
|
|
pass->setFloat("washLevels", 5.f);
|
|
pass->setFloat("edgeThreshold", 0.18f);
|
|
pass->setFloat("diffusion", 3.5f);
|
|
pass->setFloat("inkDensity", 0.75f);
|
|
pass->setFloat("edgeStrength", 1.1f);
|
|
pass->setFloat("paperR", 0.96f);
|
|
pass->setFloat("paperG", 0.93f);
|
|
pass->setFloat("paperB", 0.86f);
|
|
} else if (std::string(id) == "pixel") {
|
|
pass->setFloat("pixelSize", 6.f);
|
|
pass->setFloat("paletteSteps", 5.f);
|
|
pass->setFloat("ditherStrength", 0.12f);
|
|
pass->setFloat("toonBands", 3.f);
|
|
pass->setFloat("sharpness", 1.f);
|
|
pass->setFloat("outline", 0.95f);
|
|
}
|
|
|
|
std::unique_ptr<ImageData> styled(applyPostToFrame(gfx, srcFrame, pass));
|
|
delete pass;
|
|
REQUIRE(styled.get() != nullptr);
|
|
|
|
const std::string name = std::string("cylinder_") + id + ".png";
|
|
saveImageDataPng(styled.get(), outDir + "/" + name);
|
|
|
|
// Sanity: some non-background pixels exist (cylinder albedo is warm).
|
|
int warm = 0;
|
|
const int w = styled->getWidth();
|
|
const int h = styled->getHeight();
|
|
for (int y = h / 4; y < 3 * h / 4; y += 4) {
|
|
for (int x = w / 4; x < 3 * w / 4; x += 4) {
|
|
Colorf c = styled->getPixel(x, y);
|
|
if (c.r + c.g + c.b > 0.2f) ++warm;
|
|
}
|
|
}
|
|
CHECK_GT(warm, 20);
|
|
|
|
// Live gallery: hold each style on the window so the look is visible.
|
|
Texture *preview = gfx->newTexture(styled.get());
|
|
REQUIRE(preview != nullptr);
|
|
gfx->setBackgroundColor(::Color(0.08f, 0.08f, 0.1f, 1.f));
|
|
for (int frame = 0; frame < 40; ++frame) {
|
|
gfx->clearScreen();
|
|
gfx->drawTexturedRect(preview, 0.f, 0.f, float(gfx->getWidth()), float(gfx->getHeight()),
|
|
::Color(1.f, 1.f, 1.f, 1.f));
|
|
gfx->present();
|
|
SDL_Event e;
|
|
while (SDL_PollEvent(&e)) {
|
|
if (e.type == SDL_QUIT) break;
|
|
}
|
|
SDL_Delay(16);
|
|
}
|
|
}
|
|
|
|
win->close();
|
|
}
|