Rive tess vector and meshes

Not sure why github sees a huge change on the catch.hpp file, locally it seems unchanged to me. That's the bulk of the linecount here, so maybe just ignore that file?

The actual important stuff:
- uses earcut and libtess2 for triangulation
- earcut benchmarks are insanely fast, but it fails on any complex path, doesn't do winding rules, etc. I have a pretty lame heuristic in there that tries to use it when we think we can get away with it
- libtess2 for anything multi-path (still need to pass winding rule down, however, that'll be in a follow up pr)
- re-added the stroker I built here: https://rive-app.github.io/stroke-exploration/
- stroker internally builds a tristrip, I convert that to triangles here so sokol can use the same pipeline for drawing the fills and strokes
- clipping with the stencil buffer by drawing clipping shapes additively and then testing on stencil buffer value == clipping shape count. makes it easy to propagate between draw calls and selectively remove clipping shapes when there are minor changes between draw calls (nice for Marty animation). Due to sokol's pipeline model, we have to build N pipelines for however many clipping shapes we want to support (I picked an arbitrary number) as we need to hardcode the stencil test value/ref.
- some obtuse blending

Demos:
https://2dimensions.slack.com/archives/CHMAP278R/p1660948586557719
https://2dimensions.slack.com/archives/CHMAP278R/p1660952553086529

Diffs=
b0d342067 Shader expects good input.
5f642c48c Undo changes to catch.hpp.
937797ace Tweaks based on PR feedback.
576a54a79 fix test
bbd4e5a8b running formatter
959f22dcb More efficient pipeline changes.
b57f553e0 Strokes and clipping working.
56cb62b64 re-adding stroking
4d6253217 Only draw fills for now.
51f383844 Fix clipping.
d52476967 Working on clipping
2f5e858bd Adding solid color to shader
5bf705b1c Rendering
28492f8dc Adding abstraction for sokol render path.
917b0d5e4 Adding triangulation for simple case.
4f3f8af21 Match master
33e81ceb8 Starting to work on triangulation
This commit is contained in:
luigi-rosso
2022-08-22 23:52:40 +00:00
parent 000e628e29
commit 88797e58f6
37 changed files with 2334 additions and 289 deletions

View File

@@ -15,6 +15,18 @@ if [[ ! -d "$DEPENDENCIES/sokol" ]]; then
popd
fi
if [[ ! -d "$DEPENDENCIES/earcut.hpp" ]]; then
pushd $DEPENDENCIES_SCRIPTS
./get_earcut.sh
popd
fi
if [[ ! -d "$DEPENDENCIES/libtess2" ]]; then
pushd $DEPENDENCIES_SCRIPTS
./get_libtess2.sh
popd
fi
export PREMAKE=$DEPENDENCIES/bin/premake5
pushd ..

View File

@@ -18,10 +18,13 @@ do
includedirs {
'../include',
rive .. '/include',
dependencies .. '/sokol'
dependencies .. '/sokol',
dependencies .. '/earcut.hpp/include/mapbox',
dependencies .. '/libtess2/Include'
}
files {
'../src/**.cpp'
'../src/**.cpp',
dependencies .. '/libtess2/Source/**.c'
}
buildoptions {'-Wall', '-fno-exceptions', '-fno-rtti', '-Werror=format'}
@@ -68,6 +71,8 @@ end
project 'rive_tess_tests'
do
dependson 'rive_tess_renderer'
dependson 'rive'
kind 'ConsoleApp'
language 'C++'
cppdialect 'C++17'
@@ -75,15 +80,18 @@ do
targetdir '%{cfg.system}/bin/%{cfg.buildcfg}'
objdir '%{cfg.system}/obj/%{cfg.buildcfg}'
includedirs {
'../../dev/test/include',
rive .. 'dev/test/include', -- for catch.hpp
rive .. 'test', -- for things like rive_file_reader.hpp
'../include',
rive .. '/include',
dependencies .. '/sokol'
dependencies .. '/sokol',
dependencies .. '/earcut.hpp/include/mapbox'
}
files {
'../test/**.cpp'
'../test/**.cpp',
rive .. 'utils/no_op_factory.cpp'
}
links {'rive_tess_renderer'}
links {'rive_tess_renderer', 'rive'}
buildoptions {'-Wall', '-fno-exceptions', '-fno-rtti', '-Werror=format'}
defines {'TESTING'}

View File

@@ -24,14 +24,13 @@ public:
void reset();
void resetRenderOffset();
void nextRenderOffset(std::size_t& start, std::size_t& end);
bool nextRenderOffset(std::size_t& start, std::size_t& end);
void extrude(const SegmentedContour* contour,
bool isClosed,
StrokeJoin join,
StrokeCap cap,
float strokeWidth,
const Mat2D& transform);
float strokeWidth);
};
} // namespace rive
#endif

View File

@@ -2,6 +2,7 @@
#define _RIVE_SEGMENTED_CONTOUR_HPP_
#include "rive/math/vec2d.hpp"
#include "rive/math/mat2d.hpp"
#include "rive/math/aabb.hpp"
#include <vector>
@@ -31,14 +32,16 @@ private:
float t2);
public:
const Span<const Vec2D> contourPoints() const;
const Span<const Vec2D> contourPoints(uint32_t endOffset = 0) const;
const std::size_t contourSize() const;
SegmentedContour(float threshold);
float threshold() const;
void threshold(float value);
const AABB& bounds() const;
void contour(const RawPath& rawPath);
void contour(const RawPath& rawPath, const Mat2D& transform);
};
} // namespace rive
#endif

View File

@@ -12,26 +12,51 @@ namespace rive {
class SokolRenderImage : public RenderImage {
private:
sg_image m_Image;
sg_image m_image;
public:
SokolRenderImage(sg_image image);
sg_image image() const { return m_Image; }
sg_image image() const { return m_image; }
};
class SokolTessRenderer : public TessRenderer {
private:
sg_pipeline m_MeshPipeline;
static const std::size_t maxClippingPaths = 16;
sg_pipeline m_meshPipeline;
sg_pipeline m_currentPipeline = {0};
int m_clipCount = 0;
// Src Over Pipelines
sg_pipeline m_pathPipeline[maxClippingPaths + 1];
// Screen Pipelines
sg_pipeline m_pathScreenPipeline[maxClippingPaths + 1];
// Additive
sg_pipeline m_pathAdditivePipeline[maxClippingPaths + 1];
// Multiply
sg_pipeline m_pathMultiplyPipeline[maxClippingPaths + 1];
sg_pipeline m_incClipPipeline;
sg_pipeline m_decClipPipeline;
sg_buffer m_boundsIndices;
std::vector<SubPath> m_ClipPaths;
void applyClipping();
void setPipeline(sg_pipeline pipeline);
public:
SokolTessRenderer();
~SokolTessRenderer();
void orthographicProjection(float left,
float right,
float bottom,
float top,
float near,
float far) override;
void drawPath(RenderPath* path, RenderPaint* paint) override;
void drawImage(const RenderImage*, BlendMode, float opacity) override;
void drawImageMesh(const RenderImage*,
rcp<RenderBuffer> vertices_f32,
@@ -39,6 +64,8 @@ public:
rcp<RenderBuffer> indices_u16,
BlendMode,
float opacity) override;
void restore() override;
void reset();
};
} // namespace rive
#endif

