944 lines
39 KiB
C++
944 lines
39 KiB
C++
#include "zeroerr/unittest.h"
|
|
#include "GraphicsParitySupport.h"
|
|
|
|
#include "common/config.h"
|
|
#include "filesystem/FileData.h"
|
|
#include "graphics/AlphaMask.h"
|
|
#include "graphics/Canvas.h"
|
|
#include "graphics/ClipSpace.h"
|
|
#include "graphics/Graphics.h"
|
|
#include "graphics/Light.h"
|
|
#include "graphics/Mesh.h"
|
|
#include "graphics/PrimitiveDrawList.h"
|
|
#include "graphics/RenderControl.h"
|
|
#include "graphics/Shader.h"
|
|
#include "graphics/Texture.h"
|
|
#include "graphics/TextureSampler.h"
|
|
#include "image/Image.h"
|
|
#include "image/ImageData.h"
|
|
#include "pixelworld/PixelWorld.h"
|
|
#include "pixelworld/graphics/PixelWorldGraphics.h"
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <glm/ext/matrix_clip_space.hpp>
|
|
#include <memory>
|
|
|
|
using namespace eve::graphics;
|
|
|
|
using namespace eve::graphics::parity_test;
|
|
|
|
|
|
TEST_CASE("graphics.backendParity.textureUpdateAndAlphaBlend") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
const std::string backend = gfx->getBackendName();
|
|
REQUIRE(backend == expectedBackendName());
|
|
|
|
const uint8_t red[4] = {255, 0, 0, 255};
|
|
const uint8_t green[4] = {0, 255, 0, 255};
|
|
Texture *texture = gfx->newTexture(1, 1, red);
|
|
REQUIRE(texture != nullptr);
|
|
Texture *stable = texture;
|
|
REQUIRE(gfx->updateTexture(texture, 1, 1, green));
|
|
REQUIRE(texture == stable);
|
|
// Same-size streaming updates must retain the Vulkan image and descriptor.
|
|
// The former replacement path exhausted the shared descriptor pool after
|
|
// a few hundred frames in the volume-fluid presentation loop.
|
|
for (int frame = 0; frame < 400; ++frame) REQUIRE(gfx->updateTexture(texture, 1, 1, (frame & 1) ? red : green));
|
|
REQUIRE(!gfx->updateTexture(texture, 2, 1, green));
|
|
REQUIRE(gfx->updateTextureRegion(texture, 0, 0, 1, 1, red).ok());
|
|
CHECK(!gfx->updateTextureRegion(texture, 1, 0, 1, 1, red).ok());
|
|
CHECK(!gfx->updateTextureRegion(texture, 0, 0, 1, 1,
|
|
std::span<const uint8_t>(red, 3)).ok());
|
|
const TextureRegionUpload rejectedBatch[] = {
|
|
{0, 0, 1, 1, green, 0},
|
|
{1, 0, 1, 1, green, 0},
|
|
};
|
|
CHECK(!gfx->updateTextureRegions(texture, rejectedBatch).ok());
|
|
|
|
Canvas *canvas = gfx->newCanvas(64, 64);
|
|
REQUIRE(canvas != nullptr);
|
|
gfx->setCanvas(canvas);
|
|
gfx->clear(Color(0.f, 0.f, 1.f, 1.f), std::nullopt, std::nullopt);
|
|
gfx->drawTexturedRect(texture, 8.f, 8.f, 24.f, 24.f, Color(1.f));
|
|
gfx->drawSolidRect(32.f, 8.f, 24.f, 24.f, Color(1.f, 0.f, 0.f, 0.5f),
|
|
BlendMode::Alpha);
|
|
gfx->setCanvas();
|
|
|
|
std::unique_ptr<eve::image::ImageData> image(canvas->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
const uint8_t *background = pixel(*image, 2, 2);
|
|
REQUIRE(background[0] < 8);
|
|
REQUIRE(background[1] < 8);
|
|
REQUIRE(background[2] > 247);
|
|
|
|
const uint8_t *updated = pixel(*image, 16, 16);
|
|
REQUIRE(updated[0] > 247);
|
|
REQUIRE(updated[1] < 8);
|
|
REQUIRE(updated[2] < 8);
|
|
|
|
const uint8_t *blended = pixel(*image, 40, 16);
|
|
const bool redBlended = blended[0] >= 126 && blended[0] <= 129;
|
|
REQUIRE(redBlended);
|
|
REQUIRE(blended[1] < 8);
|
|
const bool blueBlended = blended[2] >= 126 && blended[2] <= 129;
|
|
REQUIRE(blueBlended);
|
|
writeParityArtifact(*image, "texture_update_alpha_blend", backend);
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.pixelWorldMillionPixelAtlas") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
|
|
eve::pixelworld::PixelWorld world(4242);
|
|
// Radius 724 covers every cell in [0,1024)^2, creating a genuine one-million
|
|
// pixel viewport while also proving off-viewport chunks are clipped by the adapter.
|
|
REQUIRE(world.paintCircleChecked(512, 512, 724, "stone").ok());
|
|
eve::pixelworld_graphics::PixelWorldAtlasRenderer renderer(0, 0, 1024, 1024);
|
|
CHECK(renderer.sync(&world, gfx) == 256);
|
|
REQUIRE(renderer.texture() != nullptr);
|
|
CHECK(renderer.texture()->width == 1024);
|
|
CHECK(renderer.texture()->height == 1024);
|
|
CHECK(renderer.uploadCount() == 1);
|
|
CHECK(renderer.totalUploadedChunks() == 256);
|
|
CHECK(renderer.renderedRevision() == world.revision());
|
|
|
|
world.setMaterial(1, 1, "sand");
|
|
world.setMaterial(1022, 1022, "water");
|
|
CHECK(renderer.sync(&world, gfx) == 2);
|
|
CHECK(renderer.uploadCount() == 2);
|
|
CHECK(renderer.totalUploadedChunks() == 258);
|
|
CHECK(renderer.renderedRevision() == world.revision());
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.draw2dUvRotationAndBlendModes") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
const std::string backend = gfx->getBackendName();
|
|
const bool supportedBackend = backend == "vulkan" || backend == "webgpu";
|
|
REQUIRE(supportedBackend);
|
|
|
|
const uint8_t stripes[] = {
|
|
255, 0, 0, 255,
|
|
0, 255, 0, 255,
|
|
0, 0, 255, 255,
|
|
255, 255, 255, 255,
|
|
};
|
|
Texture *texture = gfx->newTexture(4, 1, stripes);
|
|
REQUIRE(texture != nullptr);
|
|
gfx->setTextureSampler(texture, TextureSampler::nearest());
|
|
|
|
Canvas *canvas = gfx->newCanvas(64, 64);
|
|
REQUIRE(canvas != nullptr);
|
|
gfx->setCanvas(canvas);
|
|
gfx->clear(Color(0.f, 0.f, 0.f, 1.f), std::nullopt, std::nullopt);
|
|
|
|
// Crop the second texel. Nearest sampling makes the expected green region exact.
|
|
gfx->drawTexturedRectShaderUV(texture, nullptr, 2.f, 2.f, 18.f, 12.f, 0.25f, 0.f, 0.5f,
|
|
1.f, Color(1.f), false, BlendMode::Opaque);
|
|
|
|
// A 90-degree turn changes the wide rectangle into a narrow vertical footprint.
|
|
gfx->drawSolidRectRotated(34.f, 10.f, 18.f, 6.f, 90.f, Color(1.f, 1.f, 0.f, 1.f),
|
|
BlendMode::Opaque);
|
|
gfx->drawTexturedRectShaderUVRotated(texture, nullptr, 50.f, 10.f, 18.f, 6.f, 90.f, 0.f,
|
|
0.f, 0.25f, 1.f, Color(1.f), false,
|
|
BlendMode::Opaque);
|
|
|
|
gfx->drawSolidRect(2.f, 28.f, 12.f, 12.f, Color(0.f, 0.f, 0.5f, 1.f), BlendMode::Opaque);
|
|
gfx->drawSolidRect(2.f, 28.f, 12.f, 12.f, Color(0.5f, 0.f, 0.f, 1.f),
|
|
BlendMode::Additive);
|
|
|
|
gfx->drawSolidRect(18.f, 28.f, 12.f, 12.f, Color(0.f, 0.f, 1.f, 1.f), BlendMode::Opaque);
|
|
gfx->drawSolidRect(18.f, 28.f, 12.f, 12.f, Color(0.25f, 0.f, 0.f, 0.25f),
|
|
BlendMode::Premultiplied);
|
|
|
|
gfx->drawSolidRect(34.f, 28.f, 12.f, 12.f, Color(0.5f, 1.f, 0.25f, 1.f),
|
|
BlendMode::Opaque);
|
|
gfx->drawSolidRect(34.f, 28.f, 12.f, 12.f, Color(0.5f, 0.25f, 1.f, 1.f),
|
|
BlendMode::Multiply);
|
|
gfx->setCanvas();
|
|
|
|
std::unique_ptr<eve::image::ImageData> image(canvas->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
|
|
const uint8_t *crop = pixel(*image, 10, 8);
|
|
REQUIRE(crop[0] < 8);
|
|
REQUIRE(crop[1] > 247);
|
|
REQUIRE(crop[2] < 8);
|
|
|
|
const uint8_t *rotatedSolid = pixel(*image, 34, 3);
|
|
REQUIRE(rotatedSolid[0] > 247);
|
|
REQUIRE(rotatedSolid[1] > 247);
|
|
REQUIRE(rotatedSolid[2] < 8);
|
|
const uint8_t *outsideSolid = pixel(*image, 27, 10);
|
|
REQUIRE(outsideSolid[0] < 8);
|
|
REQUIRE(outsideSolid[1] < 8);
|
|
REQUIRE(outsideSolid[2] < 8);
|
|
|
|
const uint8_t *rotatedTexture = pixel(*image, 50, 3);
|
|
REQUIRE(rotatedTexture[0] > 247);
|
|
REQUIRE(rotatedTexture[1] < 8);
|
|
REQUIRE(rotatedTexture[2] < 8);
|
|
|
|
const uint8_t *additive = pixel(*image, 8, 34);
|
|
const bool additiveRed = additive[0] >= 126 && additive[0] <= 129;
|
|
REQUIRE(additiveRed);
|
|
REQUIRE(additive[1] < 8);
|
|
const bool additiveBlue = additive[2] >= 126 && additive[2] <= 129;
|
|
REQUIRE(additiveBlue);
|
|
|
|
const uint8_t *premultiplied = pixel(*image, 24, 34);
|
|
const bool premultipliedRed = premultiplied[0] >= 62 && premultiplied[0] <= 65;
|
|
REQUIRE(premultipliedRed);
|
|
REQUIRE(premultiplied[1] < 8);
|
|
const bool premultipliedBlue = premultiplied[2] >= 190 && premultiplied[2] <= 193;
|
|
REQUIRE(premultipliedBlue);
|
|
|
|
const uint8_t *multiply = pixel(*image, 40, 34);
|
|
const bool multiplyRed = multiply[0] >= 62 && multiply[0] <= 65;
|
|
const bool multiplyGreen = multiply[1] >= 62 && multiply[1] <= 65;
|
|
const bool multiplyBlue = multiply[2] >= 62 && multiply[2] <= 65;
|
|
REQUIRE(multiplyRed);
|
|
REQUIRE(multiplyGreen);
|
|
REQUIRE(multiplyBlue);
|
|
writeParityArtifact(*image, "draw2d_uv_rotation_blend_modes", backend);
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.lighting2dNormalMapAndMultipleLights") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
const std::string backend = gfx->getBackendName();
|
|
const bool supportedBackend = backend == "vulkan" || backend == "webgpu";
|
|
REQUIRE(supportedBackend);
|
|
|
|
const uint8_t white[] = {255, 255, 255, 255};
|
|
const uint8_t flatNormal[] = {128, 128, 255, 255};
|
|
const uint8_t rightNormal[] = {255, 128, 128, 255};
|
|
Texture *albedo = gfx->newTexture(1, 1, white);
|
|
Texture *flat = gfx->newTexture(1, 1, flatNormal);
|
|
Texture *right = gfx->newTexture(1, 1, rightNormal);
|
|
REQUIRE(albedo != nullptr);
|
|
REQUIRE(flat != nullptr);
|
|
REQUIRE(right != nullptr);
|
|
|
|
Lighting2DUBO directional{};
|
|
directional.ambient = Color(0.05f, 0.05f, 0.05f, 0.f);
|
|
directional.meta = Color(1.f, 64.f, 64.f, 0.f);
|
|
directional.lights[0].posRadius = Color(1.f, 0.f, 0.f, 0.f);
|
|
directional.lights[0].color = Color(0.5f, 0.5f, 0.5f, 0.f);
|
|
|
|
Canvas *normalCanvas = gfx->newCanvas(64, 64);
|
|
REQUIRE(normalCanvas != nullptr);
|
|
gfx->setCanvas(normalCanvas);
|
|
gfx->clear(Color(0.f, 0.f, 0.f, 1.f), std::nullopt, std::nullopt);
|
|
gfx->setLighting2D(directional);
|
|
gfx->drawTexturedRectLitUV(albedo, flat, 4.f, 8.f, 24.f, 24.f, 0.f, 0.f, 1.f, 1.f,
|
|
Color(1.f));
|
|
gfx->drawTexturedRectLitUV(albedo, right, 36.f, 8.f, 24.f, 24.f, 0.f, 0.f, 1.f, 1.f,
|
|
Color(1.f));
|
|
gfx->setCanvas();
|
|
|
|
std::unique_ptr<eve::image::ImageData> normalImage(normalCanvas->newImageData());
|
|
REQUIRE(normalImage.get() != nullptr);
|
|
const uint8_t *flatLit = pixel(*normalImage, 16, 20);
|
|
const uint8_t *rightLit = pixel(*normalImage, 48, 20);
|
|
REQUIRE(flatLit[0] > 32);
|
|
REQUIRE(rightLit[0] > flatLit[0] + 50);
|
|
REQUIRE(rightLit[1] > flatLit[1] + 50);
|
|
REQUIRE(rightLit[2] > flatLit[2] + 50);
|
|
writeParityArtifact(*normalImage, "lighting2d_normal_map", backend);
|
|
|
|
Lighting2DUBO points{};
|
|
points.ambient = Color(0.02f, 0.02f, 0.02f, 0.f);
|
|
points.meta = Color(2.f, 64.f, 64.f, 0.f);
|
|
points.lights[0].posRadius = Color(10.f, 48.f, 0.f, 28.f);
|
|
points.lights[0].color = Color(0.9f, 0.f, 0.f, 0.f);
|
|
points.lights[1].posRadius = Color(54.f, 48.f, 0.f, 28.f);
|
|
points.lights[1].color = Color(0.f, 0.f, 0.9f, 0.f);
|
|
|
|
Canvas *pointCanvas = gfx->newCanvas(64, 64);
|
|
REQUIRE(pointCanvas != nullptr);
|
|
gfx->setCanvas(pointCanvas);
|
|
gfx->clear(Color(0.f, 0.f, 0.f, 1.f), std::nullopt, std::nullopt);
|
|
gfx->setLighting2D(points);
|
|
gfx->drawTexturedRectLitUV(albedo, flat, 2.f, 36.f, 60.f, 24.f, 0.f, 0.f, 1.f, 1.f,
|
|
Color(1.f));
|
|
gfx->setCanvas();
|
|
|
|
std::unique_ptr<eve::image::ImageData> pointImage(pointCanvas->newImageData());
|
|
REQUIRE(pointImage.get() != nullptr);
|
|
const uint8_t *leftLight = pixel(*pointImage, 10, 48);
|
|
const uint8_t *rightLight = pixel(*pointImage, 54, 48);
|
|
REQUIRE(leftLight[0] > leftLight[2] + 80);
|
|
REQUIRE(rightLight[2] > rightLight[0] + 80);
|
|
REQUIRE(leftLight[0] > 120);
|
|
REQUIRE(rightLight[2] > 120);
|
|
writeParityArtifact(*pointImage, "lighting2d_multiple_lights", backend);
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.textureSamplingRepeatAndMipmaps") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
REQUIRE(gfx->getMaxAnisotropy() >= 1.f);
|
|
const std::string backend = gfx->getBackendName();
|
|
const bool supportedBackend = backend == "vulkan" || backend == "webgpu";
|
|
REQUIRE(supportedBackend);
|
|
|
|
const uint8_t redBlue[] = {255, 0, 0, 255, 0, 0, 255, 255};
|
|
Texture *nearestTexture = gfx->newTexture(2, 1, redBlue);
|
|
Texture *repeatTexture = gfx->newTexture(2, 1, redBlue);
|
|
Texture *linearTexture = gfx->newTexture(2, 1, redBlue);
|
|
REQUIRE(nearestTexture != nullptr);
|
|
REQUIRE(repeatTexture != nullptr);
|
|
REQUIRE(linearTexture != nullptr);
|
|
|
|
uint8_t checker[4 * 4]{};
|
|
for (int x = 0; x < 4; ++x) {
|
|
uint8_t *p = checker + x * 4;
|
|
p[x % 2 == 0 ? 0 : 1] = 255;
|
|
p[3] = 255;
|
|
}
|
|
TextureCreateInfo mipInfo;
|
|
mipInfo.generateMipmaps = true;
|
|
Texture *mipped = gfx->newTexture(4, 1, checker, mipInfo);
|
|
REQUIRE(mipped != nullptr);
|
|
REQUIRE(mipped->getMipmapCount() == 3);
|
|
const bool mipSamplingEnabled = mipped->getSampler().mipmap == MipmapMode::Linear;
|
|
REQUIRE(mipSamplingEnabled);
|
|
|
|
Canvas *canvas = gfx->newCanvas(64, 64);
|
|
REQUIRE(canvas != nullptr);
|
|
gfx->setCanvas(canvas);
|
|
gfx->clear(Color(0.f, 0.f, 0.f, 1.f), std::nullopt, std::nullopt);
|
|
|
|
gfx->setTextureSampler(nearestTexture, TextureSampler::nearest());
|
|
gfx->drawTexturedRectShaderUV(nearestTexture, nullptr, 2.f, 2.f, 28.f, 12.f, 0.f, 0.f, 1.f, 1.f,
|
|
Color(1.f), false, BlendMode::Opaque);
|
|
|
|
TextureSampler repeat = TextureSampler::nearest();
|
|
repeat.repeatU = true;
|
|
gfx->setTextureSampler(repeatTexture, repeat);
|
|
gfx->drawTexturedRectShaderUV(repeatTexture, nullptr, 34.f, 2.f, 28.f, 12.f, 0.f, 0.f, 2.f, 1.f,
|
|
Color(1.f), false, BlendMode::Opaque);
|
|
|
|
gfx->setTextureSampler(linearTexture, TextureSampler::linear());
|
|
gfx->drawTexturedRectShaderUV(linearTexture, nullptr, 2.f, 20.f, 28.f, 12.f, 0.f, 0.f, 1.f, 1.f,
|
|
Color(1.f), false, BlendMode::Opaque);
|
|
|
|
TextureSampler lastMip = TextureSampler::nearest();
|
|
lastMip.mipmap = MipmapMode::Nearest;
|
|
lastMip.minLod = 2.f;
|
|
lastMip.maxLod = 2.f;
|
|
gfx->setTextureSampler(mipped, lastMip);
|
|
gfx->drawTexturedRect(mipped, 34.f, 20.f, 28.f, 28.f, Color(1.f));
|
|
gfx->setCanvas();
|
|
|
|
std::unique_ptr<eve::image::ImageData> image(canvas->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
const uint8_t *nearestLeft = pixel(*image, 8, 8);
|
|
const uint8_t *nearestRight = pixel(*image, 24, 8);
|
|
REQUIRE(nearestLeft[0] > 247);
|
|
REQUIRE(nearestLeft[2] < 8);
|
|
REQUIRE(nearestRight[0] < 8);
|
|
REQUIRE(nearestRight[2] > 247);
|
|
|
|
const uint8_t *repeat0 = pixel(*image, 38, 8);
|
|
const uint8_t *repeat1 = pixel(*image, 45, 8);
|
|
const uint8_t *repeat2 = pixel(*image, 52, 8);
|
|
const uint8_t *repeat3 = pixel(*image, 59, 8);
|
|
REQUIRE(repeat0[0] > 247);
|
|
REQUIRE(repeat1[2] > 247);
|
|
REQUIRE(repeat2[0] > 247);
|
|
REQUIRE(repeat3[2] > 247);
|
|
|
|
const uint8_t *linearBoundary = pixel(*image, 16, 26);
|
|
REQUIRE(linearBoundary[0] > 96);
|
|
REQUIRE(linearBoundary[2] > 96);
|
|
|
|
const uint8_t *mip = pixel(*image, 48, 34);
|
|
const bool mipRed = mip[0] >= 126 && mip[0] <= 129;
|
|
const bool mipGreen = mip[1] >= 126 && mip[1] <= 129;
|
|
REQUIRE(mipRed);
|
|
REQUIRE(mipGreen);
|
|
REQUIRE(mip[2] < 8);
|
|
writeParityArtifact(*image, "texture_sampling_repeat_mipmaps", backend);
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.webgpuCustomShaderLifetime") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
REQUIRE(gfx->getBackendName() == expectedBackendName());
|
|
if (gfx->getBackendName() != "webgpu") {
|
|
return;
|
|
}
|
|
|
|
const char *frag = R"wgsl(
|
|
struct FSIn {
|
|
@location(0) color: vec4f,
|
|
@location(1) uv: vec2f,
|
|
};
|
|
@group(0) @binding(0) var mainTex: texture_2d<f32>;
|
|
@group(0) @binding(2) var mainSamp: sampler;
|
|
@fragment
|
|
fn fs_main(in: FSIn) -> @location(0) vec4f {
|
|
let sampled = textureSample(mainTex, mainSamp, in.uv);
|
|
return vec4f(sampled.r * 0.25, sampled.g * 0.5, sampled.b, sampled.a) * in.color;
|
|
}
|
|
)wgsl";
|
|
Shader *shader = gfx->newShaderFromWgsl({}, frag);
|
|
REQUIRE(shader != nullptr);
|
|
REQUIRE(shader->gpuHandle != nullptr);
|
|
|
|
const uint8_t white[4] = {255, 255, 255, 255};
|
|
Texture *texture = gfx->newTexture(1, 1, white);
|
|
Canvas *canvas = gfx->newCanvas(64, 64);
|
|
REQUIRE(texture != nullptr);
|
|
REQUIRE(canvas != nullptr);
|
|
gfx->setCanvas(canvas);
|
|
gfx->clear(Color(0.f, 0.f, 0.f, 1.f), std::nullopt, std::nullopt);
|
|
gfx->drawTexturedRectShader(texture, shader, 8.f, 8.f, 48.f, 48.f,
|
|
Color(1.f, 1.f, 1.f, 1.f));
|
|
gfx->setCanvas(nullptr);
|
|
|
|
const Color center = canvas->getPixel(32, 32);
|
|
REQUIRE(center.r > 0.20f);
|
|
REQUIRE(center.r < 0.30f);
|
|
REQUIRE(center.g > 0.45f);
|
|
REQUIRE(center.g < 0.55f);
|
|
REQUIRE(center.b > 0.95f);
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.dynamicMeshUpdate") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
|
|
const float positions[] = {
|
|
-1.f, -1.f, 0.f,
|
|
1.f, -1.f, 0.f,
|
|
0.f, 1.f, 0.f,
|
|
};
|
|
const uint32_t indices[] = {0, 1, 2};
|
|
Mesh *mesh = gfx->newMeshFromArrays(positions, nullptr, nullptr, 3, indices, 3);
|
|
REQUIRE(mesh != nullptr);
|
|
void *stableGpuHandle = mesh->gpuHandle;
|
|
|
|
const float moved[] = {
|
|
-2.f, -1.f, 0.f,
|
|
2.f, -1.f, 0.f,
|
|
0.f, 2.f, 0.f,
|
|
};
|
|
REQUIRE(gfx->updateMeshVertices(mesh, moved, nullptr, nullptr, 3, nullptr, 0));
|
|
REQUIRE(mesh->gpuHandle == stableGpuHandle);
|
|
REQUIRE(mesh->gpuVertexCount == 3);
|
|
REQUIRE(mesh->indexCount == 3);
|
|
|
|
const uint32_t invalid[] = {0, 1, 3};
|
|
REQUIRE(!gfx->updateMeshVertices(mesh, moved, nullptr, nullptr, 3, invalid, 3));
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.gbufferAlphaCutout") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
|
|
const float positions[] = {
|
|
-1.f, -1.f, 0.5f, 1.f, -1.f, 0.5f, 1.f, 1.f, 0.5f, -1.f, 1.f, 0.5f,
|
|
};
|
|
const float normals[] = {
|
|
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 uvs[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
|
|
// These 3D parity fixtures use identity clip transforms rather than
|
|
// perspectiveVulkanRH_ZO, so reverse the quad to preserve the engine's
|
|
// framebuffer-space CCW front-face convention.
|
|
const uint32_t indices[] = {0, 2, 1, 2, 0, 3};
|
|
Mesh *mesh = gfx->newMeshFromArrays(positions, normals, uvs, 4, indices, 6);
|
|
REQUIRE(mesh != nullptr);
|
|
|
|
const uint8_t cutoutTexture[] = {255, 0, 0, 0, 255, 0, 0, 255};
|
|
Texture *albedo = gfx->newTexture(2, 1, cutoutTexture);
|
|
REQUIRE(albedo != nullptr);
|
|
|
|
gfx->beginGBufferPass(64, 64);
|
|
gfx->drawMeshGBufferAlpha(mesh, glm::mat4(1.f), glm::mat4(1.f), 0.1f, 100.f, albedo);
|
|
gfx->endGBufferPass();
|
|
|
|
std::unique_ptr<eve::image::ImageData> image(gfx->readGBufferToImageData("albedo"));
|
|
REQUIRE(image.get() != nullptr);
|
|
writeParityArtifact(*image, "gbuffer_alpha_cutout", gfx->getBackendName());
|
|
const uint8_t *discarded = pixel(*image, 16, 16);
|
|
REQUIRE(discarded[0] < 8);
|
|
const uint8_t *kept = pixel(*image, 48, 16);
|
|
REQUIRE(kept[0] > 247);
|
|
REQUIRE(kept[1] < 8);
|
|
REQUIRE(kept[2] < 8);
|
|
|
|
std::unique_ptr<eve::image::ImageData> normal(gfx->readGBufferToImageData("normal"));
|
|
REQUIRE(normal.get() != nullptr);
|
|
const uint8_t *encodedNormal = pixel(*normal, 48, 16);
|
|
const bool normalXCentered = encodedNormal[0] >= 127 && encodedNormal[0] <= 128;
|
|
const bool normalYCentered = encodedNormal[1] >= 127 && encodedNormal[1] <= 128;
|
|
REQUIRE(normalXCentered);
|
|
REQUIRE(normalYCentered);
|
|
REQUIRE(encodedNormal[2] > 247);
|
|
writeParityArtifact(*normal, "gbuffer_world_normal", gfx->getBackendName());
|
|
|
|
std::unique_ptr<eve::image::ImageData> depth(gfx->readGBufferToImageData("depth"));
|
|
REQUIRE(depth.get() != nullptr);
|
|
const uint8_t *linearDepth = pixel(*depth, 48, 16);
|
|
const bool expectedLinearDepth = linearDepth[0] <= 1;
|
|
REQUIRE(expectedLinearDepth);
|
|
REQUIRE(linearDepth[1] == linearDepth[0]);
|
|
REQUIRE(linearDepth[2] == linearDepth[0]);
|
|
writeParityArtifact(*depth, "gbuffer_linear_depth", gfx->getBackendName());
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.decalLayerProjection") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
const float positions[] = {
|
|
-1.f, -1.f, 0.5f, 1.f, -1.f, 0.5f, 1.f, 1.f, 0.5f, -1.f, 1.f, 0.5f,
|
|
};
|
|
const float normals[] = {
|
|
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 uvs[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
|
|
const uint32_t indices[] = {0, 2, 1, 2, 0, 3};
|
|
Mesh *mesh = gfx->newMeshFromArrays(positions, normals, uvs, 4, indices, 6);
|
|
REQUIRE(mesh != nullptr);
|
|
const uint8_t white[] = {255, 255, 255, 255};
|
|
const uint8_t red[] = {200, 20, 20, 255};
|
|
Texture *base = gfx->newTexture(1, 1, white);
|
|
Texture *decal = gfx->newTexture(1, 1, red);
|
|
REQUIRE(base != nullptr);
|
|
REQUIRE(decal != nullptr);
|
|
|
|
gfx->beginGBufferPass(64, 64);
|
|
gfx->drawMeshGBuffer(mesh, glm::mat4(1.f), glm::mat4(1.f), 0.1f, 100.f, base);
|
|
gfx->endGBufferPass();
|
|
gfx->beginDecalPass(64, 64);
|
|
gfx->setDecalCamera(glm::mat4(1.f), 0.1f, 100.f);
|
|
glm::mat4 decalModel(1.f);
|
|
decalModel[2][2] = 2.f;
|
|
gfx->drawDecal(decalModel, decal, nullptr, nullptr, nullptr, 1.f, 0.f, 0.f, 0.f, 0.f);
|
|
gfx->endDecalPass();
|
|
|
|
std::unique_ptr<eve::image::ImageData> image(gfx->readDecalLayerToImageData("albedo"));
|
|
REQUIRE(image.get() != nullptr);
|
|
writeParityArtifact(*image, "decal_layer_projection", gfx->getBackendName());
|
|
const uint8_t *center = pixel(*image, 32, 32);
|
|
const bool decalCenterRed = center[0] > 150 && center[1] < 80;
|
|
REQUIRE(decalCenterRed);
|
|
const uint8_t *corner = pixel(*image, 2, 2);
|
|
REQUIRE(corner[0] < 20);
|
|
|
|
Lighting3DPack lighting{};
|
|
lighting.ambient = glm::vec4(1.f, 1.f, 1.f, 0.f);
|
|
gfx->setMesh3DLighting(lighting);
|
|
gfx->setMesh3DViewProj(glm::mat4(1.f));
|
|
gfx->setMesh3DView(glm::mat4(1.f));
|
|
gfx->setMesh3DCameraPos(glm::vec3(0.f, 0.f, 3.f));
|
|
Canvas *forwardTarget = gfx->newCanvas(64, 64);
|
|
REQUIRE(forwardTarget != nullptr);
|
|
gfx->begin3DFrameToCanvas(forwardTarget);
|
|
gfx->drawMesh(mesh, glm::mat4(1.f), base, Color(1.f));
|
|
gfx->end3DFrameToCanvas();
|
|
std::unique_ptr<eve::image::ImageData> composited(forwardTarget->newImageData());
|
|
REQUIRE(composited.get() != nullptr);
|
|
const uint8_t *compositedCenter = pixel(*composited, 32, 32);
|
|
const bool centerHasRedDecal = compositedCenter[0] > compositedCenter[1] + 80;
|
|
REQUIRE(centerHasRedDecal);
|
|
const uint8_t *compositedCorner = pixel(*composited, 2, 2);
|
|
const bool cornerHasBase = compositedCorner[0] > 180 && compositedCorner[1] > 180;
|
|
REQUIRE(cornerHasBase);
|
|
writeParityArtifact(*composited, "decal_forward_composite", gfx->getBackendName());
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.pbrNormalParallaxAndCellBomb") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
const std::string backend = gfx->getBackendName();
|
|
const bool supportedBackend = backend == "vulkan" || backend == "webgpu";
|
|
REQUIRE(supportedBackend);
|
|
|
|
const float positions[] = {
|
|
-1.f, -1.f, 0.5f, 1.f, -1.f, 0.5f, 1.f, 1.f, 0.5f, -1.f, 1.f, 0.5f,
|
|
};
|
|
const float normals[] = {
|
|
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 uvs[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
|
|
const uint32_t indices[] = {0, 2, 1, 2, 0, 3};
|
|
Mesh *mesh = gfx->newMeshFromArrays(positions, normals, uvs, 4, indices, 6);
|
|
REQUIRE(mesh != nullptr);
|
|
|
|
const uint8_t stripes[] = {
|
|
255, 32, 16, 255, 255, 32, 16, 255, 16, 32, 255, 255, 16, 32, 255, 255,
|
|
255, 32, 16, 255, 255, 32, 16, 255, 16, 32, 255, 255, 16, 32, 255, 255,
|
|
};
|
|
const uint8_t flatNormal[] = {128, 128, 255, 255};
|
|
const uint8_t tiltedNormal[] = {255, 128, 128, 255};
|
|
const uint8_t raisedHeight[] = {64, 64, 64, 255};
|
|
Texture *albedo = gfx->newTexture(8, 1, stripes);
|
|
Texture *flat = gfx->newTexture(1, 1, flatNormal);
|
|
Texture *tilted = gfx->newTexture(1, 1, tiltedNormal);
|
|
Texture *height = gfx->newTexture(1, 1, raisedHeight);
|
|
REQUIRE(albedo != nullptr);
|
|
REQUIRE(flat != nullptr);
|
|
REQUIRE(tilted != nullptr);
|
|
REQUIRE(height != nullptr);
|
|
gfx->setTextureSampler(albedo, TextureSampler::nearest());
|
|
|
|
Lighting3DPack lighting{};
|
|
lighting.ambient = glm::vec4(0.02f, 0.02f, 0.02f, 0.f);
|
|
lighting.count = 1;
|
|
lighting.lights[0].posRadius = glm::vec4(1.f, 0.f, 0.15f, 0.f);
|
|
lighting.lights[0].color = glm::vec4(0.85f, 0.85f, 0.85f, 1.f);
|
|
gfx->setMesh3DLighting(lighting);
|
|
gfx->setMesh3DViewProj(glm::mat4(1.f));
|
|
gfx->setMesh3DView(glm::mat4(1.f));
|
|
gfx->setMesh3DCameraPos(glm::vec3(1.2f, 0.f, 3.f));
|
|
gfx->setMesh3DMaterial(0.f, 0.8f);
|
|
|
|
auto render = [&](Texture *normal, Texture *heightMap, float parallax, float bombStrength,
|
|
const char *artifact) {
|
|
gfx->setMesh3DNormalTexture(normal);
|
|
gfx->setMesh3DHeightTexture(heightMap);
|
|
gfx->setMesh3DParallax(parallax, 12.f, 12.f);
|
|
gfx->setMesh3DTexCellBomb(4.f, bombStrength, 1.f);
|
|
Canvas *target = gfx->newCanvas(64, 64);
|
|
REQUIRE(target != nullptr);
|
|
gfx->begin3DFrameToCanvas(target);
|
|
gfx->drawMesh(mesh, glm::mat4(1.f), albedo, Color(1.f));
|
|
gfx->end3DFrameToCanvas();
|
|
std::unique_ptr<eve::image::ImageData> image(target->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
writeParityArtifact(*image, artifact, backend);
|
|
return image;
|
|
};
|
|
|
|
auto baseline = render(flat, height, 0.f, 0.f, "pbr_material_baseline");
|
|
auto normalMapped = render(tilted, height, 0.f, 0.f, "pbr_material_normal_map");
|
|
auto parallaxMapped = render(flat, height, 0.3f, 0.f, "pbr_material_parallax");
|
|
auto cellBombed = render(flat, height, 0.f, 1.f, "pbr_material_cell_bomb");
|
|
|
|
// The albedo texture requests nearest filtering. The center boundary must
|
|
// stay a hard blue-to-red transition on both backends; using WebGPU's
|
|
// backend-global linear sampler here blends the two texels.
|
|
const uint8_t *nearestBlue = pixel(*baseline, 31, 32);
|
|
const uint8_t *nearestRed = pixel(*baseline, 32, 32);
|
|
REQUIRE(nearestBlue[2] > nearestBlue[0] + 50);
|
|
REQUIRE(nearestRed[0] > nearestRed[2] + 50);
|
|
REQUIRE(imageRgbDifference(*baseline, *normalMapped) > 100000u);
|
|
REQUIRE(imageRgbDifference(*baseline, *parallaxMapped) > 50000u);
|
|
REQUIRE(imageRgbDifference(*baseline, *cellBombed) > 50000u);
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.pbrEnvironmentAndCloudShadow") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
const std::string backend = gfx->getBackendName();
|
|
const bool supportedBackend = backend == "vulkan" || backend == "webgpu";
|
|
REQUIRE(supportedBackend);
|
|
|
|
const float positions[] = {
|
|
-1.f, -1.f, 0.5f, 1.f, -1.f, 0.5f, 1.f, 1.f, 0.5f, -1.f, 1.f, 0.5f,
|
|
};
|
|
const float normals[] = {
|
|
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 uvs[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
|
|
const uint32_t indices[] = {0, 2, 1, 2, 0, 3};
|
|
Mesh *mesh = gfx->newMeshFromArrays(positions, normals, uvs, 4, indices, 6);
|
|
REQUIRE(mesh != nullptr);
|
|
|
|
const uint8_t white[] = {255, 255, 255, 255};
|
|
const uint8_t flatNormal[] = {128, 128, 255, 255};
|
|
const uint8_t greenCube[] = {
|
|
16, 220, 32, 255, 16, 220, 32, 255, 16, 220, 32, 255,
|
|
16, 220, 32, 255, 16, 220, 32, 255, 16, 220, 32, 255,
|
|
};
|
|
Texture *albedo = gfx->newTexture(1, 1, white);
|
|
Texture *normal = gfx->newTexture(1, 1, flatNormal);
|
|
Texture *environment = gfx->newCubemap(1, greenCube);
|
|
REQUIRE(albedo != nullptr);
|
|
REQUIRE(normal != nullptr);
|
|
REQUIRE(environment != nullptr);
|
|
gfx->setMesh3DNormalTexture(normal);
|
|
gfx->setMesh3DHeightTexture(nullptr);
|
|
gfx->setMesh3DParallax(0.f);
|
|
gfx->setMesh3DTexCellBomb(1.f, 0.f);
|
|
gfx->setMesh3DViewProj(glm::mat4(1.f));
|
|
gfx->setMesh3DView(glm::mat4(1.f));
|
|
gfx->setMesh3DCameraPos(glm::vec3(0.f, 0.f, 3.f));
|
|
|
|
auto render = [&](const char *artifact) {
|
|
Canvas *target = gfx->newCanvas(64, 64);
|
|
REQUIRE(target != nullptr);
|
|
gfx->begin3DFrameToCanvas(target);
|
|
gfx->drawMesh(mesh, glm::mat4(1.f), albedo, Color(1.f));
|
|
gfx->end3DFrameToCanvas();
|
|
std::unique_ptr<eve::image::ImageData> image(target->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
writeParityArtifact(*image, artifact, backend);
|
|
return image;
|
|
};
|
|
|
|
Lighting3DPack noLights{};
|
|
noLights.ambient = glm::vec4(0.f);
|
|
gfx->setMesh3DLighting(noLights);
|
|
gfx->setMesh3DMaterial(1.f, 0.1f);
|
|
gfx->setMesh3DEnv(nullptr, 0.f);
|
|
auto environmentOff = render("pbr_environment_off");
|
|
gfx->setMesh3DEnv(environment, 1.f);
|
|
auto environmentOn = render("pbr_environment_on");
|
|
const uint8_t *reflected = pixel(*environmentOn, 32, 32);
|
|
REQUIRE(reflected[1] > reflected[0] + 20);
|
|
REQUIRE(imageRgbDifference(*environmentOff, *environmentOn) > 20000u);
|
|
|
|
Lighting3DPack sun{};
|
|
sun.ambient = glm::vec4(0.01f, 0.01f, 0.01f, 0.f);
|
|
sun.count = 1;
|
|
sun.lights[0].posRadius = glm::vec4(0.f, 0.f, 1.f, 0.f);
|
|
sun.lights[0].color = glm::vec4(0.9f, 0.9f, 0.9f, 1.f);
|
|
gfx->setMesh3DLighting(sun);
|
|
gfx->setMesh3DMaterial(0.f, 0.8f);
|
|
gfx->setMesh3DEnv(nullptr, 0.f);
|
|
gfx->setCloudShadows(0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f);
|
|
auto cloudOff = render("pbr_cloud_shadow_off");
|
|
gfx->setCloudShadows(1.f, 1.f, 0.f, 0.f, 0.f, 0.f, 0.f);
|
|
auto cloudOn = render("pbr_cloud_shadow_on");
|
|
const uint8_t *lit = pixel(*cloudOff, 32, 32);
|
|
const uint8_t *shadowed = pixel(*cloudOn, 32, 32);
|
|
REQUIRE(lit[0] > shadowed[0] + 80);
|
|
REQUIRE(imageRgbDifference(*cloudOff, *cloudOn) > 100000u);
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.maskedMaterialTechniques") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
const float positions[] = {
|
|
-1.f, -1.f, 0.5f, 1.f, -1.f, 0.5f, 1.f, 1.f, 0.5f, -1.f, 1.f, 0.5f,
|
|
};
|
|
const float normals[] = {
|
|
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 uvs[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
|
|
const uint32_t indices[] = {0, 2, 1, 2, 0, 3};
|
|
Mesh *mesh = gfx->newMeshFromArrays(positions, normals, uvs, 4, indices, 6);
|
|
const uint8_t halfAlpha[] = {255, 255, 255, 128};
|
|
Texture *albedo = gfx->newTexture(1, 1, halfAlpha);
|
|
REQUIRE(mesh != nullptr);
|
|
REQUIRE(albedo != nullptr);
|
|
|
|
Lighting3DPack lighting{};
|
|
lighting.ambient = glm::vec4(1.f, 1.f, 1.f, 0.f);
|
|
gfx->setMesh3DLighting(lighting);
|
|
gfx->setMesh3DViewProj(glm::mat4(1.f));
|
|
gfx->setMesh3DView(glm::mat4(1.f));
|
|
gfx->setMesh3DCameraPos(glm::vec3(0.f, 0.f, 3.f));
|
|
|
|
auto render = [&](float cutoff, const char *technique, const char *artifact) {
|
|
gfx->setMesh3DSurface(SurfaceMode::Masked, BlendMode::Opaque, true, false, cutoff,
|
|
technique);
|
|
Canvas *target = gfx->newCanvas(64, 64);
|
|
REQUIRE(target != nullptr);
|
|
gfx->begin3DFrameToCanvas(target);
|
|
gfx->drawMesh(mesh, glm::mat4(1.f), albedo, Color(1.f));
|
|
gfx->end3DFrameToCanvas();
|
|
std::unique_ptr<eve::image::ImageData> image(target->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
writeParityArtifact(*image, artifact, gfx->getBackendName());
|
|
return image;
|
|
};
|
|
|
|
auto cutoffKept = render(0.4f, "cutoff", "masked_cutoff_kept");
|
|
auto cutoffDiscarded = render(0.6f, "cutoff", "masked_cutoff_discarded");
|
|
REQUIRE(pixel(*cutoffKept, 32, 32)[0] > 180);
|
|
REQUIRE(pixel(*cutoffDiscarded, 32, 32)[0] < 32);
|
|
|
|
auto visiblePixels = [](const eve::image::ImageData &image) {
|
|
int visible = 0;
|
|
for (int y = 0; y < 64; ++y)
|
|
for (int x = 0; x < 64; ++x)
|
|
if (pixel(image, x, y)[0] > 180) ++visible;
|
|
return visible;
|
|
};
|
|
const int fullyCovered = visiblePixels(*cutoffKept);
|
|
REQUIRE(fullyCovered > 3500);
|
|
|
|
for (const char *technique : {"dither", "coverage"}) {
|
|
auto dithered = render(0.5f, technique, technique);
|
|
const int visible = visiblePixels(*dithered);
|
|
// Alpha hashing is backend-specific at fragment-coordinate precision. Validate the
|
|
// material contract (partial coverage) relative to the rendered quad instead of a
|
|
// Vulkan-tuned absolute pixel count.
|
|
REQUIRE(visible * 5 > fullyCovered);
|
|
REQUIRE(visible * 20 < fullyCovered * 17);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.surfaceBlendDepthWriteAndCulling") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
const float positions[] = {
|
|
-1.f, -1.f, 0.2f, 1.f, -1.f, 0.2f, 1.f, 1.f, 0.2f, -1.f, 1.f, 0.2f,
|
|
};
|
|
const float normals[] = {
|
|
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 uvs[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f};
|
|
const uint32_t frontIndices[] = {0, 2, 1, 2, 0, 3};
|
|
const uint32_t backIndices[] = {0, 1, 2, 2, 3, 0};
|
|
Mesh *front = gfx->newMeshFromArrays(positions, normals, uvs, 4, frontIndices, 6);
|
|
Mesh *back = gfx->newMeshFromArrays(positions, normals, uvs, 4, backIndices, 6);
|
|
const uint8_t halfRed[] = {255, 0, 0, 128};
|
|
const uint8_t green[] = {0, 255, 0, 255};
|
|
const uint8_t blue[] = {0, 0, 255, 255};
|
|
const uint8_t halfGray[] = {128, 128, 128, 128};
|
|
Texture *redTexture = gfx->newTexture(1, 1, halfRed);
|
|
Texture *greenTexture = gfx->newTexture(1, 1, green);
|
|
Texture *blueTexture = gfx->newTexture(1, 1, blue);
|
|
Texture *grayTexture = gfx->newTexture(1, 1, halfGray);
|
|
REQUIRE(front != nullptr);
|
|
REQUIRE(back != nullptr);
|
|
REQUIRE(redTexture != nullptr);
|
|
REQUIRE(greenTexture != nullptr);
|
|
REQUIRE(blueTexture != nullptr);
|
|
REQUIRE(grayTexture != nullptr);
|
|
|
|
Lighting3DPack lighting{};
|
|
lighting.ambient = glm::vec4(1.f, 1.f, 1.f, 0.f);
|
|
gfx->setMesh3DLighting(lighting);
|
|
gfx->setMesh3DViewProj(glm::mat4(1.f));
|
|
gfx->setMesh3DView(glm::mat4(1.f));
|
|
gfx->setMesh3DCameraPos(glm::vec3(0.f, 0.f, 3.f));
|
|
|
|
auto renderCulling = [&](bool doubleSided, const char *artifact) {
|
|
gfx->setMesh3DSurface(SurfaceMode::Opaque, BlendMode::Opaque, true, doubleSided, 0.5f,
|
|
"cutoff");
|
|
Canvas *target = gfx->newCanvas(64, 64);
|
|
gfx->begin3DFrameToCanvas(target);
|
|
gfx->drawMesh(back, glm::mat4(1.f), greenTexture, Color(1.f));
|
|
gfx->end3DFrameToCanvas();
|
|
std::unique_ptr<eve::image::ImageData> image(target->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
writeParityArtifact(*image, artifact, gfx->getBackendName());
|
|
return image;
|
|
};
|
|
auto culled = renderCulling(false, "surface_backface_culled");
|
|
auto twoSided = renderCulling(true, "surface_double_sided");
|
|
REQUIRE(pixel(*culled, 32, 32)[1] < 32);
|
|
REQUIRE(pixel(*twoSided, 32, 32)[1] > 180);
|
|
|
|
auto renderDepthWrite = [&](bool depthWrite, const char *artifact) {
|
|
Canvas *target = gfx->newCanvas(64, 64);
|
|
gfx->begin3DFrameToCanvas(target);
|
|
gfx->setMesh3DSurface(SurfaceMode::Transparent, BlendMode::Alpha, depthWrite, false, 0.5f,
|
|
"cutoff");
|
|
gfx->drawMesh(front, glm::mat4(1.f), redTexture, Color(1.f));
|
|
glm::mat4 farther(1.f);
|
|
farther[3][2] = 0.6f;
|
|
gfx->setMesh3DSurface(SurfaceMode::Opaque, BlendMode::Opaque, true, false, 0.5f, "cutoff");
|
|
gfx->drawMesh(front, farther, greenTexture, Color(1.f));
|
|
gfx->end3DFrameToCanvas();
|
|
std::unique_ptr<eve::image::ImageData> image(target->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
writeParityArtifact(*image, artifact, gfx->getBackendName());
|
|
return image;
|
|
};
|
|
auto writesDepth = renderDepthWrite(true, "surface_transparent_depth_write");
|
|
auto skipsDepth = renderDepthWrite(false, "surface_transparent_no_depth_write");
|
|
const uint8_t *blocked = pixel(*writesDepth, 32, 32);
|
|
const uint8_t *overwritten = pixel(*skipsDepth, 32, 32);
|
|
REQUIRE(blocked[0] > 80);
|
|
REQUIRE(blocked[0] > blocked[1] + 60);
|
|
REQUIRE(overwritten[1] > 180);
|
|
REQUIRE(overwritten[0] < 20);
|
|
|
|
auto renderBlend = [&](BlendMode blend, Texture *overlay, const char *artifact) {
|
|
Canvas *target = gfx->newCanvas(64, 64);
|
|
gfx->begin3DFrameToCanvas(target);
|
|
glm::mat4 farther(1.f);
|
|
farther[3][2] = 0.6f;
|
|
gfx->setMesh3DSurface(SurfaceMode::Opaque, BlendMode::Opaque, true, false, 0.5f, "cutoff");
|
|
gfx->drawMesh(front, farther, blueTexture, Color(1.f));
|
|
gfx->setMesh3DSurface(SurfaceMode::Transparent, blend, false, false, 0.5f, "cutoff");
|
|
gfx->drawMesh(front, glm::mat4(1.f), overlay, Color(1.f));
|
|
gfx->end3DFrameToCanvas();
|
|
std::unique_ptr<eve::image::ImageData> image(target->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
writeParityArtifact(*image, artifact, gfx->getBackendName());
|
|
return image;
|
|
};
|
|
auto additive = renderBlend(BlendMode::Additive, redTexture, "surface_additive");
|
|
auto premultiplied =
|
|
renderBlend(BlendMode::Premultiplied, redTexture, "surface_premultiplied");
|
|
auto multiply = renderBlend(BlendMode::Multiply, grayTexture, "surface_multiply");
|
|
const uint8_t *added = pixel(*additive, 32, 32);
|
|
REQUIRE(added[0] > 80);
|
|
REQUIRE(added[2] > 180);
|
|
const uint8_t *premul = pixel(*premultiplied, 32, 32);
|
|
REQUIRE(premul[0] > 180);
|
|
REQUIRE(premul[2] > 80);
|
|
const uint8_t *multiplied = pixel(*multiply, 32, 32);
|
|
REQUIRE(multiplied[0] < 20);
|
|
REQUIRE(multiplied[2] > 60);
|
|
REQUIRE(multiplied[2] < 200);
|
|
}
|
|
|
|
TEST_CASE("graphics.backendParity.alphaMaskThresholdSoftnessAndInversion") {
|
|
Graphics *gfx = headlessGraphics();
|
|
REQUIRE(gfx != nullptr);
|
|
const uint8_t red[] = {255, 0, 0, 255};
|
|
const uint8_t ramp[] = {
|
|
0, 0, 0, 255,
|
|
128, 128, 128, 255,
|
|
255, 255, 255, 255,
|
|
};
|
|
Texture *color = gfx->newTexture(1, 1, red);
|
|
Texture *mask = gfx->newTexture(3, 1, ramp);
|
|
REQUIRE(color != nullptr);
|
|
REQUIRE(mask != nullptr);
|
|
gfx->setTextureSampler(mask, TextureSampler::nearest());
|
|
std::unique_ptr<AlphaMask> alphaMask(gfx->newAlphaMask());
|
|
REQUIRE(alphaMask.get() != nullptr);
|
|
alphaMask->setThreshold(0.5f);
|
|
alphaMask->setSoftness(0.15f);
|
|
|
|
auto render = [&](bool inverted, const char *artifact) {
|
|
alphaMask->setInverted(inverted);
|
|
Canvas *canvas = gfx->newCanvas(64, 64);
|
|
gfx->setCanvas(canvas);
|
|
gfx->clear(Color(0.f, 0.f, 1.f, 1.f), std::nullopt, std::nullopt);
|
|
alphaMask->draw(color, mask, 2.f, 8.f, 60.f, 48.f);
|
|
gfx->setCanvas();
|
|
std::unique_ptr<eve::image::ImageData> image(canvas->newImageData());
|
|
REQUIRE(image.get() != nullptr);
|
|
writeParityArtifact(*image, artifact, gfx->getBackendName());
|
|
return image;
|
|
};
|
|
|
|
auto normal = render(false, "alpha_mask_normal");
|
|
const uint8_t *normalLow = pixel(*normal, 10, 32);
|
|
const uint8_t *normalMid = pixel(*normal, 32, 32);
|
|
const uint8_t *normalHigh = pixel(*normal, 54, 32);
|
|
REQUIRE(normalLow[2] > 240);
|
|
REQUIRE(normalLow[0] < 15);
|
|
REQUIRE(normalMid[0] > 80);
|
|
REQUIRE(normalMid[2] > 80);
|
|
REQUIRE(normalHigh[0] > 240);
|
|
REQUIRE(normalHigh[2] < 15);
|
|
|
|
auto inverted = render(true, "alpha_mask_inverted");
|
|
const uint8_t *invertedLow = pixel(*inverted, 10, 32);
|
|
const uint8_t *invertedHigh = pixel(*inverted, 54, 32);
|
|
REQUIRE(invertedLow[0] > 240);
|
|
REQUIRE(invertedLow[2] < 15);
|
|
REQUIRE(invertedHigh[2] > 240);
|
|
REQUIRE(invertedHigh[0] < 15);
|
|
}
|