diff --git a/include/tgfx/core/Matrix3D.h b/include/tgfx/core/Matrix3D.h index ae9c14847..d83bac8dc 100644 --- a/include/tgfx/core/Matrix3D.h +++ b/include/tgfx/core/Matrix3D.h @@ -74,6 +74,13 @@ class Matrix3D { */ Vec4 getRow(int i) const; + /** + * Sets the matrix values at the given row. + * @param i Row index, valid range 0..3. + * @param v Vector containing the values to set. + */ + void setRow(int i, const Vec4& v); + /** * Returns the matrix value at the given row and column. * @param r Row index, valid range 0..3. @@ -204,6 +211,11 @@ class Matrix3D { postSkew(kxy, 0, kyx, 0, 0, 0); } + /** + * Concatenates the given matrix with this matrix, and stores the result in this matrix. M' = M * m. + */ + void preConcat(const Matrix3D& m); + /** * Concatenates this matrix with the given matrix, and stores the result in this matrix. M' = M * m. */ @@ -253,9 +265,24 @@ class Matrix3D { /** * Maps a 3D point using this matrix. + * The point is treated as (x, y, z, 1) in homogeneous coordinates. * The returned result is the coordinate after perspective division. */ - Vec3 mapVec3(const Vec3& v) const; + Vec3 mapPoint(const Vec3& point) const; + + /** + * Maps a 3D vector using this matrix. + * The vector is treated as (x, y, z, 0) in homogeneous coordinates, so translation does not + * affect the result. + */ + Vec3 mapVector(const Vec3& vector) const; + + /** + * Maps a 4D homogeneous coordinate (x, y, z, w) using this matrix. + * If the current matrix contains a perspective transformation, the returned Vec4 is not + * perspective-divided; i.e., the w component of the result may not be 1. + */ + Vec4 mapHomogeneous(float x, float y, float z, float w) const; /** * Returns true if the matrix is an identity matrix. @@ -293,11 +320,6 @@ class Matrix3D { */ void setConcat(const Matrix3D& a, const Matrix3D& b); - /** - * Concatenates the given matrix with this matrix, and stores the result in this matrix. M' = M * m. - */ - void preConcat(const Matrix3D& m); - /** * Pre-concatenates a scale to this matrix. M' = M * S. */ @@ -308,34 +330,25 @@ class Matrix3D { */ Matrix3D transpose() const; - /** - * Maps a 4D point (x, y, z, w) using this matrix. - * If the current matrix contains a perspective transformation, the returned Vec4 is not - * perspective-divided; i.e., the w component of the result may not be 1. - */ - Vec4 mapPoint(float x, float y, float z, float w) const; - Vec4 getCol(int i) const { Vec4 v; memcpy(&v, values + i * 4, sizeof(Vec4)); return v; } - void setAll(float m00, float m01, float m02, float m03, float m10, float m11, float m12, - float m13, float m20, float m21, float m22, float m23, float m30, float m31, - float m32, float m33); - - void setRow(int i, const Vec4& v) { - values[i + 0] = v.x; - values[i + 4] = v.y; - values[i + 8] = v.z; - values[i + 12] = v.w; - } - + /** + * Sets the matrix values at the given column. + * @param i Column index, valid range 0..3. + * @param v Vector containing the values to set. + */ void setColumn(int i, const Vec4& v) { memcpy(&values[i * 4], v.ptr(), sizeof(v)); } + void setAll(float m00, float m01, float m02, float m03, float m10, float m11, float m12, + float m13, float m20, float m21, float m22, float m23, float m30, float m31, + float m32, float m33); + void setIdentity() { *this = Matrix3D(); } @@ -353,7 +366,7 @@ class Matrix3D { } Vec4 operator*(const Vec4& v) const { - return this->mapPoint(v.x, v.y, v.z, v.w); + return this->mapHomogeneous(v.x, v.y, v.z, v.w); } float values[16] = {.0f}; diff --git a/include/tgfx/core/Vec.h b/include/tgfx/core/Vec.h index 7ee8973c5..4eae2704a 100644 --- a/include/tgfx/core/Vec.h +++ b/include/tgfx/core/Vec.h @@ -172,6 +172,13 @@ struct Vec3 { return sqrtf(Dot(*this, *this)); } + /** + * Returns the squared length of the vector. + */ + float lengthSquared() const { + return Dot(*this, *this); + } + /** * Returns a pointer to the vector's immutable data. */ diff --git a/include/tgfx/layers/Layer.h b/include/tgfx/layers/Layer.h index 33fb33514..adb2617d2 100644 --- a/include/tgfx/layers/Layer.h +++ b/include/tgfx/layers/Layer.h @@ -37,6 +37,7 @@ class DisplayList; class DrawArgs; class RegionTransformer; class RootLayer; +class Render3DContext; struct LayerStyleSource; struct MaskData; class BackgroundContext; @@ -125,6 +126,8 @@ class Layer : public std::enable_shared_from_this { /** * Sets the blend mode of the layer. + * Note: Layers inside a 3D Rendering Context (see preserve3D()) always use SrcOver blend mode + * regardless of this setting. */ void setBlendMode(BlendMode value); @@ -139,6 +142,8 @@ class Layer : public std::enable_shared_from_this { /** * Sets whether the layer passes through its background to sublayers. + * Note: Layers that can start or extend a 3D Rendering Context (see preserve3D()) always disable + * pass-through background regardless of this setting. */ void setPassThroughBackground(bool value); @@ -178,6 +183,39 @@ class Layer : public std::enable_shared_from_this { */ void setMatrix3D(const Matrix3D& value); + /** + * Returns whether the layer preserves the 3D state of its content and child layers. The default + * value is false. + * + * When false, content and child layers are projected onto the layer's local space. Child layers + * are drawn in the order they were added, so later-added opaque layers completely cover earlier + * ones. + * + * When true, 3D rendering is enabled. If the parent layer has preserve3D disabled, this layer + * establishes a new 3D Rendering Context. If the parent also has preserve3D enabled, this layer + * inherits and extends the parent's context. + * + * Within a 3D Rendering Context, all child layers share the coordinate space of the context + * root's parent (or the DisplayList if no parent exists). Depth occlusion is applied based on + * actual 3D positions: opaque pixels closer to the observer occlude those farther away at the + * same xy coordinates. + * + * Note: preserve3D falls back to false behavior when any of the following conditions are met: + * 1. Layer styles is not empty. + * 2. Filters is not empty. + * 3. Mask is not empty. + * These features require projecting child layers into the current layer's local coordinate + * system, which is incompatible with 3D context preservation. + */ + bool preserve3D() const { + return _preserve3D; + } + + /** + * Sets whether the layer preserves the 3D state of its content and child layers. + */ + void setPreserve3D(bool value); + /** * Returns whether the layer is visible. The default value is true. */ @@ -217,6 +255,8 @@ class Layer : public std::enable_shared_from_this { /** * Sets whether the layer is allowed to be composited as a separate group from their parent. + * Note: Layers inside a 3D Rendering Context (see preserve3D()) always apply alpha individually + * to each element regardless of this setting. */ void setAllowsGroupOpacity(bool value); @@ -233,6 +273,14 @@ class Layer : public std::enable_shared_from_this { /** * Sets the list of layer styles applied to the layer. + * Note: Background-dependent layer styles (e.g., BackgroundBlurStyle) have the following + * limitations: + * 1. Layers that start a 3D Rendering Context (see preserve3D()) disable background styles for + * the entire subtree rooted at that layer. The 3D Rendering Context uses a different rendering + * strategy that has not yet fully adapted background layer drawing. + * 2. Layers with a 3D or projection transformation disable background styles for all descendant + * layers (excluding the layer itself), because descendants cannot correctly obtain the + * background. */ void setLayerStyles(const std::vector>& value); @@ -577,17 +625,22 @@ class Layer : public std::enable_shared_from_this { void drawDirectly(const DrawArgs& args, Canvas* canvas, float alpha); - void drawDirectly(const DrawArgs& args, Canvas* canvas, float alpha, - const std::vector& styleExtraSourceTypes); - void drawContents(const DrawArgs& args, Canvas* canvas, float alpha, const LayerStyleSource* layerStyleSource = nullptr, - const Layer* stopChild = nullptr, - const std::vector& styleExtraSourceTypes = {}); + const Layer* stopChild = nullptr); bool drawChildren(const DrawArgs& args, Canvas* canvas, float alpha, const Layer* stopChild = nullptr); + void drawByStarting3DContext(const DrawArgs& args, Canvas* canvas); + + std::optional createChildArgs(const DrawArgs& args, Canvas* canvas, Layer* child, + bool skipBackground, int childIndex, + int lastBackgroundIndex); + + void drawChild(const DrawArgs& args, Canvas* canvas, Layer* child, float alpha, + const Matrix3D& transform3D, Render3DContext* context3D, bool started3DContext); + float drawBackgroundLayers(const DrawArgs& args, Canvas* canvas); std::unique_ptr getLayerStyleSource(const DrawArgs& args, const Matrix& matrix, @@ -598,7 +651,7 @@ class Layer : public std::enable_shared_from_this { /** * Gets the background image of the minimum axis-aligned bounding box after drawing the layer - * subtree with the current layer as the root node. + * subtree with the current layer as the root node */ std::shared_ptr getBoundsBackgroundImage(const DrawArgs& args, float contentScale, Point* offset); @@ -608,12 +661,8 @@ class Layer : public std::enable_shared_from_this { void drawLayerStyles(const DrawArgs& args, Canvas* canvas, float alpha, const LayerStyleSource* source, LayerStylePosition position); - void drawLayerStyles(const DrawArgs& args, Canvas* canvas, float alpha, - const LayerStyleSource* source, LayerStylePosition position, - const std::vector& styleExtraSourceTypes); - void drawBackgroundLayerStyles(const DrawArgs& args, Canvas* canvas, float alpha, - const Matrix3D& transform); + const Matrix3D& transform3D); bool getLayersUnderPointInternal(float x, float y, std::vector>* results); @@ -657,27 +706,18 @@ class Layer : public std::enable_shared_from_this { const Matrix3D* transform3D, const std::shared_ptr& maskFilter); - std::shared_ptr getContentImage( - const DrawArgs& args, const Matrix& contentMatrix, const std::optional& clipBounds, - const std::vector& extraSourceTypes, Matrix* imageMatrix); + std::shared_ptr getContentImage(const DrawArgs& args, const Matrix& contentMatrix, + const std::optional& clipBounds, + Matrix* imageMatrix); - std::shared_ptr getPassThroughContentImage( - const DrawArgs& args, Canvas* canvas, const std::optional& clipBounds, - const std::vector& extraSourceTypes, Matrix* imageMatrix); + std::shared_ptr getPassThroughContentImage(const DrawArgs& args, Canvas* canvas, + const std::optional& clipBounds, + Matrix* imageMatrix); std::optional computeContentBounds(const std::optional& clipBounds, bool excludeEffects); - /** - * Returns the equivalent transformation matrix adapted for a custom anchor point. - * The matrix is defined based on a local coordinate system, with the transformation anchor point - * being the origin of that coordinate system. This function returns an affine transformation - * matrix that produces the same visual effect when using any point within this coordinate system - * as the new origin and anchor point. - * @param matrix The original transformation matrix. - * @param anchor The specified anchor point. - */ - Matrix3D anchorAdaptedMatrix(const Matrix3D& matrix, const Point& anchor) const; + bool canPreserve3D() const; void invalidateSubtree(); @@ -703,6 +743,7 @@ class Layer : public std::enable_shared_from_this { float _alpha = 1.0f; // The actual transformation matrix that determines the geometric position of the layer Matrix3D _matrix3D = {}; + bool _preserve3D = false; std::shared_ptr _mask = nullptr; Layer* maskOwner = nullptr; std::unique_ptr _scrollRect = nullptr; diff --git a/include/tgfx/layers/filters/LayerFilter.h b/include/tgfx/layers/filters/LayerFilter.h index ea14b3251..41c86dfe6 100644 --- a/include/tgfx/layers/filters/LayerFilter.h +++ b/include/tgfx/layers/filters/LayerFilter.h @@ -53,8 +53,7 @@ class LayerFilter : public LayerProperty { BlurFilter, ColorMatrixFilter, DropShadowFilter, - InnerShadowFilter, - Transform3DFilter + InnerShadowFilter }; virtual Type type() const { diff --git a/src/core/Matrix3D.cpp b/src/core/Matrix3D.cpp index c22bbdff8..eacfb8b21 100644 --- a/src/core/Matrix3D.cpp +++ b/src/core/Matrix3D.cpp @@ -206,6 +206,14 @@ Vec4 Matrix3D::getRow(int i) const { return {values[i], values[i + 4], values[i + 8], values[i + 12]}; } +void Matrix3D::setRow(int i, const Vec4& v) { + DEBUG_ASSERT(i >= 0 && i < 4); + values[i] = v.x; + values[i + 4] = v.y; + values[i + 8] = v.z; + values[i + 12] = v.w; +} + float Matrix3D::getTranslateX() const { return values[TRANS_X]; } @@ -247,9 +255,11 @@ void Matrix3D::preTranslate(float tx, float ty, float tz) { } void Matrix3D::postTranslate(float tx, float ty, float tz) { - values[12] += tx; - values[13] += ty; - values[14] += tz; + Vec4 t = {tx, ty, tz, 0}; + setColumn(0, getCol(0) + t * values[3]); + setColumn(1, getCol(1) + t * values[7]); + setColumn(2, getCol(2) + t * values[11]); + setColumn(3, getCol(3) + t * values[15]); } void Matrix3D::postSkew(float kxy, float kxz, float kyx, float kyz, float kzx, float kzy) { @@ -258,6 +268,10 @@ void Matrix3D::postSkew(float kxy, float kxz, float kyx, float kyz, float kzx, f postConcat(m); } +void Matrix3D::preConcat(const Matrix3D& m) { + setConcat(*this, m); +} + void Matrix3D::postConcat(const Matrix3D& m) { setConcat(m, *this); } @@ -293,7 +307,9 @@ Matrix3D Matrix3D::Perspective(float fovyDegress, float aspect, float nearZ, flo } Rect Matrix3D::mapRect(const Rect& src) const { - if (hasPerspective()) { + if (isIdentity()) { + return src; + } else if (hasPerspective()) { return MapRectPerspective(src, values); } else { return MapRectAffine(src, values); @@ -307,11 +323,26 @@ void Matrix3D::mapRect(Rect* rect) const { *rect = mapRect(*rect); } -Vec3 Matrix3D::mapVec3(const Vec3& v) const { - auto r = this->mapPoint(v.x, v.y, v.z, 1.f); +Vec3 Matrix3D::mapPoint(const Vec3& point) const { + auto r = mapHomogeneous(point.x, point.y, point.z, 1.f); return {IEEEFloatDivide(r.x, r.w), IEEEFloatDivide(r.y, r.w), IEEEFloatDivide(r.z, r.w)}; } +Vec3 Matrix3D::mapVector(const Vec3& vector) const { + auto r = mapHomogeneous(vector.x, vector.y, vector.z, 0.f); + return {r.x, r.y, r.z}; +} + +Vec4 Matrix3D::mapHomogeneous(float x, float y, float z, float w) const { + auto c0 = getCol(0); + auto c1 = getCol(1); + auto c2 = getCol(2); + auto c3 = getCol(3); + + const Vec4 result = (c0 * x + c1 * y + c2 * z + c3 * w); + return result; +} + bool Matrix3D::operator==(const Matrix3D& other) const { if (this == &other) { return true; @@ -353,10 +384,6 @@ void Matrix3D::setConcat(const Matrix3D& a, const Matrix3D& b) { setColumn(3, m3); } -void Matrix3D::preConcat(const Matrix3D& m) { - setConcat(*this, m); -} - void Matrix3D::preScale(float sx, float sy, float sz) { if (sx == 1 && sy == 1 && sz == 1) { return; @@ -377,16 +404,6 @@ Matrix3D Matrix3D::transpose() const { return m; } -Vec4 Matrix3D::mapPoint(float x, float y, float z, float w) const { - auto c0 = getCol(0); - auto c1 = getCol(1); - auto c2 = getCol(2); - auto c3 = getCol(3); - - const Vec4 result = (c0 * x + c1 * y + c2 * z + c3 * w); - return result; -} - void Matrix3D::setAll(float m00, float m01, float m02, float m03, float m10, float m11, float m12, float m13, float m20, float m21, float m22, float m23, float m30, float m31, float m32, float m33) { diff --git a/src/core/Matrix3DUtils.cpp b/src/core/Matrix3DUtils.cpp new file mode 100644 index 000000000..48bc7bf3d --- /dev/null +++ b/src/core/Matrix3DUtils.cpp @@ -0,0 +1,73 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "core/Matrix3DUtils.h" +#include "core/Matrix2D.h" +#include "utils/MathExtra.h" + +namespace tgfx { + +bool Matrix3DUtils::IsRectBehindCamera(const Rect& rect, const Matrix3D& matrix) { + return matrix.mapHomogeneous(rect.left, rect.top, 0, 1).w <= 0 || + matrix.mapHomogeneous(rect.left, rect.bottom, 0, 1).w <= 0 || + matrix.mapHomogeneous(rect.right, rect.top, 0, 1).w <= 0 || + matrix.mapHomogeneous(rect.right, rect.bottom, 0, 1).w <= 0; +} + +Matrix3D Matrix3DUtils::OriginAdaptedMatrix3D(const Matrix3D& matrix3D, const Point& newOrigin) { + auto offsetMatrix = Matrix3D::MakeTranslate(newOrigin.x, newOrigin.y, 0); + auto invOffsetMatrix = Matrix3D::MakeTranslate(-newOrigin.x, -newOrigin.y, 0); + return invOffsetMatrix * matrix3D * offsetMatrix; +} + +bool Matrix3DUtils::IsMatrix3DAffine(const Matrix3D& matrix) { + return FloatNearlyZero(matrix.getRowColumn(0, 2)) && FloatNearlyZero(matrix.getRowColumn(1, 2)) && + matrix.getRow(2) == Vec4(0, 0, 1, 0) && matrix.getRow(3) == Vec4(0, 0, 0, 1); +} + +Matrix Matrix3DUtils::GetMayLossyAffineMatrix(const Matrix3D& matrix) { + auto affineMatrix = Matrix::I(); + affineMatrix.setAll(matrix.getRowColumn(0, 0), matrix.getRowColumn(0, 1), + matrix.getRowColumn(0, 3), matrix.getRowColumn(1, 0), + matrix.getRowColumn(1, 1), matrix.getRowColumn(1, 3)); + return affineMatrix; +} + +Rect Matrix3DUtils::InverseMapRect(const Rect& rect, const Matrix3D& matrix) { + float values[16] = {}; + matrix.getColumnMajor(values); + auto matrix2D = Matrix2D::MakeAll(values[0], values[1], values[3], values[4], values[5], + values[7], values[12], values[13], values[15]); + Matrix2D inversedMatrix; + if (!matrix2D.invert(&inversedMatrix)) { + return Rect::MakeEmpty(); + } + return inversedMatrix.mapRect(rect); +} + +Matrix3D Matrix3DUtils::ScaleAdaptedMatrix3D(const Matrix3D& matrix, float scale) { + if (FloatNearlyEqual(scale, 1.0f)) { + return matrix; + } + auto invScale = 1.0f / scale; + auto invScaleMatrix = Matrix3D::MakeScale(invScale, invScale, 1.0f); + auto scaleMatrix = Matrix3D::MakeScale(scale, scale, 1.0f); + return scaleMatrix * matrix * invScaleMatrix; +} + +} // namespace tgfx diff --git a/src/core/Matrix3DUtils.h b/src/core/Matrix3DUtils.h new file mode 100644 index 000000000..3dd1db50f --- /dev/null +++ b/src/core/Matrix3DUtils.h @@ -0,0 +1,79 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "tgfx/core/Matrix.h" +#include "tgfx/core/Matrix3D.h" +#include "tgfx/core/Rect.h" + +namespace tgfx { + +class Matrix3DUtils { + public: + /** + * Checks if any vertex of the rect is behind the camera after applying the 3D transformation. + * A vertex is considered behind the camera when w <= 0, where w = 0 means the vertex is at the + * camera plane (infinitely far), which is also treated as behind. + * @param rect The rect in local coordinate system to be checked. + * @param matrix The 3D transformation matrix containing camera and projection information. + */ + static bool IsRectBehindCamera(const Rect& rect, const Matrix3D& matrix); + + /** + * Returns an adapted transformation matrix for a new coordinate system established at the + * specified point. The original matrix defines a transformation in a coordinate system with the + * origin (0, 0) as the anchor point. When establishing a new coordinate system at an arbitrary + * point within this space, this function computes the equivalent matrix that produces the same + * visual transformation effect in the new coordinate system. + * @param matrix3D The original transformation matrix defined with the origin (0, 0) as the anchor + * point. + * @param newOrigin The point at which to establish the new coordinate system as its origin. + */ + static Matrix3D OriginAdaptedMatrix3D(const Matrix3D& matrix3D, const Point& newOrigin); + + /** + * Determines if the 4x4 matrix contains only 2D affine transformations, i.e., no Z-axis related + * transformations or projection transformations. + */ + static bool IsMatrix3DAffine(const Matrix3D& matrix); + + /** + * When a 4x4 matrix does not contain Z-axis related transformations and projection + * transformations, this function returns an equivalent 2D affine transformation. Otherwise, the + * return value will lose information about Z-axis related transformations and projection + * transformations. + */ + static Matrix GetMayLossyAffineMatrix(const Matrix3D& matrix); + + /** + * Inverse maps a rect through the matrix. Returns an empty rect if the matrix is not invertible. + */ + static Rect InverseMapRect(const Rect& rect, const Matrix3D& matrix); + + /** + * Adjusts a 3D transformation matrix so that the projection result can be correctly scaled. + * This ensures the visual effect of "project first, then scale" rather than "scale first, then + * project", which would cause incorrect perspective effects. + * @param matrix The original 3D transformation matrix. + * @param scale The scale factor to apply to the projection result. + */ + static Matrix3D ScaleAdaptedMatrix3D(const Matrix3D& matrix, float scale); +}; + +} // namespace tgfx diff --git a/src/core/filters/Transform3DImageFilter.cpp b/src/core/filters/Transform3DImageFilter.cpp index 20b96c9fa..22cd83295 100644 --- a/src/core/filters/Transform3DImageFilter.cpp +++ b/src/core/filters/Transform3DImageFilter.cpp @@ -18,11 +18,13 @@ #include "Transform3DImageFilter.h" #include "core/Matrix2D.h" +#include "core/Matrix3DUtils.h" #include "core/utils/MathExtra.h" #include "core/utils/PlacementPtr.h" #include "gpu/DrawingManager.h" +#include "gpu/QuadsVertexProvider.h" #include "gpu/TPArgs.h" -#include "gpu/ops/Rect3DDrawOp.h" +#include "gpu/ops/Quads3DDrawOp.h" #include "gpu/processors/TextureEffect.h" #include "gpu/proxies/RenderTargetProxy.h" #include "tgfx/core/Matrix3D.h" @@ -38,9 +40,19 @@ Transform3DImageFilter::Transform3DImageFilter(const Matrix3D& matrix, bool hide } Rect Transform3DImageFilter::onFilterBounds(const Rect& rect, MapDirection mapDirection) const { + if (_matrix.isIdentity()) { + return rect; + } + + // Adapt the matrix to keep the z-component of vertex coordinates unchanged. + auto drawMatrix = _matrix; + drawMatrix.setRow(2, {0, 0, 1, 0}); + if (Matrix3DUtils::IsRectBehindCamera(rect, _matrix)) { + return Rect::MakeEmpty(); + } + if (mapDirection == MapDirection::Forward) { - auto result = _matrix.mapRect(rect); - return result; + return drawMatrix.mapRect(rect); } // All vertices inside the rect have an initial z-coordinate of 0, so the third column of the 4x4 @@ -48,13 +60,14 @@ Rect Transform3DImageFilter::onFilterBounds(const Rect& rect, MapDirection mapDi // we do not care about the final projected z-axis coordinate, the third row can also be ignored. // Therefore, the 4x4 matrix can be simplified to a 3x3 matrix. float values[16] = {}; - _matrix.getColumnMajor(values); + drawMatrix.getColumnMajor(values); auto matrix2D = Matrix2D::MakeAll(values[0], values[1], values[3], values[4], values[5], values[7], values[12], values[13], values[15]); Matrix2D inversedMatrix; if (!matrix2D.invert(&inversedMatrix)) { - DEBUG_ASSERT(false); - return rect; + // The matrix is singular, meaning the 2D plane projects to a line or point (e.g., rotating 90 + // degrees around the X-axis). In this case, there is no visible content to draw. + return Rect::MakeEmpty(); } auto result = inversedMatrix.mapRect(rect); return result; @@ -62,6 +75,17 @@ Rect Transform3DImageFilter::onFilterBounds(const Rect& rect, MapDirection mapDi std::shared_ptr Transform3DImageFilter::lockTextureProxy( std::shared_ptr source, const Rect& renderBounds, const TPArgs& args) const { + auto srcW = static_cast(source->width()); + auto srcH = static_cast(source->height()); + auto srcModelRect = Rect::MakeXYWH(0.f, 0.f, srcW, srcH); + if (Matrix3DUtils::IsRectBehindCamera(srcModelRect, _matrix)) { + return nullptr; + } + + // Adapt the matrix to keep the z-component of vertex coordinates unchanged, preventing rendering + // artifacts caused by rotated image fragments failing the depth test. + auto drawMatrix = _matrix; + drawMatrix.setRow(2, {0, 0, 1, 0}); float dstDrawWidth = renderBounds.width(); float dstDrawHeight = renderBounds.height(); DEBUG_ASSERT(args.drawScale > 0.f); @@ -77,14 +101,11 @@ std::shared_ptr Transform3DImageFilter::lockTextureProxy( source->isAlphaOnly(), 1, args.mipmapped, ImageOrigin::TopLeft, args.backingFit); auto sourceTextureProxy = source->lockTextureProxy(args); - auto srcW = static_cast(source->width()); - auto srcH = static_cast(source->height()); // The default transformation anchor is at the top-left origin (0,0) of the image; user-defined // anchors are included in the matrix. - auto srcModelRect = Rect::MakeXYWH(0.f, 0.f, srcW, srcH); // SrcProjectRect is the result of projecting srcRect onto the canvas. RenderBounds describes a // subregion that needs to be drawn within it. - auto srcProjectRect = _matrix.mapRect(srcModelRect); + auto srcProjectRect = drawMatrix.mapRect(srcModelRect); // ndcScale and ndcOffset are used to scale and translate the NDC coordinates to ensure that only // the content within RenderBounds is drawn to the render target. This clips regions beyond the // clip space. @@ -105,12 +126,13 @@ std::shared_ptr Transform3DImageFilter::lockTextureProxy( auto drawingManager = args.context->drawingManager(); auto allocator = args.context->drawingAllocator(); - auto vertexProvider = RectsVertexProvider::MakeFrom(allocator, srcModelRect, AAType::Coverage); + auto vertexProvider = QuadsVertexProvider::MakeFrom(allocator, srcModelRect, AAType::Coverage); + const Size viewportSize(static_cast(renderTarget->width()), static_cast(renderTarget->height())); - const Rect3DDrawArgs drawArgs{_matrix, ndcScale, ndcOffset, viewportSize}; + const Quads3DDrawArgs drawArgs{drawMatrix, ndcScale, ndcOffset, viewportSize}; auto drawOp = - Rect3DDrawOp::Make(args.context, std::move(vertexProvider), args.renderFlags, drawArgs); + Quads3DDrawOp::Make(args.context, std::move(vertexProvider), args.renderFlags, drawArgs); const SamplingArgs samplingArgs = {TileMode::Decal, TileMode::Decal, {}, SrcRectConstraint::Fast}; // Ensure the vertex texture sampling coordinates are in the range [0, 1] DEBUG_ASSERT(srcW > 0 && srcH > 0); diff --git a/src/gpu/QuadCW.h b/src/gpu/QuadCW.h new file mode 100644 index 000000000..4d96fecc9 --- /dev/null +++ b/src/gpu/QuadCW.h @@ -0,0 +1,59 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/utils/Log.h" +#include "tgfx/core/Point.h" + +namespace tgfx { + +/** + * QuadCW represents a quadrilateral with vertices in clockwise order. + * Used for logical quad operations where edge connectivity matters. + * + * Vertex layout (clockwise): + * p0 -----> p1 + * ^ | + * | v + * p3 <----- p2 + * + * Edge definitions (matching QUAD_AA_FLAG_EDGE_*): + * EDGE_01: p0 -> p1 + * EDGE_12: p1 -> p2 + * EDGE_23: p2 -> p3 + * EDGE_30: p3 -> p0 + */ +class QuadCW { + public: + constexpr QuadCW() = default; + + constexpr QuadCW(const Point& p0, const Point& p1, const Point& p2, const Point& p3) + : points{p0, p1, p2, p3} { + } + + const Point& point(size_t i) const { + DEBUG_ASSERT(i < 4); + return points[i]; + } + + private: + Point points[4] = {}; +}; + +} // namespace tgfx diff --git a/src/gpu/QuadRecord.h b/src/gpu/QuadRecord.h new file mode 100644 index 000000000..d72c8eeee --- /dev/null +++ b/src/gpu/QuadRecord.h @@ -0,0 +1,63 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "gpu/QuadCW.h" +#include "tgfx/core/Color.h" + +namespace tgfx { + +/** + * AA flags for each edge of a quad. Edge is defined by vertex indices in clockwise order. + */ +static constexpr unsigned QUAD_AA_FLAG_EDGE_01 = 0b0001; // Edge from vertex 0 to vertex 1 +static constexpr unsigned QUAD_AA_FLAG_EDGE_12 = 0b0010; // Edge from vertex 1 to vertex 2 +static constexpr unsigned QUAD_AA_FLAG_EDGE_23 = 0b0100; // Edge from vertex 2 to vertex 3 +static constexpr unsigned QUAD_AA_FLAG_EDGE_30 = 0b1000; // Edge from vertex 3 to vertex 0 +static constexpr unsigned QUAD_AA_FLAG_NONE = 0b0000; +static constexpr unsigned QUAD_AA_FLAG_ALL = 0b1111; + +/** + * QuadRecord stores a CW-ordered quad with per-edge AA flags for rendering. + * Vertices are in clockwise order. For triangles, point(2) == point(3). + */ +struct QuadRecord { + QuadRecord() = default; + + QuadRecord(const QuadCW& quad, unsigned aaFlags, const Color& color = {}) + : quad(quad), aaFlags(aaFlags), color(color) { + } + + /** + * Four vertices in clockwise order. + */ + QuadCW quad; + + /** + * Per-edge AA flags. + */ + unsigned aaFlags = QUAD_AA_FLAG_NONE; + + /** + * Vertex color for blending. + */ + Color color = {}; +}; + +} // namespace tgfx diff --git a/src/gpu/QuadsVertexProvider.cpp b/src/gpu/QuadsVertexProvider.cpp new file mode 100644 index 000000000..4f666602b --- /dev/null +++ b/src/gpu/QuadsVertexProvider.cpp @@ -0,0 +1,232 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "QuadsVertexProvider.h" +#include +#include "core/utils/ColorHelper.h" + +namespace tgfx { + +// CW to Z-order mapping for GPU triangle strip. +// CW order: 0=top-left, 1=top-right, 2=bottom-right, 3=bottom-left +// Z-order: 0=left-top, 1=left-bottom, 2=right-top, 3=right-bottom +// Mapping: Z[0]=CW[0], Z[1]=CW[3], Z[2]=CW[1], Z[3]=CW[2] +static constexpr size_t CW_TO_Z_ORDER[] = {0, 3, 1, 2}; + +// Maps edge index (0-3) to corresponding AA flag. +static constexpr unsigned EDGE_AA_FLAGS[] = { + QUAD_AA_FLAG_EDGE_01, // Edge 0: v0 -> v1 + QUAD_AA_FLAG_EDGE_12, // Edge 1: v1 -> v2 + QUAD_AA_FLAG_EDGE_23, // Edge 2: v2 -> v3 + QUAD_AA_FLAG_EDGE_30 // Edge 3: v3 -> v0 +}; + +/** + * Writes a quad's vertices to the output buffer in Z-order. + * @param totalVerts Output buffer for all vertex data. + * @param globalIndex Current write position in totalVerts, updated after writing. + * @param quadVerts The 4 vertices of the quad in CW order. + * @param coverage Coverage value for AA (1.0 for inner, 0.0 for outer). + * @param hasColor Whether to write color data. + * @param compressedColor Compressed color value to write if hasColor is true. + */ +static void WriteQuadVertices(float* totalVerts, size_t& globalIndex, const Point* quadVerts, + float coverage, bool hasColor, float compressedColor) { + for (int i = 0; i < 4; ++i) { + const Point& p = quadVerts[CW_TO_Z_ORDER[i]]; + totalVerts[globalIndex++] = p.x; + totalVerts[globalIndex++] = p.y; + totalVerts[globalIndex++] = coverage; + if (hasColor) { + totalVerts[globalIndex++] = compressedColor; + } + } +} + +/** + * NonAAQuadsVertexProvider generates vertices for non-AA quad rendering. + */ +class NonAAQuadsVertexProvider : public QuadsVertexProvider { + public: + NonAAQuadsVertexProvider(PlacementArray&& quads, AAType aaType, bool hasColor, + std::shared_ptr reference) + : QuadsVertexProvider(std::move(quads), aaType, hasColor, std::move(reference)) { + } + + size_t vertexCount() const override { + // Each quad = 4 vertices, vertex data: position(2) + [color(1)] + const size_t perVertexCount = _hasColor ? 3 : 2; + return quads.size() * 4 * perVertexCount; + } + + void getVertices(float* vertices) const override { + size_t index = 0; + for (size_t i = 0; i < quads.size(); ++i) { + auto& record = quads[i]; + float compressedColor = 0.f; + if (_hasColor) { + const uint32_t uintColor = ToUintPMColor(record->color, nullptr); + compressedColor = *reinterpret_cast(&uintColor); + } + // Write 4 vertices in Z-order to match index buffer layout. + for (size_t j = 0; j < 4; ++j) { + const Point& p = record->quad.point(CW_TO_Z_ORDER[j]); + vertices[index++] = p.x; + vertices[index++] = p.y; + if (_hasColor) { + vertices[index++] = compressedColor; + } + } + } + } +}; + +/** + * AAQuadsVertexProvider generates vertices for per-edge AA quad rendering. + */ +class AAQuadsVertexProvider : public QuadsVertexProvider { + public: + AAQuadsVertexProvider(PlacementArray&& quads, AAType aaType, bool hasColor, + std::shared_ptr reference) + : QuadsVertexProvider(std::move(quads), aaType, hasColor, std::move(reference)) { + } + + size_t vertexCount() const override { + // Each AA quad = 8 vertices (4 inner + 4 outer) + // Vertex data: position(2) + coverage(1) + [color(1)] + const size_t perVertexCount = _hasColor ? 4 : 3; + return quads.size() * 8 * perVertexCount; + } + + void getVertices(float* vertices) const override { + size_t index = 0; + for (size_t i = 0; i < quads.size(); ++i) { + auto& record = quads[i]; + float compressedColor = 0.f; + if (_hasColor) { + const uint32_t uintColor = ToUintPMColor(record->color, nullptr); + compressedColor = *reinterpret_cast(&uintColor); + } + writeAAQuadVertices(vertices, index, *record, compressedColor); + } + } + + private: + void writeAAQuadVertices(float* vertices, size_t& index, const QuadRecord& record, + float compressedColor) const { + const QuadCW& quad = record.quad; + // Compute inward normals for each edge (perpendicular to edge, pointing inward for CW winding) + Point normals[4]; + for (size_t i = 0; i < 4; ++i) { + const size_t next = (i + 1) % 4; + const Point edge = quad.point(next) - quad.point(i); + const float len = std::sqrt(edge.x * edge.x + edge.y * edge.y); + if (len > 0) { + normals[i] = Point::Make(-edge.y / len, edge.x / len); + } else { + normals[i] = Point::Make(0, 0); + } + } + + // AA offset distance for vertex expansion/contraction + constexpr float kAAOffset = 0.5f; + Point insetVertices[4]; + Point outsetVertices[4]; + for (size_t i = 0; i < 4; ++i) { + // Each vertex is affected by two edges (the one ending at it and the one starting from it) + const size_t prevEdge = (i + 3) % 4; // Edge ending at this vertex + const size_t nextEdge = i; // Edge starting at this vertex + const bool prevNeedsAA = (record.aaFlags & EDGE_AA_FLAGS[prevEdge]) != 0; + const bool nextNeedsAA = (record.aaFlags & EDGE_AA_FLAGS[nextEdge]) != 0; + // Compute offset direction based on which edges need AA: + // - Both edges AA: use bisector (sum of two normals) + // - Only one edge AA: use that edge's normal only + // - No edges AA: no offset + Point offsetDir = {}; + if (prevNeedsAA && nextNeedsAA) { + offsetDir = normals[prevEdge] + normals[nextEdge]; + } else if (prevNeedsAA) { + offsetDir = normals[prevEdge]; + } else if (nextNeedsAA) { + offsetDir = normals[nextEdge]; + } + const float offsetLen = std::sqrt(offsetDir.x * offsetDir.x + offsetDir.y * offsetDir.y); + if (offsetLen > 0) { + offsetDir.x /= offsetLen; + offsetDir.y /= offsetLen; + } + + // Note: For acute angles, the geometrically correct miter point would be farther than + // kAAOffset from the corner (distance = kAAOffset / cos(theta/2)). However, we intentionally + // use a fixed offset distance because: + // 1. Coverage is interpolated between vertices, so exact miter geometry isn't required for AA. + // 2. True miter points can extend infinitely at very acute angles, causing numerical issues. + // 3. Simpler calculation with better performance. + const Point corner = quad.point(i); + insetVertices[i] = corner + offsetDir * kAAOffset; + outsetVertices[i] = corner - offsetDir * kAAOffset; + } + + // Write inner quad (coverage = 1.0) and outer quad (coverage = 0.0) in Z-order + WriteQuadVertices(vertices, index, insetVertices, 1.0f, _hasColor, compressedColor); + WriteQuadVertices(vertices, index, outsetVertices, 0.0f, _hasColor, compressedColor); + } +}; + +PlacementPtr QuadsVertexProvider::MakeFrom(BlockAllocator* allocator, + const Rect& rect, AAType aaType, + const Color& color) { + QuadCW quad(Point::Make(rect.left, rect.top), Point::Make(rect.right, rect.top), + Point::Make(rect.right, rect.bottom), Point::Make(rect.left, rect.bottom)); + auto record = allocator->make(quad, QUAD_AA_FLAG_ALL, color); + std::vector> quads; + quads.push_back(std::move(record)); + return MakeFrom(allocator, std::move(quads), aaType); +} + +PlacementPtr QuadsVertexProvider::MakeFrom( + BlockAllocator* allocator, std::vector>&& quads, AAType aaType) { + if (quads.empty()) { + return nullptr; + } + bool hasColor = false; + if (quads.size() > 1) { + auto& firstColor = quads.front()->color; + for (auto& record : quads) { + if (record->color != firstColor) { + hasColor = true; + break; + } + } + } + auto quadArray = allocator->makeArray(std::move(quads)); + if (aaType == AAType::Coverage) { + return allocator->make(std::move(quadArray), aaType, hasColor, + allocator->addReference()); + } + return allocator->make(std::move(quadArray), aaType, hasColor, + allocator->addReference()); +} + +QuadsVertexProvider::QuadsVertexProvider(PlacementArray&& quads, AAType aaType, + bool hasColor, std::shared_ptr reference) + : VertexProvider(std::move(reference)), quads(std::move(quads)), _aaType(aaType), + _hasColor(hasColor) { +} + +} // namespace tgfx diff --git a/src/gpu/QuadsVertexProvider.h b/src/gpu/QuadsVertexProvider.h new file mode 100644 index 000000000..1378a64bf --- /dev/null +++ b/src/gpu/QuadsVertexProvider.h @@ -0,0 +1,84 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/utils/BlockAllocator.h" +#include "gpu/AAType.h" +#include "gpu/QuadRecord.h" +#include "gpu/VertexProvider.h" +#include "tgfx/core/Rect.h" + +namespace tgfx { + +/** + * QuadsVertexProvider provides vertex data for rendering a batch of 3D quads with per-edge AA. + */ +class QuadsVertexProvider : public VertexProvider { + public: + /** + * Creates a QuadsVertexProvider from a single rect with all edges marked for AA. + */ + static PlacementPtr MakeFrom(BlockAllocator* allocator, const Rect& rect, + AAType aaType, const Color& color = {}); + + /** + * Creates a QuadsVertexProvider from a list of quad records. + */ + static PlacementPtr MakeFrom(BlockAllocator* allocator, + std::vector>&& quads, + AAType aaType); + + /** + * Returns the number of quads in the provider. + */ + size_t quadCount() const { + return quads.size(); + } + + /** + * Returns the AAType of the provider. + */ + AAType aaType() const { + return _aaType; + } + + /** + * Returns true if the provider generates colors. + */ + bool hasColor() const { + return _hasColor; + } + + /** + * Returns the first color in the provider. + */ + const Color& firstColor() const { + return quads.front()->color; + } + + protected: + QuadsVertexProvider(PlacementArray&& quads, AAType aaType, bool hasColor, + std::shared_ptr reference); + + PlacementArray quads = {}; + AAType _aaType = AAType::None; + bool _hasColor = false; +}; + +} // namespace tgfx diff --git a/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.cpp b/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.cpp index 4b2fb784c..873051771 100644 --- a/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.cpp +++ b/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.cpp @@ -24,17 +24,17 @@ static constexpr char UniformTransformMatrixName[] = "transformMatrix"; static constexpr char UniformNdcScaleName[] = "ndcScale"; static constexpr char UniformNdcOffsetName[] = "ndcOffset"; -PlacementPtr Transform3DGeometryProcessor::Make( +PlacementPtr QuadPerEdgeAA3DGeometryProcessor::Make( BlockAllocator* allocator, AAType aa, const Matrix3D& matrix, const Vec2& ndcScale, - const Vec2& ndcOffset) { - return allocator->make(aa, matrix, ndcScale, ndcOffset); + const Vec2& ndcOffset, std::optional commonColor) { + return allocator->make(aa, matrix, ndcScale, ndcOffset, + commonColor); } -GLSLQuadPerEdgeAA3DGeometryProcessor::GLSLQuadPerEdgeAA3DGeometryProcessor(AAType aa, - const Matrix3D& matrix, - const Vec2& ndcScale, - const Vec2& ndcOffset) - : Transform3DGeometryProcessor(aa, matrix, ndcScale, ndcOffset) { +GLSLQuadPerEdgeAA3DGeometryProcessor::GLSLQuadPerEdgeAA3DGeometryProcessor( + AAType aa, const Matrix3D& matrix, const Vec2& ndcScale, const Vec2& ndcOffset, + std::optional commonColor) + : QuadPerEdgeAA3DGeometryProcessor(aa, matrix, ndcScale, ndcOffset, commonColor) { } void GLSLQuadPerEdgeAA3DGeometryProcessor::emitCode(EmitArgs& args) const { @@ -55,10 +55,15 @@ void GLSLQuadPerEdgeAA3DGeometryProcessor::emitCode(EmitArgs& args) const { fragBuilder->codeAppendf("%s = vec4(1.0);", args.outputCoverage.c_str()); } - // The default fragment processor color rendering logic requires a color uniform. - auto colorName = - uniformHandler->addUniform("Color", UniformFormat::Float4, ShaderStage::Fragment); - fragBuilder->codeAppendf("%s = %s;", args.outputColor.c_str(), colorName.c_str()); + if (commonColor.has_value()) { + auto colorName = + uniformHandler->addUniform("Color", UniformFormat::Float4, ShaderStage::Fragment); + fragBuilder->codeAppendf("%s = %s;", args.outputColor.c_str(), colorName.c_str()); + } else { + auto colorVar = varyingHandler->addVarying("Color", SLType::Float4); + vertBuilder->codeAppendf("%s = %s;", colorVar.vsOut().c_str(), color.name().c_str()); + fragBuilder->codeAppendf("%s = %s;", args.outputColor.c_str(), colorVar.fsIn().c_str()); + } auto transformMatrixName = uniformHandler->addUniform( UniformTransformMatrixName, UniformFormat::Float4x4, ShaderStage::Vertex); args.vertBuilder->codeAppendf("vec4 clipPoint = %s * vec4(%s, 0.0, 1.0);", @@ -77,7 +82,9 @@ void GLSLQuadPerEdgeAA3DGeometryProcessor::setData(UniformData* vertexUniformDat UniformData* fragmentUniformData, FPCoordTransformIter* transformIter) const { setTransformDataHelper(Matrix::I(), vertexUniformData, transformIter); - fragmentUniformData->setData("Color", defaultColor); + if (commonColor.has_value()) { + fragmentUniformData->setData("Color", *commonColor); + } vertexUniformData->setData("transformMatrix", matrix); vertexUniformData->setData("ndcScale", ndcScale); vertexUniformData->setData("ndcOffset", ndcOffset); diff --git a/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.h b/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.h index 7fa1c6ca3..e5345d28f 100644 --- a/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.h +++ b/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.h @@ -18,28 +18,26 @@ #pragma once -#include "gpu/processors/Transform3DGeometryProcessor.h" +#include "gpu/processors/QuadPerEdgeAA3DGeometryProcessor.h" namespace tgfx { /** * The implementation of QuadPerEdgeAA3DGeometryProcessor using GLSL. */ -class GLSLQuadPerEdgeAA3DGeometryProcessor final : public Transform3DGeometryProcessor { +class GLSLQuadPerEdgeAA3DGeometryProcessor final : public QuadPerEdgeAA3DGeometryProcessor { public: /** * Creates a GLSLQuadPerEdgeAA3DGeometryProcessor instance with the specified parameters. */ explicit GLSLQuadPerEdgeAA3DGeometryProcessor(AAType aa, const Matrix3D& matrix, - const Vec2& ndcScale, const Vec2& ndcOffset); + const Vec2& ndcScale, const Vec2& ndcOffset, + std::optional commonColor); void emitCode(EmitArgs& args) const override; void setData(UniformData* vertexUniformData, UniformData* fragmentUniformData, FPCoordTransformIter* transformIter) const override; - - private: - Color defaultColor = Color::White(); }; } // namespace tgfx diff --git a/src/gpu/ops/DrawOp.h b/src/gpu/ops/DrawOp.h index 1494f20b1..66cb09d75 100644 --- a/src/gpu/ops/DrawOp.h +++ b/src/gpu/ops/DrawOp.h @@ -25,7 +25,7 @@ namespace tgfx { class DrawOp { public: - enum class Type { RectDrawOp, RRectDrawOp, ShapeDrawOp, AtlasTextOp, Rect3DDrawOp }; + enum class Type { RectDrawOp, RRectDrawOp, ShapeDrawOp, AtlasTextOp, Quads3DDrawOp }; virtual ~DrawOp() = default; diff --git a/src/gpu/ops/Rect3DDrawOp.cpp b/src/gpu/ops/Quads3DDrawOp.cpp similarity index 66% rename from src/gpu/ops/Rect3DDrawOp.cpp rename to src/gpu/ops/Quads3DDrawOp.cpp index 89de3ceec..f38dbf03c 100644 --- a/src/gpu/ops/Rect3DDrawOp.cpp +++ b/src/gpu/ops/Quads3DDrawOp.cpp @@ -16,38 +16,37 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "Rect3DDrawOp.h" +#include "Quads3DDrawOp.h" #include "core/utils/ColorHelper.h" #include "core/utils/MathExtra.h" #include "gpu/GlobalCache.h" #include "gpu/ProxyProvider.h" -#include "gpu/processors/Transform3DGeometryProcessor.h" +#include "gpu/processors/QuadPerEdgeAA3DGeometryProcessor.h" #include "inspect/InspectorMark.h" #include "tgfx/core/RenderFlags.h" namespace tgfx { -// The maximum number of vertices per non-AA quad. +// The maximum number of indices per non-AA quad. static constexpr uint32_t IndicesPerNonAAQuad = 6; -// The maximum number of vertices per AA quad. +// The maximum number of indices per AA quad. static constexpr uint32_t IndicesPerAAQuad = 30; -PlacementPtr Rect3DDrawOp::Make(Context* context, - PlacementPtr provider, - uint32_t renderFlags, - const Rect3DDrawArgs& drawArgs) { +PlacementPtr Quads3DDrawOp::Make(Context* context, + PlacementPtr provider, + uint32_t renderFlags, + const Quads3DDrawArgs& drawArgs) { if (provider == nullptr) { return nullptr; } auto allocator = context->drawingAllocator(); - auto drawOp = allocator->make(allocator, provider.get(), drawArgs); - CAPUTRE_RECT_MESH(drawOp.get(), provider.get()); - if (provider->aaType() == AAType::Coverage || provider->rectCount() > 1 || provider->lineJoin()) { + auto drawOp = allocator->make(allocator, provider.get(), drawArgs); + if (provider->aaType() == AAType::Coverage || provider->quadCount() > 1) { drawOp->indexBufferProxy = context->globalCache()->getRectIndexBuffer( - provider->aaType() == AAType::Coverage, provider->lineJoin()); + provider->aaType() == AAType::Coverage, std::nullopt); } - if (provider->rectCount() <= 1) { - // If we only have one rect, it is not worth the async task overhead. + if (provider->quadCount() <= 1) { + // If we only have one quad, it is not worth the async task overhead. renderFlags |= RenderFlags::DisableAsyncTask; } drawOp->vertexBufferProxyView = @@ -55,25 +54,17 @@ PlacementPtr Rect3DDrawOp::Make(Context* context, return drawOp; } -Rect3DDrawOp::Rect3DDrawOp(BlockAllocator* allocator, RectsVertexProvider* provider, - const Rect3DDrawArgs& drawArgs) - : DrawOp(allocator, provider->aaType()), drawArgs(drawArgs), rectCount(provider->rectCount()) { - if (!provider->hasUVCoord()) { - auto matrix = provider->firstMatrix(); - matrix.invert(&matrix); - uvMatrix = matrix; - } +Quads3DDrawOp::Quads3DDrawOp(BlockAllocator* allocator, QuadsVertexProvider* provider, + const Quads3DDrawArgs& drawArgs) + : DrawOp(allocator, provider->aaType()), drawArgs(drawArgs), quadCount(provider->quadCount()) { if (!provider->hasColor()) { - commonColor = ToPMColor(provider->firstColor(), provider->dstColorSpace()); + commonColor = ToPMColor(provider->firstColor(), nullptr); } - hasSubset = provider->hasSubset(); } -PlacementPtr Rect3DDrawOp::onMakeGeometryProcessor(RenderTarget* renderTarget) { - ATTRIBUTE_NAME("rectCount", static_cast(rectCount)); +PlacementPtr Quads3DDrawOp::onMakeGeometryProcessor(RenderTarget* renderTarget) { + ATTRIBUTE_NAME("quadCount", static_cast(quadCount)); ATTRIBUTE_NAME("commonColor", commonColor); - ATTRIBUTE_NAME("uvMatrix", uvMatrix); - ATTRIBUTE_NAME("hasSubset", hasSubset); // The actual size of the rendered texture is larger than the valid size, while the current // NDC coordinates were calculated based on the valid size, so they need to be adjusted // accordingly. @@ -95,11 +86,11 @@ PlacementPtr Rect3DDrawOp::onMakeGeometryProcessor(RenderTarg ndcScale.y = -ndcScale.y; ndcOffset.y = -ndcOffset.y; } - return Transform3DGeometryProcessor::Make(allocator, aaType, drawArgs.transformMatrix, ndcScale, - ndcOffset); + return QuadPerEdgeAA3DGeometryProcessor::Make(allocator, aaType, drawArgs.transformMatrix, + ndcScale, ndcOffset, commonColor); } -void Rect3DDrawOp::onDraw(RenderPass* renderPass) { +void Quads3DDrawOp::onDraw(RenderPass* renderPass) { std::shared_ptr indexBuffer = nullptr; if (indexBufferProxy) { indexBuffer = indexBufferProxy->getBuffer(); @@ -115,7 +106,7 @@ void Rect3DDrawOp::onDraw(RenderPass* renderPass) { renderPass->setIndexBuffer(indexBuffer ? indexBuffer->gpuBuffer() : nullptr); if (indexBuffer != nullptr) { auto numIndicesPerQuad = aaType == AAType::Coverage ? IndicesPerAAQuad : IndicesPerNonAAQuad; - renderPass->drawIndexed(PrimitiveType::Triangles, 0, rectCount * numIndicesPerQuad); + renderPass->drawIndexed(PrimitiveType::Triangles, 0, quadCount * numIndicesPerQuad); } else { renderPass->draw(PrimitiveType::TriangleStrip, 0, 4); } diff --git a/src/gpu/ops/Rect3DDrawOp.h b/src/gpu/ops/Quads3DDrawOp.h similarity index 75% rename from src/gpu/ops/Rect3DDrawOp.h rename to src/gpu/ops/Quads3DDrawOp.h index 1f520bbca..19adbb184 100644 --- a/src/gpu/ops/Rect3DDrawOp.h +++ b/src/gpu/ops/Quads3DDrawOp.h @@ -17,9 +17,10 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #pragma once + #include "DrawOp.h" #include "core/utils/PlacementPtr.h" -#include "gpu/RectsVertexProvider.h" +#include "gpu/QuadsVertexProvider.h" #include "gpu/proxies/GPUBufferProxy.h" #include "gpu/proxies/VertexBufferView.h" #include "tgfx/core/Matrix3D.h" @@ -28,9 +29,9 @@ namespace tgfx { /** - * PerspectiveRenderArgs defines arguments for perspective rendering. + * Quads3DDrawArgs defines arguments for 3D quad rendering. */ -struct Rect3DDrawArgs { +struct Quads3DDrawArgs { /** * The transformation matrix from local space to clip space. */ @@ -52,33 +53,34 @@ struct Rect3DDrawArgs { Size viewportSize = Size(1.f, 1.f); }; -class Rect3DDrawOp : public DrawOp { +/** + * Quads3DDrawOp draws a batch of 3D quads with per-edge anti-aliasing support. + */ +class Quads3DDrawOp : public DrawOp { public: /** - * Create a new RectDrawOp for the specified vertex provider. - */ - static PlacementPtr Make(Context* context, - PlacementPtr provider, - uint32_t renderFlags, const Rect3DDrawArgs& drawArgs); + * Creates a new Quads3DDrawOp for the specified vertex provider. + */ + static PlacementPtr Make(Context* context, + PlacementPtr provider, + uint32_t renderFlags, const Quads3DDrawArgs& drawArgs); private: - Rect3DDrawOp(BlockAllocator* allocator, RectsVertexProvider* provider, - const Rect3DDrawArgs& drawArgs); + Quads3DDrawOp(BlockAllocator* allocator, QuadsVertexProvider* provider, + const Quads3DDrawArgs& drawArgs); PlacementPtr onMakeGeometryProcessor(RenderTarget* renderTarget) override; void onDraw(RenderPass* renderPass) override; Type type() override { - return Type::Rect3DDrawOp; + return Type::Quads3DDrawOp; } - Rect3DDrawArgs drawArgs; + Quads3DDrawArgs drawArgs; - size_t rectCount = 0; + size_t quadCount = 0; std::optional commonColor = std::nullopt; - std::optional uvMatrix = std::nullopt; - bool hasSubset = false; std::shared_ptr indexBufferProxy = nullptr; std::shared_ptr vertexBufferProxyView = nullptr; diff --git a/src/gpu/processors/Transform3DGeometryProcessor.cpp b/src/gpu/processors/QuadPerEdgeAA3DGeometryProcessor.cpp similarity index 68% rename from src/gpu/processors/Transform3DGeometryProcessor.cpp rename to src/gpu/processors/QuadPerEdgeAA3DGeometryProcessor.cpp index 7855fa845..1f5ee3b1d 100644 --- a/src/gpu/processors/Transform3DGeometryProcessor.cpp +++ b/src/gpu/processors/QuadPerEdgeAA3DGeometryProcessor.cpp @@ -16,24 +16,28 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////// -#include "Transform3DGeometryProcessor.h" +#include "QuadPerEdgeAA3DGeometryProcessor.h" namespace tgfx { -Transform3DGeometryProcessor::Transform3DGeometryProcessor(AAType aa, const Matrix3D& transform, - const Vec2& ndcScale, - const Vec2& ndcOffset) +QuadPerEdgeAA3DGeometryProcessor::QuadPerEdgeAA3DGeometryProcessor( + AAType aa, const Matrix3D& transform, const Vec2& ndcScale, const Vec2& ndcOffset, + std::optional commonColor) : GeometryProcessor(ClassID()), aa(aa), matrix(transform), ndcScale(ndcScale), - ndcOffset(ndcOffset) { + ndcOffset(ndcOffset), commonColor(commonColor) { position = {"aPosition", VertexFormat::Float2}; if (aa == AAType::Coverage) { coverage = {"inCoverage", VertexFormat::Float}; } - setVertexAttributes(&position, 2); + if (!commonColor.has_value()) { + color = {"inColor", VertexFormat::UByte4Normalized}; + } + setVertexAttributes(&position, 3); } -void Transform3DGeometryProcessor::onComputeProcessorKey(BytesKey* bytesKey) const { +void QuadPerEdgeAA3DGeometryProcessor::onComputeProcessorKey(BytesKey* bytesKey) const { uint32_t flags = (aa == AAType::Coverage ? 1 : 0); + flags |= commonColor.has_value() ? 2 : 0; bytesKey->write(flags); } diff --git a/src/gpu/processors/Transform3DGeometryProcessor.h b/src/gpu/processors/QuadPerEdgeAA3DGeometryProcessor.h similarity index 61% rename from src/gpu/processors/Transform3DGeometryProcessor.h rename to src/gpu/processors/QuadPerEdgeAA3DGeometryProcessor.h index a3800e667..129779d6b 100644 --- a/src/gpu/processors/Transform3DGeometryProcessor.h +++ b/src/gpu/processors/QuadPerEdgeAA3DGeometryProcessor.h @@ -27,31 +27,35 @@ namespace tgfx { /** * A geometry processor for rendering 3D transformed quads with optional per-edge anti-aliasing. */ -class Transform3DGeometryProcessor : public GeometryProcessor { +class QuadPerEdgeAA3DGeometryProcessor : public GeometryProcessor { public: /** - * Creates a Transform3DGeometryProcessor instance with the specified parameters. + * Creates a QuadPerEdgeAA3DGeometryProcessor instance with the specified parameters. */ - static PlacementPtr Make(BlockAllocator* allocator, AAType aa, - const Matrix3D& matrix, - const Vec2& ndcScale, - const Vec2& ndcOffset); + static PlacementPtr Make(BlockAllocator* allocator, AAType aa, + const Matrix3D& matrix, + const Vec2& ndcScale, + const Vec2& ndcOffset, + std::optional commonColor); std::string name() const override { - return "Transform3DGeometryProcessor"; + return "QuadPerEdgeAA3DGeometryProcessor"; } protected: DEFINE_PROCESSOR_CLASS_ID - explicit Transform3DGeometryProcessor(AAType aa, const Matrix3D& transform, const Vec2& ndcScale, - const Vec2& ndcOffset); + explicit QuadPerEdgeAA3DGeometryProcessor(AAType aa, const Matrix3D& transform, + const Vec2& ndcScale, const Vec2& ndcOffset, + std::optional commonColor); void onComputeProcessorKey(BytesKey* bytesKey) const override; Attribute position = {}; - Attribute coverage = {}; + // Vertex color. Only used when vertex colors differ within the rendering program. Otherwise, + // commonColor is used. + Attribute color = {}; AAType aa = AAType::None; @@ -68,6 +72,10 @@ class Transform3DGeometryProcessor : public GeometryProcessor { */ Vec2 ndcScale = Vec2(0.f, 0.f); Vec2 ndcOffset = Vec2(0.f, 0.f); + + // If all vertex colors within the rendering program are the same, this property stores that + // color; otherwise, it is empty. + std::optional commonColor = std::nullopt; }; } // namespace tgfx diff --git a/src/inspect/Protocol.h b/src/inspect/Protocol.h index 22dacfae0..e026b4d63 100644 --- a/src/inspect/Protocol.h +++ b/src/inspect/Protocol.h @@ -95,15 +95,15 @@ enum class OpTaskType : uint8_t { RRectDrawOp, ShapeDrawOp, AtlasTextOp, - Rect3DDrawOp, + Quads3DDrawOp, DstTextureCopyOp, ResolveOp, OpTaskTypeSize, }; static std::unordered_map DrawOpTypeToOpTaskType = { - {0, OpTaskType::RectDrawOp}, {1, OpTaskType::RRectDrawOp}, {2, OpTaskType::ShapeDrawOp}, - {3, OpTaskType::AtlasTextOp}, {4, OpTaskType::Rect3DDrawOp}, + {0, OpTaskType::RectDrawOp}, {1, OpTaskType::RRectDrawOp}, {2, OpTaskType::ShapeDrawOp}, + {3, OpTaskType::AtlasTextOp}, {4, OpTaskType::Quads3DDrawOp}, }; enum class CustomEnumType : uint8_t { diff --git a/src/inspect/serialization/LayerFilterSerialization.cpp b/src/inspect/serialization/LayerFilterSerialization.cpp index bf2bee286..33c57dcbb 100644 --- a/src/inspect/serialization/LayerFilterSerialization.cpp +++ b/src/inspect/serialization/LayerFilterSerialization.cpp @@ -18,7 +18,6 @@ #include "LayerFilterSerialization.h" #include "core/utils/Log.h" -#include "layers/filters/Transform3DFilter.h" #include "tgfx/layers/filters/BlendFilter.h" #include "tgfx/layers/filters/BlurFilter.h" #include "tgfx/layers/filters/ColorMatrixFilter.h" @@ -130,10 +129,6 @@ std::shared_ptr LayerFilterSerialization::Serialize(const LayerFilter* lay case Types::LayerFilterType::InnerShadowFilter: SerializeInnerShadowFilterImpl(fbb, layerFilter, map); break; - case Types::LayerFilterType::Transform3DFilter: - // Filters stored within the Layer itself need to be serialized. Transform3DFilter type - // filters are not stored inside the Layer and do not require processing. - break; } SerializeUtils::SerializeEnd(fbb, startMap, contentMap); return Data::MakeWithCopy(fbb.GetBuffer().data(), fbb.GetBuffer().size()); diff --git a/src/layers/DrawArgs.h b/src/layers/DrawArgs.h index c9b28b4c3..e1bf8cbab 100644 --- a/src/layers/DrawArgs.h +++ b/src/layers/DrawArgs.h @@ -18,8 +18,11 @@ #pragma once +#include +#include "compositing3d/Render3DContext.h" #include "layers/BackgroundContext.h" #include "tgfx/gpu/Context.h" +#include "tgfx/layers/layerstyles/LayerStyle.h" namespace tgfx { @@ -44,7 +47,14 @@ class DrawArgs { uint32_t renderFlags = 0; // Whether to exclude effects during the drawing process. + // Note: When set to true, all layer styles and filters will be skipped, and styleSourceTypes + // will be ignored. bool excludeEffects = false; + // Specifies which layer style types to draw based on their extra source type. + // Note: This field is only effective when excludeEffects is false. + std::vector styleSourceTypes = {LayerStyleExtraSourceType::None, + LayerStyleExtraSourceType::Contour, + LayerStyleExtraSourceType::Background}; // Determines the draw mode of the Layer. DrawMode drawMode = DrawMode::Normal; // The rectangle area to be drawn. This is used for clipping the drawing area. @@ -59,5 +69,11 @@ class DrawArgs { // The maximum cache size (single edge) for subtree layer caching. Set to 0 to disable // subtree layer cache. int subtreeCacheMaxSize = 0; + + // The 3D render context to be used during the drawing process. + // Note: this could be nullptr. All layers within the 3D rendering context need to maintain their + // respective 3D states to achieve per-pixel depth occlusion effects. These layers are composited + // through the Compositor and do not need to be drawn to the Canvas. + std::shared_ptr render3DContext = nullptr; }; } // namespace tgfx diff --git a/src/layers/Layer.cpp b/src/layers/Layer.cpp index 6de395c53..b09256b43 100644 --- a/src/layers/Layer.cpp +++ b/src/layers/Layer.cpp @@ -22,8 +22,10 @@ #include #include #include +#include "compositing3d/Context3DCompositor.h" #include "core/MCState.h" #include "core/Matrix2D.h" +#include "core/Matrix3DUtils.h" #include "core/filters/Transform3DImageFilter.h" #include "core/images/TextureImage.h" #include "core/utils/Log.h" @@ -36,18 +38,52 @@ #include "layers/RootLayer.h" #include "layers/SubtreeCache.h" #include "layers/contents/LayerContent.h" -#include "layers/filters/Transform3DFilter.h" #include "tgfx/core/ColorSpace.h" #include "tgfx/core/PictureRecorder.h" #include "tgfx/core/Surface.h" #include "tgfx/layers/ShapeLayer.h" namespace tgfx { + // The minimum size (longest edge) for subtree cache. This prevents creating excessively small // mipmap levels that would be inefficient to cache. static constexpr int SUBTREE_CACHE_MIN_SIZE = 32; static std::atomic_bool AllowsEdgeAntialiasing = true; static std::atomic_bool AllowsGroupOpacity = false; +static const std::vector StyleSourceTypesFor3DContext = { + LayerStyleExtraSourceType::None, LayerStyleExtraSourceType::Contour}; + +static bool HasStyleSource(const std::vector& types, + LayerStyleExtraSourceType type) { + return std::find(types.begin(), types.end(), type) != types.end(); +} + +static void RemoveStyleSource(std::vector& types, + LayerStyleExtraSourceType type) { + types.erase(std::remove(types.begin(), types.end(), type), types.end()); +} + +/** + * Clips the canvas using the scroll rect. If the sublayer's Matrix contains 3D transformations or + * projection transformations, because this matrix has been merged into the Canvas, it can be + * directly completed through the clipping rectangle. Otherwise, the canvas will not contain this + * matrix information, and clipping needs to be done by transforming the Path. + */ +static void ClipScrollRect(Canvas* canvas, const Rect* scrollRect, const Matrix3D& transform, + bool isAffine) { + if (scrollRect == nullptr) { + return; + } + + if (isAffine) { + canvas->clipRect(*scrollRect); + } else { + Path path; + path.addRect(*scrollRect); + path.transform3D(transform); + canvas->clipPath(path); + } +} struct MaskData { Path clipPath = {}; @@ -62,24 +98,6 @@ struct LayerStyleSource { Point contourOffset = {}; }; -// Determine if the 4*4 matrix contains only 2D affine transformations, i.e., no Z-axis related -// transformations or projection transformations -static bool IsMatrix3DAffine(const Matrix3D& matrix) { - return FloatNearlyZero(matrix.getRowColumn(0, 2)) && FloatNearlyZero(matrix.getRowColumn(1, 2)) && - matrix.getRow(2) == Vec4(0, 0, 1, 0) && matrix.getRow(3) == Vec4(0, 0, 0, 1); -} - -// When a 4x4 matrix does not contain Z-axis related transformations and projection transformations, -// this function returns an equivalent 2D affine transformation. Otherwise, the return value will -// lose information about Z-axis related transformations and projection transformations. -static Matrix GetMayLossyAffineMatrix(const Matrix3D& matrix) { - auto affineMatrix = Matrix::I(); - affineMatrix.setAll(matrix.getRowColumn(0, 0), matrix.getRowColumn(0, 1), - matrix.getRowColumn(0, 3), matrix.getRowColumn(1, 0), - matrix.getRowColumn(1, 1), matrix.getRowColumn(1, 3)); - return affineMatrix; -} - static std::optional MapClipBoundsToContent(const std::optional& clipBounds, const Matrix3D* transform3D) { if (transform3D != nullptr && clipBounds.has_value()) { @@ -253,6 +271,50 @@ static int GetMipmapCacheLongEdge(int maxSize, float contentScale, const Rect& l return currentLongEdge; } +/** + * Creates a 3D rendering context for compositing layers with 3D transformations. + * @param bounds The bounding rectangle of the 3D context in DisplayList coordinates. + */ +static std::shared_ptr Create3DContext(const DrawArgs& args, Canvas* canvas, + const Rect& bounds) { + if (args.renderRect == nullptr || args.context == nullptr) { + DEBUG_ASSERT(false); + return nullptr; + } + + // The processing area of the compositor is consistent with the actual effective drawing area. + auto clipBoundsCanvas = args.blurBackground ? args.blurBackground->getCanvas() : canvas; + // The clip bounds may be slightly larger than the dirty region. + auto clipBounds = GetClipBounds(clipBoundsCanvas); + if (!clipBounds.has_value()) { + DEBUG_ASSERT(false); + return nullptr; + } + auto validRenderRect = *clipBounds; + if (!validRenderRect.intersect(bounds)) { + return nullptr; + } + // validRenderRect is in DisplayList coordinate system. Apply canvas scale to adapt to pixel + // coordinates, avoiding excessive scaling when rendering to offscreen texture which would affect + // the final visual quality. + auto contentScale = canvas->getMatrix().getMaxScale(); + if (contentScale <= 0.0f) { + return nullptr; + } + validRenderRect.scale(contentScale, contentScale); + validRenderRect.roundOut(); + if (validRenderRect.isEmpty()) { + return nullptr; + } + + auto compositor = std::make_shared( + *args.context, static_cast(validRenderRect.width()), + static_cast(validRenderRect.height())); + auto offset = Point::Make(validRenderRect.left, validRenderRect.top); + return std::make_shared(std::move(compositor), offset, contentScale, + args.dstColorSpace, args.blurBackground); +} + bool Layer::DefaultAllowsEdgeAntialiasing() { return AllowsEdgeAntialiasing; } @@ -329,7 +391,7 @@ Point Layer::position() const { return {_matrix3D.getRowColumn(0, 3), _matrix3D.getRowColumn(1, 3)}; } - auto result = matrix3D().mapVec3(Vec3(0, 0, 0)); + auto result = matrix3D().mapPoint(Vec3(0, 0, 0)); return {result.x, result.y}; } @@ -341,7 +403,7 @@ void Layer::setPosition(const Point& value) { _matrix3D.setRowColumn(0, 3, value.x); _matrix3D.setRowColumn(1, 3, value.y); } else { - auto curPos = _matrix3D.mapVec3(Vec3(0, 0, 0)); + auto curPos = _matrix3D.mapPoint(Vec3(0, 0, 0)); if (FloatNearlyEqual(curPos.x, value.x) && FloatNearlyEqual(curPos.y, value.y)) { return; } @@ -376,7 +438,16 @@ void Layer::setMatrix3D(const Matrix3D& value) { return; } _matrix3D = value; - bitFields.matrix3DIsAffine = IsMatrix3DAffine(value); + bitFields.matrix3DIsAffine = Matrix3DUtils::IsMatrix3DAffine(value); + invalidateTransform(); +} + +void Layer::setPreserve3D(bool value) { + if (_preserve3D == value) { + return; + } + _preserve3D = value; + // Changing preserve3D alters the meaning of the layer's transform, so invalidate it. invalidateTransform(); } @@ -724,34 +795,49 @@ Rect Layer::getBounds(const Layer* targetCoordinateSpace, bool computeTightBound } Rect Layer::getBoundsInternal(const Matrix3D& coordinateMatrix, bool computeTightBounds) { - if (computeTightBounds || bitFields.dirtyDescendents) { + // Non-leaf nodes in a 3D rendering context layer tree must preserve the 3D state of their + // sublayers, and cannot reuse the localBounds cache that has been flattened to the layer's local + // coordinate system. + if (computeTightBounds || bitFields.dirtyDescendents || canPreserve3D()) { return computeBounds(coordinateMatrix, computeTightBounds); } if (!localBounds) { localBounds = std::make_unique(computeBounds(Matrix3D::I(), computeTightBounds)); } - auto result = coordinateMatrix.mapRect(*localBounds); - if (!IsMatrix3DAffine(coordinateMatrix)) { - // while the matrix is not affine, the layer will draw with a 3D filter, so the bounds should - // round out. - result.roundOut(); + return coordinateMatrix.mapRect(*localBounds); +} + +static Rect ComputeContentBounds(const LayerContent& content, const Rect& contentBounds, + const Matrix3D& coordinateMatrix, bool applyMatrixAtEnd, + bool computeTightBounds) { + auto contentMatrix = applyMatrixAtEnd ? Matrix3D::I() : coordinateMatrix; + if (!computeTightBounds) { + return contentMatrix.mapRect(contentBounds); + } + + if (Matrix3DUtils::IsMatrix3DAffine(contentMatrix)) { + return content.getTightBounds(Matrix3DUtils::GetMayLossyAffineMatrix(contentMatrix)); } - return result; + auto bounds = content.getTightBounds(Matrix::I()); + return contentMatrix.mapRect(bounds); } Rect Layer::computeBounds(const Matrix3D& coordinateMatrix, bool computeTightBounds) { - bool isAffine = IsMatrix3DAffine(coordinateMatrix); + auto canPreserve3D = this->canPreserve3D(); bool hasEffects = !_layerStyles.empty() || !_filters.empty(); - // When has effects or non-affine, compute in local coordinates first, then apply matrix at end. - bool applyMatrixAtEnd = hasEffects || !isAffine; - auto contentMatrix = applyMatrixAtEnd ? Matrix::I() : GetMayLossyAffineMatrix(coordinateMatrix); + // When preserving 3D, the matrix must be passed down to preserve 3D state. + // Otherwise, when has effects, compute in local coordinates first, then apply matrix at end. + bool isAffine = Matrix3DUtils::IsMatrix3DAffine(coordinateMatrix); + bool applyMatrixAtEnd = !canPreserve3D && (hasEffects || !isAffine); Rect bounds = {}; if (auto content = getContent()) { - if (computeTightBounds) { - bounds.join(content->getTightBounds(contentMatrix)); - } else { - bounds.join(contentMatrix.mapRect(content->getBounds())); + auto contentBounds = content->getBounds(); + bool behindCamera = + !isAffine && Matrix3DUtils::IsRectBehindCamera(contentBounds, coordinateMatrix); + if (!behindCamera) { + bounds.join(ComputeContentBounds(*content, contentBounds, coordinateMatrix, applyMatrixAtEnd, + computeTightBounds)); } } @@ -761,11 +847,14 @@ Rect Layer::computeBounds(const Matrix3D& coordinateMatrix, bool computeTightBou continue; } auto childMatrix = child->getMatrixWithScrollRect(); - childMatrix.postConcat(Matrix3D(contentMatrix)); + if (!canPreserve3D) { + childMatrix.setRow(2, {0, 0, 1, 0}); + } + childMatrix.postConcat(applyMatrixAtEnd ? Matrix3D::I() : coordinateMatrix); auto childBounds = child->getBoundsInternal(childMatrix, computeTightBounds); if (child->_scrollRect) { - auto relatvieScrollRect = childMatrix.mapRect(*child->_scrollRect); - if (!childBounds.intersect(relatvieScrollRect)) { + auto relativeScrollRect = childMatrix.mapRect(*child->_scrollRect); + if (!childBounds.intersect(relativeScrollRect)) { continue; } } @@ -781,6 +870,8 @@ Rect Layer::computeBounds(const Matrix3D& coordinateMatrix, bool computeTightBou } if (hasEffects) { + // Filters and styles disable 3D context ability, so this block is unreachable for 3D context. + DEBUG_ASSERT(!canPreserve3D); auto layerBounds = bounds; for (auto& layerStyle : _layerStyles) { DEBUG_ASSERT(layerStyle != nullptr); @@ -794,10 +885,10 @@ Rect Layer::computeBounds(const Matrix3D& coordinateMatrix, bool computeTightBou } if (applyMatrixAtEnd) { - bounds = coordinateMatrix.mapRect(bounds); - if (!isAffine) { - bounds.roundOut(); + if (!isAffine && Matrix3DUtils::IsRectBehindCamera(bounds, coordinateMatrix)) { + return {}; } + bounds = coordinateMatrix.mapRect(bounds); } return bounds; } @@ -823,7 +914,7 @@ Point Layer::globalToLocal(const Point& globalPoint) const { Point Layer::localToGlobal(const Point& localPoint) const { auto globalMatrix = getGlobalMatrix(); - auto result = globalMatrix.mapVec3({localPoint.x, localPoint.y, 0}); + auto result = globalMatrix.mapPoint({localPoint.x, localPoint.y, 0}); return {result.x, result.y}; } @@ -920,11 +1011,11 @@ void Layer::draw(Canvas* canvas, float alpha, BlendMode blendMode) { auto backgroundRect = clippedBounds; backgroundRect.scale(scale, scale); auto backgroundMatrix = Matrix::I(); - if (IsMatrix3DAffine(globalToLocalMatrix)) { + if (Matrix3DUtils::IsMatrix3DAffine(globalToLocalMatrix)) { // If the transformation from the current layer node to the root node only contains 2D affine // transformations, then draw the real layer background and calculate the accurate // transformation matrix of the background image in the layer's local coordinate system - backgroundMatrix = GetMayLossyAffineMatrix(globalToLocalMatrix); + backgroundMatrix = Matrix3DUtils::GetMayLossyAffineMatrix(globalToLocalMatrix); } else { // Otherwise, it's impossible to draw an accurate background. Only the background image // corresponding to the minimum bounding rectangle of the layer node subtree after drawing can @@ -942,7 +1033,7 @@ void Layer::draw(Canvas* canvas, float alpha, BlendMode blendMode) { bounds == clippedBounds, surface->colorSpace())) { auto backgroundCanvas = backgroundContext->getCanvas(); auto actualMatrix = backgroundCanvas->getMatrix(); - bool isLocalToGlobalAffine = IsMatrix3DAffine(localToGlobalMatrix); + bool isLocalToGlobalAffine = Matrix3DUtils::IsMatrix3DAffine(localToGlobalMatrix); if (isLocalToGlobalAffine) { // The current layer node to the root node only contains 2D affine transformations, need to // superimpose the transformation matrix that maps layer coordinates to the actual drawing @@ -950,7 +1041,7 @@ void Layer::draw(Canvas* canvas, float alpha, BlendMode blendMode) { // Since the background recorder starts from the current layer, we need to pre-concatenate // localToGlobalMatrix to the background canvas matrix to ensure the coordinate space is // correct. - actualMatrix.preConcat(GetMayLossyAffineMatrix(localToGlobalMatrix)); + actualMatrix.preConcat(Matrix3DUtils::GetMayLossyAffineMatrix(localToGlobalMatrix)); } else { // Otherwise, need to superimpose the transformation matrix that maps the bounds to the // actual drawing area renderRect. @@ -981,7 +1072,14 @@ void Layer::draw(Canvas* canvas, float alpha, BlendMode blendMode) { auto blurCanvas = args.blurBackground ? args.blurBackground->getCanvas() : nullptr; AutoCanvasRestore autoRestore(canvas); AutoCanvasRestore blurAutoRestore(blurCanvas); - drawLayer(args, canvas, alpha, blendMode, nullptr); + + // Check if the current layer needs to start a 3D context. Since Layer::draw is called directly + // without going through a parent layer's drawChildren, 3D context handling must be done here. + if (canPreserve3D()) { + drawByStarting3DContext(args, canvas); + } else { + drawLayer(args, canvas, alpha, blendMode, nullptr); + } } void Layer::invalidateContent() { @@ -1077,6 +1175,13 @@ Matrix3D Layer::getGlobalMatrix() const { Matrix3D matrix = {}; auto layer = this; while (layer->_parent) { + // Check if the child layer is within a 3D rendering context. If not, adapt the matrix to + // ensure the z-component of layer rect vertex coordinates remains unchanged, so the child layer + // is projected onto the current layer first, then the accumulated matrix transformation is + // applied. If within, directly concatenate the child layer matrix to preserve its 3D state. + if (!layer->canPreserve3D()) { + matrix.setRow(2, {0, 0, 1, 0}); + } matrix.postConcat(layer->getMatrixWithScrollRect()); layer = layer->_parent; } @@ -1145,13 +1250,23 @@ void Layer::drawLayer(const DrawArgs& args, Canvas* canvas, float alpha, BlendMo drawWithSubtreeCache(args, canvas, alpha, blendMode, transform3D, maskFilter)) { return; } - bool needsOffscreen = blendMode != BlendMode::SrcOver || !bitFields.passThroughBackground || - (alpha < 1.0f && bitFields.allowsGroupOpacity) || - (!_filters.empty() && !args.excludeEffects) || needsMaskFilter || - transform3D != nullptr; + + bool needsOffscreen = false; + // For non-leaf layers in a 3D rendering context layer tree, direct rendering is always used. + // Offscreen constraints (filters, styles, masks) are already checked by the parent layer at call + // site. Group opacity, blend mode, and passThroughBackground have lower priority than enabling + // 3D rendering context and are ignored during drawing. + if (!canPreserve3D()) { + needsOffscreen = blendMode != BlendMode::SrcOver || !bitFields.passThroughBackground || + (alpha < 1.0f && bitFields.allowsGroupOpacity) || + (!_filters.empty() && !args.excludeEffects) || needsMaskFilter || + transform3D != nullptr; + } if (needsOffscreen) { + // Content and children are rendered together in an offscreen buffer. drawOffscreen(args, canvas, alpha, blendMode, transform3D, maskFilter); } else { + // Content and children are rendered independently. drawDirectly(args, canvas, alpha); } } @@ -1198,6 +1313,7 @@ bool Layer::prepareMask(const DrawArgs& args, Canvas* canvas, MaskData Layer::getMaskData(const DrawArgs& args, float scale, const std::optional& layerClipBounds) { DEBUG_ASSERT(_mask != nullptr); + DEBUG_ASSERT(args.render3DContext == nullptr); auto maskType = static_cast(bitFields.maskType); auto maskArgs = args; maskArgs.drawMode = maskType != LayerMaskType::Contour ? DrawMode::Normal : DrawMode::Contour; @@ -1205,15 +1321,19 @@ MaskData Layer::getMaskData(const DrawArgs& args, float scale, maskArgs.blurBackground = nullptr; auto relativeMatrix = _mask->getRelativeMatrix(this); - auto isMatrixAffine = IsMatrix3DAffine(relativeMatrix); + auto isMatrixAffine = Matrix3DUtils::IsMatrix3DAffine(relativeMatrix); auto affineRelativeMatrix = - isMatrixAffine ? GetMayLossyAffineMatrix(relativeMatrix) : Matrix::I(); + isMatrixAffine ? Matrix3DUtils::GetMayLossyAffineMatrix(relativeMatrix) : Matrix::I(); // Note: RecordPicture does not use layerClipBounds here. Using clipBounds may cause PathOp // errors when extracting maskPath from the picture, resulting in incorrect clip regions. auto maskPicture = RecordPicture(maskArgs.drawMode, scale, [&](Canvas* canvas) { canvas->concat(affineRelativeMatrix); - _mask->drawLayer(maskArgs, canvas, _mask->_alpha, BlendMode::SrcOver); + if (_mask->canPreserve3D()) { + _mask->drawByStarting3DContext(maskArgs, canvas); + } else { + _mask->drawLayer(maskArgs, canvas, _mask->_alpha, BlendMode::SrcOver); + } }); if (maskPicture == nullptr) { return {}; @@ -1258,9 +1378,10 @@ MaskData Layer::getMaskData(const DrawArgs& args, float scale, return {{}, MaskFilter::MakeShader(shader)}; } -std::shared_ptr Layer::getContentImage( - const DrawArgs& contentArgs, const Matrix& contentMatrix, const std::optional& clipBounds, - const std::vector& extraSourceTypes, Matrix* imageMatrix) { +std::shared_ptr Layer::getContentImage(const DrawArgs& contentArgs, + const Matrix& contentMatrix, + const std::optional& clipBounds, + Matrix* imageMatrix) { DEBUG_ASSERT(imageMatrix); auto inputBounds = computeContentBounds(clipBounds, contentArgs.excludeEffects); if (!inputBounds.has_value()) { @@ -1277,7 +1398,7 @@ std::shared_ptr Layer::getContentImage( mappedBounds.roundOut(); offscreenCanvas->clipRect(mappedBounds); offscreenCanvas->setMatrix(contentMatrix); - drawDirectly(contentArgs, offscreenCanvas, 1.0f, extraSourceTypes); + drawDirectly(contentArgs, offscreenCanvas, 1.0f); Point offset = {}; auto finalImage = ToImageWithOffset(recorder.finishRecordingAsPicture(), &offset, nullptr, contentArgs.dstColorSpace); @@ -1297,7 +1418,7 @@ std::shared_ptr Layer::getContentImage( mappedBounds.roundOut(); offscreenCanvas->clipRect(mappedBounds); offscreenCanvas->scale(contentScale, contentScale); - drawDirectly(contentArgs, offscreenCanvas, 1.0f, extraSourceTypes); + drawDirectly(contentArgs, offscreenCanvas, 1.0f); Point offset = {}; auto finalImage = ToImageWithOffset(recorder.finishRecordingAsPicture(), &offset, nullptr, contentArgs.dstColorSpace); @@ -1321,9 +1442,9 @@ std::shared_ptr Layer::getContentImage( return finalImage; } -std::shared_ptr Layer::getPassThroughContentImage( - const DrawArgs& args, Canvas* canvas, const std::optional& clipBounds, - const std::vector& extraSourceTypes, Matrix* imageMatrix) { +std::shared_ptr Layer::getPassThroughContentImage(const DrawArgs& args, Canvas* canvas, + const std::optional& clipBounds, + Matrix* imageMatrix) { DEBUG_ASSERT(imageMatrix); DEBUG_ASSERT(args.context); auto surface = canvas->getSurface(); @@ -1351,7 +1472,7 @@ std::shared_ptr Layer::getPassThroughContentImage( offscreenCanvas->translate(-surfaceRect.left, -surfaceRect.top); offscreenCanvas->drawImage(passThroughImage); offscreenCanvas->concat(passThroughImageMatrix); - drawDirectly(args, offscreenCanvas, 1.0f, extraSourceTypes); + drawDirectly(args, offscreenCanvas, 1.0f); auto finalImage = offscreenSurface->makeImageSnapshot(); offscreenCanvas->getMatrix().invert(imageMatrix); return finalImage; @@ -1377,11 +1498,23 @@ bool Layer::shouldPassThroughBackground(BlendMode blendMode, const Matrix3D* tra return false; } + // 4. Layer does not start or extend a 3D context + // (When starting or extending a 3D context, child layers need to maintain independent 3D states, + // so pass-through background is not supported) + if (canPreserve3D()) { + return false; + } + return true; } bool Layer::canUseSubtreeCache(const DrawArgs& args, BlendMode blendMode, const Matrix3D* transform3D) { + // If the layer can start or extend a 3D rendering context, child layers need to maintain + // independent 3D states, so subtree caching is not supported. + if (canPreserve3D()) { + return false; + } // The cache stores Normal mode content. Since layers with BackgroundStyle are excluded from // caching, the cached content can also be used for Background mode drawing. if (args.excludeEffects || args.drawMode == DrawMode::Contour) { @@ -1434,6 +1567,8 @@ std::shared_ptr Layer::createSubtreeCacheImage(const DrawArgs& args, floa drawArgs.renderFlags |= RenderFlags::DisableCache; drawArgs.renderRect = nullptr; drawArgs.blurBackground = nullptr; + // Cache content should be rendered to a regular texture, not to 3D compositor. + drawArgs.render3DContext = nullptr; auto pictureBounds = layerBounds; pictureBounds.scale(contentScale, contentScale); @@ -1495,6 +1630,8 @@ SubtreeCache* Layer::getValidSubtreeCache(const DrawArgs& args, int longEdge, bool Layer::drawWithSubtreeCache(const DrawArgs& args, Canvas* canvas, float alpha, BlendMode blendMode, const Matrix3D* transform3D, const std::shared_ptr& maskFilter) { + // Non-leaf nodes in 3D rendering context layer tree have caching disabled. + DEBUG_ASSERT(!canPreserve3D()); auto layerBounds = getBounds(); auto contentScale = canvas->getMatrix().getMaxScale(); auto longEdge = GetMipmapCacheLongEdge(args.subtreeCacheMaxSize, contentScale, layerBounds); @@ -1510,7 +1647,8 @@ bool Layer::drawWithSubtreeCache(const DrawArgs& args, Canvas* canvas, float alp paint.setAlpha(alpha); paint.setBlendMode(blendMode); cache->draw(args.context, longEdge, canvas, paint, transform3D); - if (args.blurBackground) { + if (args.blurBackground && + HasStyleSource(args.styleSourceTypes, LayerStyleExtraSourceType::Background)) { cache->draw(args.context, longEdge, args.blurBackground->getCanvas(), paint, transform3D); } return true; @@ -1519,18 +1657,25 @@ bool Layer::drawWithSubtreeCache(const DrawArgs& args, Canvas* canvas, float alp void Layer::drawOffscreen(const DrawArgs& args, Canvas* canvas, float alpha, BlendMode blendMode, const Matrix3D* transform3D, const std::shared_ptr& maskFilter) { - std::vector extraSourceTypes = {LayerStyleExtraSourceType::None, - LayerStyleExtraSourceType::Contour}; - if (transform3D != nullptr) { + // Non-leaf layers in a 3D rendering context layer tree never require offscreen rendering. + DEBUG_ASSERT(args.render3DContext == nullptr); + DEBUG_ASSERT(!canPreserve3D()); + auto drawBackground = + transform3D != nullptr && + HasStyleSource(args.styleSourceTypes, LayerStyleExtraSourceType::Background); + if (drawBackground) { drawBackgroundLayerStyles(args, canvas, alpha, *transform3D); - } else { - extraSourceTypes.push_back(LayerStyleExtraSourceType::Background); } auto imageMatrix = Matrix::I(); std::shared_ptr image = nullptr; - auto clipBounds = GetClipBounds(args.blurBackground ? args.blurBackground->getCanvas() : canvas); + auto clipBoundsCanvas = args.blurBackground ? args.blurBackground->getCanvas() : canvas; + auto clipBounds = GetClipBounds(clipBoundsCanvas); auto contentArgs = args; + if (drawBackground) { + RemoveStyleSource(contentArgs.styleSourceTypes, LayerStyleExtraSourceType::Background); + contentArgs.blurBackground = nullptr; + } if (shouldPassThroughBackground(blendMode, transform3D) && canvas->getSurface()) { // In pass-through mode, the image drawn to canvas contains the blended background, while @@ -1540,15 +1685,13 @@ void Layer::drawOffscreen(const DrawArgs& args, Canvas* canvas, float alpha, Ble auto contentClipBounds = MapClipBoundsToContent(canvasClipBounds, transform3D); contentArgs.blurBackground = args.blurBackground ? args.blurBackground->createSubContext(renderBounds, true) : nullptr; - image = - getPassThroughContentImage(args, canvas, contentClipBounds, extraSourceTypes, &imageMatrix); + image = getPassThroughContentImage(args, canvas, contentClipBounds, &imageMatrix); } else { auto contentClipBounds = MapClipBoundsToContent(clipBounds, transform3D); contentArgs.blurBackground = args.blurBackground && hasBackgroundStyle() ? args.blurBackground->createSubContext(renderBounds, true) : nullptr; - image = getContentImage(contentArgs, canvas->getMatrix(), contentClipBounds, extraSourceTypes, - &imageMatrix); + image = getContentImage(contentArgs, canvas->getMatrix(), contentClipBounds, &imageMatrix); } auto invertImageMatrix = Matrix::I(); @@ -1596,10 +1739,6 @@ void Layer::drawOffscreen(const DrawArgs& args, Canvas* canvas, float alpha, Ble backgroundCanvas->drawImage(image, 0.f, 0.f, sampling, &paint); } } - - // There is no scenario where LayerStyle's Position and ExtraSourceType are 'above' and - // 'background' respectively at the same time, so no special handling is needed after drawing the - // content. } std::optional Layer::computeContentBounds(const std::optional& clipBounds, @@ -1622,24 +1761,14 @@ std::optional Layer::computeContentBounds(const std::optional& clipB } void Layer::drawDirectly(const DrawArgs& args, Canvas* canvas, float alpha) { - std::vector styleExtraSourceTypes = { - LayerStyleExtraSourceType::None, LayerStyleExtraSourceType::Contour, - LayerStyleExtraSourceType::Background}; - drawDirectly(args, canvas, alpha, styleExtraSourceTypes); -} - -void Layer::drawDirectly(const DrawArgs& args, Canvas* canvas, float alpha, - const std::vector& extraSourceTypes) { auto layerStyleSource = getLayerStyleSource(args, canvas->getMatrix()); - drawContents(args, canvas, alpha, layerStyleSource.get(), nullptr, extraSourceTypes); + drawContents(args, canvas, alpha, layerStyleSource.get()); } void Layer::drawContents(const DrawArgs& args, Canvas* canvas, float alpha, - const LayerStyleSource* layerStyleSource, const Layer* stopChild, - const std::vector& extraSourceTypes) { + const LayerStyleSource* layerStyleSource, const Layer* stopChild) { if (layerStyleSource) { - drawLayerStyles(args, canvas, alpha, layerStyleSource, LayerStylePosition::Below, - extraSourceTypes); + drawLayerStyles(args, canvas, alpha, layerStyleSource, LayerStylePosition::Below); } auto content = getContent(); bool hasForeground = false; @@ -1658,8 +1787,7 @@ void Layer::drawContents(const DrawArgs& args, Canvas* canvas, float alpha, return; } if (layerStyleSource) { - drawLayerStyles(args, canvas, alpha, layerStyleSource, LayerStylePosition::Above, - extraSourceTypes); + drawLayerStyles(args, canvas, alpha, layerStyleSource, LayerStylePosition::Above); } if (hasForeground) { content->drawForeground(canvas, alpha, bitFields.allowsEdgeAntialiasing); @@ -1684,70 +1812,144 @@ bool Layer::drawChildren(const DrawArgs& args, Canvas* canvas, float alpha, } } } + + // TODO: Support background styles for subsequent layers of 3D layers. + // 3D layer matrices are not written to the background canvas, so child layers cannot obtain + // correct background content. Background styles are temporarily disabled for the 3D layer's + // subtree (excluding the 3D layer itself). + bool skipChildBackground = + !HasStyleSource(args.styleSourceTypes, LayerStyleExtraSourceType::Background) || + !bitFields.matrix3DIsAffine; + for (size_t i = 0; i < _children.size(); ++i) { auto& child = _children[i]; if (child.get() == stopChild) { return false; } - if (child->maskOwner) { + if (child->maskOwner || !child->visible() || child->_alpha <= 0) { continue; } - if (!child->visible() || child->_alpha <= 0) { + auto childArgsOpt = createChildArgs(args, canvas, child.get(), skipChildBackground, + static_cast(i), lastBackgroundLayerIndex); + if (!childArgsOpt.has_value()) { continue; } - auto childArgs = args; - if (static_cast(i) < lastBackgroundLayerIndex) { - childArgs.forceDrawBackground = true; - } else { - childArgs.forceDrawBackground = false; - if (static_cast(i) > lastBackgroundLayerIndex) { - childArgs.blurBackground = nullptr; - } - } + auto childArgs = std::move(*childArgsOpt); AutoCanvasRestore autoRestore(canvas); auto backgroundCanvas = childArgs.blurBackground ? childArgs.blurBackground->getCanvas() : nullptr; AutoCanvasRestore autoRestoreBg(backgroundCanvas); - auto childMatrix = child->getMatrixWithScrollRect(); + auto childTransform3D = child->getMatrixWithScrollRect(); // If the sublayer's Matrix contains 3D transformations or projection transformations, then // treat its Matrix as an identity matrix here, and let the sublayer handle its actual position // through 3D filter methods, - const bool isChildMatrixAffine = IsMatrix3DAffine(childMatrix); - auto childAffineMatrix = - isChildMatrixAffine ? GetMayLossyAffineMatrix(childMatrix) : Matrix::I(); + const bool isChildMatrixAffine = Matrix3DUtils::IsMatrix3DAffine(childTransform3D); + auto childAffineMatrix = (isChildMatrixAffine && !canPreserve3D()) + ? Matrix3DUtils::GetMayLossyAffineMatrix(childTransform3D) + : Matrix::I(); canvas->concat(childAffineMatrix); - auto clipChildScrollRectHandler = [&](Canvas& clipCanvas) { - if (child->_scrollRect) { - // If the sublayer's Matrix contains 3D transformations or projection transformations, then - // because this matrix has been merged into the Canvas, it can be directly completed through - // the clipping rectangle. Otherwise, the canvas will not contain this matrix information, - // and clipping needs to be done by transforming the Path. - if (isChildMatrixAffine) { - clipCanvas.clipRect(*child->_scrollRect); - } else { - auto path = Path(); - path.addRect(*(child->_scrollRect)); - path.transform3D(childMatrix); - clipCanvas.clipPath(path); - } - } - }; - if (child->_scrollRect) { - clipChildScrollRectHandler(*canvas); - } + ClipScrollRect(canvas, child->_scrollRect.get(), childTransform3D, isChildMatrixAffine); if (backgroundCanvas) { backgroundCanvas->concat(childAffineMatrix); - clipChildScrollRectHandler(*backgroundCanvas); + ClipScrollRect(backgroundCanvas, child->_scrollRect.get(), childTransform3D, + isChildMatrixAffine); } - auto transform = isChildMatrixAffine ? nullptr : &childMatrix; - child->drawLayer(childArgs, canvas, child->_alpha * alpha, - static_cast(child->bitFields.blendMode), transform); + auto context3D = + args.render3DContext ? args.render3DContext.get() : childArgs.render3DContext.get(); + bool started3DContext = !args.render3DContext && childArgs.render3DContext != nullptr; + drawChild(childArgs, canvas, child.get(), alpha, childTransform3D, context3D, started3DContext); } return true; } +void Layer::drawByStarting3DContext(const DrawArgs& args, Canvas* canvas) { + DEBUG_ASSERT(canPreserve3D()); + DEBUG_ASSERT(args.render3DContext == nullptr); + + auto newContext = Create3DContext(args, canvas, getBounds(_parent)); + if (newContext == nullptr) { + return; + } + + auto contextArgs = args; + contextArgs.render3DContext = newContext; + // Layers inside a 3D rendering context need to maintain independent 3D state. This means layers + // drawn later may become the background, making it impossible to know the final background when + // drawing each layer. Therefore, background styles are disabled. + contextArgs.styleSourceTypes = StyleSourceTypesFor3DContext; + + auto offscreenCanvas = + newContext->beginRecording(getMatrixWithScrollRect(), bitFields.allowsEdgeAntialiasing); + drawLayer(contextArgs, offscreenCanvas, _alpha, BlendMode::SrcOver); + newContext->endRecording(); + newContext->finishAndDrawTo(canvas, bitFields.allowsEdgeAntialiasing); +} + +std::optional Layer::createChildArgs(const DrawArgs& args, Canvas* canvas, Layer* child, + bool skipBackground, int childIndex, + int lastBackgroundIndex) { + auto childArgs = args; + if (skipBackground) { + RemoveStyleSource(childArgs.styleSourceTypes, LayerStyleExtraSourceType::Background); + childArgs.blurBackground = nullptr; + } + if (childIndex < lastBackgroundIndex) { + childArgs.forceDrawBackground = true; + } else { + childArgs.forceDrawBackground = false; + if (childIndex > lastBackgroundIndex) { + childArgs.blurBackground = nullptr; + } + } + // Handle 3D context state transitions: + // - If parent has no 3D context and child can preserve 3D, child becomes the root of a new 3D + // subtree, so create a new context. + // - If parent has 3D context but child cannot preserve 3D, child is a leaf node in the 3D + // subtree. Clear render3DContext to prevent child layers from participating in 3D compositing. + // The entire subtree is rendered as a flat image with this layer's 3D transform applied. + auto childCanPreserve3D = child->canPreserve3D(); + if (!args.render3DContext && childCanPreserve3D) { + auto childBounds = child->getBounds(this); + if (childBounds.isEmpty()) { + return std::nullopt; + } + childArgs.render3DContext = Create3DContext(childArgs, canvas, childBounds); + if (childArgs.render3DContext == nullptr) { + return std::nullopt; + } + // Layers inside a 3D rendering context need to maintain independent 3D state. This means + // layers drawn later may become the background, making it impossible to know the final + // background when drawing each layer. Therefore, background styles are disabled. + childArgs.styleSourceTypes = StyleSourceTypesFor3DContext; + childArgs.blurBackground = nullptr; + } else if (args.render3DContext && !childCanPreserve3D) { + childArgs.render3DContext = nullptr; + } + return childArgs; +} + +void Layer::drawChild(const DrawArgs& args, Canvas* canvas, Layer* child, float alpha, + const Matrix3D& transform3D, Render3DContext* context3D, + bool started3DContext) { + if (!context3D) { + // Child is completely outside any 3D context, draw normally. + const Matrix3D* transform = + Matrix3DUtils::IsMatrix3DAffine(transform3D) ? nullptr : &transform3D; + auto blendMode = static_cast(child->bitFields.blendMode); + child->drawLayer(args, canvas, child->_alpha * alpha, blendMode, transform); + return; + } + auto offscreenCanvas = + context3D->beginRecording(transform3D, child->bitFields.allowsEdgeAntialiasing); + child->drawLayer(args, offscreenCanvas, child->_alpha * alpha, BlendMode::SrcOver, nullptr); + context3D->endRecording(); + if (started3DContext) { + context3D->finishAndDrawTo(canvas, child->bitFields.allowsEdgeAntialiasing); + } +} + float Layer::drawBackgroundLayers(const DrawArgs& args, Canvas* canvas) { if (!_parent) { return _alpha; @@ -1761,7 +1963,7 @@ float Layer::drawBackgroundLayers(const DrawArgs& args, Canvas* canvas) { // clipping rectangle. Otherwise, the canvas does not carry this matrix information, and clipping // needs to be performed by transforming the Path. if (bitFields.matrix3DIsAffine) { - canvas->concat(GetMayLossyAffineMatrix(getMatrixWithScrollRect())); + canvas->concat(Matrix3DUtils::GetMayLossyAffineMatrix(getMatrixWithScrollRect())); if (_scrollRect) { canvas->clipRect(*_scrollRect); } @@ -1788,6 +1990,8 @@ std::unique_ptr Layer::getLayerStyleSource(const DrawArgs& arg DrawArgs drawArgs = args; drawArgs.blurBackground = nullptr; drawArgs.excludeEffects = bitFields.excludeChildEffectsInLayerStyle; + // Layer style source content should be rendered to a regular texture, not to 3D compositor. + drawArgs.render3DContext = nullptr; // Use Mode::Contour to record the contour of the content, to prevent the subsequent use of // AlphaThresholdFilter from turning semi-transparent pixels into opaque pixels, which would cause // severe aliasing. @@ -1840,8 +2044,8 @@ std::shared_ptr Layer::getBackgroundImage(const DrawArgs& args, float con // the root node, making it impossible to obtain an accurate background image and stretch it into // a rectangle, please use the getBoundsBackgroundImage interface to get the background image // corresponding to the minimum bounding rectangle of the current layer subtree after drawing. - DEBUG_ASSERT(IsMatrix3DAffine(localToGlobalMatrix)); - auto affiineLocalToGlobalMatrix = GetMayLossyAffineMatrix(localToGlobalMatrix); + DEBUG_ASSERT(Matrix3DUtils::IsMatrix3DAffine(localToGlobalMatrix)); + auto affiineLocalToGlobalMatrix = Matrix3DUtils::GetMayLossyAffineMatrix(localToGlobalMatrix); Matrix affineGlobalToLocalMatrix = {}; if (!affiineLocalToGlobalMatrix.invert(&affineGlobalToLocalMatrix)) { return nullptr; @@ -1867,8 +2071,7 @@ std::shared_ptr Layer::getBoundsBackgroundImage(const DrawArgs& args, flo // Calculate the transformation matrix for drawing the background image within renderBounds to // bounds. auto matrix = Matrix::MakeTrans(-renderBounds.left, -renderBounds.top); - matrix.postScale(bounds.width() * contentScale / renderBounds.width(), - bounds.height() * contentScale / renderBounds.height()); + matrix.postScale(bounds.width() / renderBounds.width(), bounds.height() / renderBounds.height()); matrix.postTranslate(bounds.left, bounds.top); canvas->setMatrix(matrix); @@ -1904,15 +2107,6 @@ void Layer::drawBackgroundImage(const DrawArgs& args, Canvas& canvas) { void Layer::drawLayerStyles(const DrawArgs& args, Canvas* canvas, float alpha, const LayerStyleSource* source, LayerStylePosition position) { - std::vector extraSourceTypes = {LayerStyleExtraSourceType::None, - LayerStyleExtraSourceType::Contour, - LayerStyleExtraSourceType::Background}; - drawLayerStyles(args, canvas, alpha, source, position, extraSourceTypes); -} - -void Layer::drawLayerStyles(const DrawArgs& args, Canvas* canvas, float alpha, - const LayerStyleSource* source, LayerStylePosition position, - const std::vector& extraSourceTypes) { DEBUG_ASSERT(source != nullptr && !FloatNearlyZero(source->contentScale)); auto& contour = source->contour; auto contourOffset = source->contourOffset - source->contentOffset; @@ -1922,8 +2116,7 @@ void Layer::drawLayerStyles(const DrawArgs& args, Canvas* canvas, float alpha, for (const auto& layerStyle : _layerStyles) { DEBUG_ASSERT(layerStyle != nullptr); if (layerStyle->position() != position || - std::find(extraSourceTypes.begin(), extraSourceTypes.end(), - layerStyle->extraSourceType()) == extraSourceTypes.end()) { + !HasStyleSource(args.styleSourceTypes, layerStyle->extraSourceType())) { continue; } PictureRecorder recorder = {}; @@ -1945,7 +2138,7 @@ void Layer::drawLayerStyles(const DrawArgs& args, Canvas* canvas, float alpha, // background image corresponding to the minimum axis-aligned bounding rectangle after // projection can be obtained. auto background = - IsMatrix3DAffine(getGlobalMatrix()) + Matrix3DUtils::IsMatrix3DAffine(getGlobalMatrix()) ? getBackgroundImage(args, source->contentScale, &backgroundOffset) : getBoundsBackgroundImage(args, source->contentScale, &backgroundOffset); if (background != nullptr) { @@ -1988,7 +2181,7 @@ void Layer::drawLayerStyles(const DrawArgs& args, Canvas* canvas, float alpha, } void Layer::drawBackgroundLayerStyles(const DrawArgs& args, Canvas* canvas, float alpha, - const Matrix3D& transform) { + const Matrix3D& transform3D) { auto styleSource = getLayerStyleSource(args, canvas->getMatrix(), true); if (styleSource == nullptr) { return; @@ -1999,7 +2192,7 @@ void Layer::drawBackgroundLayerStyles(const DrawArgs& args, Canvas* canvas, floa // ensuring that only the actual rendering region of the layer reveals the background. std::shared_ptr styleSourceFilter = nullptr; auto bounds = getBounds(); - auto transformedBounds = transform.mapRect(bounds); + auto transformedBounds = transform3D.mapRect(bounds); transformedBounds.roundOut(); if (styleSource->content != nullptr) { // The object of styleSourceMatrix is Image. When drawing directly, Image doesn't care about @@ -2008,11 +2201,12 @@ void Layer::drawBackgroundLayerStyles(const DrawArgs& args, Canvas* canvas, floa // StyleSourceMatrix acts on the content of styleSource, which is a subregion extracted from // within the Layer. For transformations based on the local coordinate system of this subregion, // anchor point adaptation is required for the matrix described based on the layer. - auto styleSourceMatrix = anchorAdaptedMatrix(transform, styleSourceAnchor); + auto styleSourceMatrix = Matrix3DUtils::OriginAdaptedMatrix3D(transform3D, styleSourceAnchor); styleSourceMatrix.postScale(bounds.width() / transformedBounds.width(), bounds.height() / transformedBounds.height(), 1.0f); - auto transform3DFilter = Transform3DFilter::Make(styleSourceMatrix); - styleSourceFilter = transform3DFilter->getImageFilter(styleSource->contentScale); + styleSourceMatrix = + Matrix3DUtils::ScaleAdaptedMatrix3D(styleSourceMatrix, styleSource->contentScale); + styleSourceFilter = std::make_shared(styleSourceMatrix); styleSource->content = styleSource->content->makeWithFilter(styleSourceFilter); } @@ -2025,7 +2219,7 @@ void Layer::drawBackgroundLayerStyles(const DrawArgs& args, Canvas* canvas, floa Path styleClipPath = {}; AutoCanvasRestore autoRestore(canvas); styleClipPath.addRect(bounds); - styleClipPath.transform3D(transform); + styleClipPath.transform3D(transform3D); // StyleClipPath is the final clipping path based on the parent node, which must be called // before setting the matrix, otherwise it will be affected by the matrix. canvas->clipPath(styleClipPath); @@ -2040,8 +2234,9 @@ void Layer::drawBackgroundLayerStyles(const DrawArgs& args, Canvas* canvas, floa canvas->concat(styleMatrix); // When LayerStyle's ExtraSourceType is Background, its Position can only be Below, so there's no // need to handle the Position Above case here. - drawLayerStyles(args, canvas, alpha, styleSource.get(), LayerStylePosition::Below, - {LayerStyleExtraSourceType::Background}); + auto styleArgs = args; + styleArgs.styleSourceTypes = {LayerStyleExtraSourceType::Background}; + drawLayerStyles(styleArgs, canvas, alpha, styleSource.get(), LayerStylePosition::Below); } bool Layer::getLayersUnderPointInternal(float x, float y, @@ -2105,21 +2300,13 @@ void Layer::updateRenderBounds(std::shared_ptr transformer, b maxBackgroundOutset = 0; minBackgroundOutset = std::numeric_limits::max(); auto contentScale = 1.0f; - // Ensure the 3D filter is not released during the entire function lifetime, otherwise the - // transformer will have abnormal render bounds calculation - auto filter3DVector = std::vector>{}; - if (!_layerStyles.empty() || !_filters.empty() || !bitFields.matrix3DIsAffine) { + if (!_layerStyles.empty() || !_filters.empty()) { + // Filters and styles interrupt 3D rendering context, so non-root layers inside 3D rendering + // context can ignore parent filters and styles when calculating dirty regions. + DEBUG_ASSERT(!canPreserve3D()); if (transformer) { contentScale = transformer->getMaxScale(); } - // The filter and style should calculate bounds based on the original size. The externally - // provided Transformer already contains matrix data, which will be applied to the computed size - // at the end, including scaling, rotation, and other transformations. - if (!bitFields.matrix3DIsAffine) { - filter3DVector.push_back(Transform3DFilter::Make(_matrix3D)); - transformer = - RegionTransformer::MakeFromFilters(filter3DVector, 1.0f, std::move(transformer)); - } transformer = RegionTransformer::MakeFromFilters(_filters, 1.0f, std::move(transformer)); transformer = RegionTransformer::MakeFromStyles(_layerStyles, 1.0f, std::move(transformer)); } @@ -2156,13 +2343,19 @@ void Layer::updateRenderBounds(std::shared_ptr transformer, b continue; } auto childMatrix = child->getMatrixWithScrollRect(); + std::shared_ptr childTransformer = nullptr; + if (canPreserve3D() || child->canPreserve3D()) { + // Child is inside a 3D rendering context - allow combining matrices. + childTransformer = RegionTransformer::MakeFromMatrix3D(childMatrix, transformer, true); + } else if (child->bitFields.matrix3DIsAffine) { + // Child is a 2D layer outside 3D context. + childTransformer = RegionTransformer::MakeFromMatrix( + Matrix3DUtils::GetMayLossyAffineMatrix(childMatrix), transformer); + } else { + // Child has 3D transform but is outside 3D context - don't combine matrices. + childTransformer = RegionTransformer::MakeFromMatrix3D(childMatrix, transformer); + } std::optional clipRect = std::nullopt; - // If the sublayer's Matrix contains 3D transformations or projection transformations, then its - // Matrix is considered as an identity matrix here, and its actual position is left to the - // sublayer to calculate through the 3D filter method. - auto childAffineMatrix = - IsMatrix3DAffine(childMatrix) ? GetMayLossyAffineMatrix(childMatrix) : Matrix::I(); - auto childTransformer = RegionTransformer::MakeFromMatrix(childAffineMatrix, transformer); if (child->_scrollRect) { clipRect = *child->_scrollRect; } @@ -2185,21 +2378,23 @@ void Layer::updateRenderBounds(std::shared_ptr transformer, b childTransformer = RegionTransformer::MakeFromClip(*clipRect, std::move(childTransformer)); } auto childForceDirty = forceDirty || child->bitFields.dirtyTransform; - child->updateRenderBounds(childTransformer, childForceDirty); + child->updateRenderBounds(std::move(childTransformer), childForceDirty); child->bitFields.dirtyTransform = false; if (!child->maskOwner) { renderBounds.join(child->renderBounds); } } auto backOutset = 0.f; - for (auto& style : _layerStyles) { - DEBUG_ASSERT(style != nullptr); - if (style->extraSourceType() != LayerStyleExtraSourceType::Background) { - continue; + if (!renderBounds.isEmpty()) { + for (auto& style : _layerStyles) { + DEBUG_ASSERT(style != nullptr); + if (style->extraSourceType() != LayerStyleExtraSourceType::Background) { + continue; + } + auto outset = style->filterBackground(Rect::MakeEmpty(), contentScale); + backOutset = std::max(backOutset, outset.right); + backOutset = std::max(backOutset, outset.bottom); } - auto outset = style->filterBackground(Rect::MakeEmpty(), contentScale); - backOutset = std::max(backOutset, outset.right); - backOutset = std::max(backOutset, outset.bottom); } if (backOutset > 0) { maxBackgroundOutset = std::max(backOutset, maxBackgroundOutset); @@ -2222,8 +2417,9 @@ void Layer::checkBackgroundStyles(std::shared_ptr transformer // When marking the dirty region for 3D layer background styles, the influence range of layer // styles acts on the final projected Bounds, which has already applied 3D matrix transformation, // so the identity matrix can be directly used here. - auto affineChildMatrix = - IsMatrix3DAffine(childMatrix) ? GetMayLossyAffineMatrix(childMatrix) : Matrix::I(); + auto affineChildMatrix = Matrix3DUtils::IsMatrix3DAffine(childMatrix) + ? Matrix3DUtils::GetMayLossyAffineMatrix(childMatrix) + : Matrix::I(); auto childTransformer = RegionTransformer::MakeFromMatrix(affineChildMatrix, transformer); child->checkBackgroundStyles(childTransformer); } @@ -2298,15 +2494,9 @@ std::shared_ptr Layer::createBackgroundContext( minBackgroundOutset * scale, viewMatrix, colorSpace); } -Matrix3D Layer::anchorAdaptedMatrix(const Matrix3D& matrix, const Point& anchor) const { - // In the new coordinate system defined with anchor as the anchor point and origin, the reference - // anchor point for the matrix transformation described by the layer's _matrix is located at - // point (-anchor.x, -anchor.y). That is, to maintain the same transformation effect, first - // translate the anchor point to the new coordinate origin, apply the original matrix, and then - // reverse translate the anchor point. - auto offsetMatrix = Matrix3D::MakeTranslate(anchor.x, anchor.y, 0); - auto invOffsetMatrix = Matrix3D::MakeTranslate(-anchor.x, -anchor.y, 0); - return invOffsetMatrix * matrix * offsetMatrix; +bool Layer::canPreserve3D() const { + return _preserve3D && _filters.empty() && _layerStyles.empty() && !hasValidMask() && + _scrollRect == nullptr; } void Layer::invalidateSubtree() { diff --git a/src/layers/RegionTransformer.cpp b/src/layers/RegionTransformer.cpp index eed60193a..fe1e329ca 100644 --- a/src/layers/RegionTransformer.cpp +++ b/src/layers/RegionTransformer.cpp @@ -17,6 +17,7 @@ ///////////////////////////////////////////////////////////////////////////////////////////////// #include "RegionTransformer.h" +#include "core/Matrix3DUtils.h" #include "core/utils/Log.h" namespace tgfx { @@ -98,6 +99,31 @@ class MatrixRegionTransformer : public RegionTransformer { } }; +class Matrix3DRegionTransformer : public RegionTransformer { + public: + Matrix3DRegionTransformer(const Matrix3D& matrix, std::shared_ptr outer, + bool canCombine = false) + : RegionTransformer(std::move(outer)), matrix(matrix), canCombine(canCombine) { + } + + Matrix3D matrix; + bool canCombine = false; + + protected: + void onTransform(Rect* bounds) const override { + // Check if the rect is behind the camera before applying 3D transformation + if (Matrix3DUtils::IsRectBehindCamera(*bounds, matrix)) { + bounds->setEmpty(); + return; + } + *bounds = matrix.mapRect(*bounds); + } + + bool isMatrix3D() const override { + return true; + } +}; + std::shared_ptr RegionTransformer::MakeFromClip( const Rect& clipRect, std::shared_ptr outer) { if (!outer || !outer->isClip()) { @@ -142,6 +168,28 @@ std::shared_ptr RegionTransformer::MakeFromMatrix( return std::make_shared(combineMatrix, outer->outer); } +std::shared_ptr RegionTransformer::MakeFromMatrix3D( + const Matrix3D& matrix, std::shared_ptr outer, bool canCombine) { + if (matrix == Matrix3D::I()) { + return outer; + } + + if (!outer || !outer->isMatrix3D()) { + return std::make_shared(matrix, std::move(outer), canCombine); + } + + auto outerMatrix3D = static_cast(outer.get()); + // Only combine if both allow combining + if (canCombine && outerMatrix3D->canCombine) { + auto combineMatrix = matrix; + combineMatrix.postConcat(outerMatrix3D->matrix); + return std::make_shared(combineMatrix, outer->outer, canCombine); + } + + // Don't combine, create new transformer + return std::make_shared(matrix, std::move(outer), canCombine); +} + RegionTransformer::RegionTransformer(std::shared_ptr outer) : outer(std::move(outer)) { } @@ -170,4 +218,17 @@ void RegionTransformer::getTotalMatrix(Matrix* matrix) const { } } +std::optional RegionTransformer::getConsecutiveMatrix3D() const { + if (!isMatrix3D()) { + return std::nullopt; + } + auto result = static_cast(this)->matrix; + auto current = outer; + while (current && current->isMatrix3D()) { + result.postConcat(static_cast(current.get())->matrix); + current = current->outer; + } + return result; +} + } // namespace tgfx diff --git a/src/layers/RegionTransformer.h b/src/layers/RegionTransformer.h index 3f1c36a60..e8fbc679f 100644 --- a/src/layers/RegionTransformer.h +++ b/src/layers/RegionTransformer.h @@ -18,11 +18,15 @@ #pragma once +#include +#include +#include "tgfx/core/Matrix3D.h" #include "tgfx/core/Rect.h" #include "tgfx/layers/filters/LayerFilter.h" #include "tgfx/layers/layerstyles/LayerStyle.h" namespace tgfx { + /** * The RegionTransformer class is used to transform a rectangle region. */ @@ -54,6 +58,17 @@ class RegionTransformer { static std::shared_ptr MakeFromMatrix( const Matrix& matrix, std::shared_ptr outer = nullptr); + /** + * Creates a RegionTransformer that applies the given 3D matrix transformation to the given + * rectangle. + * @param matrix The 3D transformation matrix to apply. + * @param outer The outer RegionTransformer to chain with. + * @param canCombine Whether this transformer can combine with outer Matrix3D transformers. + */ + static std::shared_ptr MakeFromMatrix3D( + const Matrix3D& matrix, std::shared_ptr outer = nullptr, + bool canCombine = false); + explicit RegionTransformer(std::shared_ptr outer); virtual ~RegionTransformer() = default; @@ -65,6 +80,12 @@ class RegionTransformer { float getMaxScale() const; + /** + * Returns the accumulated matrix from consecutive Matrix3DRegionTransformers. + * Returns nullopt if this transformer is not a Matrix3DRegionTransformer. + */ + std::optional getConsecutiveMatrix3D() const; + protected: virtual void onTransform(Rect* bounds) const = 0; @@ -76,6 +97,10 @@ class RegionTransformer { return false; } + virtual bool isMatrix3D() const { + return false; + } + void getTotalMatrix(Matrix* matrix) const; private: diff --git a/src/layers/compositing3d/BspTree.cpp b/src/layers/compositing3d/BspTree.cpp new file mode 100644 index 000000000..7874b96fb --- /dev/null +++ b/src/layers/compositing3d/BspTree.cpp @@ -0,0 +1,89 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "BspTree.h" + +namespace tgfx { + +BspNode::BspNode(std::unique_ptr polygon) : data(std::move(polygon)) { +} + +BspNode::~BspNode() = default; + +BspTree::BspTree(std::deque> polygons) { + if (polygons.empty()) { + return; + } + + _root = std::make_unique(std::move(polygons.front())); + polygons.pop_front(); + buildTree(_root.get(), &polygons); +} + +BspTree::~BspTree() = default; + +/** + * Recursively partitions polygons by the node's plane into front/back lists, then builds subtrees. + * Complexity: O(n log n) average case, O(n * 2^n) worst case when every split intersects all + * remaining polygons. + */ +void BspTree::buildTree(BspNode* node, std::deque>* polygons) { + std::deque> frontList; + std::deque> backList; + + while (!polygons->empty()) { + auto polygon = std::move(polygons->front()); + polygons->pop_front(); + + std::unique_ptr newFront; + std::unique_ptr newBack; + bool isCoplanar = false; + + node->data->splitAnother(std::move(polygon), &newFront, &newBack, &isCoplanar); + + if (isCoplanar) { + if (newFront) { + node->coplanarsFront.push_back(std::move(newFront)); + } + if (newBack) { + node->coplanarsBack.push_back(std::move(newBack)); + } + } else { + if (newFront) { + frontList.push_back(std::move(newFront)); + } + if (newBack) { + backList.push_back(std::move(newBack)); + } + } + } + + if (!backList.empty()) { + node->backChild = std::make_unique(std::move(backList.front())); + backList.pop_front(); + buildTree(node->backChild.get(), &backList); + } + + if (!frontList.empty()) { + node->frontChild = std::make_unique(std::move(frontList.front())); + frontList.pop_front(); + buildTree(node->frontChild.get(), &frontList); + } +} + +} // namespace tgfx diff --git a/src/layers/compositing3d/BspTree.h b/src/layers/compositing3d/BspTree.h new file mode 100644 index 000000000..8a6215ade --- /dev/null +++ b/src/layers/compositing3d/BspTree.h @@ -0,0 +1,107 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include "DrawPolygon3D.h" + +namespace tgfx { + +/** + * BspNode represents a node in the BSP tree. + * Front/back are defined relative to the normal of the plane represented by 'data'. + */ +struct BspNode { + explicit BspNode(std::unique_ptr data); + ~BspNode(); + + std::unique_ptr data = nullptr; + std::vector> coplanarsFront = {}; + std::vector> coplanarsBack = {}; + std::unique_ptr frontChild = nullptr; + std::unique_ptr backChild = nullptr; +}; + +/** + * BspTree implements Binary Space Partitioning for correct depth sorting of 3D polygons. + * It splits intersecting polygons along plane intersections. + */ +class BspTree { + public: + /** + * Constructs a BSP tree from a list of polygons. + * The first polygon is used as the root splitting plane. + * @param polygons The list of polygons to process. + */ + explicit BspTree(std::deque> polygons); + + ~BspTree(); + + /** + * Traverses the tree in back-to-front order relative to the camera. + * Calls the action handler for each polygon in correct depth order. + * @param action A callable that takes a const DrawPolygon3D* parameter. + */ + template + void traverseBackToFront(Action action) const { + if (_root) { + traverseNode(action, _root.get()); + } + } + + private: + void buildTree(BspNode* node, std::deque>* polygons); + + template + void visitNode(Action& action, const BspNode* node, const BspNode* firstChild, + const BspNode* secondChild, + const std::vector>& firstCoplanars, + const std::vector>& secondCoplanars) const { + if (firstChild) { + traverseNode(action, firstChild); + } + for (const auto& polygon : firstCoplanars) { + action(polygon.get()); + } + action(node->data.get()); + for (const auto& polygon : secondCoplanars) { + action(polygon.get()); + } + if (secondChild) { + traverseNode(action, secondChild); + } + } + + template + void traverseNode(Action& action, const BspNode* node) const { + if (node->data->isFacingPositiveZ()) { + visitNode(action, node, node->backChild.get(), node->frontChild.get(), node->coplanarsBack, + node->coplanarsFront); + } else { + visitNode(action, node, node->frontChild.get(), node->backChild.get(), node->coplanarsFront, + node->coplanarsBack); + } + } + + std::unique_ptr _root = nullptr; +}; + +} // namespace tgfx diff --git a/src/layers/compositing3d/Context3DCompositor.cpp b/src/layers/compositing3d/Context3DCompositor.cpp new file mode 100644 index 000000000..e333e8443 --- /dev/null +++ b/src/layers/compositing3d/Context3DCompositor.cpp @@ -0,0 +1,230 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "Context3DCompositor.h" +#include +#include "BspTree.h" +#include "core/images/TextureImage.h" +#include "core/utils/MathExtra.h" +#include "gpu/DrawingManager.h" +#include "gpu/ProxyProvider.h" +#include "gpu/QuadRecord.h" +#include "gpu/QuadsVertexProvider.h" +#include "gpu/ops/Quads3DDrawOp.h" +#include "gpu/processors/TextureEffect.h" + +namespace tgfx { + +// Tolerance for determining if a vertex lies on the original rectangle's edge. +static constexpr float kAAEpsilon = 0.01f; + +// Flags indicating the edges of a rectangle. +static constexpr unsigned RECT_EDGE_LEFT = 0b1000; +static constexpr unsigned RECT_EDGE_TOP = 0b0001; +static constexpr unsigned RECT_EDGE_RIGHT = 0b0010; +static constexpr unsigned RECT_EDGE_BOTTOM = 0b0100; + +static inline AAType GetAAType(int sampleCount, bool antiAlias) { + if (sampleCount > 1) { + return AAType::MSAA; + } + if (antiAlias) { + return AAType::Coverage; + } + return AAType::None; +} + +/** + * Determines which edges of the rect this point lies on. + */ +static unsigned DeterminePointOnRectEdge(const Point& point, const Rect& rect) { + unsigned edges = 0; + if (std::abs(point.x - rect.left) < kAAEpsilon) { + edges |= RECT_EDGE_LEFT; + } + if (std::abs(point.x - rect.right) < kAAEpsilon) { + edges |= RECT_EDGE_RIGHT; + } + if (std::abs(point.y - rect.top) < kAAEpsilon) { + edges |= RECT_EDGE_TOP; + } + if (std::abs(point.y - rect.bottom) < kAAEpsilon) { + edges |= RECT_EDGE_BOTTOM; + } + return edges; +} + +/** + * Returns true if both endpoints of a quad edge lie on the same edge of the original rectangle, + * indicating that this edge is an exterior edge of the original rectangle. + */ +static bool IsExteriorEdge(unsigned cornerAEdge, unsigned cornerBEdge) { + return (cornerAEdge & cornerBEdge) != 0; +} + +/** + * Computes per-edge AA flags for a quad. An edge needs AA if both endpoints lie on the same + * edge of the original rect (i.e., it's an exterior edge, not a BSP split edge). + */ +static unsigned GetQuadAAFlags(const QuadCW& quad, const Rect& rect) { + const unsigned p0 = DeterminePointOnRectEdge(quad.point(0), rect); + const unsigned p1 = DeterminePointOnRectEdge(quad.point(1), rect); + const unsigned p2 = DeterminePointOnRectEdge(quad.point(2), rect); + const unsigned p3 = DeterminePointOnRectEdge(quad.point(3), rect); + + unsigned aaFlags = QUAD_AA_FLAG_NONE; + if (IsExteriorEdge(p0, p1)) { + aaFlags |= QUAD_AA_FLAG_EDGE_01; + } + if (IsExteriorEdge(p1, p2)) { + aaFlags |= QUAD_AA_FLAG_EDGE_12; + } + if (IsExteriorEdge(p2, p3)) { + aaFlags |= QUAD_AA_FLAG_EDGE_23; + } + if (IsExteriorEdge(p3, p0)) { + aaFlags |= QUAD_AA_FLAG_EDGE_30; + } + return aaFlags; +} + +/** + * Returns the quad to draw and its AA flags based on whether it's a sub-quad or the original rect. + */ +static std::pair GetQuadAndAAFlags(const Rect& originalRect, AAType aaType, + const QuadCW* subQuad) { + QuadCW quad; + unsigned aaFlags = QUAD_AA_FLAG_NONE; + + if (subQuad != nullptr) { + quad = *subQuad; + if (aaType == AAType::Coverage) { + aaFlags = GetQuadAAFlags(quad, originalRect); + } + } else { + quad = QuadCW(Point::Make(originalRect.left, originalRect.top), + Point::Make(originalRect.right, originalRect.top), + Point::Make(originalRect.right, originalRect.bottom), + Point::Make(originalRect.left, originalRect.bottom)); + if (aaType == AAType::Coverage) { + aaFlags = QUAD_AA_FLAG_ALL; + } + } + + return {quad, aaFlags}; +} + +Context3DCompositor::Context3DCompositor(const Context& context, int width, int height) + : _width(width), _height(height) { + _targetColorProxy = + context.proxyProvider()->createRenderTargetProxy({}, width, height, PixelFormat::RGBA_8888); + DEBUG_ASSERT(_targetColorProxy != nullptr); +} + +void Context3DCompositor::addImage(std::shared_ptr image, const Matrix3D& matrix, + float alpha, bool antiAlias) { + auto polygon = std::make_unique(std::move(image), matrix, _nextOrderIndex++, alpha, + antiAlias); + _polygons.push_back(std::move(polygon)); +} + +void Context3DCompositor::drawPolygon(const DrawPolygon3D* polygon) { + if (!polygon->isSplit()) { + drawQuads(polygon, {}); + } else { + drawQuads(polygon, polygon->toQuads()); + } +} + +void Context3DCompositor::drawQuads(const DrawPolygon3D* polygon, + const std::vector& subQuads) { + DEBUG_ASSERT(_targetColorProxy != nullptr); + auto context = _targetColorProxy->getContext(); + DEBUG_ASSERT(context != nullptr); + auto aaType = GetAAType(_targetColorProxy->sampleCount(), polygon->antiAlias()); + const auto& image = polygon->image(); + auto srcW = static_cast(image->width()); + auto srcH = static_cast(image->height()); + Rect originalRect = Rect::MakeWH(srcW, srcH); + + auto allocator = context->drawingAllocator(); + // Wrap alpha as vertex color to enable semi-transparent pixel blending. + Color vertexColor(1, 1, 1, polygon->alpha()); + std::vector> quadRecords; + if (subQuads.empty()) { + auto [quad, aaFlags] = GetQuadAndAAFlags(originalRect, aaType, nullptr); + quadRecords.push_back(allocator->make(quad, aaFlags, vertexColor)); + } else { + for (const auto& subQuad : subQuads) { + auto [quad, aaFlags] = GetQuadAndAAFlags(originalRect, aaType, &subQuad); + quadRecords.push_back(allocator->make(quad, aaFlags, vertexColor)); + } + } + + // Flatten z-axis to keep vertices at their original depth, preventing clipping space culling. + auto matrix = polygon->matrix(); + matrix.setRow(2, {0, 0, 1, 0}); + auto widthF = static_cast(_width); + auto heightF = static_cast(_height); + // Map projected vertex coordinates from render target texture space to NDC space. + const Vec2 ndcScale(2.0f / widthF, 2.0f / heightF); + const Vec2 ndcOffset(-1.f, -1.f); + auto vertexProvider = QuadsVertexProvider::MakeFrom(allocator, std::move(quadRecords), aaType); + const Size viewportSize(static_cast(_width), static_cast(_height)); + const Quads3DDrawArgs drawArgs{matrix, ndcScale, ndcOffset, viewportSize}; + auto drawOp = Quads3DDrawOp::Make(context, std::move(vertexProvider), 0, drawArgs); + + const SamplingArgs samplingArgs = {TileMode::Clamp, TileMode::Clamp, {}, SrcRectConstraint::Fast}; + auto textureImage = image->makeTextureImage(context); + if (textureImage == nullptr) { + return; + } + auto sourceTextureProxy = std::static_pointer_cast(textureImage)->getTextureProxy(); + /** + * Ensure the vertex texture sampling coordinates are in the range [0, 1]. The size obtained from + * Image is the original size, while the texture size generated by Image is the size after + * applying DrawScale. Texture sampling requires corresponding scaling. + */ + DEBUG_ASSERT(srcW > 0 && srcH > 0); + auto uvMatrix = Matrix::MakeScale(static_cast(sourceTextureProxy->width()) / srcW, + static_cast(sourceTextureProxy->height()) / srcH); + auto fragmentProcessor = + TextureEffect::Make(allocator, std::move(sourceTextureProxy), samplingArgs, &uvMatrix); + drawOp->addColorFP(std::move(fragmentProcessor)); + _drawOps.emplace_back(std::move(drawOp)); +} + +std::shared_ptr Context3DCompositor::finish() { + auto context = _targetColorProxy->getContext(); + DEBUG_ASSERT(context != nullptr); + + if (!_polygons.empty()) { + BspTree bspTree(std::move(_polygons)); + _polygons.clear(); + bspTree.traverseBackToFront([this](const DrawPolygon3D* polygon) { drawPolygon(polygon); }); + } + + auto opArray = context->drawingAllocator()->makeArray(std::move(_drawOps)); + context->drawingManager()->addOpsRenderTask(_targetColorProxy, std::move(opArray), + PMColor::Transparent()); + auto image = TextureImage::Wrap(_targetColorProxy->asTextureProxy(), ColorSpace::SRGB()); + _targetColorProxy = nullptr; + return image; +} + +} // namespace tgfx diff --git a/src/layers/compositing3d/Context3DCompositor.h b/src/layers/compositing3d/Context3DCompositor.h new file mode 100644 index 000000000..2a9f85ea8 --- /dev/null +++ b/src/layers/compositing3d/Context3DCompositor.h @@ -0,0 +1,78 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include "DrawPolygon3D.h" +#include "core/utils/PlacementPtr.h" +#include "gpu/ops/DrawOp.h" +#include "gpu/proxies/RenderTargetProxy.h" + +namespace tgfx { + +/** + * Context3DCompositor handles compositing of 3D transformed images using BSP tree for correct + * depth sorting. It splits intersecting regions to ensure correct occlusion and blending order. + */ +class Context3DCompositor { + public: + Context3DCompositor(const Context& context, int width, int height); + + /** + * Returns the width of the compositor in pixels. + */ + int width() const { + return _width; + } + + /** + * Returns the height of the compositor in pixels. + */ + int height() const { + return _height; + } + + /** + * Adds an image with 3D transformation for compositing. + * @param image The source image to draw. + * @param matrix The 3D transformation matrix applied to the image. + * @param alpha The layer alpha for transparency. + * @param antiAlias Whether to enable edge antialiasing when the render target does not support MSAA. + */ + void addImage(std::shared_ptr image, const Matrix3D& matrix, float alpha, bool antiAlias); + + /** + * Draws all added images with correct depth ordering and blending. + * @return The composited image. + */ + std::shared_ptr finish(); + + private: + void drawPolygon(const DrawPolygon3D* polygon); + void drawQuads(const DrawPolygon3D* polygon, const std::vector& subQuads); + + int _width = 0; + int _height = 0; + std::shared_ptr _targetColorProxy = nullptr; + std::deque> _polygons = {}; + std::vector> _drawOps = {}; + int _nextOrderIndex = 0; +}; + +} // namespace tgfx diff --git a/src/layers/compositing3d/DrawPolygon3D.cpp b/src/layers/compositing3d/DrawPolygon3D.cpp new file mode 100644 index 000000000..92e5105f6 --- /dev/null +++ b/src/layers/compositing3d/DrawPolygon3D.cpp @@ -0,0 +1,256 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "DrawPolygon3D.h" +#include +#include +#include "core/utils/Log.h" +#include "core/utils/MathExtra.h" + +namespace tgfx { + +// Distance tolerance for determining which side of a plane a point lies on. +static constexpr float SplitThreshold = 0.05f; + +static Vec3 InterpolatePoint(const Vec3& from, const Vec3& to, float delta) { + return Vec3(from.x + (to.x - from.x) * delta, from.y + (to.y - from.y) * delta, + from.z + (to.z - from.z) * delta); +} + +static size_t NextIndex(size_t i, size_t count) { + return (i + 1) % count; +} + +static size_t PrevIndex(size_t i, size_t count) { + return (i + count - 1) % count; +} + +static Point ProjectPoint(const Matrix3D& matrix, const Vec3& point) { + auto result = matrix.mapPoint(point); + return Point::Make(result.x, result.y); +} + +static void CollectSplitPoints(const std::vector& points, const Vec3& startIntersection, + const Vec3& endIntersection, size_t beginIndex, size_t endIndex, + std::vector* result) { + result->push_back(startIntersection); + size_t numPoints = points.size(); + for (size_t index = beginIndex; index != endIndex; index = NextIndex(index, numPoints)) { + result->push_back(points[index]); + } + if (result->back() != endIntersection) { + result->push_back(endIntersection); + } +} + +DrawPolygon3D::DrawPolygon3D(std::shared_ptr image, const Matrix3D& matrix, int orderIndex, + float alpha, bool antiAlias) + : _orderIndex(orderIndex), _alpha(alpha), _antiAlias(antiAlias), _image(std::move(image)), + _matrix(matrix) { + auto srcW = static_cast(_image->width()); + auto srcH = static_cast(_image->height()); + + Vec3 corners[4] = { + Vec3(0.0f, 0.0f, 0.0f), + Vec3(srcW, 0.0f, 0.0f), + Vec3(srcW, srcH, 0.0f), + Vec3(0.0f, srcH, 0.0f), + }; + + // Caller guarantees that vertices after transformation do not intersect the observer's z-plane. + _points.reserve(4); + for (const auto& corner : corners) { + _points.push_back(matrix.mapPoint(corner)); + } + + constructNormal(); +} + +DrawPolygon3D::DrawPolygon3D(std::shared_ptr image, const Matrix3D& matrix, + std::vector points, const Vec3& normal, int orderIndex, + float alpha, bool antiAlias) + : _points(std::move(points)), _normal(normal), _orderIndex(orderIndex), _isSplit(true), + _alpha(alpha), _antiAlias(antiAlias), _image(std::move(image)), _matrix(matrix) { +} + +// Computes the normal by averaging cross products of opposite vertex pairs from the first vertex. +// Works correctly for convex polygons with 3+ vertices. +void DrawPolygon3D::constructNormal() { + Vec3 newNormal(0.0f, 0.0f, 0.0f); + auto delta = _points.size() / 2; + for (size_t i = 1; i + delta < _points.size(); i++) { + auto v1 = _points[i] - _points[0]; + auto v2 = _points[i + delta] - _points[0]; + newNormal += Vec3::Cross(v1, v2); + } + + float length = newNormal.length(); + if (!FloatNearlyZero(length) && !FloatNearlyEqual(length, 1.0f)) { + newNormal = newNormal * (1.0f / length); + } + _normal = newNormal; +} + +float DrawPolygon3D::signedDistanceTo(const Vec3& point) const { + return Vec3::Dot(point - _points[0], _normal); +} + +void DrawPolygon3D::splitAnother(std::unique_ptr polygon, + std::unique_ptr* front, + std::unique_ptr* back, bool* isCoplanar) const { + // Tolerance for checking if the normal vector has unit length. + DEBUG_ASSERT(std::abs(_normal.lengthSquared() - 1.0f) <= 0.001f); + + const size_t numPoints = polygon->_points.size(); + std::vector vertexDistance(numPoints); + size_t posCount = 0; + size_t negCount = 0; + + for (size_t i = 0; i < numPoints; i++) { + vertexDistance[i] = signedDistanceTo(polygon->_points[i]); + if (vertexDistance[i] < -SplitThreshold) { + ++negCount; + } else if (vertexDistance[i] > SplitThreshold) { + ++posCount; + } else { + vertexDistance[i] = 0.0f; + } + } + + // The polygon is coplanar with this polygon. + if (posCount == 0 && negCount == 0) { + *isCoplanar = true; + // TGFX uses post-order traversal: smaller orderIndex (child) should be in front (drawn later). + if (polygon->_orderIndex <= _orderIndex) { + *front = std::move(polygon); + } else { + *back = std::move(polygon); + } + return; + } + + // The polygon is entirely on one side of this polygon. + *isCoplanar = false; + if (negCount == 0) { + *front = std::move(polygon); + return; + } + if (posCount == 0) { + *back = std::move(polygon); + return; + } + + // The polygon intersects with this polygon. + size_t frontBegin = 0; + size_t backBegin = 0; + size_t preFrontBegin = 0; + size_t preBackBegin = 0; + for (size_t i = 0; i < numPoints; i++) { + if (vertexDistance[i] > 0.0f) { + frontBegin = i; + break; + } + } + while (vertexDistance[preFrontBegin = PrevIndex(frontBegin, numPoints)] > 0.0f) { + frontBegin = preFrontBegin; + } + for (size_t i = 0; i < numPoints; i++) { + if (vertexDistance[i] < 0.0f) { + backBegin = i; + break; + } + } + while (vertexDistance[preBackBegin = PrevIndex(backBegin, numPoints)] < 0.0f) { + backBegin = preBackBegin; + } + + // First vertex of the front fragment (same side as normal), on the intersection line. + Vec3 prePosIntersection = InterpolatePoint( + polygon->_points[preFrontBegin], polygon->_points[frontBegin], + vertexDistance[preFrontBegin] / (vertexDistance[preFrontBegin] - vertexDistance[frontBegin])); + // First vertex of the back fragment (opposite side of normal), on the intersection line. + Vec3 preNegIntersection = InterpolatePoint( + polygon->_points[preBackBegin], polygon->_points[backBegin], + vertexDistance[preBackBegin] / (vertexDistance[preBackBegin] - vertexDistance[backBegin])); + + std::vector frontPoints; + CollectSplitPoints(polygon->_points, prePosIntersection, preNegIntersection, frontBegin, + backBegin, &frontPoints); + std::vector backPoints; + CollectSplitPoints(polygon->_points, preNegIntersection, prePosIntersection, backBegin, + frontBegin, &backPoints); + + *front = std::unique_ptr( + new DrawPolygon3D(polygon->_image, polygon->_matrix, std::move(frontPoints), polygon->_normal, + polygon->_orderIndex, polygon->_alpha, polygon->_antiAlias)); + *back = std::unique_ptr( + new DrawPolygon3D(polygon->_image, polygon->_matrix, std::move(backPoints), polygon->_normal, + polygon->_orderIndex, polygon->_alpha, polygon->_antiAlias)); + + DEBUG_ASSERT((*front)->_points.size() >= 3); + DEBUG_ASSERT((*back)->_points.size() >= 3); +} + +bool DrawPolygon3D::isFacingPositiveZ() const { + return _normal.z > 0.0f; +} + +std::vector DrawPolygon3D::toQuads() const { + std::vector quads; + size_t n = _points.size(); + if (n < 3) { + DEBUG_ASSERT(false); + return quads; + } + Matrix3D inverseMatrix; + if (!_matrix.invert(&inverseMatrix)) { + DEBUG_ASSERT(false); + return quads; + } + + // Project all 3D points to 2D local space + std::vector localPoints; + localPoints.reserve(n); + for (const auto& point : _points) { + localPoints.push_back(ProjectPoint(inverseMatrix, point)); + } + + if (n == 3) { + // Triangle: degenerate to quad (p2 == p3) + quads.push_back(QuadCW(localPoints[0], localPoints[1], localPoints[2], localPoints[2])); + return quads; + } + if (n == 4) { + // Quadrilateral: direct mapping + quads.push_back(QuadCW(localPoints[0], localPoints[1], localPoints[2], localPoints[3])); + return quads; + } + // n > 4: Fan decomposition into quads, each quad covers two triangles when possible. + for (size_t i = 1; i + 2 < n; i += 2) { + auto p3 = (i + 2 < n) ? localPoints[i + 2] : localPoints[i + 1]; + quads.push_back(QuadCW(localPoints[0], localPoints[i], localPoints[i + 1], p3)); + } + // Handle remaining triangle if odd number of triangles. + if ((n - 2) % 2 == 1) { + quads.push_back( + QuadCW(localPoints[0], localPoints[n - 2], localPoints[n - 1], localPoints[n - 1])); + } + return quads; +} + +} // namespace tgfx diff --git a/src/layers/compositing3d/DrawPolygon3D.h b/src/layers/compositing3d/DrawPolygon3D.h new file mode 100644 index 000000000..303b63022 --- /dev/null +++ b/src/layers/compositing3d/DrawPolygon3D.h @@ -0,0 +1,113 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "gpu/QuadCW.h" +#include "tgfx/core/Image.h" +#include "tgfx/core/Matrix3D.h" + +namespace tgfx { + +/** + * DrawPolygon3D represents a splittable 3D polygon for BSP tree processing. + * It stores transformed 3D vertices in screen space and supports splitting by other polygons. + */ +class DrawPolygon3D { + public: + /** + * Constructs a polygon from an image's 2D bounds and a 3D transformation matrix. + * The transform is applied immediately to convert vertices to screen space. + * @param orderIndex Used for sorting coplanar polygons. Smaller values are placed in front + * (along the polygon's normal direction). + */ + DrawPolygon3D(std::shared_ptr image, const Matrix3D& matrix, int orderIndex, float alpha, + bool antiAlias); + + /** + * Splits the given polygon by this polygon's plane. + * For coplanar polygons, smaller orderIndex goes to front (drawn later in TGFX's post-order). + * @param polygon The polygon to split (ownership will be transferred). + * @param front Output: portion in front of this plane, entire polygon if not split, or nullptr. + * @param back Output: portion behind this plane, entire polygon if not split, or nullptr. + * @param isCoplanar Output: true if polygon is coplanar with this plane. + */ + void splitAnother(std::unique_ptr polygon, std::unique_ptr* front, + std::unique_ptr* back, bool* isCoplanar) const; + + /** + * Returns the signed distance from a point to this polygon's plane. + * Positive means in front (same side as normal), negative means behind. + */ + float signedDistanceTo(const Vec3& point) const; + + const std::vector& points() const { + return _points; + } + + bool isSplit() const { + return _isSplit; + } + + float alpha() const { + return _alpha; + } + + const std::shared_ptr& image() const { + return _image; + } + + const Matrix3D& matrix() const { + return _matrix; + } + + bool antiAlias() const { + return _antiAlias; + } + + bool isFacingPositiveZ() const; + + /** + * Converts this polygon to a list of quads for rendering. + * Each quad contains 4 vertices in local space (clockwise order). + * For triangles, the last two vertices are the same. + * @return A list of quads in clockwise vertex order. + */ + std::vector toQuads() const; + + private: + // Constructs a polygon from already-transformed 3D points (used for split polygons). + DrawPolygon3D(std::shared_ptr image, const Matrix3D& matrix, std::vector points, + const Vec3& normal, int orderIndex, float alpha, bool antiAlias); + + void constructNormal(); + + std::vector _points = {}; + Vec3 _normal = {0.0f, 0.0f, 1.0f}; + int _orderIndex = 0; + // Whether this polygon was split from another polygon. + bool _isSplit = false; + float _alpha = 1.0f; + bool _antiAlias = true; + std::shared_ptr _image = nullptr; + Matrix3D _matrix = {}; +}; + +} // namespace tgfx diff --git a/src/layers/compositing3d/Render3DContext.cpp b/src/layers/compositing3d/Render3DContext.cpp new file mode 100644 index 000000000..1497b6d5e --- /dev/null +++ b/src/layers/compositing3d/Render3DContext.cpp @@ -0,0 +1,114 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#include "Render3DContext.h" +#include "Context3DCompositor.h" +#include "core/Matrix3DUtils.h" +#include "core/utils/MathExtra.h" +#include "layers/BackgroundContext.h" +#include "tgfx/core/Canvas.h" +#include "tgfx/core/Image.h" +#include "tgfx/core/Paint.h" + +namespace tgfx { + +static std::shared_ptr PictureToImage(std::shared_ptr picture, Point* offset, + std::shared_ptr colorSpace) { + if (picture == nullptr) { + return nullptr; + } + auto bounds = picture->getBounds(); + bounds.roundOut(); + auto matrix = Matrix::MakeTrans(-bounds.x(), -bounds.y()); + auto image = Image::MakeFrom(std::move(picture), static_cast(bounds.width()), + static_cast(bounds.height()), &matrix, std::move(colorSpace)); + if (offset) { + offset->x = bounds.left; + offset->y = bounds.top; + } + return image; +} + +Canvas* Render3DContext::beginRecording(const Matrix3D& childTransform, bool antialiasing) { + auto baseTransform = _stateStack.empty() ? Matrix3D::I() : _stateStack.top().transform; + auto newTransform = childTransform; + newTransform.postConcat(baseTransform); + _stateStack.emplace(newTransform, antialiasing); + auto canvas = _stateStack.top().recorder.beginRecording(); + + DEBUG_ASSERT(!FloatNearlyZero(_contentScale)); + auto invScale = 1.0f / _contentScale; + // The bounds of the 3D rendering context, inverse-mapped through newTransform + // to get the clip rect in local layer coordinate space. + auto contextBounds = Rect::MakeXYWH(_offset.x * invScale, _offset.y * invScale, + static_cast(_compositor->width()) * invScale, + static_cast(_compositor->height()) * invScale); + auto localClipRect = Matrix3DUtils::InverseMapRect(contextBounds, newTransform); + if (!localClipRect.isEmpty()) { + canvas->clipRect(localClipRect); + } + canvas->scale(_contentScale, _contentScale); + return canvas; +} + +void Render3DContext::endRecording() { + if (_stateStack.empty()) { + return; + } + auto& state = _stateStack.top(); + auto picture = state.recorder.finishRecordingAsPicture(); + auto layerTransform = state.transform; + auto antialiasing = state.antialiasing; + _stateStack.pop(); + + Point pictureOffset = {}; + auto image = PictureToImage(std::move(picture), &pictureOffset, _colorSpace); + if (image == nullptr) { + return; + } + + DEBUG_ASSERT(!FloatNearlyZero(_contentScale)); + auto invScale = 1.0f / _contentScale; + auto imageOrigin = Point::Make(pictureOffset.x * invScale, pictureOffset.y * invScale); + auto imageTransform = Matrix3DUtils::OriginAdaptedMatrix3D(layerTransform, imageOrigin); + imageTransform = Matrix3DUtils::ScaleAdaptedMatrix3D(imageTransform, _contentScale); + imageTransform.postTranslate(pictureOffset.x - _offset.x, pictureOffset.y - _offset.y, 0); + _compositor->addImage(image, imageTransform, 1.0f, antialiasing); +} + +void Render3DContext::finishAndDrawTo(Canvas* canvas, bool antialiasing) { + auto context3DImage = _compositor->finish(); + // The final texture has been scaled proportionally during generation, so draw it at its actual + // size on the canvas. + AutoCanvasRestore autoRestore(canvas); + auto imageMatrix = Matrix::MakeScale(1.0f / _contentScale, 1.0f / _contentScale); + imageMatrix.preTranslate(_offset.x, _offset.y); + canvas->concat(imageMatrix); + canvas->drawImage(context3DImage); + + if (_backgroundContext) { + Paint paint = {}; + paint.setAntiAlias(antialiasing); + auto backgroundCanvas = _backgroundContext->getCanvas(); + AutoCanvasRestore autoRestoreBg(backgroundCanvas); + backgroundCanvas->concat(imageMatrix); + backgroundCanvas->drawImage(context3DImage, &paint); + } +} + +} // namespace tgfx diff --git a/src/layers/compositing3d/Render3DContext.h b/src/layers/compositing3d/Render3DContext.h new file mode 100644 index 000000000..c96f24c17 --- /dev/null +++ b/src/layers/compositing3d/Render3DContext.h @@ -0,0 +1,95 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2026 Tencent. All rights reserved. +// +// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// https://opensource.org/licenses/BSD-3-Clause +// +// unless required by applicable law or agreed to in writing, software distributed under the +// license is distributed on an "as is" basis, without warranties or conditions of any kind, +// either express or implied. see the license for the specific language governing permissions +// and limitations under the license. +// +///////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include "tgfx/core/Canvas.h" +#include "tgfx/core/ColorSpace.h" +#include "tgfx/core/Matrix3D.h" +#include "tgfx/core/PictureRecorder.h" +#include "tgfx/core/Point.h" + +namespace tgfx { + +class BackgroundContext; +class Context3DCompositor; + +struct Render3DContextState { + Render3DContextState(const Matrix3D& transform, bool antialiasing) + : transform(transform), antialiasing(antialiasing) { + } + + Matrix3D transform = {}; + bool antialiasing = true; + PictureRecorder recorder = {}; +}; + +/** + * Manages the rendering state for layers in a 3D context, handling recording, transformation + * accumulation, and compositing of layer content with perspective effects. + */ +class Render3DContext { + public: + /** + * Creates a new Render3DContext. + * @param compositor The compositor used to combine 3D layer images. + * @param offset The offset of the 3D context in the target canvas coordinate space. + * @param contentScale The scale factor applied to the content for higher resolution rendering. + * @param colorSpace The color space used for intermediate images. + * @param backgroundContext The background context for blur effects, or nullptr if not needed. + */ + Render3DContext(std::shared_ptr compositor, const Point& offset, + float contentScale, std::shared_ptr colorSpace, + std::shared_ptr backgroundContext) + : _compositor(std::move(compositor)), _offset(offset), _contentScale(contentScale), + _colorSpace(std::move(colorSpace)), _backgroundContext(std::move(backgroundContext)) { + } + + /** + * Begins recording a new layer with the specified transform and antialiasing setting. + * @param childTransform The 3D transform to apply to the layer content. + * @param antialiasing Whether to enable edge antialiasing for this layer. + * @return A canvas to draw the layer content on. + */ + Canvas* beginRecording(const Matrix3D& childTransform, bool antialiasing); + + /** + * Ends recording the current layer and adds it to the compositor. + */ + void endRecording(); + + /** + * Finishes the 3D rendering and draws the result to the target canvas. + * @param canvas The target canvas to draw the composited result on. + * @param antialiasing Whether to enable antialiasing when drawing to background context. + */ + void finishAndDrawTo(Canvas* canvas, bool antialiasing); + + private: + std::shared_ptr _compositor = nullptr; + Point _offset = {}; + float _contentScale = 1.0f; + + std::shared_ptr _colorSpace = nullptr; + std::shared_ptr _backgroundContext = nullptr; + std::stack _stateStack = {}; +}; + +} // namespace tgfx diff --git a/src/layers/filters/Transform3DFilter.cpp b/src/layers/filters/Transform3DFilter.cpp deleted file mode 100644 index 0955e78c3..000000000 --- a/src/layers/filters/Transform3DFilter.cpp +++ /dev/null @@ -1,68 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making tgfx available. -// -// Copyright (C) 2025 Tencent. All rights reserved. -// -// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at -// -// https://opensource.org/licenses/BSD-3-Clause -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#include "layers/filters/Transform3DFilter.h" -#include "core/filters/Transform3DImageFilter.h" -#include "core/utils/Log.h" -#include "core/utils/MathExtra.h" - -namespace tgfx { - -std::shared_ptr Transform3DFilter::Make(const Matrix3D& matrix) { - return std::shared_ptr(new Transform3DFilter(matrix)); -} - -void Transform3DFilter::setMatrix(const Matrix3D& matrix) { - if (_matrix == matrix) { - return; - } - _matrix = matrix; - invalidateFilter(); -} - -void Transform3DFilter::setHideBackFace(bool hideBackFace) { - if (_hideBackFace == hideBackFace) { - return; - } - _hideBackFace = hideBackFace; - invalidateFilter(); -} - -Transform3DFilter::Transform3DFilter(const Matrix3D& matrix) : _matrix(matrix) { -} - -std::shared_ptr Transform3DFilter::onCreateImageFilter(float scale) { - // The order of model scaling and projection affects the final result. Here, the expected behavior - // is to project first and then scale. Multiple matrices are modified as follows. - // For example, if a model is scaled by 2x and then rotated 90 degrees around the X axis, the - // expected rendering result is that the projection of the scaled model is also enlarged by 2x - // compared to the unscaled model. However, if the projection is applied to the already scaled - // model, not only will its length be doubled, but it will also be closer to the observer, - // resulting in a projection scale much greater than 2. - auto adjustedMatrix = _matrix; - if (!FloatNearlyEqual(scale, 1.0f)) { - DEBUG_ASSERT(!FloatNearlyZero(scale)); - auto invScaleMatrix = Matrix3D::MakeScale(1.0f / scale, 1.0f / scale, 1.0f); - auto scaleMatrix = Matrix3D::MakeScale(scale, scale, 1.0f); - adjustedMatrix = scaleMatrix * _matrix * invScaleMatrix; - } - auto filter = std::make_shared(adjustedMatrix, _hideBackFace); - return filter; -} - -} // namespace tgfx diff --git a/src/layers/filters/Transform3DFilter.h b/src/layers/filters/Transform3DFilter.h deleted file mode 100644 index e1346682a..000000000 --- a/src/layers/filters/Transform3DFilter.h +++ /dev/null @@ -1,79 +0,0 @@ -///////////////////////////////////////////////////////////////////////////////////////////////// -// -// Tencent is pleased to support the open source community by making tgfx available. -// -// Copyright (C) 2025 Tencent. All rights reserved. -// -// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except -// in compliance with the License. You may obtain a copy of the License at -// -// https://opensource.org/licenses/BSD-3-Clause -// -// unless required by applicable law or agreed to in writing, software distributed under the -// license is distributed on an "as is" basis, without warranties or conditions of any kind, -// either express or implied. see the license for the specific language governing permissions -// and limitations under the license. -// -///////////////////////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "tgfx/layers/filters/LayerFilter.h" - -namespace tgfx { - -/** - * Transform3DFilter is a filter that applies a perspective transformation to the input layer. - */ -class Transform3DFilter final : public LayerFilter { - public: - /** - * Creates a Transform3DFilter with the specified transformation matrix. - * The transformation matrix transforms 3D model coordinates to destination coordinates for x and - * y before perspective division. The z value is mapped to the [-1, 1] range before perspective - * division; content outside this z range will be clipped. - */ - static std::shared_ptr Make(const Matrix3D& matrix); - - /** - * Returns the 3D transformation matrix. This matrix transforms 3D model coordinates to - * destination coordinates for x and y before perspective division. The z value is mapped to the - * [-1, 1] range before perspective division; content outside this z range will be clipped. - */ - Matrix3D matrix() const { - return _matrix; - } - - void setMatrix(const Matrix3D& matrix); - - /** - * Returns whether to hide the back face of the content after the 3D transformation. The default - * value is false, which means both the front and back faces are drawn. - * When the layer is first created, the front face is oriented toward the user by default. After - * applying certain 3D transformations, such as rotating 180 degrees around the X axis, the back - * face of the layer may face the user. - */ - bool hideBackFace() const { - return _hideBackFace; - } - - /** - * Sets whether to hide the back face of the content after the 3D transformation. - */ - void setHideBackFace(bool hideBackFace); - - private: - explicit Transform3DFilter(const Matrix3D& matrix); - - Type type() const override { - return Type::Transform3DFilter; - } - - std::shared_ptr onCreateImageFilter(float scale) override; - - Matrix3D _matrix = Matrix3D::I(); - - bool _hideBackFace = false; -}; - -} // namespace tgfx diff --git a/test/baseline/version.json b/test/baseline/version.json index 8145d8d56..8dffcdfbd 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -35,7 +35,7 @@ "LumaFilterToRec2020": "82494664", "LumaFilterToRec601": "82494664", "LumaFilterToSRGB": "82494664", - "Matrix3DShapeStroke": "543054b5", + "Matrix3DShapeStroke": "49e449da", "MatrixShapeStroke": "eefb7a46", "MultiImageRect_NOSCALE_NEAREST_LINEAR": "4edccb64", "MultiImageRect_NOSCALE_NEAREST_NEAREST": "4edccb64", @@ -96,10 +96,10 @@ "ReverseFilterBounds_dropShadowOnly": "b6757c2d", "ReverseFilterBounds_inner": "b6757c2d", "RuntimeEffect": "eefb7a46", - "Transform3DImageFilterCSSBasic": "eefb7a46", - "Transform3DImageFilterStandardClip": "eefb7a46", - "Transorm3DImageFilterStandardBasic": "eefb7a46", - "Transorm3DImageFilterStandardScale": "8e0d2bd7", + "Transform3DImageFilterCSSBasic": "49e449da", + "Transform3DImageFilterStandardClip": "49e449da", + "Transorm3DImageFilterStandardBasic": "49e449da", + "Transorm3DImageFilterStandardScale": "49e449da", "blur": "880d5a6c", "blur-large-pixel": "6b4e5ce0", "dropShadow": "ee2cc771", @@ -226,9 +226,11 @@ "Layer_drawRRect": "e39a63cd", "Layer_hitTestPoint": "cd6a8dff", "Layer_hitTestPointNested": "4d602959", - "Matrix_3D": "ba7ed034", - "Matrix_3D_2D": "c65c21d6", - "Matrix_3D_2D_3D": "543054b5", + "Matrix_3D": "89a0614a", + "Matrix_3D_2D": "89a0614a", + "Matrix_3D_2D_3D": "89a0614a", + "Matrix_3D_2D_3D_Preserve3D": "89a0614a", + "Matrix_Behind_Viewer": "3c65b221", "PartialDrawLayer": "af282892", "PartialDrawLayer_shapeLayer": "af282892", "PassThoughAndNormal": "43cd416", diff --git a/test/src/LayerTest.cpp b/test/src/LayerTest.cpp index 35e4e31c3..b0cc3ff7a 100644 --- a/test/src/LayerTest.cpp +++ b/test/src/LayerTest.cpp @@ -1950,15 +1950,9 @@ TGFX_TEST(LayerTest, PassThrough_Test) { Layer::SetDefaultAllowsGroupOpacity(value); } -static Matrix3D MakePerspectiveMatrix(float farZ) { +static inline Matrix3D MakePerspectiveMatrix() { auto perspectiveMatrix = Matrix3D::I(); constexpr float eyeDistance = 1200.f; - constexpr float shift = 10.f; - const float nearZ = eyeDistance - shift; - const float m22 = (2 - (farZ + nearZ) / eyeDistance) / (farZ - nearZ); - perspectiveMatrix.setRowColumn(2, 2, m22); - const float m23 = -1.f + nearZ / eyeDistance - perspectiveMatrix.getRowColumn(2, 2) * nearZ; - perspectiveMatrix.setRowColumn(2, 3, m23); perspectiveMatrix.setRowColumn(3, 2, -1.f / eyeDistance); return perspectiveMatrix; } @@ -1982,20 +1976,17 @@ TGFX_TEST(LayerTest, Matrix) { auto contentLayer = SolidLayer::Make(); contentLayer->setColor(Color::FromRGBA(151, 153, 46, 255)); + auto contentLayerSize = Size::Make(360.f, 320.f); + contentLayer->setWidth(contentLayerSize.width); + contentLayer->setHeight(contentLayerSize.height); { - auto layerSize = Size::Make(360.f, 320.f); - contentLayer->setWidth(layerSize.width); - contentLayer->setHeight(layerSize.height); auto anchor = Point::Make(0.3f, 0.3f); - auto offsetToAnchorMatrix = - Matrix3D::MakeTranslate(-anchor.x * layerSize.width, -anchor.y * layerSize.height, 0.f); - auto invOffsetToAnchorMatrix = - Matrix3D::MakeTranslate(anchor.x * layerSize.width, anchor.y * layerSize.height, 0.f); + auto offsetToAnchorMatrix = Matrix3D::MakeTranslate(-anchor.x * contentLayerSize.width, + -anchor.y * contentLayerSize.height, 0.f); + auto invOffsetToAnchorMatrix = Matrix3D::MakeTranslate(anchor.x * contentLayerSize.width, + anchor.y * contentLayerSize.height, 0.f); auto modelMatrix = Matrix3D::MakeRotate({0.f, 1.f, 0.f}, -45.f); - // Choose an appropriate far plane to avoid clipping during rotation. - auto maxLength = static_cast(std::max(layerSize.width, layerSize.height)) * 2.f; - auto farZ = std::min(-maxLength, -500.f); - auto perspectiveMatrix = MakePerspectiveMatrix(farZ); + auto perspectiveMatrix = MakePerspectiveMatrix(); auto origin = Point::Make(120, 40); auto originTranslateMatrix = Matrix3D::MakeTranslate(origin.x, origin.y, 0.f); auto transformMatrix = originTranslateMatrix * invOffsetToAnchorMatrix * perspectiveMatrix * @@ -2036,11 +2027,8 @@ TGFX_TEST(LayerTest, Matrix) { modelMatrix.postRotate({0.f, 0.f, 1.f}, 45.f); modelMatrix.preRotate({1.f, 0.f, 0.f}, 45.f); modelMatrix.preRotate({0.f, 1.f, 0.f}, 45.f); - modelMatrix.postTranslate(0.f, 0.f, 100.f); - // Choose an appropriate far plane to avoid clipping during rotation. - auto maxLength = static_cast(std::max(image->width(), image->height())) * 2.f; - auto farZ = std::min(-maxLength, -500.f); - auto perspectiveMatrix = MakePerspectiveMatrix(farZ); + modelMatrix.postTranslate(0.f, 0.f, 20.f); + auto perspectiveMatrix = MakePerspectiveMatrix(); // The origin coordinates of the layer in the local coordinate system when no model // transformation (excluding XY translation) is applied auto origin = Point::Make(125, 105); @@ -2052,15 +2040,27 @@ TGFX_TEST(LayerTest, Matrix) { imageLayer->setMatrix3D(imageMatrix3D); displayList->render(surface.get()); - EXPECT_EQ(imageLayer->getBounds(contentLayer.get()), Rect::MakeLTRB(65, 0, 298, 281)); - EXPECT_EQ(imageLayer->getBounds(displayList->root()), Rect::MakeLTRB(99, 15, 190, 162)); + auto imageToContentBounds = imageLayer->getBounds(contentLayer.get()); + imageToContentBounds.roundOut(); + EXPECT_EQ(imageToContentBounds, Rect::MakeLTRB(73, 10, 290, 272)); + auto imageToDisplayListBounds = imageLayer->getBounds(displayList->root()); + imageToDisplayListBounds.roundOut(); + EXPECT_EQ(imageToDisplayListBounds, Rect::MakeLTRB(102, 21, 187, 158)); EXPECT_TRUE(Baseline::Compare(surface, "LayerTest/Matrix_3D")); + auto imageBlurLayer = SolidLayer::Make(); + imageBlurLayer->setColor(Color::FromRGBA(235, 5, 112, 70)); + imageBlurLayer->setWidth(170); + imageBlurLayer->setHeight(70); + imageBlurLayer->setMatrix(Matrix::MakeTrans(-30.f, 20.f)); + imageBlurLayer->setLayerStyles({BackgroundBlurStyle::Make(10, 10)}); + imageLayer->addChild(imageBlurLayer); auto affineMatrix = Matrix::MakeTrans(50, 50); imageLayer->setMatrix(affineMatrix); displayList->render(surface.get()); EXPECT_TRUE(Baseline::Compare(surface, "LayerTest/Matrix_3D_2D")); + imageBlurLayer->removeFromParent(); imageLayer->setMatrix3D(imageMatrix3D); EXPECT_TRUE(imageLayer->matrix().isIdentity()); auto rect = Rect::MakeXYWH(50, 50, 200, 100); @@ -2079,10 +2079,7 @@ TGFX_TEST(LayerTest, Matrix) { auto invOffsetToAnchorMatrix = Matrix3D::MakeTranslate(anchor.x * layerSize.width, anchor.y * layerSize.height, 0.f); auto modelMatrix = Matrix3D::MakeRotate({0.f, 1.f, 0.f}, 45.f); - // Choose an appropriate far plane to avoid clipping during rotation. - auto maxLength = static_cast(std::max(layerSize.width, layerSize.height)) * 2.f; - auto farZ = std::min(-maxLength, -500.f); - auto perspectiveMatrix = MakePerspectiveMatrix(farZ); + auto perspectiveMatrix = MakePerspectiveMatrix(); auto origin = Point::Make(0, 0); auto originTranslateMatrix = Matrix3D::MakeTranslate(origin.x, origin.y, 0.f); auto transformMatrix = originTranslateMatrix * invOffsetToAnchorMatrix * perspectiveMatrix * @@ -2092,6 +2089,34 @@ TGFX_TEST(LayerTest, Matrix) { displayList->root()->addChild(shaperLayer); displayList->render(surface.get()); EXPECT_TRUE(Baseline::Compare(surface, "LayerTest/Matrix_3D_2D_3D")); + + contentLayer->setPreserve3D(true); + imageToContentBounds = imageLayer->getBounds(contentLayer.get()); + imageToContentBounds.roundOut(); + EXPECT_EQ(imageToContentBounds, Rect::MakeLTRB(-51, 10, 333, 279)); + imageToDisplayListBounds = imageLayer->getBounds(displayList->root()); + imageToDisplayListBounds.roundOut(); + EXPECT_EQ(imageToDisplayListBounds, Rect::MakeLTRB(62, 21, 206, 159)); + displayList->render(surface.get()); + EXPECT_TRUE(Baseline::Compare(surface, "LayerTest/Matrix_3D_2D_3D_Preserve3D")); + + { + auto anchor = Point::Make(0.3f, 0.3f); + auto offsetToAnchorMatrix = Matrix3D::MakeTranslate(-anchor.x * contentLayerSize.width, + -anchor.y * contentLayerSize.height, 0.f); + auto invOffsetToAnchorMatrix = Matrix3D::MakeTranslate(anchor.x * contentLayerSize.width, + anchor.y * contentLayerSize.height, 0.f); + auto modelMatrix = Matrix3D::MakeRotate({0.f, 1.f, 0.f}, -45.f); + modelMatrix.postTranslate(0.f, 0.f, 1200.f); + auto perspectiveMatrix = MakePerspectiveMatrix(); + auto origin = Point::Make(120, 40); + auto originTranslateMatrix = Matrix3D::MakeTranslate(origin.x, origin.y, 0.f); + auto transformMatrix = originTranslateMatrix * invOffsetToAnchorMatrix * perspectiveMatrix * + modelMatrix * offsetToAnchorMatrix; + contentLayer->setMatrix3D(transformMatrix); + } + displayList->render(surface.get()); + EXPECT_TRUE(Baseline::Compare(surface, "LayerTest/Matrix_Behind_Viewer")); } TGFX_TEST(LayerTest, DisplayListBackground) {