Add CPU vector graphics library

This commit is contained in:
Nikita Lisitsa 2023-06-06 13:22:00 +03:00
parent 93e5691d8a
commit 18c49f7740
27 changed files with 905 additions and 0 deletions

6
libs/vecr/CMakeLists.txt Normal file
View file

@ -0,0 +1,6 @@
file(GLOB_RECURSE PSEMEK_VECR_HEADERS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "include/*.hpp")
file(GLOB_RECURSE PSEMEK_VECR_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "source/*.cpp")
psemek_add_library(psemek-vecr ${PSEMEK_VECR_HEADERS} ${PSEMEK_VECR_SOURCES})
target_include_directories(psemek-vecr PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(psemek-vecr PUBLIC psemek-util psemek-geom psemek-io psemek-gfx)

View file

@ -0,0 +1,17 @@
#pragma once
#include <psemek/vecr/invert.hpp>
#include <psemek/vecr/intersect.hpp>
namespace psemek::vecr
{
struct add
{
std::vector<any> shapes;
float smooth = 0.f;
};
sdf_sample sdf(add const & s, geom::point<float, 2> const & p);
}

View file

@ -0,0 +1,51 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
#include <functional>
namespace psemek::vecr
{
struct any
{
any() = default;
any(any const &) = default;
any(any &&) = default;
template <typename Shape>
any(Shape && shape)
{
*this = std::forward<Shape>(shape);
}
any & operator = (any &&) = default;
any & operator = (any const &) = default;
template <typename Shape>
any & operator = (Shape && shape)
{
sdf_ = [shape = std::forward<Shape>(shape)](geom::point<float, 2> const & p){
return sdf(shape, p);
};
return *this;
}
explicit operator bool() const
{
return static_cast<bool>(sdf_);
}
friend sdf_sample sdf(any const & s, geom::point<float, 2> const & p);
private:
std::function<sdf_sample(geom::point<float, 2> const &)> sdf_;
};
inline sdf_sample sdf(any const & s, geom::point<float, 2> const & p)
{
return s.sdf_ ? s.sdf_(p) : sdf_sample{};
}
}

View file

@ -0,0 +1,22 @@
#pragma once
#include <psemek/gfx/color.hpp>
#include <functional>
namespace psemek::vecr
{
using blend_mode = std::function<gfx::color_4f(gfx::color_4f const & dst, gfx::color_4f const & src)>;
constexpr auto blend = [](gfx::color_4f const & dst, gfx::color_4f const & src)
{
return gfx::blend(dst, src);
};
constexpr auto overlay = [](gfx::color_4f const & dst, gfx::color_4f const & src)
{
return dst + src;
};
}

View file

@ -0,0 +1,27 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
namespace psemek::vecr
{
template <typename Shape>
struct border
{
Shape shape;
};
template <typename Shape>
sdf_sample sdf(border<Shape> const & s, geom::point<float, 2> const & p)
{
auto result = sdf(s.shape, p);
if (result.value < 0.f)
{
result.value *= -1.f;
result.gradient *= -1.f;
}
return result;
}
}

View file

@ -0,0 +1,17 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
namespace psemek::vecr
{
struct circle
{
geom::point<float, 2> center;
float radius;
};
sdf_sample sdf(circle const & s, geom::point<float, 2> const & p);
}

View file

@ -0,0 +1,70 @@
#pragma once
#include <psemek/vecr/any.hpp>
#include <psemek/gfx/color.hpp>
#include <functional>
namespace psemek::vecr
{
gfx::color_4f colorize(gfx::color_rgba const & color, geom::point<float, 2> const & p, sdf_sample const & sample);
struct gradient
{
gfx::color_rgba color0 = {0, 0, 0, 0};
gfx::color_rgba color1 = {0, 0, 0, 0};
any shape = {};
};
gfx::color_4f colorize(gradient const & gradient, geom::point<float, 2> const & p, sdf_sample const & sample);
struct lighting
{
any shape = {};
gfx::color_rgba color0 = {0, 0, 0, 0};
gfx::color_rgba color1 = {0, 0, 0, 0};
float slope = 1.f;
geom::vector<float, 3> direction = {0.f, 0.f, 1.f};
};
gfx::color_4f colorize(lighting const & lighting, geom::point<float, 2> const & p, sdf_sample const & sample);
struct any_colorizer
{
any_colorizer() = default;
any_colorizer(any_colorizer &&) = default;
any_colorizer(any_colorizer const &) = default;
any_colorizer & operator = (any_colorizer const &) = default;
any_colorizer & operator = (any_colorizer &&) = default;
template <typename Colorizer>
any_colorizer(Colorizer && colorizer)
{
(*this) = std::forward<Colorizer>(colorizer);
}
template <typename Colorizer>
any_colorizer & operator = (Colorizer && colorizer)
{
colorizer_ = [colorizer = std::forward<Colorizer>(colorizer)](geom::point<float, 2> const & p, sdf_sample const & sample){
return colorize(colorizer, p, sample);
};
return *this;
}
explicit operator bool() const
{
return static_cast<bool>(colorizer_);
}
friend gfx::color_4f colorize(any_colorizer const & colorizer, geom::point<float, 2> const & p, sdf_sample const & sample)
{
return colorizer.colorizer_ ? colorizer.colorizer_(p, sample) : gfx::color_4f::zero();
}
private:
std::function<gfx::color_4f(geom::point<float, 2> const &, sdf_sample const &)> colorizer_;
};
}

View file

@ -0,0 +1,58 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
#include <optional>
namespace psemek::vecr
{
template <typename Shape>
struct exact
{
Shape shape;
int iterations = 8;
};
template <typename Shape>
sdf_sample sdf(exact<Shape> const & s, geom::point<float, 2> const & p)
{
// Newton-Raphson method
std::optional<sdf_sample> first_sample;
auto q = p;
for (int i = 0; i < s.iterations; ++i)
{
auto sample = sdf(s.shape, q);
if (first_sample) first_sample = sample;
auto d = geom::length_sqr(sample.gradient);
if (d > 0.f)
q -= (sample.value / d) * sample.gradient;
else
break;
}
auto r = p - q;
auto l = geom::length(r);
sdf_sample result;
result.value = l;
if (l > 0.f)
result.gradient = r / l;
else
result.gradient = {0.f, 0.f};
if (first_sample->value < 0.f)
{
result.value *= -1.f;
result.gradient *= -1.f;
}
return result;
}
}

View file

@ -0,0 +1,15 @@
#pragma once
#include <psemek/vecr/path.hpp>
namespace psemek::vecr
{
struct fill
{
path border;
};
sdf_sample sdf(fill const & s, geom::point<float, 2> const & p);
}

View file

@ -0,0 +1,24 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
namespace psemek::vecr
{
template <typename Shape>
struct grow
{
Shape shape;
float distance;
};
template <typename Shape>
sdf_sample sdf(grow<Shape> const & s, geom::point<float, 2> const & p)
{
auto result = sdf(s.shape, p);
result.value -= s.distance;
return result;
}
}

View file

@ -0,0 +1,20 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
namespace psemek::vecr
{
struct halfspace
{
geom::point<float, 2> origin;
geom::vector<float, 2> normal;
};
inline sdf_sample sdf(halfspace const & s, geom::point<float, 2> const & p)
{
return {geom::dot(p - s.origin, s.normal), s.normal};
}
}

View file

@ -0,0 +1,21 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/vecr/any.hpp>
#include <psemek/geom/point.hpp>
#include <psemek/geom/math.hpp>
#include <vector>
namespace psemek::vecr
{
struct intersect
{
std::vector<any> shapes;
float smooth = 0.f;
};
sdf_sample sdf(intersect const & s, geom::point<float, 2> const & p);
}

View file

@ -0,0 +1,24 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
namespace psemek::vecr
{
template <typename Shape>
struct invert
{
Shape shape;
};
template <typename Shape>
sdf_sample sdf(invert<Shape> const & s, geom::point<float, 2> const & p)
{
auto result = sdf(s.shape, p);
result.value *= -1.f;
result.gradient *= -1.f;
return result;
}
}

View file

@ -0,0 +1,37 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
namespace psemek::vecr
{
template <typename Shape>
struct mirror
{
Shape shape;
geom::point<float, 2> origin{0.f, 0.f};
geom::vector<float, 2> axis{1.f, 0.f};
};
template <typename Shape>
sdf_sample sdf(mirror<Shape> const & s, geom::point<float, 2> const & p)
{
sdf_sample result;
auto r = p - s.origin;
auto d = geom::dot(r, s.axis);
if (d < 0.f)
{
result = sdf(s.shape, p);
}
else
{
result = sdf(s.shape, p - (2.f * d) * s.axis);
result.gradient -= (2.f * geom::dot(result.gradient, s.axis)) * s.axis;
}
return result;
}
}

View file

@ -0,0 +1,18 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
#include <vector>
namespace psemek::vecr
{
struct path
{
std::vector<geom::point<float, 2>> points;
};
sdf_sample sdf(path const & s, geom::point<float, 2> const & p, bool closed = false);
}

View file

@ -0,0 +1,42 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/vecr/any.hpp>
#include <psemek/vecr/colorizer.hpp>
#include <psemek/vecr/blend_mode.hpp>
#include <psemek/gfx/pixmap.hpp>
namespace psemek::vecr
{
struct primitive
{
any mask = {};
float blur = 1.f;
any_colorizer colorizer = {};
blend_mode blend = vecr::blend;
};
struct renderer
{
void reset(geom::vector<std::size_t, 2> const & size, std::size_t samples = 4, gfx::color_rgba const & color = {0, 0, 0, 0});
geom::vector<std::size_t, 2> size() const;
std::size_t samples() const;
gfx::pixmap_rgba const & result() const;
void clear(gfx::color_rgba const & color = {0, 0, 0, 0});
void draw(primitive const & primitive);
private:
std::size_t samples_ = 4;
gfx::pixmap_rgba canvas_;
mutable gfx::pixmap_rgba result_;
mutable bool need_resolve_ = false;
void resolve() const;
};
}

View file

@ -0,0 +1,14 @@
#pragma once
#include <psemek/geom/vector.hpp>
namespace psemek::vecr
{
struct sdf_sample
{
float value = std::numeric_limits<float>::infinity();
geom::vector<float, 2> gradient{0.f, 0.f};
};
}

View file

@ -0,0 +1,23 @@
#pragma once
#include <psemek/vecr/invert.hpp>
#include <psemek/vecr/intersect.hpp>
namespace psemek::vecr
{
template <typename Shape1, typename Shape2>
struct subtract
{
Shape1 shape1;
Shape2 shape2;
float smooth = 0.f;
};
template <typename Shape1, typename Shape2>
sdf_sample sdf(subtract<Shape1, Shape2> const & s, geom::point<float, 2> const & p)
{
return sdf(intersect{{s.shape1, invert{s.shape2}}, s.smooth}, p);
}
}

View file

@ -0,0 +1,28 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
#include <psemek/geom/box.hpp>
#include <cmath>
namespace psemek::vecr
{
template <typename Shape>
struct tile
{
Shape shape;
geom::box<float, 2> cell;
};
template <typename Shape>
sdf_sample sdf(tile<Shape> const & s, geom::point<float, 2> const & p)
{
auto q = p;
for (std::size_t i : {0, 1})
q[i] = s.cell[i].min + std::fmod(q[i] - s.cell[i].min, s.cell[i].length());
return sdf(s.shape, q);
}
}

View file

@ -0,0 +1,93 @@
#pragma once
#include <psemek/vecr/sdf.hpp>
#include <psemek/geom/point.hpp>
#include <psemek/geom/matrix.hpp>
#include <psemek/geom/homogeneous.hpp>
#include <psemek/geom/gauss.hpp>
namespace psemek::vecr
{
template <typename Shape>
struct translate
{
Shape shape;
geom::vector<float, 2> delta{0.f, 0.f};
};
template <typename Shape>
struct rotate
{
Shape shape;
float angle = 0.f;
geom::point<float, 2> origin{0.f, 0.f};
};
template <typename Shape>
struct scale
{
Shape shape;
geom::vector<float, 2> factor{1.f, 1.f};
geom::point<float, 2> origin{0.f, 0.f};
};
template <typename Shape>
struct transform
{
Shape shape;
geom::matrix<float, 3, 3> matrix = geom::matrix<float, 3, 3>::identity();
};
template <typename Shape>
sdf_sample sdf(translate<Shape> const & s, geom::point<float, 2> const & p)
{
return sdf(s.shape, p - s.delta);
}
template <typename Shape>
sdf_sample sdf(rotate<Shape> const & s, geom::point<float, 2> const & p)
{
auto result = sdf(s.shape, s.origin + geom::rotate(p - s.origin, -s.angle));
result.gradient = geom::rotate(result.gradient, s.angle);
return result;
}
template <typename Shape>
sdf_sample sdf(scale<Shape> const & s, geom::point<float, 2> const & p)
{
auto result = sdf(s.shape, s.origin + geom::pointwise_mult(p - s.origin, {1.f / s.factor[0], 1.f / s.factor[1]}));
auto old_grad = geom::length(result.gradient);
result.gradient[0] /= s.factor[0];
result.gradient[1] /= s.factor[1];
if (auto new_grad = geom::length(result.gradient); new_grad != 0.f)
result.value *= (old_grad / new_grad);
return result;
}
template <typename Shape>
sdf_sample sdf(transform<Shape> const & s, geom::point<float, 2> const & p)
{
auto minv = *geom::inverse(s.matrix);
auto q = minv * geom::homogeneous(p);
auto result = sdf(s.shape, geom::as_point(q));
auto old_grad = geom::length(result.gradient);
geom::matrix<float, 2, 2> mgrad;
mgrad[0][0] = (minv[0][0] * q[2] - minv[2][0] * q[0]) / (q[2] * q[2]);
mgrad[0][1] = (minv[1][0] * q[2] - minv[2][0] * q[1]) / (q[2] * q[2]);
mgrad[1][0] = (minv[0][1] * q[2] - minv[2][1] * q[0]) / (q[2] * q[2]);
mgrad[1][1] = (minv[1][1] * q[2] - minv[2][1] * q[1]) / (q[2] * q[2]);
result.gradient = mgrad * result.gradient;
if (auto new_grad = geom::length(result.gradient); new_grad != 0.f)
result.value *= (old_grad / new_grad);
return result;
}
}

41
libs/vecr/source/add.cpp Normal file
View file

@ -0,0 +1,41 @@
#include <psemek/vecr/add.hpp>
#include <psemek/geom/math.hpp>
#include <vector>
namespace psemek::vecr
{
sdf_sample sdf(add const & s, geom::point<float, 2> const & p)
{
sdf_sample result;
if (s.smooth == 0.f)
{
for (auto const & ss : s.shapes)
{
auto r = sdf(ss, p);
if (geom::make_min(result.value, r.value))
result.gradient = r.gradient;
}
}
else
{
float v = 0.f;
for (auto const & ss : s.shapes)
{
auto r = sdf(ss, p);
float vs = std::exp(- r.value / s.smooth);
v += vs;
result.gradient += r.gradient * vs;
}
result.value = - std::log(v) * s.smooth;
result.gradient /= v;
}
return result;
}
}

View file

@ -0,0 +1,15 @@
#include <psemek/vecr/circle.hpp>
namespace psemek::vecr
{
sdf_sample sdf(circle const & s, geom::point<float, 2> const & p)
{
auto r = p - s.center;
auto l = geom::length(r);
if (l > 0.f)
return {l - s.radius, r / l};
return {l - s.radius, {0.f, 0.f}};
}
}

View file

@ -0,0 +1,33 @@
#include <psemek/vecr/colorizer.hpp>
#include <psemek/geom/swizzle.hpp>
namespace psemek::vecr
{
gfx::color_4f colorize(gfx::color_rgba const & color, geom::point<float, 2> const &, sdf_sample const &)
{
return gfx::to_colorf(color);
}
gfx::color_4f colorize(gradient const & gradient, geom::point<float, 2> const & p, sdf_sample const &)
{
return gfx::lerp(
gfx::to_colorf(gradient.color0),
gfx::to_colorf(gradient.color1),
geom::clamp(sdf(gradient.shape, p).value + 0.5f, {0.f, 1.f})
);
}
gfx::color_4f colorize(lighting const & lighting, geom::point<float, 2> const & p, sdf_sample const & sample)
{
auto const real_sample = lighting.shape ? sdf(lighting.shape, p) : sample;
auto normal = geom::swizzle<0, 1, -1>(real_sample.gradient * lighting.slope);
normal[2] = 1.f;
auto factor = 0.5f + 0.5f * geom::dot(geom::normalized(normal), lighting.direction);
return gfx::lerp(gfx::to_colorf(lighting.color0), gfx::to_colorf(lighting.color1), factor);
}
}

32
libs/vecr/source/fill.cpp Normal file
View file

@ -0,0 +1,32 @@
#include <psemek/vecr/fill.hpp>
#include <psemek/geom/orientation.hpp>
namespace psemek::vecr
{
sdf_sample sdf(fill const & s, geom::point<float, 2> const & p)
{
auto result = sdf(s.border, p, true);
int inside = 0;
for (std::size_t i = 0; i < s.border.points.size(); ++i)
{
auto const j = (i + 1) % s.border.points.size();
bool const s0 = s.border.points[i][1] <= p[1];
bool const s1 = p[1] < s.border.points[j][1];
auto const sign = geom::orientation(s.border.points[i], s.border.points[j], p);
if (s0 && s1 && sign == geom::sign_t::positive) inside += 1;
if (!s0 && !s1 && sign == geom::sign_t::negative) inside -= 1;
}
if (inside != 0)
{
result.value *= -1.f;
result.gradient *= -1.f;
}
return result;
}
}

View file

@ -0,0 +1,42 @@
#include <psemek/vecr/intersect.hpp>
#include <psemek/geom/math.hpp>
#include <vector>
namespace psemek::vecr
{
sdf_sample sdf(intersect const & s, geom::point<float, 2> const & p)
{
sdf_sample result;
if (s.smooth == 0.f)
{
result.value = -std::numeric_limits<float>::infinity();
for (auto const & ss : s.shapes)
{
auto r = sdf(ss, p);
if (geom::make_max(result.value, r.value))
result.gradient = r.gradient;
}
}
else
{
float v = 0.f;
for (auto const & ss : s.shapes)
{
auto r = sdf(ss, p);
float vs = std::exp(r.value / s.smooth);
v += vs;
result.gradient += r.gradient * vs;
}
result.value = std::log(v) * s.smooth;
result.gradient /= v;
}
return result;
}
}

30
libs/vecr/source/path.cpp Normal file
View file

@ -0,0 +1,30 @@
#include <psemek/vecr/path.hpp>
#include <psemek/geom/interval.hpp>
#include <psemek/geom/math.hpp>
namespace psemek::vecr
{
sdf_sample sdf(path const & s, geom::point<float, 2> const & p, bool closed)
{
sdf_sample result;
for (std::size_t i = 0; i + (closed ? 0 : 1) < s.points.size(); ++i)
{
auto const j = (i + 1) % s.points.size();
auto const e = s.points[j] - s.points[i];
auto const v = p - s.points[i];
auto const t = geom::clamp(geom::dot(v, e) / geom::dot(e, e), {0.f, 1.f});
auto const q = v - t * e;
auto const l = geom::length(q);
if (geom::make_min(result.value, l))
{
if (l > 0.f)
result.gradient = q / l;
else
result.gradient = {0.f, 0.f};
}
}
return result;
}
}

View file

@ -0,0 +1,85 @@
#include <psemek/vecr/renderer.hpp>
#include <psemek/geom/swizzle.hpp>
namespace psemek::vecr
{
void renderer::reset(geom::vector<std::size_t, 2> const & size, std::size_t samples, gfx::color_rgba const & color)
{
samples_ = samples;
canvas_.assign({size[0] * samples, size[1] * samples}, color);
result_.assign({size[0], size[1]}, color);
need_resolve_ = false;
}
geom::vector<std::size_t, 2> renderer::size() const
{
return {result_.width(), result_.height()};
}
std::size_t renderer::samples() const
{
return samples_;
}
gfx::pixmap_rgba const & renderer::result() const
{
resolve();
return result_;
}
void renderer::clear(gfx::color_rgba const & color)
{
canvas_.fill(color);
result_.fill(color);
need_resolve_ = false;
}
void renderer::draw(primitive const & primitive)
{
float const aa = primitive.blur / 2.f;
for (auto const & idx : canvas_.indices())
{
geom::point const center{(idx[0] + 0.5f) / samples_, (idx[1] + 0.5f) / samples_};
auto const sample = sdf(primitive.mask, center);
if (sample.value > aa) continue;
float blur = 1.f;
if (sample.value > - aa)
blur = (aa - sample.value) / (2.f * aa);
auto color = colorize(primitive.colorizer, center, sample);
color[3] *= blur;
canvas_(idx) = gfx::to_coloru8(primitive.blend(gfx::to_colorf(canvas_(idx)), color));
}
need_resolve_ = true;
}
void renderer::resolve() const
{
if (!need_resolve_) return;
for (auto const & idx : result_.indices())
{
gfx::color_4f sum{0.f, 0.f, 0.f, 0.f};
for (std::size_t y = 0; y < samples_; ++y)
{
for (std::size_t x = 0; x < samples_; ++x)
{
sum += gfx::premult(gfx::to_colorf(canvas_({idx[0] * samples_ + x, idx[1] * samples_ + y})));
}
}
result_(idx) = gfx::to_coloru8(gfx::unpremult(sum / (1.f * samples_ * samples_)));
}
need_resolve_ = false;
}
}