psemek/libs/gfx/source/painter2.cpp
2026-07-21 18:08:56 +03:00

473 lines
15 KiB
C++

#include <psemek/gfx/painter2.hpp>
#include <psemek/gfx/program.hpp>
#include <psemek/gfx/buffer.hpp>
#include <psemek/gfx/array.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/math/translation.hpp>
#include <psemek/math/rotation.hpp>
#include <psemek/math/scale.hpp>
namespace psemek::gfx
{
namespace
{
// MVP (Model-View-Projection) matrix is assumed to be always a 2D affine transform
// Thus, it can be passed as 2x 3D vectors (the rows of the corresponding 2x3 affine matrix)
// MVPW (Model-View-Projection-Window) matrix composes MVP with the viewport transform ([-1..1] -> [0..width] in pixels)
// Its inverse is used in gradient computations for analytical SDF rendering
char const sdf_vertex_common[] = R"(
uniform vec2 u_viewport_size;
layout (location = 0) in vec3 in_mvp_0;
layout (location = 1) in vec3 in_mvp_1;
layout (location = 2) in vec2 in_mvpw_inv_0;
layout (location = 3) in vec2 in_mvpw_inv_1;
layout (location = 4) in vec4 in_color;
layout (location = 5) in float in_half_width;
mat2 adj(mat2 m)
{
return mat2(
m[1][1], -m[1][0],
-m[0][1], m[0][0]
);
}
out mat2 mvpw_inv_t;
out mat2 mvpw_t;
out vec2 vertex;
out vec4 color;
out float half_width;
mat2x3 mvp_t = mat2x3(in_mvp_0, in_mvp_1);
mat2 w = mat2(u_viewport_size.x / 2.0, 0.0, 0.0, - u_viewport_size.y / 2.0);
mat2 mvpw_adj_t = adj(mat2(mvp_t) * w);
vec2 extend(vec2 n1, vec2 n2)
{
// Compute the screen-space normals
n1 = normalize(n1 * mvpw_adj_t);
n2 = normalize(n2 * mvpw_adj_t);
// Find the screen-space bisector for n1 and n2
vec2 n = (n1 + n2) / (1.0 + dot(n1, n2));
// Transform to object space
return n * mvpw_inv_t;
}
vec2 get_vertex();
void main()
{
mvpw_t = mat2(mvp_t) * w;
mvpw_inv_t = mat2(in_mvpw_inv_0, in_mvpw_inv_1);
vertex = get_vertex();
color = in_color;
half_width = in_half_width;
gl_Position = vec4(vec3(vertex, 1.0) * mvp_t, 0.0, 1.0);
}
)";
char const sdf_fill_fragment_common[] = R"(
in vec2 vertex;
in mat2 mvpw_inv_t;
in vec4 color;
layout (location = 0) out vec4 out_color;
// (grad_x, grad_y, distance)
vec3 eval_sdf(vec2 p);
void main()
{
vec3 sdf = eval_sdf(vertex);
vec2 d_grad_screen = mvpw_inv_t * sdf.xy;
float d_grad_screen_length = length(d_grad_screen);
float aa = d_grad_screen_length * sqrt(0.5);
out_color = color * smoothstep(aa, -aa, sdf.z);
}
)";
char const quad_vs[] = R"(
const vec2 QUAD_VERTICES[6] = vec2[6](
vec2(-1.0, -1.0),
vec2( 1.0, -1.0),
vec2(-1.0, 1.0),
vec2(-1.0, 1.0),
vec2( 1.0, -1.0),
vec2( 1.0, 1.0)
);
vec2 get_vertex()
{
vec2 v = QUAD_VERTICES[gl_VertexID];
// Extend the quad in screen-space by half-width and an extra 1px for anti-aliasing
v += (in_half_width + 1.0) * extend(vec2(v.x, 0.0), vec2(0.0, v.y));
return v;
}
)";
char const circle_sdf[] = R"(
vec3 eval_sdf(vec2 p)
{
float l = length(p);
return vec3(p / l, l - 1.0);
}
)";
char const square_sdf[] = R"(
vec3 eval_sdf(vec2 p)
{
vec2 delta = abs(p) - vec2(1.0);
if (delta.x > 0.0 || delta.y > 0.0) {
vec2 g = max(delta, vec2(0.0));
float l = length(g);
return vec3(g * (sign(p) / l), l);
} else {
if (delta.x > delta.y) {
return vec3(sign(p.x), 0.0, delta.x);
} else {
return vec3(0.0, sign(p.y), delta.y);
}
}
}
)";
char const sdf_stroke_fragment_common[] = R"(
in vec2 vertex;
in mat2 mvpw_t;
in mat2 mvpw_inv_t;
in vec4 color;
in float half_width;
layout (location = 0) out vec4 out_color;
vec3 eval_sdf(vec2 p);
void main()
{
// Newton-Raphson iteration to find the closest point on the boundary
vec2 vertex_screen = vertex * mvpw_t;
vec2 p_screen = vertex_screen;
for (int i = 0; i < 4; ++i)
{
vec2 p_local = p_screen * mvpw_inv_t;
vec3 sdf_value = eval_sdf(p_local);
p_screen = p_local * mvpw_t;
vec2 d_grad_screen = mvpw_inv_t * sdf_value.xy;
p_screen -= (sdf_value.z / dot(d_grad_screen, d_grad_screen)) * d_grad_screen;
}
float d = length(vertex_screen - p_screen);
float aa = sqrt(0.5);
out_color = color * smoothstep(aa, -aa, d - half_width);
}
)";
gfx::shader_source const quad_vs_source{gl::sys::shader_prelude(), sdf_vertex_common, quad_vs};
using transform_type = painter2::transform_type;
using fill_style = painter2::fill_style;
using stroke_style = painter2::stroke_style;
struct context
{
gfx::buffer instance_buffer;
gfx::program fill_circle_program{quad_vs_source, {gl::sys::shader_prelude(), sdf_fill_fragment_common, circle_sdf}};
gfx::program fill_square_program{quad_vs_source, {gl::sys::shader_prelude(), sdf_fill_fragment_common, square_sdf}};
gfx::program stroke_circle_program{quad_vs_source, {gl::sys::shader_prelude(), sdf_stroke_fragment_common, circle_sdf}};
gfx::program stroke_square_program{quad_vs_source, {gl::sys::shader_prelude(), sdf_stroke_fragment_common, square_sdf}};
gfx::array array;
context();
};
struct primitive_instance
{
transform_type mvp;
math::matrix<float, 2, 2> mvpw_inv;
gfx::color_rgba color;
float half_width; // unused when filling
};
struct batch
{
gfx::program * program;
std::vector<primitive_instance> instances;
};
struct frame_builder_impl
: painter2::frame_builder
{
frame_builder_impl(context & context, math::vector<int, 2> viewport_size);
void push_matrix(transform_type const & matrix) override;
void pop_matrix() override;
void reset_matrix() override;
void fill_circle(math::point<float, 2> const & center, float radius, fill_style const & style) override;
void fill_ellipse(math::point<float, 2> const & center, math::vector<float, 2> const & radii, float rotation, fill_style const & style) override;
void fill_square(math::point<float, 2> const & center, float extent, float rotation, fill_style const & style) override;
void fill_rectangle(math::point<float, 2> const & center, math::vector<float, 2> const & extent, float rotation, fill_style const & style) override;
void stroke_circle(math::point<float, 2> const & center, float radius, stroke_style const & style) override;
void stroke_ellipse(math::point<float, 2> const & center, math::vector<float, 2> const & radii, float rotation, stroke_style const & style) override;
void stroke_square(math::point<float, 2> const & center, float extent, float rotation, stroke_style const & style) override;
void stroke_rectangle(math::point<float, 2> const & center, math::vector<float, 2> const & extent, float rotation, stroke_style const & style) override;
void flush() override;
~frame_builder_impl() override
{
flush();
}
private:
context & context_;
math::vector<int, 2> viewport_size_;
transform_type viewport_transform_;
std::vector<transform_type> matrix_stack_;
std::vector<batch> batches_;
batch * get_batch(gfx::program * program);
void add_ellipse(math::point<float, 2> const & center, math::vector<float, 2> const & radii, float rotation, fill_style const & style);
void add_rectangle(math::point<float, 2> const & center, math::vector<float, 2> const & extent, float rotation, fill_style const & style);
void add_ellipse(math::point<float, 2> const & center, math::vector<float, 2> const & radii, float rotation, stroke_style const & style);
void add_rectangle(math::point<float, 2> const & center, math::vector<float, 2> const & extent, float rotation, stroke_style const & style);
primitive_instance make_primitive(transform_type const & model, fill_style const & style);
primitive_instance make_primitive(transform_type const & model, stroke_style const & style);
transform_type make_transform(math::point<float, 2> const & center, math::vector<float, 2> const & scale, float rotation);
std::pair<transform_type, math::matrix<float, 2, 2>> make_mvp(transform_type const & model);
};
context::context()
{
array.bind();
instance_buffer.bind();
gl::EnableVertexAttribArray(0);
gl::EnableVertexAttribArray(1);
gl::EnableVertexAttribArray(2);
gl::EnableVertexAttribArray(3);
gl::EnableVertexAttribArray(4);
gl::EnableVertexAttribArray(5);
gl::VertexAttribDivisor(0, 1);
gl::VertexAttribDivisor(1, 1);
gl::VertexAttribDivisor(2, 1);
gl::VertexAttribDivisor(3, 1);
gl::VertexAttribDivisor(4, 1);
gl::VertexAttribDivisor(5, 1);
gl::VertexAttribPointer(0, 3, gl::FLOAT, gl::FALSE, sizeof(primitive_instance), (char *)offsetof(primitive_instance, mvp));
gl::VertexAttribPointer(1, 3, gl::FLOAT, gl::FALSE, sizeof(primitive_instance), (char *)offsetof(primitive_instance, mvp) + 3 * sizeof(float));
gl::VertexAttribPointer(2, 2, gl::FLOAT, gl::FALSE, sizeof(primitive_instance), (char *)offsetof(primitive_instance, mvpw_inv));
gl::VertexAttribPointer(3, 2, gl::FLOAT, gl::FALSE, sizeof(primitive_instance), (char *)offsetof(primitive_instance, mvpw_inv) + 2 * sizeof(float));
gl::VertexAttribPointer(4, 4, gl::UNSIGNED_BYTE, gl::TRUE, sizeof(primitive_instance), (char *)offsetof(primitive_instance, color));
gl::VertexAttribPointer(5, 1, gl::FLOAT, gl::FALSE, sizeof(primitive_instance), (char *)offsetof(primitive_instance, half_width));
gfx::array::null().bind();
}
frame_builder_impl::frame_builder_impl(context & context, math::vector<int, 2> viewport_size)
: context_(context)
, viewport_size_(viewport_size)
, viewport_transform_(
math::translation{math::cast<float>(viewport_size) / 2.f}.transform() *
math::scale{math::pointwise_mult(
math::cast<float>(viewport_size),
math::vector{0.5f, -0.5f}
)}.transform()
)
{
matrix_stack_.push_back(transform_type::identity());
}
void frame_builder_impl::push_matrix(painter2::transform_type const & matrix)
{
matrix_stack_.push_back(matrix_stack_.back() * matrix);
}
void frame_builder_impl::pop_matrix()
{
if (matrix_stack_.size() > 1)
matrix_stack_.pop_back();
}
void frame_builder_impl::reset_matrix()
{
matrix_stack_.resize(1);
}
void frame_builder_impl::fill_circle(math::point<float, 2> const & center, float radius, fill_style const & style)
{
add_ellipse(center, {radius, radius}, 0.f, style);
}
void frame_builder_impl::fill_ellipse(math::point<float, 2> const & center, math::vector<float, 2> const & radii, float rotation, fill_style const & style)
{
add_ellipse(center, radii, rotation, style);
}
void frame_builder_impl::fill_square(math::point<float, 2> const & center, float extent, float rotation, fill_style const & style)
{
add_rectangle(center, {extent, extent}, rotation, style);
}
void frame_builder_impl::fill_rectangle(math::point<float, 2> const & center, math::vector<float, 2> const & extent, float rotation, fill_style const & style)
{
add_rectangle(center, extent, rotation, style);
}
void frame_builder_impl::stroke_circle(math::point<float, 2> const & center, float radius, stroke_style const & style)
{
add_ellipse(center, {radius, radius}, 0.f, style);
}
void frame_builder_impl::stroke_ellipse(math::point<float, 2> const & center, math::vector<float, 2> const & radii, float rotation, stroke_style const & style)
{
add_ellipse(center, radii, rotation, style);
}
void frame_builder_impl::stroke_square(math::point<float, 2> const & center, float extent, float rotation, stroke_style const & style)
{
add_rectangle(center, {extent, extent}, rotation, style);
}
void frame_builder_impl::stroke_rectangle(math::point<float, 2> const & center, math::vector<float, 2> const & extent, float rotation, stroke_style const & style)
{
add_rectangle(center, extent, rotation, style);
}
void frame_builder_impl::flush()
{
if (batches_.empty())
return;
gl::Enable(gl::BLEND);
gl::BlendEquation(gl::FUNC_ADD);
gl::BlendFunc(gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
auto const viewport_size_f = math::cast<float>(viewport_size_);
context_.array.bind();
for (auto const & batch : batches_)
{
if (batch.instances.empty()) continue;
auto & program = *batch.program;
program.bind();
program["u_viewport_size"] = viewport_size_f;
context_.instance_buffer.load(batch.instances);
gl::DrawArraysInstanced(gl::TRIANGLES, 0, 6, batch.instances.size());
}
gfx::array::null().bind();
gfx::program::null().bind();
batches_.clear();
}
batch * frame_builder_impl::get_batch(gfx::program * program)
{
if (batches_.empty() || batches_.back().program != program)
batches_.push_back({.program = program, .instances = {}});
return &batches_.back();
}
void frame_builder_impl::add_ellipse(math::point<float, 2> const & center, math::vector<float, 2> const & radii, float rotation, fill_style const & style)
{
get_batch(&context_.fill_circle_program)->instances.push_back(make_primitive(make_transform(center, radii, rotation), style));
}
void frame_builder_impl::add_rectangle(math::point<float, 2> const & center, math::vector<float, 2> const & extent, float rotation, fill_style const & style)
{
get_batch(&context_.fill_square_program)->instances.push_back(make_primitive(make_transform(center, extent, rotation), style));
}
void frame_builder_impl::add_ellipse(math::point<float, 2> const & center, math::vector<float, 2> const & radii, float rotation, stroke_style const & style)
{
get_batch(&context_.stroke_circle_program)->instances.push_back(make_primitive(make_transform(center, radii, rotation), style));
}
void frame_builder_impl::add_rectangle(math::point<float, 2> const & center, math::vector<float, 2> const & extent, float rotation, stroke_style const & style)
{
get_batch(&context_.stroke_square_program)->instances.push_back(make_primitive(make_transform(center, extent, rotation), style));
}
primitive_instance frame_builder_impl::make_primitive(transform_type const & model, fill_style const & style)
{
auto [mvp, mvpw_inv] = make_mvp(model);
return {
.mvp = mvp,
.mvpw_inv = mvpw_inv,
.color = style.color,
.half_width = 0.f,
};
}
primitive_instance frame_builder_impl::make_primitive(transform_type const & model, stroke_style const & style)
{
auto [mvp, mvpw_inv] = make_mvp(model);
return {
.mvp = mvp,
.mvpw_inv = mvpw_inv,
.color = style.color,
.half_width = style.width * 0.5f,
};
}
transform_type frame_builder_impl::make_transform(math::point<float, 2> const & center, math::vector<float, 2> const & scale, float rotation)
{
return math::translation<float, 2>(center - center.zero()).transform() *
math::plane_rotation<float, 2>(0, 1, rotation).transform() *
math::scale<float, 2>(scale).transform();
}
std::pair<transform_type, math::matrix<float, 2, 2>> frame_builder_impl::make_mvp(transform_type const & model)
{
auto mvp = matrix_stack_.back() * model;
auto mvpw_inv = math::inverse(viewport_transform_.linear_matrix() * mvp.linear_matrix()).value();
return {mvp, mvpw_inv};
}
}
struct painter2::impl
{
struct context context;
};
painter2::painter2()
: pimpl_(make_impl())
{}
painter2::~painter2()
{}
std::unique_ptr<painter2::frame_builder> painter2::begin_frame(math::vector<int, 2> viewport_size)
{
return std::make_unique<frame_builder_impl>(impl().context, viewport_size);
}
}