View File

@@ -6,7 +6,7 @@
namespace rive {
///
/// A reference to a sub-path added to a ContourRenderPath with its relative
/// A reference to a sub-path added to a TessRenderPath with its relative
/// transform.
///
class SubPath {

View File

@@ -4,15 +4,38 @@
#include "rive/math/raw_path.hpp"
#include "rive/renderer.hpp"
#include "rive/span.hpp"
#include "rive/tess/segmented_contour.hpp"
#include "rive/tess/sub_path.hpp"
#include "earcut.hpp"
namespace rive {
class ContourStroke;
class TessRenderPath : public RenderPath {
private:
// TessRenderPath stores a RawPath so that it can use utility classes
// that will work off of RawPath (like segmenting the contour and then
// triangulating the segmented contour).
RawPath m_RawPath;
FillRule m_FillRule;
RawPath m_rawPath;
FillRule m_fillRule;
// We hold a reference to the segmented contour so it can reserve and
// reuse storage when re-contouring.
SegmentedContour m_segmentedContour;
mapbox::detail::Earcut<uint16_t> m_earcut;
bool m_isContourDirty = true;
bool m_isTriangulationDirty = true;
Mat2D m_contourTransform;
bool m_isClosed;
protected:
std::vector<SubPath> m_subPaths;
virtual void addTriangles(Span<const Vec2D> vertices, Span<const uint16_t> indices) = 0;
virtual void setTriangulatedBounds(const AABB& value) = 0;
void contour(const Mat2D& transform);
void triangulate(TessRenderPath* containerPath);
public:
TessRenderPath();
@@ -20,11 +43,26 @@ public:
~TessRenderPath();
void reset() override;
void fillRule(FillRule value) override;
bool empty() const;
// In Rive a Path is used as a simple container (with no commands) when
// it aggregates multiple paths.
bool isContainer() const { return !m_subPaths.empty(); }
void moveTo(float x, float y) override;
void lineTo(float x, float y) override;
void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override;
void close() override;
void addRenderPath(RenderPath* path, const Mat2D& transform) override;
const SegmentedContour& segmentedContour() const;
bool triangulate();
void extrudeStroke(ContourStroke* stroke,
StrokeJoin join,
StrokeCap cap,
float strokeWidth,
const Mat2D& transform);
const RawPath& rawPath() const;
};
} // namespace rive
#endif

View File

@@ -24,7 +24,6 @@ protected:
Mat4 m_Projection;
std::list<RenderState> m_Stack;
bool m_IsClippingDirty = false;
std::vector<SubPath> m_ClipPaths;
public:
TessRenderer();
@@ -37,19 +36,11 @@ public:
float near,
float far) = 0;
///
/// Checks if clipping is dirty and clears the clipping flag. Hard
/// expectation for whoever checks this to also apply it. That's why
/// it's not marked const.
///
bool needsToApplyClipping();
void save() override;
void restore() override;
void transform(const Mat2D& transform) override;
const Mat2D& transform() { return m_Stack.back().transform; }
void clipPath(RenderPath* path) override;
void drawPath(RenderPath* path, RenderPaint* paint) override;
void drawImage(const RenderImage*, BlendMode, float opacity) override;
void drawImageMesh(const RenderImage*,
rcp<RenderBuffer> vertices_f32,

View File

@@ -16,42 +16,37 @@ void ContourStroke::reset() {
void ContourStroke::resetRenderOffset() { m_RenderOffset = 0; }
void ContourStroke::nextRenderOffset(std::size_t& start, std::size_t& end) {
assert(m_RenderOffset < m_Offsets.size());
bool ContourStroke::nextRenderOffset(std::size_t& start, std::size_t& end) {
if (m_RenderOffset == m_Offsets.size()) {
return false;
}
start = m_RenderOffset == 0 ? 0 : m_Offsets[m_RenderOffset - 1];
end = m_Offsets[m_RenderOffset++];
return true;
}
void ContourStroke::extrude(const SegmentedContour* contour,
bool isClosed,
StrokeJoin join,
StrokeCap cap,
float strokeWidth,
const Mat2D& transform) {
// TODO: if transform is identity, no need to copy and transform
// contourPoints->points.
float strokeWidth) {
auto contourPoints = contour->contourPoints();
std::vector<Vec2D> points(contourPoints.begin(), contourPoints.end());
auto pointCount = points.size();
if (pointCount < 6) {
if (pointCount < 2) {
return;
}
for (int i = 4; i < pointCount; i++) {
Vec2D& point = points[i];
point = transform * point; // point, transform);
}
auto startOffset = m_TriangleStrip.size();
Vec2D lastPoint = points[4];
Vec2D lastDiff = points[5] - lastPoint;
Vec2D lastPoint = points[0];
Vec2D lastDiff = points[1] - lastPoint;
float lastLength = lastDiff.length();
Vec2D lastDiffNormalized = lastDiff / lastLength;
Vec2D perpendicularStrokeDiff =
Vec2D(lastDiffNormalized.y * -strokeWidth, lastDiffNormalized.x * strokeWidth);
Vec2D lastA = lastPoint - perpendicularStrokeDiff;
Vec2D lastA = lastPoint + perpendicularStrokeDiff;
Vec2D lastB = lastPoint - perpendicularStrokeDiff;
if (!isClosed) {
@@ -86,15 +81,15 @@ void ContourStroke::extrude(const SegmentedContour* contour,
m_TriangleStrip.push_back(lastA);
m_TriangleStrip.push_back(lastB);
pointCount -= isClosed ? 6 : 5;
pointCount -= isClosed ? 1 : 0;
std::size_t adjustedPointCount = isClosed ? pointCount + 1 : pointCount;
for (std::size_t i = 1; i < adjustedPointCount; i++) {
const Vec2D& point = points[(i % pointCount) + 4];
const Vec2D& point = points[i % pointCount];
Vec2D diff, diffNormalized, next;
float length;
if (i < adjustedPointCount - 1 || isClosed) {
diff = (next = points[((i + 1) % pointCount) + 4]) - point;
diff = (next = points[(i + 1) % pointCount]) - point;
length = diff.length();
diffNormalized = diff / length;
} else {

View File

@@ -5,7 +5,9 @@
using namespace rive;
SegmentedContour::SegmentedContour(float threshold) :
m_threshold(threshold), m_thresholdSquared(threshold * threshold) {}
m_bounds(AABB::forExpansion()),
m_threshold(threshold),
m_thresholdSquared(threshold * threshold) {}
float SegmentedContour::threshold() const { return m_threshold; }
void SegmentedContour::threshold(float value) {
@@ -13,7 +15,11 @@ void SegmentedContour::threshold(float value) {
m_thresholdSquared = value * value;
}
const AABB& SegmentedContour::bounds() const { return m_bounds; }
void SegmentedContour::addVertex(Vec2D vertex) {}
void SegmentedContour::addVertex(Vec2D vertex) {
m_contourPoints.push_back(vertex);
AABB::expandTo(m_bounds, vertex);
}
void SegmentedContour::penDown() {
if (m_isPenDown) {
return;
@@ -22,21 +28,20 @@ void SegmentedContour::penDown() {
m_penDown = m_pen;
addVertex(m_penDown);
}
void SegmentedContour::close() {
if (!m_isPenDown) {
return;
}
m_pen = m_penDown;
m_isPenDown = false;
// TODO: Can we optimize and not dupe this point if it's the last point
// already in the list? For example: a procedural triangle closes itself
// with a lineTo the first point.
addVertex(m_penDown);
}
const Span<const Vec2D> SegmentedContour::contourPoints() const {
return Span<const Vec2D>(m_contourPoints.data(), m_contourPoints.size());
const std::size_t SegmentedContour::contourSize() const { return m_contourPoints.size(); }
const Span<const Vec2D> SegmentedContour::contourPoints(uint32_t endOffset) const {
assert(endOffset <= m_contourPoints.size());
return Span<const Vec2D>(m_contourPoints.data(), m_contourPoints.size() - endOffset);
}
void SegmentedContour::segmentCubic(const Vec2D& from,
@@ -62,31 +67,32 @@ void SegmentedContour::segmentCubic(const Vec2D& from,
}
}
void SegmentedContour::contour(const RawPath& rawPath) {
void SegmentedContour::contour(const RawPath& rawPath, const Mat2D& transform) {
m_contourPoints.clear();
// First four vertices are the bounds.
m_contourPoints.emplace_back(Vec2D());
m_contourPoints.emplace_back(Vec2D());
m_contourPoints.emplace_back(Vec2D());
m_contourPoints.emplace_back(Vec2D());
RawPath::Iter iter(rawPath);
// Possible perf consideration: could add second path that doesn't transform
// if transform is the identity.
while (auto rec = iter.next()) {
switch (rec.verb) {
case PathVerb::move:
m_isPenDown = false;
m_pen = rec.pts[0];
m_pen = transform * rec.pts[0];
break;
case PathVerb::line:
penDown();
m_pen = rec.pts[0];
addVertex(rec.pts[0]);
m_pen = transform * rec.pts[0];
addVertex(m_pen);
break;
case PathVerb::cubic:
penDown();
segmentCubic(m_pen, rec.pts[0], rec.pts[1], rec.pts[2], 0.0f, 1.0f);
m_pen = rec.pts[2];
segmentCubic(m_pen,
transform * rec.pts[0],
transform * rec.pts[1],
transform * rec.pts[2],
0.0f,
1.0f);
m_pen = transform * rec.pts[2];
break;
case PathVerb::close: close(); break;
case PathVerb::quad:
@@ -95,25 +101,5 @@ void SegmentedContour::contour(const RawPath& rawPath) {
break;
}
}
// TODO: when we stroke we may want to differentiate whether or not the path
// actually closed.
close();
// TODO: consider if there's a case with no points.
Vec2D& first = m_contourPoints[0];
first.x = m_bounds.minX;
first.y = m_bounds.minY;
Vec2D& second = m_contourPoints[1];
second.x = m_bounds.maxX;
second.y = m_bounds.minY;
Vec2D& third = m_contourPoints[2];
third.x = m_bounds.maxX;
third.y = m_bounds.maxY;
Vec2D& fourth = m_contourPoints[3];
fourth.x = m_bounds.minX;
fourth.y = m_bounds.maxY;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,18 @@
@ctype mat4 rive::Mat4
@ctype vec2 rive::Vec2D
@vs vs
uniform vs_params {
mat4 mvp;
};
in vec2 pos;
in vec2 position;
in vec2 texcoord0;
out vec2 uv;
void main() {
gl_Position = mvp * vec4(pos.x, pos.y, 0.0, 1.0);
gl_Position = mvp * vec4(position.x, position.y, 0.0, 1.0);
uv = texcoord0;
}
@end
@@ -27,4 +28,71 @@ void main() {
}
@end
@program rive_tess vs fs
@program rive_tess vs fs
@vs vs_path
uniform vs_path_params {
mat4 mvp;
};
in vec2 position;
out vec2 pos;
void main() {
pos = position;
gl_Position = mvp * vec4(position, 0.0, 1.0);
}
@end
@fs fs_path
uniform fs_path_uniforms {
int fillType;
vec4 colors[16];
vec4 stops[4];
vec2 gradientStart;
vec2 gradientEnd;
int stopCount;
};
in vec2 pos;
out vec4 frag_color;
void main() {
if (fillType == 0) {
// Solid color.
frag_color = colors[0];
}
else {
float f;
if(fillType == 1) {
// Linear gradient.
vec2 toEnd = gradientEnd - gradientStart;
float lengthSquared = toEnd.x * toEnd.x + toEnd.y * toEnd.y;
f = dot(pos - gradientStart, toEnd) / lengthSquared;
}
else {
// fillType == 2 (Radial gradient)
f = distance(gradientStart, pos) / distance(gradientStart, gradientEnd);
}
vec4 color =
mix(colors[0], colors[1], smoothstep(stops[0][0], stops[0][1], f));
for (int i = 1; i < 15; ++i)
{
if (i >= stopCount - 1)
{
break;
}
color = mix(color,
colors[i + 1],
smoothstep(stops[i/4][i%4], stops[(i+1)/4][(i+1)%4], f));
}
frag_color = color;
}
}
@end
@program rive_tess_path vs_path fs_path

View File

@@ -11,6 +11,7 @@ public:
void cap(StrokeCap value) override {}
void blendMode(BlendMode value) override {}
void shader(rcp<RenderShader>) override {}
void invalidateStroke() override {}
};
class NoOpRenderPath : public RenderPath {
@@ -28,37 +29,3 @@ public:
};
SokolFactory::SokolFactory() {}
rcp<RenderShader> SokolFactory::makeLinearGradient(float sx,
float sy,
float ex,
float ey,
const ColorInt colors[], // [count]
const float stops[], // [count]
size_t count) {
return nullptr;
}
rcp<RenderShader> SokolFactory::makeRadialGradient(float cx,
float cy,
float radius,
const ColorInt colors[], // [count]
const float stops[], // [count]
size_t count) {
return nullptr;
}
// Returns a full-formed RenderPath -- can be treated as immutable
std::unique_ptr<RenderPath> SokolFactory::makeRenderPath(Span<const Vec2D> points,
Span<const PathVerb> verbs,
FillRule) {
return std::make_unique<NoOpRenderPath>();
}
std::unique_ptr<RenderPath> SokolFactory::makeEmptyRenderPath() {
return std::make_unique<NoOpRenderPath>();
}
std::unique_ptr<RenderPaint> SokolFactory::makeRenderPaint() {
return std::make_unique<NoOpRenderPaint>();
}

View File

@@ -1,10 +1,140 @@
#include "rive/tess/sokol/sokol_tess_renderer.hpp"
#include "rive/tess/sokol/sokol_factory.hpp"
#include "rive/tess/tess_render_path.hpp"
#include "rive/tess/contour_stroke.hpp"
#include "generated/shader.h"
#include <unordered_set>
using namespace rive;
SokolRenderImage::SokolRenderImage(sg_image image) : m_Image(image) {}
static void fillColorBuffer(float* buffer, ColorInt value) {
buffer[0] = (float)colorRed(value) / 0xFF;
buffer[1] = (float)colorGreen(value) / 0xFF;
buffer[2] = (float)colorBlue(value) / 0xFF;
buffer[3] = colorOpacity(value);
}
class SokolRenderPath : public TessRenderPath {
public:
SokolRenderPath() {}
~SokolRenderPath() {
sg_destroy_buffer(m_vertexBuffer);
sg_destroy_buffer(m_indexBuffer);
}
private:
std::vector<Vec2D> m_vertices;
std::vector<uint16_t> m_indices;
sg_buffer m_vertexBuffer = {0};
sg_buffer m_indexBuffer = {0};
std::size_t m_boundsIndex = 0;
protected:
void addTriangles(rive::Span<const rive::Vec2D> vts, rive::Span<const uint16_t> idx) override {
m_vertices.insert(m_vertices.end(), vts.begin(), vts.end());
m_indices.insert(m_indices.end(), idx.begin(), idx.end());
}
void setTriangulatedBounds(const AABB& value) override {
m_boundsIndex = m_vertices.size();
m_vertices.emplace_back(Vec2D(value.minX, value.minY));
m_vertices.emplace_back(Vec2D(value.maxX, value.minY));
m_vertices.emplace_back(Vec2D(value.maxX, value.maxY));
m_vertices.emplace_back(Vec2D(value.minX, value.maxY));
}
public:
void reset() override {
TessRenderPath::reset();
m_vertices.clear();
m_indices.clear();
}
void drawStroke(ContourStroke* stroke) {
if (isContainer()) {
for (auto& subPath : m_subPaths) {
reinterpret_cast<SokolRenderPath*>(subPath.path())->drawStroke(stroke);
}
return;
}
std::size_t start, end;
stroke->nextRenderOffset(start, end);
sg_draw(start < 2 ? 0 : (start - 2) * 3, end - start < 2 ? 0 : (end - start - 2) * 3, 1);
}
void drawFill() {
if (triangulate()) {
sg_destroy_buffer(m_vertexBuffer);
sg_destroy_buffer(m_indexBuffer);
if (m_indices.size() == 0 || m_vertices.size() == 0) {
m_vertexBuffer = {0};
m_indexBuffer = {0};
return;
}
m_vertexBuffer = sg_make_buffer((sg_buffer_desc){
.type = SG_BUFFERTYPE_VERTEXBUFFER,
.data =
{
m_vertices.data(),
m_vertices.size() * sizeof(Vec2D),
},
});
m_indexBuffer = sg_make_buffer((sg_buffer_desc){
.type = SG_BUFFERTYPE_INDEXBUFFER,
.data =
{
m_indices.data(),
m_indices.size() * sizeof(uint16_t),
},
});
}
if (m_vertexBuffer.id == 0) {
return;
}
sg_bindings bind = {
.vertex_buffers[0] = m_vertexBuffer,
.index_buffer = m_indexBuffer,
};
sg_apply_bindings(&bind);
sg_draw(0, m_indices.size(), 1);
}
void drawBounds(const sg_buffer& boundsIndexBuffer) {
if (m_vertexBuffer.id == 0) {
return;
}
sg_bindings bind = {
.vertex_buffers[0] = m_vertexBuffer,
.vertex_buffer_offsets[0] = (int)(m_boundsIndex * sizeof(Vec2D)),
.index_buffer = boundsIndexBuffer,
};
sg_apply_bindings(&bind);
sg_draw(0, 6, 1);
}
};
// Returns a full-formed RenderPath -- can be treated as immutable
std::unique_ptr<RenderPath> SokolFactory::makeRenderPath(Span<const Vec2D> points,
Span<const PathVerb> verbs,
FillRule) {
return std::make_unique<SokolRenderPath>();
}
std::unique_ptr<RenderPath> SokolFactory::makeEmptyRenderPath() {
return std::make_unique<SokolRenderPath>();
}
SokolRenderImage::SokolRenderImage(sg_image image) : m_image(image) {}
class SokolBuffer : public RenderBuffer {
private:
@@ -53,13 +183,50 @@ rcp<RenderBuffer> SokolFactory::makeBufferF32(Span<const float> span) {
}));
}
SokolTessRenderer::SokolTessRenderer() {
m_MeshPipeline = sg_make_pipeline((sg_pipeline_desc){
sg_pipeline vectorPipeline(sg_shader shader,
sg_blend_state blend,
sg_stencil_state stencil,
sg_color_mask colorMask = SG_COLORMASK_RGBA) {
return sg_make_pipeline((sg_pipeline_desc){
.layout =
{
.attrs =
{
[ATTR_vs_pos] = {.format = SG_VERTEXFORMAT_FLOAT2, .buffer_index = 0},
[ATTR_vs_path_position] =
{
.format = SG_VERTEXFORMAT_FLOAT2,
.buffer_index = 0,
},
},
},
.shader = shader,
.index_type = SG_INDEXTYPE_UINT16,
.cull_mode = SG_CULLMODE_NONE,
.depth =
{
.compare = SG_COMPAREFUNC_ALWAYS,
.write_enabled = false,
},
.colors =
{
[0] =
{
.write_mask = colorMask,
.blend = blend,
},
},
.stencil = stencil,
.label = "path-pipeline",
});
}
SokolTessRenderer::SokolTessRenderer() {
m_meshPipeline = sg_make_pipeline((sg_pipeline_desc){
.layout =
{
.attrs =
{
[ATTR_vs_position] = {.format = SG_VERTEXFORMAT_FLOAT2, .buffer_index = 0},
[ATTR_vs_texcoord0] = {.format = SG_VERTEXFORMAT_FLOAT2, .buffer_index = 1},
},
},
@@ -86,6 +253,232 @@ SokolTessRenderer::SokolTessRenderer() {
},
.label = "mesh-pipeline",
});
auto uberShader = sg_make_shader(rive_tess_path_shader_desc(sg_query_backend()));
assert(maxClippingPaths < 256);
// Src Over Pipelines
{
m_pathPipeline[0] = vectorPipeline(uberShader,
{
.enabled = true,
.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
},
{
.enabled = false,
});
for (std::size_t i = 1; i <= maxClippingPaths; i++) {
m_pathPipeline[i] =
vectorPipeline(uberShader,
{
.enabled = true,
.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
},
{
.enabled = true,
.ref = (uint8_t)i,
.read_mask = 0xFF,
.write_mask = 0x00,
.front =
{
.compare = SG_COMPAREFUNC_EQUAL,
},
.back =
{
.compare = SG_COMPAREFUNC_EQUAL,
},
});
}
}
// Screen Pipelines
{
m_pathScreenPipeline[0] =
vectorPipeline(uberShader,
{
.enabled = true,
.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
.dst_factor_rgb = SG_BLENDFACTOR_ONE,
.src_factor_alpha = SG_BLENDFACTOR_SRC_ALPHA,
.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
},
{
.enabled = false,
});
for (std::size_t i = 1; i <= maxClippingPaths; i++) {
m_pathScreenPipeline[i] =
vectorPipeline(uberShader,
{
.enabled = true,
.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
.dst_factor_rgb = SG_BLENDFACTOR_ONE,
.src_factor_alpha = SG_BLENDFACTOR_SRC_ALPHA,
.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
},
{
.enabled = true,
.ref = (uint8_t)i,
.read_mask = 0xFF,
.write_mask = 0x00,
.front =
{
.compare = SG_COMPAREFUNC_EQUAL,
},
.back =
{
.compare = SG_COMPAREFUNC_EQUAL,
},
});
}
}
// Additive Pipelines
{
m_pathAdditivePipeline[0] = vectorPipeline(uberShader,
{
.enabled = true,
.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
.dst_factor_rgb = SG_BLENDFACTOR_ONE,
},
{
.enabled = false,
});
for (std::size_t i = 1; i <= maxClippingPaths; i++) {
m_pathAdditivePipeline[i] =
vectorPipeline(uberShader,
{
.enabled = true,
.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
.dst_factor_rgb = SG_BLENDFACTOR_ONE,
},
{
.enabled = true,
.ref = (uint8_t)i,
.read_mask = 0xFF,
.write_mask = 0x00,
.front =
{
.compare = SG_COMPAREFUNC_EQUAL,
},
.back =
{
.compare = SG_COMPAREFUNC_EQUAL,
},
});
}
}
// Multiply Pipelines
{
m_pathMultiplyPipeline[0] =
vectorPipeline(uberShader,
{
.enabled = true,
.src_factor_rgb = SG_BLENDFACTOR_DST_COLOR,
.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
},
{
.enabled = false,
});
for (std::size_t i = 1; i <= maxClippingPaths; i++) {
m_pathMultiplyPipeline[i] =
vectorPipeline(uberShader,
{
.enabled = true,
.src_factor_rgb = SG_BLENDFACTOR_DST_COLOR,
.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
},
{
.enabled = true,
.ref = (uint8_t)i,
.read_mask = 0xFF,
.write_mask = 0x00,
.front =
{
.compare = SG_COMPAREFUNC_EQUAL,
},
.back =
{
.compare = SG_COMPAREFUNC_EQUAL,
},
});
}
}
m_incClipPipeline = vectorPipeline(uberShader,
{
.enabled = false,
},
{
.enabled = true,
.read_mask = 0xFF,
.write_mask = 0xFF,
.front =
{
.compare = SG_COMPAREFUNC_ALWAYS,
.depth_fail_op = SG_STENCILOP_KEEP,
.fail_op = SG_STENCILOP_KEEP,
.pass_op = SG_STENCILOP_INCR_CLAMP,
},
.back =
{
.compare = SG_COMPAREFUNC_ALWAYS,
.depth_fail_op = SG_STENCILOP_KEEP,
.fail_op = SG_STENCILOP_KEEP,
.pass_op = SG_STENCILOP_INCR_CLAMP,
},
},
SG_COLORMASK_NONE);
m_decClipPipeline = vectorPipeline(uberShader,
{
.enabled = false,
},
{
.enabled = true,
.read_mask = 0xFF,
.write_mask = 0xFF,
.front =
{
.compare = SG_COMPAREFUNC_ALWAYS,
.depth_fail_op = SG_STENCILOP_KEEP,
.fail_op = SG_STENCILOP_KEEP,
.pass_op = SG_STENCILOP_DECR_CLAMP,
},
.back =
{
.compare = SG_COMPAREFUNC_ALWAYS,
.depth_fail_op = SG_STENCILOP_KEEP,
.fail_op = SG_STENCILOP_KEEP,
.pass_op = SG_STENCILOP_DECR_CLAMP,
},
},
SG_COLORMASK_NONE);
uint16_t indices[] = {0, 1, 2, 0, 2, 3};
m_boundsIndices = sg_make_buffer((sg_buffer_desc){
.type = SG_BUFFERTYPE_INDEXBUFFER,
.data = SG_RANGE(indices),
});
}
SokolTessRenderer::~SokolTessRenderer() {
sg_destroy_buffer(m_boundsIndices);
sg_destroy_pipeline(m_meshPipeline);
sg_destroy_pipeline(m_incClipPipeline);
sg_destroy_pipeline(m_decClipPipeline);
for (std::size_t i = 0; i <= maxClippingPaths; i++) {
sg_destroy_pipeline(m_pathPipeline[i]);
sg_destroy_pipeline(m_pathScreenPipeline[i]);
}
}
void SokolTessRenderer::orthographicProjection(float left,
@@ -149,7 +542,7 @@ void SokolTessRenderer::drawImageMesh(const RenderImage* renderImage,
const Mat2D& world = transform();
vs_params.mvp = m_Projection * world;
sg_apply_pipeline(m_MeshPipeline);
setPipeline(m_meshPipeline);
sg_bindings bind = {
.vertex_buffers[0] = static_cast<SokolBuffer*>(vertices_f32.get())->buffer(),
.vertex_buffers[1] = static_cast<SokolBuffer*>(uvCoords_f32.get())->buffer(),
@@ -159,6 +552,366 @@ void SokolTessRenderer::drawImageMesh(const RenderImage* renderImage,
sg_apply_bindings(&bind);
sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_params, SG_RANGE_REF(vs_params));
// printf("INDICES: %zu\n", indices_u16->count());
sg_draw(0, indices_u16->count(), 1);
}
}
class SokolGradient : public RenderShader {
private:
Vec2D m_start;
Vec2D m_end;
int m_type;
std::vector<float> m_colors;
std::vector<float> m_stops;
bool m_isVisible = false;
private:
// General gradient
SokolGradient(int type, const ColorInt colors[], const float stops[], size_t count) :
m_type(type) {
m_stops.resize(count);
m_colors.resize(count * 4);
for (int i = 0, colorIndex = 0; i < count; i++, colorIndex += 4) {
fillColorBuffer(&m_colors[colorIndex], colors[i]);
m_stops[i] = stops[i];
if (m_colors[colorIndex + 3] > 0.0f) {
m_isVisible = true;
}
}
}
public:
// Linear gradient
SokolGradient(float sx,
float sy,
float ex,
float ey,
const ColorInt colors[],
const float stops[],
size_t count) :
SokolGradient(1, colors, stops, count) {
m_start = Vec2D(sx, sy);
m_end = Vec2D(ex, ey);
}
SokolGradient(float cx,
float cy,
float radius,
const ColorInt colors[], // [count]
const float stops[], // [count]
size_t count) :
SokolGradient(2, colors, stops, count) {
m_start = Vec2D(cx, cy);
m_end = Vec2D(cx + radius, cy);
}
void bind(fs_path_uniforms_t& uniforms) {
auto stopCount = m_stops.size();
uniforms.fillType = m_type;
uniforms.stopCount = stopCount;
uniforms.gradientStart = m_start;
uniforms.gradientEnd = m_end;
for (int i = 0; i < stopCount; i++) {
auto colorBufferIndex = i * 4;
for (int j = 0; j < 4; j++) {
uniforms.colors[i][j] = m_colors[colorBufferIndex + j];
}
uniforms.stops[i / 4][i % 4] = m_stops[i];
}
}
};
rcp<RenderShader> SokolFactory::makeLinearGradient(float sx,
float sy,
float ex,
float ey,
const ColorInt colors[],
const float stops[],
size_t count) {
return rcp<RenderShader>(new SokolGradient(sx, sy, ex, ey, colors, stops, count));
}
rcp<RenderShader> SokolFactory::makeRadialGradient(float cx,
float cy,
float radius,
const ColorInt colors[], // [count]
const float stops[], // [count]
size_t count) {
return rcp<RenderShader>(new SokolGradient(cx, cy, radius, colors, stops, count));
}
class SokolRenderPaint : public RenderPaint {
private:
fs_path_uniforms_t m_uniforms = {0};
rcp<RenderShader> m_shader;
RenderPaintStyle m_style;
std::unique_ptr<ContourStroke> m_stroke;
bool m_strokeDirty = false;
float m_strokeThickness = 0.0f;
StrokeJoin m_strokeJoin;
StrokeCap m_strokeCap;
sg_buffer m_strokeVertexBuffer = {0};
sg_buffer m_strokeIndexBuffer = {0};
std::vector<std::size_t> m_StrokeOffsets;
BlendMode m_blendMode = BlendMode::srcOver;
public:
void color(ColorInt value) override {
fillColorBuffer(m_uniforms.colors[0], value);
m_uniforms.fillType = 0;
}
void style(RenderPaintStyle value) override {
m_style = value;
switch (m_style) {
case RenderPaintStyle::fill:
m_stroke = nullptr;
m_strokeDirty = false;
break;
case RenderPaintStyle::stroke:
m_stroke = std::make_unique<ContourStroke>();
m_strokeDirty = true;
break;
}
}
RenderPaintStyle style() const { return m_style; }
void thickness(float value) override {
m_strokeThickness = value;
m_strokeDirty = true;
}
void join(StrokeJoin value) override {
m_strokeJoin = value;
m_strokeDirty = true;
}
void cap(StrokeCap value) override {
m_strokeCap = value;
m_strokeDirty = true;
}
void invalidateStroke() override {
if (m_stroke) {
m_strokeDirty = true;
}
}
void blendMode(BlendMode value) override { m_blendMode = value; }
BlendMode blendMode() const { return m_blendMode; }
void shader(rcp<RenderShader> shader) override { m_shader = shader; }
void draw(SokolRenderPath* path) {
if (m_shader) {
static_cast<SokolGradient*>(m_shader.get())->bind(m_uniforms);
}
sg_apply_uniforms(SG_SHADERSTAGE_FS, SLOT_fs_path_uniforms, SG_RANGE_REF(m_uniforms));
if (m_stroke != nullptr) {
if (m_strokeDirty) {
static Mat2D identity;
m_stroke->reset();
path->extrudeStroke(m_stroke.get(),
m_strokeJoin,
m_strokeCap,
m_strokeThickness / 2.0f,
identity);
m_strokeDirty = false;
const std::vector<Vec2D>& strip = m_stroke->triangleStrip();
sg_destroy_buffer(m_strokeVertexBuffer);
sg_destroy_buffer(m_strokeIndexBuffer);
auto size = strip.size();
if (size <= 2) {
m_strokeVertexBuffer = {0};
m_strokeIndexBuffer = {0};
return;
}
m_strokeVertexBuffer = sg_make_buffer((sg_buffer_desc){
.type = SG_BUFFERTYPE_VERTEXBUFFER,
.data =
{
strip.data(),
strip.size() * sizeof(Vec2D),
},
});
// Let's use a tris index buffer so we can keep the same sokol pipeline.
std::vector<uint16_t> indices;
// Build them by stroke offsets (where each offset represents a sub-path, or a move
// to)
m_stroke->resetRenderOffset();
m_StrokeOffsets.clear();
while (true) {
std::size_t strokeStart, strokeEnd;
if (!m_stroke->nextRenderOffset(strokeStart, strokeEnd)) {
break;
}
std::size_t length = strokeEnd - strokeStart;
if (length > 2) {
for (std::size_t i = 0, end = length - 2; i < end; i++) {
if ((i % 2) == 1) {
indices.push_back(i + strokeStart);
indices.push_back(i + 1 + strokeStart);
indices.push_back(i + 2 + strokeStart);
} else {
indices.push_back(i + strokeStart);
indices.push_back(i + 2 + strokeStart);
indices.push_back(i + 1 + strokeStart);
}
}
m_StrokeOffsets.push_back(indices.size());
}
}
m_strokeIndexBuffer = sg_make_buffer((sg_buffer_desc){
.type = SG_BUFFERTYPE_INDEXBUFFER,
.data =
{
indices.data(),
indices.size() * sizeof(uint16_t),
},
});
}
if (m_strokeVertexBuffer.id == 0) {
return;
}
sg_bindings bind = {
.vertex_buffers[0] = m_strokeVertexBuffer,
.index_buffer = m_strokeIndexBuffer,
};
sg_apply_bindings(&bind);
m_stroke->resetRenderOffset();
// path->drawStroke(m_stroke.get());
std::size_t start = 0;
for (auto end : m_StrokeOffsets) {
sg_draw(start, end - start, 1);
start = end;
}
} else {
path->drawFill();
}
}
};
std::unique_ptr<RenderPaint> SokolFactory::makeRenderPaint() {
return std::make_unique<SokolRenderPaint>();
}
void SokolTessRenderer::restore() {
TessRenderer::restore();
if (m_Stack.size() == 1) {
// When we've fully restored, immediately update clip to not wait for next draw.
applyClipping();
m_currentPipeline = {0};
}
}
void SokolTessRenderer::applyClipping() {
if (!m_IsClippingDirty) {
return;
}
m_IsClippingDirty = false;
RenderState& state = m_Stack.back();
auto currentClipLength = m_ClipPaths.size();
if (currentClipLength == state.clipPaths.size()) {
// Same length so now check if they're all the same.
bool allSame = true;
for (std::size_t i = 0; i < currentClipLength; i++) {
if (state.clipPaths[i].path() != m_ClipPaths[i].path()) {
allSame = false;
break;
}
}
if (allSame) {
return;
}
}
vs_path_params_t vs_params;
fs_path_uniforms_t uniforms = {0};
// Decr any paths from the last clip that are gone.
std::unordered_set<RenderPath*> alreadyApplied;
for (auto appliedPath : m_ClipPaths) {
bool decr = true;
for (auto nextClipPath : state.clipPaths) {
if (nextClipPath.path() == appliedPath.path()) {
decr = false;
alreadyApplied.insert(appliedPath.path());
break;
}
}
if (decr) {
// Draw appliedPath.path() with decr pipeline
setPipeline(m_decClipPipeline);
vs_params.mvp = m_Projection * appliedPath.transform();
sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_path_params, SG_RANGE_REF(vs_params));
sg_apply_uniforms(SG_SHADERSTAGE_FS, SLOT_fs_path_uniforms, SG_RANGE_REF(uniforms));
auto sokolPath = static_cast<SokolRenderPath*>(appliedPath.path());
sokolPath->drawFill();
}
}
// Incr any paths that are added.
for (auto nextClipPath : state.clipPaths) {
if (alreadyApplied.count(nextClipPath.path())) {
// Already applied.
continue;
}
// Draw nextClipPath.path() with incr pipeline
setPipeline(m_incClipPipeline);
vs_params.mvp = m_Projection * nextClipPath.transform();
sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_path_params, SG_RANGE_REF(vs_params));
sg_apply_uniforms(SG_SHADERSTAGE_FS, SLOT_fs_path_uniforms, SG_RANGE_REF(uniforms));
auto sokolPath = static_cast<SokolRenderPath*>(nextClipPath.path());
sokolPath->drawFill();
}
// Pick which pipeline to use for draw path operations.
// TODO: something similar for draw mesh.
m_clipCount = state.clipPaths.size();
m_ClipPaths = state.clipPaths;
}
void SokolTessRenderer::reset() { m_currentPipeline = {0}; }
void SokolTessRenderer::setPipeline(sg_pipeline pipeline) {
if (m_currentPipeline.id == pipeline.id) {
return;
}
m_currentPipeline = pipeline;
sg_apply_pipeline(pipeline);
}
void SokolTessRenderer::drawPath(RenderPath* path, RenderPaint* paint) {
auto sokolPaint = static_cast<SokolRenderPaint*>(paint);
applyClipping();
vs_path_params_t vs_params;
const Mat2D& world = transform();
vs_params.mvp = m_Projection * world;
switch (sokolPaint->blendMode()) {
case BlendMode::srcOver: setPipeline(m_pathPipeline[m_clipCount]); break;
case BlendMode::screen: setPipeline(m_pathScreenPipeline[m_clipCount]); break;
case BlendMode::colorDodge: setPipeline(m_pathAdditivePipeline[m_clipCount]); break;
case BlendMode::multiply: setPipeline(m_pathMultiplyPipeline[m_clipCount]); break;
default: setPipeline(m_pathScreenPipeline[m_clipCount]); break;
}
sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_params, SG_RANGE_REF(vs_params));
static_cast<SokolRenderPaint*>(paint)->draw(static_cast<SokolRenderPath*>(path));
}

View File

@@ -1,19 +1,165 @@
#include "rive/tess/tess_render_path.hpp"
#include "rive/tess/contour_stroke.hpp"
#include "tesselator.h"
static const float contourThreshold = 1.0f;
using namespace rive;
TessRenderPath::TessRenderPath() {}
TessRenderPath::TessRenderPath() : m_segmentedContour(contourThreshold) {}
TessRenderPath::TessRenderPath(Span<const Vec2D> points,
Span<const PathVerb> verbs,
FillRule fillRule) :
m_RawPath(points.data(), points.size(), verbs.data(), verbs.size()), m_FillRule(fillRule) {}
m_rawPath(points.data(), points.size(), verbs.data(), verbs.size()),
m_fillRule(fillRule),
m_segmentedContour(contourThreshold) {}
TessRenderPath::~TessRenderPath() {}
void TessRenderPath::reset() { m_RawPath.reset(); }
void TessRenderPath::fillRule(FillRule value) {}
void TessRenderPath::moveTo(float x, float y) { m_RawPath.moveTo(x, y); }
void TessRenderPath::lineTo(float x, float y) { m_RawPath.lineTo(x, y); }
void TessRenderPath::cubicTo(float ox, float oy, float ix, float iy, float x, float y) {
m_RawPath.cubicTo(ox, oy, ix, iy, x, y);
void TessRenderPath::reset() {
m_rawPath.rewind();
m_subPaths.clear();
m_isContourDirty = m_isTriangulationDirty = true;
}
void TessRenderPath::close() { m_RawPath.close(); }
void TessRenderPath::fillRule(FillRule value) { m_fillRule = value; }
void TessRenderPath::moveTo(float x, float y) { m_rawPath.moveTo(x, y); }
void TessRenderPath::lineTo(float x, float y) { m_rawPath.lineTo(x, y); }
void TessRenderPath::cubicTo(float ox, float oy, float ix, float iy, float x, float y) {
m_rawPath.cubicTo(ox, oy, ix, iy, x, y);
}
void TessRenderPath::close() {
m_rawPath.close();
m_isClosed = true;
}
void TessRenderPath::addRenderPath(RenderPath* path, const Mat2D& transform) {
m_subPaths.emplace_back(SubPath(path, transform));
}
const SegmentedContour& TessRenderPath::segmentedContour() const { return m_segmentedContour; }
// Helper for earcut to understand Vec2D
namespace mapbox {
namespace util {
template <> struct nth<0, Vec2D> {
inline static auto get(const Vec2D& t) { return t.x; };
};
template <> struct nth<1, Vec2D> {
inline static auto get(const Vec2D& t) { return t.y; };
};
} // namespace util
} // namespace mapbox
const RawPath& TessRenderPath::rawPath() const { return m_rawPath; }
void* stdAlloc(void* userData, unsigned int size) {
int* allocated = (int*)userData;
TESS_NOTUSED(userData);
*allocated += (int)size;
return malloc(size);
}
void stdFree(void* userData, void* ptr) {
TESS_NOTUSED(userData);
free(ptr);
}
bool TessRenderPath::triangulate() {
if (!m_isTriangulationDirty) {
return false;
}
m_isTriangulationDirty = false;
triangulate(this);
return true;
}
void TessRenderPath::triangulate(TessRenderPath* containerPath) {
AABB bounds = AABB::forExpansion();
// If there's a single path, we're going to try to assume the user isn't
// doing any funky self overlapping winding and we'll try to triangulate it
// quickly as a single polygon.
if (m_subPaths.size() == 0) {
if (!m_rawPath.empty()) {
Mat2D identity;
contour(identity);
bounds = m_segmentedContour.bounds();
auto contour = m_segmentedContour.contourPoints();
auto contours = Span(&contour, 1);
m_earcut(contours);
containerPath->addTriangles(contour, toSpan(m_earcut.indices));
}
} else {
TESStesselator* tess = nullptr;
for (SubPath& subPath : m_subPaths) {
auto subRenderPath = static_cast<TessRenderPath*>(subPath.path());
if (subRenderPath->isContainer()) {
subRenderPath->triangulate(containerPath);
} else if (!subRenderPath->empty()) {
if (tess == nullptr) {
tess = tessNewTess(nullptr);
}
subRenderPath->contour(subPath.transform());
const SegmentedContour& segmentedContour = subRenderPath->segmentedContour();
auto contour = segmentedContour.contourPoints();
tessAddContour(tess, 2, contour.data(), sizeof(float) * 2, contour.size());
bounds.expand(segmentedContour.bounds());
}
}
if (tess != nullptr) {
if (tessTesselate(tess, TESS_WINDING_POSITIVE, TESS_POLYGONS, 3, 2, 0)) {
auto verts = tessGetVertices(tess);
// const int* vinds = tessGetVertexIndices(tess);
auto nverts = tessGetVertexCount(tess);
auto elems = tessGetElements(tess);
auto nelems = tessGetElementCount(tess);
std::vector<uint16_t> indices;
for (int i = 0; i < nelems * 3; i++) {
indices.push_back(elems[i]);
}
containerPath->addTriangles(Span(reinterpret_cast<const Vec2D*>(verts), nverts),
toSpan(indices));
}
tessDeleteTess(tess);
}
}
containerPath->setTriangulatedBounds(bounds);
}
void TessRenderPath::contour(const Mat2D& transform) {
if (!m_isContourDirty && transform == m_contourTransform) {
return;
}
m_isContourDirty = false;
m_contourTransform = transform;
m_segmentedContour.contour(m_rawPath, transform);
}
void TessRenderPath::extrudeStroke(ContourStroke* stroke,
StrokeJoin join,
StrokeCap cap,
float strokeWidth,
const Mat2D& transform) {
if (isContainer()) {
for (auto& subPath : m_subPaths) {
static_cast<TessRenderPath*>(subPath.path())
->extrudeStroke(stroke, join, cap, strokeWidth, subPath.transform());
}
return;
}
contour(transform);
stroke->extrude(&m_segmentedContour, m_isClosed, join, cap, strokeWidth);
}
bool TessRenderPath::empty() const { return m_rawPath.empty(); }

View File

@@ -20,39 +20,18 @@ void TessRenderer::restore() {
}
}
bool TessRenderer::needsToApplyClipping() {
if (!m_IsClippingDirty) {
return false;
}
m_IsClippingDirty = false;
RenderState& state = m_Stack.back();
auto currentClipLength = m_ClipPaths.size();
if (currentClipLength == state.clipPaths.size()) {
// Same length so now check if they're all the same.
bool allSame = true;
for (std::size_t i = 0; i < currentClipLength; i++) {
if (state.clipPaths[i].path() != m_ClipPaths[i].path()) {
allSame = false;
break;
}
}
if (allSame) {
return false;
}
}
m_ClipPaths = state.clipPaths;
return true;
}
void TessRenderer::transform(const Mat2D& transform) {
Mat2D& stackMat = m_Stack.back().transform;
stackMat = stackMat * transform;
}
void TessRenderer::clipPath(RenderPath* path) {}
void TessRenderer::drawPath(RenderPath* path, RenderPaint* paint) {}
void TessRenderer::clipPath(RenderPath* path) {
RenderState& state = m_Stack.back();
state.clipPaths.emplace_back(SubPath(path, state.transform));
m_IsClippingDirty = true;
}
void TessRenderer::drawImage(const RenderImage*, BlendMode, float opacity) {}
void TessRenderer::drawImageMesh(const RenderImage*,
rcp<RenderBuffer> vertices_f32,

BIN
tess/test/assets/marty.riv Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,40 @@
#include "rive/shapes/path.hpp"
#include "rive/math/mat2d.hpp"
#include "rive/tess/tess_render_path.hpp"
#include "rive_file_reader.hpp"
class TestRenderPath : public rive::TessRenderPath {
public:
std::vector<rive::Vec2D> vertices;
std::vector<uint16_t> indices;
protected:
virtual void addTriangles(rive::Span<const rive::Vec2D> vts,
rive::Span<const uint16_t> idx) override {
vertices.insert(vertices.end(), vts.begin(), vts.end());
indices.insert(indices.end(), idx.begin(), idx.end());
}
void setTriangulatedBounds(const rive::AABB& value) override {}
};
TEST_CASE("simple triangle path triangulates as expected", "[file]") {
auto file = ReadRiveFile("../test/assets/triangle.riv");
auto artboard = file->artboard();
artboard->advance(0.0f);
auto path = artboard->find<rive::Path>("triangle_path");
REQUIRE(path != nullptr);
TestRenderPath renderPath;
path->buildPath(renderPath);
rive::Mat2D identity;
TestRenderPath shapeRenderPath;
shapeRenderPath.addRenderPath(&renderPath, identity);
shapeRenderPath.triangulate();
REQUIRE(shapeRenderPath.vertices.size() == 3);
REQUIRE(shapeRenderPath.indices.size() == 3);
REQUIRE(shapeRenderPath.indices[0] == 0);
REQUIRE(shapeRenderPath.indices[1] == 1);
REQUIRE(shapeRenderPath.indices[2] == 2);
}