Add stroke alpha blend reference tests
This commit is contained in:
@@ -94,8 +94,8 @@ Known local toolchain state:
|
||||
including foundation event/logging/task queue coverage, PNG metadata and
|
||||
decode, PPI header/layout, settings document, document
|
||||
snapshot/per-layer-frame/move/duration/face-pixel/PPI export coverage,
|
||||
snapshot-embedded face-payload rejection, paint brush/stroke/stroke-script
|
||||
coverage, renderer shader descriptor and OpenGL capability coverage, UI
|
||||
snapshot-embedded face-payload rejection, paint brush/final-blend/
|
||||
stroke-alpha-blend/stroke/stroke-script coverage, renderer shader descriptor and OpenGL capability coverage, UI
|
||||
color parsing, and layout XML parse coverage.
|
||||
- `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by
|
||||
`pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture.
|
||||
|
||||
@@ -34,8 +34,8 @@ and validation command.
|
||||
| ABR import | `ABR`, `Brush` | `pp_assets`, `pp_paint` | Sample ABR and malformed ABR |
|
||||
| PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture |
|
||||
| Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
|
||||
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | CPU reference and GPU golden |
|
||||
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | CPU reference vectors and GPU parity |
|
||||
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference and GPU golden |
|
||||
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors plus GPU parity |
|
||||
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
|
||||
|
||||
## Layers And Animation
|
||||
@@ -80,4 +80,3 @@ and validation command.
|
||||
| Logging/crash reporting | `log`, BugTrap/AppCenter | `pp_foundation`, platform wrappers | Log formatting and platform compile |
|
||||
| Headless automation | none yet | `tools/pano_cli` | JSON command fixtures |
|
||||
| Tracing | none yet | `pp_foundation` | Span nesting/timing tests |
|
||||
|
||||
|
||||
@@ -318,7 +318,8 @@ asset-level RGBA PNG payload decoding, and a pure typed settings document
|
||||
model, with
|
||||
corrupt/truncated/unsupported, extreme-dimension, and key/value limit tests.
|
||||
`pp_paint` has started with pure brush parameter validation/stamp evaluation,
|
||||
CPU reference math for the five current shader blend modes, and deterministic
|
||||
CPU reference math for the five current final RGBA shader blend modes plus the
|
||||
shader-style stroke-alpha blend modes used by pattern/dual-brush mixing, and deterministic
|
||||
stroke spacing/interpolation plus a pure text stroke-script parser.
|
||||
`pp_document` has
|
||||
started with a pure canvas/layer/frame model, alpha-lock metadata, snapshot
|
||||
@@ -537,7 +538,7 @@ Implementation tasks:
|
||||
- layer blend
|
||||
- equirect export
|
||||
- readback bounds
|
||||
- Add CPU reference tests for blend modes.
|
||||
- Add CPU reference tests for final RGBA and stroke-alpha blend modes.
|
||||
- Compare GPU output to golden/reference data with explicit tolerances.
|
||||
|
||||
Gate:
|
||||
|
||||
@@ -44,6 +44,67 @@ namespace {
|
||||
return stroke;
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_screen(float base, float stroke) noexcept
|
||||
{
|
||||
return base + stroke - (base * stroke);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_hard_light(float base, float stroke) noexcept
|
||||
{
|
||||
return stroke < 0.5F
|
||||
? base * (stroke * 2.0F)
|
||||
: blend_stroke_screen(base, 2.0F * stroke - 1.0F);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_hard_mix(float base, float stroke) noexcept
|
||||
{
|
||||
if (base == 0.0F) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
return base + stroke < 0.5F ? 0.0F : saturate(base + stroke);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_color_dodge(float base, float stroke) noexcept
|
||||
{
|
||||
if (base == 0.0F) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
if (stroke == 1.0F) {
|
||||
return 1.0F;
|
||||
}
|
||||
|
||||
return base / (1.0F - stroke);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_color_burn(float base, float stroke) noexcept
|
||||
{
|
||||
if (base == 1.0F) {
|
||||
return 1.0F;
|
||||
}
|
||||
|
||||
if (stroke == 0.0F) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
return 1.0F - std::min(1.0F, (1.0F - base) / stroke);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_linear_height(float base, float stroke, float depth) noexcept
|
||||
{
|
||||
const auto partial = (1.0F - stroke) * std::pow(depth, 0.25F) + (base * depth * 10.0F);
|
||||
return base * partial;
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_stroke_height(float base, float stroke, float depth) noexcept
|
||||
{
|
||||
const auto a = std::pow(1.0F - stroke, std::max(1.0F, (1.0F - depth) * 10.0F))
|
||||
* std::pow(depth, 0.25F);
|
||||
const auto b = base * depth * 5.0F;
|
||||
return base * (a + b);
|
||||
}
|
||||
|
||||
[[nodiscard]] float blend_rgb(float base, float stroke, float base_alpha, float stroke_alpha, float alpha_total, BlendMode mode) noexcept
|
||||
{
|
||||
if (alpha_total <= 0.0F) {
|
||||
@@ -88,6 +149,44 @@ Rgba blend_pixels(Rgba base, Rgba stroke, BlendMode mode) noexcept
|
||||
};
|
||||
}
|
||||
|
||||
float blend_stroke_alpha(
|
||||
float base,
|
||||
float stroke,
|
||||
float depth,
|
||||
StrokeBlendMode mode) noexcept
|
||||
{
|
||||
base = saturate(base);
|
||||
stroke = saturate(stroke);
|
||||
depth = saturate(depth);
|
||||
|
||||
switch (mode) {
|
||||
case StrokeBlendMode::normal:
|
||||
return saturate(mix(base, stroke, depth));
|
||||
case StrokeBlendMode::multiply:
|
||||
return saturate(mix(base, base * stroke, depth));
|
||||
case StrokeBlendMode::subtract:
|
||||
return saturate(mix(base, std::max(0.0F, base - stroke), depth));
|
||||
case StrokeBlendMode::darken:
|
||||
return saturate(mix(base, std::min(base, stroke), depth));
|
||||
case StrokeBlendMode::overlay:
|
||||
return saturate(mix(base, blend_stroke_hard_light(stroke, base), depth));
|
||||
case StrokeBlendMode::color_dodge:
|
||||
return saturate(mix(base, blend_stroke_color_dodge(base, stroke), depth));
|
||||
case StrokeBlendMode::color_burn:
|
||||
return saturate(mix(base, blend_stroke_color_burn(base, stroke), depth));
|
||||
case StrokeBlendMode::linear_burn:
|
||||
return saturate(mix(base, saturate(base + stroke - 1.0F), depth));
|
||||
case StrokeBlendMode::hard_mix:
|
||||
return saturate(mix(base, blend_stroke_hard_mix(base, stroke), depth));
|
||||
case StrokeBlendMode::linear_height:
|
||||
return saturate(blend_stroke_linear_height(base, stroke, depth));
|
||||
case StrokeBlendMode::height:
|
||||
return saturate(blend_stroke_height(base, stroke, depth));
|
||||
}
|
||||
|
||||
return 1.0F;
|
||||
}
|
||||
|
||||
const char* blend_mode_name(BlendMode mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
@@ -106,4 +205,34 @@ const char* blend_mode_name(BlendMode mode) noexcept
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* stroke_blend_mode_name(StrokeBlendMode mode) noexcept
|
||||
{
|
||||
switch (mode) {
|
||||
case StrokeBlendMode::normal:
|
||||
return "normal";
|
||||
case StrokeBlendMode::multiply:
|
||||
return "multiply";
|
||||
case StrokeBlendMode::subtract:
|
||||
return "subtract";
|
||||
case StrokeBlendMode::darken:
|
||||
return "darken";
|
||||
case StrokeBlendMode::overlay:
|
||||
return "overlay";
|
||||
case StrokeBlendMode::color_dodge:
|
||||
return "color_dodge";
|
||||
case StrokeBlendMode::color_burn:
|
||||
return "color_burn";
|
||||
case StrokeBlendMode::linear_burn:
|
||||
return "linear_burn";
|
||||
case StrokeBlendMode::hard_mix:
|
||||
return "hard_mix";
|
||||
case StrokeBlendMode::linear_height:
|
||||
return "linear_height";
|
||||
case StrokeBlendMode::height:
|
||||
return "height";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,20 @@ enum class BlendMode : std::uint8_t {
|
||||
overlay,
|
||||
};
|
||||
|
||||
enum class StrokeBlendMode : std::uint8_t {
|
||||
normal,
|
||||
multiply,
|
||||
subtract,
|
||||
darken,
|
||||
overlay,
|
||||
color_dodge,
|
||||
color_burn,
|
||||
linear_burn,
|
||||
hard_mix,
|
||||
linear_height,
|
||||
height,
|
||||
};
|
||||
|
||||
struct Rgba {
|
||||
float r = 0.0F;
|
||||
float g = 0.0F;
|
||||
@@ -20,6 +34,12 @@ struct Rgba {
|
||||
};
|
||||
|
||||
[[nodiscard]] Rgba blend_pixels(Rgba base, Rgba stroke, BlendMode mode) noexcept;
|
||||
[[nodiscard]] float blend_stroke_alpha(
|
||||
float base,
|
||||
float stroke,
|
||||
float depth,
|
||||
StrokeBlendMode mode) noexcept;
|
||||
[[nodiscard]] const char* blend_mode_name(BlendMode mode) noexcept;
|
||||
[[nodiscard]] const char* stroke_blend_mode_name(StrokeBlendMode mode) noexcept;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
|
||||
using pp::paint::BlendMode;
|
||||
using pp::paint::Rgba;
|
||||
using pp::paint::StrokeBlendMode;
|
||||
using pp::paint::blend_mode_name;
|
||||
using pp::paint::blend_pixels;
|
||||
using pp::paint::blend_stroke_alpha;
|
||||
using pp::paint::stroke_blend_mode_name;
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -91,6 +94,39 @@ void clamps_inputs_and_names_modes(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, blend_mode_name(BlendMode::overlay) == std::string_view("overlay"));
|
||||
}
|
||||
|
||||
void stroke_alpha_blend_modes_match_shader_reference_vectors(pp::tests::Harness& h)
|
||||
{
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.2F, 0.8F, 0.25F, StrokeBlendMode::normal), 0.35F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.6F, 0.5F, 1.0F, StrokeBlendMode::multiply), 0.3F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.6F, 0.2F, 1.0F, StrokeBlendMode::subtract), 0.4F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.6F, 0.2F, 1.0F, StrokeBlendMode::darken), 0.2F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.75F, 0.25F, 1.0F, StrokeBlendMode::overlay), 0.625F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.4F, 0.5F, 1.0F, StrokeBlendMode::color_dodge), 0.8F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.6F, 0.5F, 1.0F, StrokeBlendMode::color_burn), 0.2F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.75F, 0.5F, 1.0F, StrokeBlendMode::linear_burn), 0.25F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.2F, 0.2F, 1.0F, StrokeBlendMode::hard_mix), 0.0F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.7F, 0.8F, 1.0F, StrokeBlendMode::hard_mix), 1.0F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.5F, 0.25F, 0.16F, StrokeBlendMode::linear_height), 0.637170F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.5F, 0.25F, 0.16F, StrokeBlendMode::height), 0.228217F));
|
||||
}
|
||||
|
||||
void stroke_alpha_blend_modes_handle_edges_and_names(pp::tests::Harness& h)
|
||||
{
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.8F, 0.2F, 0.0F, StrokeBlendMode::multiply), 0.8F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.0F, 0.8F, 1.0F, StrokeBlendMode::color_dodge), 0.0F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.8F, 1.0F, 1.0F, StrokeBlendMode::color_dodge), 1.0F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(1.0F, 0.2F, 1.0F, StrokeBlendMode::color_burn), 1.0F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.8F, 0.0F, 1.0F, StrokeBlendMode::color_burn), 0.0F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.0F, 1.0F, 1.0F, StrokeBlendMode::hard_mix), 0.0F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(-1.0F, 2.0F, 2.0F, StrokeBlendMode::normal), 1.0F));
|
||||
PP_EXPECT(h, near(blend_stroke_alpha(0.2F, 0.2F, 0.5F, static_cast<StrokeBlendMode>(255)), 1.0F));
|
||||
|
||||
PP_EXPECT(h, stroke_blend_mode_name(StrokeBlendMode::normal) == std::string_view("normal"));
|
||||
PP_EXPECT(h, stroke_blend_mode_name(StrokeBlendMode::linear_height) == std::string_view("linear_height"));
|
||||
PP_EXPECT(h, stroke_blend_mode_name(StrokeBlendMode::height) == std::string_view("height"));
|
||||
PP_EXPECT(h, stroke_blend_mode_name(static_cast<StrokeBlendMode>(255)) == std::string_view("unknown"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -101,5 +137,7 @@ int main()
|
||||
harness.run("multiply_and_screen_are_bounded", multiply_and_screen_are_bounded);
|
||||
harness.run("color_dodge_and_overlay_handle_extremes", color_dodge_and_overlay_handle_extremes);
|
||||
harness.run("clamps_inputs_and_names_modes", clamps_inputs_and_names_modes);
|
||||
harness.run("stroke_alpha_blend_modes_match_shader_reference_vectors", stroke_alpha_blend_modes_match_shader_reference_vectors);
|
||||
harness.run("stroke_alpha_blend_modes_handle_edges_and_names", stroke_alpha_blend_modes_handle_edges_and_names);
|
||||
return harness.finish();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user