| /* | |
| * Copyright 2022 Rive | |
| */ | |
| #include "rive_render_path.hpp" | |
| #include "rive/math/bezier_utils.hpp" | |
| #include "rive/math/simd.hpp" | |
| #include "rive/math/wangs_formula.hpp" | |
| #include "shaders/constants.glsl" | |
| namespace rive | |
| { | |
| RiveRenderPath::RiveRenderPath(FillRule fillRule, RawPath& rawPath) | |
| { | |
| m_fillRule = fillRule; | |
| m_rawPath.swap(rawPath); | |
| m_rawPath.pruneEmptySegments(); | |
| } | |
| void RiveRenderPath::rewind() | |
| { | |
| assert(m_rawPathMutationLockCount == 0); | |
| m_rawPath.rewind(); | |
| m_dirt = kAllDirt; | |
| } | |
| void RiveRenderPath::moveTo(float x, float y) | |
| { | |
| assert(m_rawPathMutationLockCount == 0); | |
| m_rawPath.moveTo(x, y); | |
| m_dirt = kAllDirt; | |
| } | |
| void RiveRenderPath::lineTo(float x, float y) | |
| { | |
| assert(m_rawPathMutationLockCount == 0); | |
| // Make sure to start a new contour, even if this line is empty. | |
| m_rawPath.injectImplicitMoveIfNeeded(); | |
| Vec2D p1 = {x, y}; | |
| if (m_rawPath.points().back() != p1) | |
| { | |
| m_rawPath.line(p1); | |
| } | |
| m_dirt = kAllDirt; | |
| } | |
| void RiveRenderPath::cubicTo(float ox, | |
| float oy, | |
| float ix, | |
| float iy, | |
| float x, | |
| float y) | |
| { | |
| assert(m_rawPathMutationLockCount == 0); | |
| // Make sure to start a new contour, even if this cubic is empty. | |
| m_rawPath.injectImplicitMoveIfNeeded(); | |
| Vec2D p1 = {ox, oy}; | |
| Vec2D p2 = {ix, iy}; | |
| Vec2D p3 = {x, y}; | |
| if (m_rawPath.points().back() != p1 || p1 != p2 || p2 != p3) | |
| { | |
| m_rawPath.cubic(p1, p2, p3); | |
| } | |
| m_dirt = kAllDirt; | |
| } | |
| void RiveRenderPath::close() | |
| { | |
| assert(m_rawPathMutationLockCount == 0); | |
| m_rawPath.close(); | |
| m_dirt = kAllDirt; | |
| } | |
| void RiveRenderPath::addRenderPath(RenderPath* path, const Mat2D& matrix) | |
| { | |
| assert(m_rawPathMutationLockCount == 0); | |
| RiveRenderPath* riveRenderPath = static_cast<RiveRenderPath*>(path); | |
| RawPath::Iter transformedPathIter = | |
| m_rawPath.addPath(riveRenderPath->m_rawPath, &matrix); | |
| if (matrix != Mat2D()) | |
| { | |
| // Prune any segments that became empty after the transform. | |
| m_rawPath.pruneEmptySegments(transformedPathIter); | |
| } | |
| m_dirt = kAllDirt; | |
| } | |
| const AABB& RiveRenderPath::getBounds() const | |
| { | |
| if (m_dirt & kPathBoundsDirt) | |
| { | |
| m_bounds = m_rawPath.bounds(); | |
| m_dirt &= ~kPathBoundsDirt; | |
| } | |
| return m_bounds; | |
| } | |
| float RiveRenderPath::getCoarseArea() const | |
| { | |
| if (m_dirt & kPathCoarseAreaDirt) | |
| { | |
| float a = 0; | |
| Vec2D contourP0 = {0, 0}, lastPt = {0, 0}; | |
| for (auto [verb, pts] : m_rawPath) | |
| { | |
| switch (verb) | |
| { | |
| case PathVerb::move: | |
| a += Vec2D::cross(lastPt, contourP0); | |
| contourP0 = lastPt = pts[0]; | |
| break; | |
| case PathVerb::close: | |
| break; | |
| case PathVerb::line: | |
| a += Vec2D::cross(lastPt, pts[1]); | |
| lastPt = pts[1]; | |
| break; | |
| case PathVerb::quad: | |
| RIVE_UNREACHABLE(); | |
| case PathVerb::cubic: | |
| { | |
| // Linearize the cubic in artboard space, then add up the | |
| // area for each segment. | |
| float n = ceilf( | |
| wangs_formula::cubic(pts, 1.f / kCoarseAreaTolerance)); | |
| if (n > 1) | |
| { | |
| n = std::min(n, 64.f); | |
| float4 t = float4{1, 1, 2, 2} * (1 / n); | |
| float4 dt = t.w; | |
| math::EvalCubic evalCubic(pts); | |
| for (; t.x < 1; t += dt) | |
| { | |
| float4 p = evalCubic(t); | |
| Vec2D lo = {p.x, p.y}; | |
| a += Vec2D::cross(lastPt, lo); | |
| lastPt = lo; | |
| if (t.y < 1) | |
| { | |
| Vec2D hi = {p.z, p.w}; | |
| a += Vec2D::cross(lastPt, hi); | |
| lastPt = hi; | |
| } | |
| } | |
| } | |
| a += Vec2D::cross(lastPt, pts[3]); | |
| lastPt = pts[3]; | |
| break; | |
| } | |
| } | |
| } | |
| a += Vec2D::cross(lastPt, contourP0); | |
| m_coarseArea = a * .5f; | |
| m_dirt &= ~kPathCoarseAreaDirt; | |
| } | |
| return m_coarseArea; | |
| } | |
| bool RiveRenderPath::isClockwiseDominant(const Mat2D& viewMatrix) const | |
| { | |
| float matrixDeterminant = | |
| viewMatrix[0] * viewMatrix[3] - viewMatrix[2] * viewMatrix[1]; | |
| return getCoarseArea() * matrixDeterminant >= 0; | |
| } | |
| uint64_t RiveRenderPath::getRawPathMutationID() const | |
| { | |
| static std::atomic<uint64_t> uniqueIDCounter = 0; | |
| if (m_dirt & kRawPathMutationIDDirt) | |
| { | |
| m_rawPathMutationID = ++uniqueIDCounter; | |
| m_dirt &= ~kRawPathMutationIDDirt; | |
| } | |
| return m_rawPathMutationID; | |
| } | |
| // When a blurred shape curves away from the convolution matrix, the curvature | |
| // makes the blur softer, which does not happen naturally in feathering. | |
| // | |
| // To simulate the softening effect from curving away, we flatten curves | |
| // proportionaly to curvature. This works really well for gaussian feathers, but | |
| // we may also split the curve and recurse if there is enough flattening to | |
| // become noticeable. | |
| // | |
| // TODO: Move this work to the GPU. | |
| static void add_softened_cubic_for_feathering(RawPath* featheredPath, | |
| const Vec2D p[4], | |
| float feather, | |
| float matrixMaxScale, | |
| int maxDepth = 3) | |
| { | |
| float2 p0 = simd::load2f(p), p1 = simd::load2f(p + 1), | |
| p2 = simd::load2f(p + 2), p3 = simd::load2f(p + 3); | |
| math::CubicCoeffs coeffs(p); | |
| // Find the point of maximum height on the cubic. | |
| float maxHeightT; | |
| float height = math::find_cubic_max_height(p, &maxHeightT); | |
| // Measure curvature across one standard deviation of the feather. | |
| // ("feather" is 2 std devs.) | |
| float desiredSpread = feather * .5f; | |
| // The feather gets dimmer with curvature. Find a dimming factor based on | |
| // the strength of curvature at maximum height. | |
| float theta = math::measure_cubic_local_curvature(p, | |
| coeffs, | |
| maxHeightT, | |
| desiredSpread); | |
| float dimming = 1 - theta * (1 / math::PI); | |
| // Always dim a little bit in order to avoid artifacts on tight cusps. | |
| // FIXME: This is unfortunate. There must be a better way to handle cusps. | |
| dimming = fminf(dimming, .925f); | |
| // Find a new height such that the center of the feather (currently 50% | |
| // opacity) is reduced to "50% * dimming". | |
| float desiredOpacityOnCenter = .5f * dimming; | |
| float x = gpu::inverse_gaussian_integral(desiredOpacityOnCenter) - .5f; | |
| float newHeight = height + feather * FEATHER_TEXTURE_STDDEVS * x; | |
| if (maxDepth > 0 && (height - newHeight) * matrixMaxScale > 8) | |
| { | |
| // The curve would be flattened too much. Chop at max height and | |
| // recurse. | |
| Vec2D pp[7]; | |
| math::chop_cubic_at(p, pp, maxHeightT); | |
| add_softened_cubic_for_feathering(featheredPath, | |
| pp, | |
| feather, | |
| matrixMaxScale, | |
| maxDepth - 1); | |
| add_softened_cubic_for_feathering(featheredPath, | |
| pp + 3, | |
| feather, | |
| matrixMaxScale, | |
| maxDepth - 1); | |
| return; | |
| } | |
| // Flatten the curve down to "newHeight". (Height scales linearly as we lerp | |
| // the control points to "flatLinePoints".) | |
| float4 flatLinePoints = | |
| simd::mix(p0.xyxy, p3.xyxy, float4{1.f / 3, 1.f / 3, 2.f / 3, 2.f / 3}); | |
| float softness = height != 0 ? 1 - newHeight / height : 1; | |
| // Do the "min" first so softness is 1 if anything went NaN. | |
| softness = fmaxf(0, fminf(softness, 1)); | |
| assert(softness >= 0 && softness <= 1); | |
| float4 softenedPoints = simd::unchecked_mix(simd::join(p1, p2), | |
| flatLinePoints, | |
| float4(softness)); | |
| featheredPath->cubic(math::bit_cast<Vec2D>(softenedPoints.xy), | |
| math::bit_cast<Vec2D>(softenedPoints.zw), | |
| p[3]); | |
| } | |
| rcp<RiveRenderPath> RiveRenderPath::makeSoftenedCopyForFeathering( | |
| float feather, | |
| float matrixMaxScale) | |
| { | |
| RawPath featheredPath; | |
| // Reserve a generous amount of space upfront so we hopefully don't have to | |
| // reallocate -- enough for each verb to be chopped 4 times. | |
| featheredPath.reserve(m_rawPath.verbs().size() * 4, | |
| m_rawPath.points().size() * 4); | |
| for (auto [verb, pts] : m_rawPath) | |
| { | |
| switch (verb) | |
| { | |
| case PathVerb::move: | |
| featheredPath.move(pts[0]); | |
| break; | |
| case PathVerb::line: | |
| featheredPath.line(pts[1]); | |
| break; | |
| case PathVerb::cubic: | |
| { | |
| // Start by chopping all cubics so they are convex and rotate no | |
| // more than 90 degrees. The stroke algorithm requires them not | |
| // to have inflections | |
| float T[4]; | |
| Vec2D chops[(std::size(T) + 1) * 3 + 1]; // 4 chops will produce | |
| // 16 cubic vertices. | |
| bool areCusps; | |
| // A generous cusp padding looks better empirically. | |
| constexpr static float CUSP_PADDING = 1e-2f; | |
| int n = math::find_cubic_convex_90_chops(pts, | |
| T, | |
| CUSP_PADDING, | |
| &areCusps); | |
| math::chop_cubic_at(pts, chops, T, n); | |
| Vec2D* p = chops; | |
| for (int i = 0; i <= n; ++i, p += 3) | |
| { | |
| if (areCusps && (i & 1)) | |
| { | |
| // If the chops are straddling cusps, odd-numbered chops | |
| // are the ones that pass through a cusp. | |
| featheredPath.line(p[3]); | |
| } | |
| else | |
| { | |
| add_softened_cubic_for_feathering(&featheredPath, | |
| p, | |
| feather, | |
| matrixMaxScale); | |
| } | |
| } | |
| break; | |
| } | |
| case PathVerb::close: | |
| featheredPath.close(); | |
| break; | |
| case PathVerb::quad: | |
| RIVE_UNREACHABLE(); | |
| } | |
| } | |
| return make_rcp<RiveRenderPath>(m_fillRule, featheredPath); | |
| } | |
| void RiveRenderPath::setDrawCache(gpu::RiveRenderPathDraw* drawCache, | |
| const Mat2D& mat, | |
| rive::RiveRenderPaint* riveRenderPaint) const | |
| { | |
| CacheElements& cache = | |
| m_cachedElements[riveRenderPaint->getIsStroked() ? CACHE_STROKED | |
| : CACHE_FILLED]; | |
| cache.draw = drawCache; | |
| cache.xx = mat.xx(); | |
| cache.xy = mat.xy(); | |
| cache.yx = mat.yx(); | |
| cache.yy = mat.yy(); | |
| if (riveRenderPaint->getIsStroked()) | |
| { | |
| m_cachedThickness = riveRenderPaint->getThickness(); | |
| m_cachedJoin = riveRenderPaint->getJoin(); | |
| m_cachedCap = riveRenderPaint->getCap(); | |
| } | |
| m_cachedFeather = riveRenderPaint->getFeather(); | |
| } | |
| gpu::DrawUniquePtr RiveRenderPath::getDrawCache( | |
| const Mat2D& matrix, | |
| const RiveRenderPaint* paint, | |
| FillRule fillRule, | |
| TrivialBlockAllocator* allocator, | |
| const gpu::RenderContext::FrameDescriptor& frameDesc, | |
| gpu::InterlockMode interlockMode) const | |
| { | |
| const CacheElements& cache = | |
| m_cachedElements[paint->getIsStroked() ? CACHE_STROKED : CACHE_FILLED]; | |
| if (cache.draw == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| if (paint->getIsStroked()) | |
| { | |
| if (m_cachedThickness != paint->getThickness()) | |
| { | |
| return nullptr; | |
| } | |
| if (m_cachedJoin != paint->getJoin()) | |
| { | |
| return nullptr; | |
| } | |
| if (m_cachedCap != paint->getCap()) | |
| { | |
| return nullptr; | |
| } | |
| } | |
| if (m_cachedFeather != paint->getFeather()) | |
| { | |
| return nullptr; | |
| } | |
| if (matrix.xx() != cache.xx || matrix.xy() != cache.xy || | |
| matrix.yx() != cache.yx || matrix.yy() != cache.yy) | |
| { | |
| return nullptr; | |
| } | |
| return gpu::DrawUniquePtr( | |
| allocator->make<gpu::RiveRenderPathDraw>(*cache.draw, | |
| matrix.tx(), | |
| matrix.ty(), | |
| ref_rcp(this), | |
| fillRule, | |
| paint, | |
| frameDesc, | |
| interlockMode)); | |
| } | |
| } // namespace rive |