diff --git a/include/tgfx/core/Image.h b/include/tgfx/core/Image.h index 1b5ba1373..b3a5b4cca 100644 --- a/include/tgfx/core/Image.h +++ b/include/tgfx/core/Image.h @@ -363,5 +363,6 @@ class Image { friend class ScaledImage; friend class ImageShader; friend class Types; + friend class Transform3DImageFilter; }; } // namespace tgfx diff --git a/include/tgfx/core/ImageFilter.h b/include/tgfx/core/ImageFilter.h index 7c41e7541..1cdcdb6c8 100644 --- a/include/tgfx/core/ImageFilter.h +++ b/include/tgfx/core/ImageFilter.h @@ -21,6 +21,7 @@ #include "tgfx/core/ColorFilter.h" #include "tgfx/core/Image.h" #include "tgfx/core/Matrix.h" +#include "tgfx/core/Matrix3D.h" #include "tgfx/core/TileMode.h" #include "tgfx/gpu/Context.h" #include "tgfx/gpu/RuntimeEffect.h" @@ -118,6 +119,14 @@ class ImageFilter { */ static std::shared_ptr Runtime(std::shared_ptr effect); + /** + * Creates a filter that applies a perspective transformation to the input image. + * @param matrix 3D transformation matrix used to 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 Transform3D(const Matrix3D& matrix); + virtual ~ImageFilter() = default; /** @@ -127,7 +136,7 @@ class ImageFilter { Rect filterBounds(const Rect& rect) const; protected: - enum class Type { Blur, DropShadow, InnerShadow, Color, Compose, Runtime }; + enum class Type { Blur, DropShadow, InnerShadow, Color, Compose, Runtime, Transform3D }; /** * Returns the type of this image filter. @@ -174,4 +183,5 @@ class ImageFilter { friend class FilterImage; friend class Types; }; + } // namespace tgfx diff --git a/include/tgfx/core/Matrix3D.h b/include/tgfx/core/Matrix3D.h new file mode 100644 index 000000000..ede282de6 --- /dev/null +++ b/include/tgfx/core/Matrix3D.h @@ -0,0 +1,293 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 +#include "Vec.h" +#include "tgfx/core/Rect.h" + +namespace tgfx { + +/** + * Matrix3D holds a 4x4 matrix for transforming coordinates in 3D space. This allows mapping points + * and vectors with translation, scaling, skewing, rotation, and perspective. These types of + * transformations are collectively known as projective transformations. Projective transformations + * preserve the straightness of lines but do not preserve parallelism, so parallel lines may not + * remain parallel after transformation. + * The elements of Matrix3D are in column-major order. + * Matrix3D does not have a default constructor, so it must be explicitly initialized. + */ +class Matrix3D { + public: + /** + * Creates a Matrix3D set to the identity matrix. The created Matrix3D is: + * + * | 1 0 0 0 | + * | 0 1 0 0 | + * | 0 0 1 0 | + * | 0 0 0 1 | + */ + constexpr Matrix3D() : Matrix3D(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) { + } + + /** + * Returns the matrix value at the given row and column. + * @param r Row index, valid range 0..3. + * @param c Column index, valid range 0..3. + */ + float getRowColumn(int r, int c) const { + return values[c * 4 + r]; + } + + /** + * Sets the matrix value at the given row and column. + * @param r Row index, valid range 0..3. + * @param c Column index, valid range 0..3. + */ + void setRowColumn(int r, int c, float value) { + values[c * 4 + r] = value; + } + + /** + * Returns a reference to a constant identity Matrix3D. The returned Matrix3D is: + * + * | 1 0 0 0 | + * | 0 1 0 0 | + * | 0 0 1 0 | + * | 0 0 0 1 | + */ + static const Matrix3D& I(); + + /** + * Creates a Matrix3D that scales by (sx, sy, sz). The returned matrix is: + * + * | sx 0 0 0 | + * | 0 sy 0 0 | + * | 0 0 sz 0 | + * | 0 0 0 1 | + */ + static Matrix3D MakeScale(float sx, float sy, float sz) { + return {sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1}; + } + + /** + * Creates a Matrix3D that rotates by the given angle (in degrees) around the specified axis. + * The returned matrix is: + * + * | t*x*x + c t*x*y + s*z t*x*z - s*y 0 | + * | t*x*y - s*z t*y*y + c t*y*z + s*x 0 | + * | t*x*z + s*y t*y*z - s*x t*z*z + c 0 | + * | 0 0 0 1 | + * + * where: + * x, y, z = normalized components of axis + * c = cos(degrees) + * s = sin(degrees) + * t = 1 - c + * @param axis The axis to rotate about. + * @param degrees The angle of rotation in degrees. + */ + static Matrix3D MakeRotate(const Vec3& axis, float degrees) { + Matrix3D m; + m.setRotate(axis, degrees); + return m; + } + + /** + * Creates a Matrix3D that translates by (tx, ty, tz). The returned matrix is: + * + * | 1 0 0 tx | + * | 0 1 0 ty | + * | 0 0 1 tz | + * | 0 0 0 1 | + */ + static Matrix3D MakeTranslate(float tx, float ty, float tz) { + return {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1}; + } + + /** + * Creates a view matrix for a camera. This is commonly used to transform world coordinates to + * camera (view) coordinates in 3D graphics. + * @param eye The position of the camera. + * @param center The point the camera is looking at. + * @param up The up direction for the camera. + */ + static Matrix3D LookAt(const Vec3& eye, const Vec3& center, const Vec3& up); + + /** + * Creates a standard perspective projection matrix. This matrix maps 3D coordinates into + * clip coordinates for perspective rendering. + * The standard projection model is established by defining the camera position, orientation, + * field of view, and near/far planes. Points inside the view frustum are projected onto the near + * plane. + * @param fovyDegrees Field of view angle in degrees (vertical). + * @param aspect Aspect ratio (width / height). + * @param nearZ Distance to the near clipping plane. + * @param farZ Distance to the far clipping plane. + */ + static Matrix3D Perspective(float fovyDegrees, float aspect, float nearZ, float farZ); + + /** + * Maps a rectangle using this matrix. + * If the matrix contains a perspective transformation, each corner of the rectangle is mapped as a + * 4D point (x, y, 0, 1), and the resulting rectangle is computed from the projected points + * (after perspective division). + */ + Rect mapRect(const Rect& src) const; + + friend Matrix3D operator*(const Matrix3D& a, const Matrix3D& b) { + Matrix3D result; + result.setConcat(a, b); + return result; + } + + private: + constexpr Matrix3D(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) + : values{m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33} { + } + + /** + * Copies the matrix values into a 16-element array in column-major order. + */ + void getColMajor(float buffer[16]) const { + memcpy(buffer, values, sizeof(values)); + } + + /** + * Copies the matrix values into a 16-element array in row-major order. + */ + void getRowMajor(float buffer[16]) const; + + /** + * Concatenates two matrices and stores the result in this matrix. M' = a * b. + */ + 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); + + /** + * Concatenates this matrix with the given matrix, and stores the result in this matrix. M' = M * m. + */ + void postConcat(const Matrix3D& m); + + /** + * Pre-concatenates a scale to this matrix. M' = S * M. + */ + void preScale(float sx, float sy, float sz); + + /** + * Post-concatenates a scale to this matrix. M' = M * S. + */ + void postScale(float sx, float sy, float sz); + + /** + * Pre-concatenates a translation to this matrix. M' = T * M. + */ + void preTranslate(float tx, float ty, float tz); + + /** + * Post-concatenates a translation to this matrix. M' = M * T. + */ + void postTranslate(float tx, float ty, float tz); + + /** + * Pre-concatenates a rotation to this matrix. M' = R * M. + */ + void preRotate(const Vec3& axis, float degrees); + + /** + * Post-concatenates a rotation to this matrix. M' = M * R. + */ + void postRotate(const Vec3& axis, float degrees); + + /** + * Calculates the inverse of the current matrix and stores the result in the Matrix3D object + * pointed to by inverse. + * @param inverse Pointer to the Matrix3D object used to store the inverse matrix. Must not be + * nullptr. If the current matrix is not invertible, inverse will not be modified. + * @return Returns true if the inverse matrix exists; otherwise, returns false. + */ + bool invert(Matrix3D* inverse) const; + + /** + * Returns the transpose of the current matrix. + */ + 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; + } + + void setColumn(int i, const Vec4& v) { + memcpy(&values[i * 4], v.ptr(), sizeof(v)); + } + + void setIdentity() { + *this = Matrix3D(); + } + + void setRotate(const Vec3& axis, float degrees); + + void setRotateUnit(const Vec3& axis, float degrees); + + void setRotateUnitSinCos(const Vec3& axis, float sinV, float cosV); + + bool hasPerspective() const { + return (values[3] != 0 || values[7] != 0 || values[11] != 0 || values[15] != 1); + } + + bool operator==(const Matrix3D& other) const; + + bool operator!=(const Matrix3D& other) const { + return !(other == *this); + } + + Vec4 operator*(const Vec4& v) const { + return this->mapPoint(v.x, v.y, v.z, v.w); + } + + float values[16] = {.0f}; +}; + +} // namespace tgfx diff --git a/include/tgfx/core/Vec.h b/include/tgfx/core/Vec.h new file mode 100644 index 000000000..f0dbd8823 --- /dev/null +++ b/include/tgfx/core/Vec.h @@ -0,0 +1,540 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 +#include + +namespace tgfx { + +/** + * Vec2 represents a two-dimensional vector with x and y components. + */ +struct Vec2 { + /** + * Constructs a Vec2 set to (0, 0). + */ + constexpr Vec2() : x(0), y(0) { + } + + /** + * Constructs a Vec2 with the specified x and y values. + */ + constexpr Vec2(float x, float y) : x(x), y(y) { + } + + /** + * Constructs a Vec2 by loading two floats from the given pointer. + */ + static Vec2 Load(const float* ptr) { + Vec2 v; + memcpy(&v, ptr, sizeof(Vec2)); + return v; + } + + /** + * Returns the negation of the vector, computed as (-x, -y). + */ + Vec2 operator-() const { + return {-x, -y}; + } + + /** + * Returns the sum of this vector and another vector, computed as (x + v.x, y + v.y). + */ + Vec2 operator+(const Vec2& v) const { + return {x + v.x, y + v.y}; + } + + /** + * Returns the difference between this vector and another vector, computed as (x - v.x, y - v.y). + */ + Vec2 operator-(const Vec2& v) const { + return {x - v.x, y - v.y}; + } + + /** + * Returns the component-wise product of this vector and another vector, computed as (x * v.x, y * v.y). + */ + Vec2 operator*(const Vec2& v) const { + return {x * v.x, y * v.y}; + } + + /** + * Returns the product of this vector and a scalar, computed as (v.x * s, v.y * s). + */ + friend Vec2 operator*(const Vec2& v, float s) { + return {v.x * s, v.y * s}; + } + + /** + * Returns the product of a scalar and this vector. + */ + friend Vec2 operator*(float s, const Vec2& v) { + return v * s; + } + + /** + * Returns the quotient of this vector and a scalar, computed as (v.x / s, v.y / s). + */ + friend Vec2 operator/(const Vec2& v, float s); + + /** + * The x component value. + */ + float x; + + /** + * The y component value. + */ + float y; +}; + +/** + * Vec3 represents a three-dimensional vector with x, y, and z components. + */ +struct Vec3 { + /** + * Constructs a Vec3 set to (0, 0, 0). + */ + constexpr Vec3() : x(0), y(0), z(0) { + } + + /** + * Constructs a Vec3 with the specified x, y, and z values. + */ + constexpr Vec3(float x, float y, float z) : x(x), y(y), z(z) { + } + + /** + * Returns the dot product of two vectors, computed as (a.x * b.x + a.y * b.y + a.z * b.z). + */ + static float Dot(const Vec3& a, const Vec3& b) { + return a.x * b.x + a.y * b.y + a.z * b.z; + } + + /** + * Returns the dot product of this vector and another vector. + */ + float dot(const Vec3& v) const { + return Dot(*this, v); + } + + /** + * Returns the cross product of two vectors, computed as + * (a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x). + */ + static Vec3 Cross(const Vec3& a, const Vec3& b) { + return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; + } + + /** + * Returns the cross product of this vector and another vector. + */ + Vec3 cross(const Vec3& v) const { + return Cross(*this, v); + } + + /** + * Returns the normalized (unit length) version of the given vector. + */ + static Vec3 Normalize(const Vec3& v) { + return v * (1.f / v.length()); + } + + /** + * Returns the normalized (unit length) version of this vector. + */ + Vec3 normalize() const { + return Normalize(*this); + } + + /** + * Returns the length (magnitude) of the vector. + */ + float length() const { + return sqrtf(Dot(*this, *this)); + } + + /** + * Returns a pointer to the vector's immutable data. + */ + const float* ptr() const { + return &x; + } + + /** + * Returns true if this vector is equal to another vector. + */ + bool operator==(const Vec3& v) const { + return x == v.x && y == v.y && z == v.z; + } + + /** + * Returns true if this vector is not equal to another vector. + */ + bool operator!=(const Vec3& v) const { + return !(*this == v); + } + + /** + * Returns the negation of the vector, computed as (-x, -y, -z). + */ + Vec3 operator-() const { + return {-x, -y, -z}; + } + + /** + * Returns the sum of this vector and another vector, computed as (x + v.x, y + v.y, z + v.z). + */ + Vec3 operator+(const Vec3& v) const { + return {x + v.x, y + v.y, z + v.z}; + } + + /** + * Adds another vector to this vector. Sets this vector to (x + v.x, y + v.y, z + v.z). + */ + void operator+=(const Vec3& v) { + *this = *this + v; + } + + /** + * Returns the difference between this vector and another vector, computed as + * (x - v.x, y - v.y, z - v.z). + */ + Vec3 operator-(const Vec3& v) const { + return {x - v.x, y - v.y, z - v.z}; + } + + /** + * Subtracts another vector from this vector. Sets this vector to (x - v.x, y - v.y, z - v.z). + */ + void operator-=(const Vec3& v) { + *this = *this - v; + } + + /** + * Returns the component-wise product of this vector and another vector, computed as + * (x * v.x, y * v.y, z * v.z). + */ + Vec3 operator*(const Vec3& v) const { + return {x * v.x, y * v.y, z * v.z}; + } + + /** + * Multiplies this vector component-wise by another vector. Sets this vector to + * (x * v.x, y * v.y, z * v.z). + */ + void operator*=(const Vec3& v) { + *this = *this * v; + } + + /** + * Returns the product of a vector and a scalar, computed as (v.x * s, v.y * s, v.z * s). + */ + friend Vec3 operator*(const Vec3& v, float s) { + return {v.x * s, v.y * s, v.z * s}; + } + + /** + * Returns the product of a scalar and a vector, computed as (v.x * s, v.y * s, v.z * s). + */ + friend Vec3 operator*(float s, const Vec3& v) { + return v * s; + } + + /** + * Multiplies this vector by a scalar. Sets this vector to (x * s, y * s, z * s). + */ + void operator*=(float s) { + *this = *this * s; + } + + /** + * The x component value. + */ + float x; + + /** + * The y component value. + */ + float y; + + /** + * The z component value. + */ + float z; +}; + +/** + * Vec4 represents a four-dimensional vector with x, y, z, and w components. + */ +struct Vec4 { + /** + * Constructs a Vec4 set to (0, 0, 0, 0). + */ + constexpr Vec4() : x(0), y(0), z(0), w(0) { + } + + /** + * Constructs a Vec4 where all components are set to the given value, i.e., (value, value, value, value). + */ + constexpr Vec4(float value) : x(value), y(value), z(value), w(value) { + } + + /** + * Constructs a Vec4 with the specified x, y, z, and w values. + */ + constexpr Vec4(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) { + } + + /** + * Constructs a Vec4 from a Vec3 and a w value, i.e., (v.x, v.y, v.z, w). + */ + constexpr Vec4(const Vec3& v, float w) : x(v.x), y(v.y), z(v.z), w(w) { + } + + /** + * Constructs a Vec4 by loading four floats from the given pointer. + */ + static Vec4 Load(const float* ptr) { + Vec4 v; + memcpy(&v, ptr, sizeof(Vec4)); + return v; + } + + /** + * Returns the dot product of two vectors, computed as + * (a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w). + */ + static float Dot(const Vec4& a, const Vec4& b) { + return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; + } + + /** + * Returns the dot product of this vector and another vector. + */ + float dot(const Vec4& v) const { + return Dot(*this, v); + } + + /** + * Returns the normalized (unit length) version of the given vector. + */ + static Vec4 Normalize(const Vec4& v) { + return v * (1.f / v.length()); + } + + /** + * Returns the normalized (unit length) version of this vector. + */ + Vec4 normalize() const { + return Normalize(*this); + } + + /** + * Returns the length (magnitude) of the vector. + */ + float length() const { + return sqrtf(Dot(*this, *this)); + } + + /** + * Returns a pointer to the vector's immutable data. + */ + const float* ptr() const { + return &x; + } + + /** + * Returns a pointer to the vector's mutable data. + */ + float* ptr() { + return &x; + } + + /** + * Returns true if this vector is equal to another vector. + */ + bool operator==(const Vec4& v) const { + return x == v.x && y == v.y && z == v.z && w == v.w; + } + + /** + * Returns true if this vector is not equal to another vector. + */ + bool operator!=(const Vec4& v) const { + return !(*this == v); + } + + /** + * Returns the negation of the vector, computed as (-x, -y, -z, -w). + */ + Vec4 operator-() const { + return {-x, -y, -z, -w}; + } + + /** + * Returns the sum of this vector and another vector, computed as + * (x + v.x, y + v.y, z + v.z, w + v.w). + */ + Vec4 operator+(const Vec4& v) const { + return {x + v.x, y + v.y, z + v.z, w + v.w}; + } + + /** + * Adds another vector to this vector. Sets this vector to (x + v.x, y + v.y, z + v.z, w + v.w). + */ + void operator+=(const Vec4& v) { + *this = *this + v; + } + + /** + * Returns the difference between this vector and another vector, computed as + * (x - v.x, y - v.y, z - v.z, w - v.w). + */ + Vec4 operator-(const Vec4& v) const { + return {x - v.x, y - v.y, z - v.z, w - v.w}; + } + + /** + * Subtracts another vector from this vector. Sets this vector to (x - v.x, y - v.y, z - v.z, w - v.w). + */ + void operator-=(const Vec4& v) { + *this = *this - v; + } + + /** + * Returns the component-wise product of this vector and another vector, computed as + * (x * v.x, y * v.y, z * v.z, w * v.w). + */ + Vec4 operator*(const Vec4& v) const { + return {x * v.x, y * v.y, z * v.z, w * v.w}; + } + + /** + * Multiplies this vector component-wise by another vector. Sets this vector to + * (x * v.x, y * v.y, z * v.z, w * v.w). + */ + void operator*=(const Vec4& v) { + *this = *this * v; + } + + /** + * Returns the product of a vector and a scalar, computed as (v.x * s, v.y * s, v.z * s, v.w * s). + */ + friend Vec4 operator*(const Vec4& v, float s) { + return {v.x * s, v.y * s, v.z * s, v.w * s}; + } + + /** + * Returns the product of a scalar and a vector, computed as (v.x * s, v.y * s, v.z * s, v.w * s). + */ + friend Vec4 operator*(float s, const Vec4& v) { + return v * s; + } + + /** + * Multiplies this vector by a scalar. Sets this vector to (x * s, y * s, z * s, w * s). + */ + void operator*=(float s) { + *this = *this * s; + } + + /** + * Returns the component-wise quotient of this vector and another vector, computed as + * (x / v.x, y / v.y, z / v.z, w / v.w). + */ + Vec4 operator/(const Vec4& v) const { + return {x / v.x, y / v.y, z / v.z, w / v.w}; + } + + /** + * Returns the component-wise quotient of a vector and a scalar, computed as + * (v.x / s, v.y / s, v.z / s, v.w / s). + */ + friend Vec4 operator/(const Vec4& v, float s) { + return {v.x / s, v.y / s, v.z / s, v.w / s}; + } + + /** + * Returns the i-th component of this vector. + * Valid values for i are 0, 1, 2, and 3. 0 corresponds to x, 1 to y, 2 to z, and 3 to w. + */ + float operator[](int i) const { + return this->ptr()[i]; + } + + /** + * Returns a reference to the i-th component of this vector. + * Valid values for i are 0, 1, 2, and 3. 0 corresponds to x, 1 to y, 2 to z, and 3 to w. + */ + float& operator[](int i) { + return this->ptr()[i]; + } + + /** + * The x component value. + */ + float x; + + /** + * The y component value. + */ + float y; + + /** + * The z component value. + */ + float z; + + /** + * The w component value. + */ + float w; +}; + +/** + * Shuffles the components of a Vec2 into a Vec4. + * @tparam Ix Indices specifying which components to use from the Vec2. Valid values for Ix are 0 + * and 1. 0 corresponds to x, and 1 to y. + */ +template +static inline Vec4 Shuffle(const Vec2& v) { + const float arr[2] = {v.x, v.y}; + return {arr[Ix]...}; +} + +/** + * Returns a vector containing the minimum components of two vectors. + */ +static inline Vec4 Min(const Vec4& a, const Vec4& b) { + return {a.x < b.x ? a.x : b.x, a.y < b.y ? a.y : b.y, a.z < b.z ? a.z : b.z, + a.w < b.w ? a.w : b.w}; +} + +/** + * Returns a vector containing the maximum components of two vectors. + */ +static inline Vec4 Max(const Vec4& a, const Vec4& b) { + return {a.x > b.x ? a.x : b.x, a.y > b.y ? a.y : b.y, a.z > b.z ? a.z : b.z, + a.w > b.w ? a.w : b.w}; +} + +} // namespace tgfx \ No newline at end of file diff --git a/src/core/Matrix3D.cpp b/src/core/Matrix3D.cpp new file mode 100644 index 000000000..74164b229 --- /dev/null +++ b/src/core/Matrix3D.cpp @@ -0,0 +1,364 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "tgfx/core/Matrix3D.h" +#include "utils/MathExtra.h" + +namespace tgfx { + +static void TransposeArrays(const float src[16], float dst[16]) { + dst[0] = src[0]; + dst[1] = src[4]; + dst[2] = src[8]; + dst[3] = src[12]; + dst[4] = src[1]; + dst[5] = src[5]; + dst[6] = src[9]; + dst[7] = src[13]; + dst[8] = src[2]; + dst[9] = src[6]; + dst[10] = src[10]; + dst[11] = src[14]; + dst[12] = src[3]; + dst[13] = src[7]; + dst[14] = src[11]; + dst[15] = src[15]; +} + +static bool InvertMatrix3D(const float inMat[16], float outMat[16]) { + const float a00 = inMat[0]; + const float a01 = inMat[1]; + const float a02 = inMat[2]; + const float a03 = inMat[3]; + const float a10 = inMat[4]; + const float a11 = inMat[5]; + const float a12 = inMat[6]; + const float a13 = inMat[7]; + const float a20 = inMat[8]; + const float a21 = inMat[9]; + const float a22 = inMat[10]; + const float a23 = inMat[11]; + const float a30 = inMat[12]; + const float a31 = inMat[13]; + const float a32 = inMat[14]; + const float a33 = inMat[15]; + + float b00 = a00 * a11 - a01 * a10; + float b01 = a00 * a12 - a02 * a10; + float b02 = a00 * a13 - a03 * a10; + float b03 = a01 * a12 - a02 * a11; + float b04 = a01 * a13 - a03 * a11; + float b05 = a02 * a13 - a03 * a12; + float b06 = a20 * a31 - a21 * a30; + float b07 = a20 * a32 - a22 * a30; + float b08 = a20 * a33 - a23 * a30; + float b09 = a21 * a32 - a22 * a31; + float b10 = a21 * a33 - a23 * a31; + float b11 = a22 * a33 - a23 * a32; + + const float determinant = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + if (FloatNearlyZero(determinant, FLOAT_NEARLY_ZERO * FLOAT_NEARLY_ZERO * FLOAT_NEARLY_ZERO)) { + return false; + } + const float invdet = 1.f / determinant; + b00 *= invdet; + b01 *= invdet; + b02 *= invdet; + b03 *= invdet; + b04 *= invdet; + b05 *= invdet; + b06 *= invdet; + b07 *= invdet; + b08 *= invdet; + b09 *= invdet; + b10 *= invdet; + b11 *= invdet; + + outMat[0] = a11 * b11 - a12 * b10 + a13 * b09; + outMat[1] = a02 * b10 - a01 * b11 - a03 * b09; + outMat[2] = a31 * b05 - a32 * b04 + a33 * b03; + outMat[3] = a22 * b04 - a21 * b05 - a23 * b03; + outMat[4] = a12 * b08 - a10 * b11 - a13 * b07; + outMat[5] = a00 * b11 - a02 * b08 + a03 * b07; + outMat[6] = a32 * b02 - a30 * b05 - a33 * b01; + outMat[7] = a20 * b05 - a22 * b02 + a23 * b01; + outMat[8] = a10 * b10 - a11 * b08 + a13 * b06; + outMat[9] = a01 * b08 - a00 * b10 - a03 * b06; + outMat[10] = a30 * b04 - a31 * b02 + a33 * b00; + outMat[11] = a21 * b02 - a20 * b04 - a23 * b00; + outMat[12] = a11 * b07 - a10 * b09 - a12 * b06; + outMat[13] = a00 * b09 - a01 * b07 + a02 * b06; + outMat[14] = a31 * b01 - a30 * b03 - a32 * b00; + outMat[15] = a20 * b03 - a21 * b01 + a22 * b00; + + if (!FloatsAreFinite(outMat, 16)) { + return false; + } + + return true; +} + +static Rect MapRectAffine(const Rect& srcRect, const float mat[16]) { + constexpr Vec4 flip{1.f, 1.f, -1.f, -1.f}; + + auto c0 = Shuffle<0, 1, 0, 1>(Vec2::Load(mat)) * flip; + auto c1 = Shuffle<0, 1, 0, 1>(Vec2::Load(mat + 4)) * flip; + auto c3 = Shuffle<0, 1, 0, 1>(Vec2::Load(mat + 12)); + + auto p0 = Min(c0 * srcRect.left + c1 * srcRect.top, c0 * srcRect.right + c1 * srcRect.top); + auto p1 = Min(c0 * srcRect.left + c1 * srcRect.bottom, c0 * srcRect.right + c1 * srcRect.bottom); + auto minMax = c3 + flip * Min(p0, p1); + + return {minMax.x, minMax.y, minMax.z, minMax.w}; +} + +static Rect MapRectPerspective(const Rect& srcRect, const float mat[16]) { + auto c0 = Vec4::Load(mat); + auto c1 = Vec4::Load(mat + 4); + auto c3 = Vec4::Load(mat + 12); + + auto tl = c0 * srcRect.left + c1 * srcRect.top + c3; + auto tr = c0 * srcRect.right + c1 * srcRect.top + c3; + auto bl = c0 * srcRect.left + c1 * srcRect.bottom + c3; + auto br = c0 * srcRect.right + c1 * srcRect.bottom + c3; + + constexpr Vec4 flip{1.f, 1.f, -1.f, -1.f}; + auto project = [&flip](const Vec4& p0, const Vec4& p1, const Vec4& p2) { + const float w0 = p0[3]; + if (constexpr float w0PlaneDistance = 1.f / (1 << 14); w0 >= w0PlaneDistance) { + return flip * Shuffle<0, 1, 0, 1>(Vec2(p0.x, p0.y)) / w0; + } else { + auto clip = [&](const Vec4& p) { + if (const float w = p[3]; w >= w0PlaneDistance) { + const float t = (w0PlaneDistance - w0) / (w - w0); + auto c = (t * Vec2::Load(p.ptr()) + (1.f - t) * Vec2::Load(p0.ptr())) / w0PlaneDistance; + return flip * Shuffle<0, 1, 0, 1>(c); + } else { + return Vec4(std::numeric_limits::infinity()); + } + }; + return Min(clip(p1), clip(p2)); + } + }; + + auto p0 = Min(project(tl, tr, bl), project(tr, br, tl)); + auto p1 = Min(project(br, bl, tr), project(bl, tl, br)); + auto minMax = flip * Min(p0, p1); + return {minMax.x, minMax.y, minMax.z, minMax.w}; +} + +const Matrix3D& Matrix3D::I() { + static constexpr Matrix3D identity; + return identity; +} + +Matrix3D Matrix3D::LookAt(const Vec3& eye, const Vec3& center, const Vec3& up) { + auto viewZ = Vec3::Normalize(eye - center); + auto viewX = Vec3::Normalize(up.cross(viewZ)); + auto viewY = viewZ.cross(viewX); + const Matrix3D m(viewX.x, viewY.x, viewZ.x, 0.f, viewX.y, viewY.y, viewZ.y, 0.f, viewX.z, viewY.z, + viewZ.z, 0.f, -viewX.dot(eye), -viewY.dot(eye), -viewZ.dot(eye), 1.f); + return m; +} + +Matrix3D Matrix3D::Perspective(float fovyDegress, float aspect, float nearZ, float farZ) { + auto fovyRadians = DegreesToRadians(fovyDegress); + const float cotan = 1.f / tanf(fovyRadians / 2.f); + + const Matrix3D m(cotan / aspect, 0.f, 0.f, 0.f, 0.f, cotan, 0.f, 0.f, 0.f, 0.f, + (nearZ + farZ) / (nearZ - farZ), -1.f, 0.f, 0.f, + (2.f * nearZ * farZ) / (nearZ - farZ), 0.f); + return m; +} + +Rect Matrix3D::mapRect(const Rect& src) const { + if (hasPerspective()) { + return MapRectPerspective(src, values); + } else { + return MapRectAffine(src, values); + } +} + +void Matrix3D::getRowMajor(float buffer[16]) const { + TransposeArrays(values, buffer); +} + +void Matrix3D::setConcat(const Matrix3D& a, const Matrix3D& b) { + auto c0 = a.getCol(0); + auto c1 = a.getCol(1); + auto c2 = a.getCol(2); + auto c3 = a.getCol(3); + + auto compute = [&](Vec4 v) { return c0 * v[0] + c1 * v[1] + c2 * v[2] + c3 * v[3]; }; + + auto m0 = compute(b.getCol(0)); + auto m1 = compute(b.getCol(1)); + auto m2 = compute(b.getCol(2)); + auto m3 = compute(b.getCol(3)); + + setColumn(0, m0); + setColumn(1, m1); + setColumn(2, m2); + setColumn(3, m3); +} + +void Matrix3D::preConcat(const Matrix3D& m) { + setConcat(*this, m); +} + +void Matrix3D::postConcat(const Matrix3D& m) { + setConcat(m, *this); +} + +void Matrix3D::preScale(float sx, float sy, float sz) { + if (sx == 1 && sy == 1 && sz == 1) { + return; + } + + auto c0 = getCol(0); + auto c1 = getCol(1); + auto c2 = getCol(2); + + setColumn(0, c0 * sx); + setColumn(1, c1 * sy); + setColumn(2, c2 * sz); +} + +void Matrix3D::postScale(float sx, float sy, float sz) { + if (sx == 1 && sy == 1 && sz == 1) { + return; + } + auto m = MakeScale(sx, sy, sz); + this->postConcat(m); +} + +void Matrix3D::preTranslate(float tx, float ty, float tz) { + auto c0 = getCol(0); + auto c1 = getCol(1); + auto c2 = getCol(2); + auto c3 = getCol(3); + + setColumn(3, (c0 * tx + c1 * ty + c2 * tz + c3)); +} + +void Matrix3D::postTranslate(float tx, float ty, float tz) { + values[12] += tx; + values[13] += ty; + values[14] += tz; +} + +void Matrix3D::preRotate(const Vec3& axis, float degrees) { + auto m = MakeRotate(axis, degrees); + preConcat(m); +} + +void Matrix3D::postRotate(const Vec3& axis, float degrees) { + auto m = MakeRotate(axis, degrees); + postConcat(m); +} + +bool Matrix3D::invert(Matrix3D* inverse) const { + float result[16]; + if (!InvertMatrix3D(values, result)) { + return false; + } + memcpy(inverse->values, result, sizeof(result)); + return true; +} + +Matrix3D Matrix3D::transpose() const { + Matrix3D m; + TransposeArrays(values, m.values); + 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) { + values[0] = m00; + values[1] = m01; + values[2] = m02; + values[3] = m03; + values[4] = m10; + values[5] = m11; + values[6] = m12; + values[7] = m13; + values[8] = m20; + values[9] = m21; + values[10] = m22; + values[11] = m23; + values[12] = m30; + values[13] = m31; + values[14] = m32; + values[15] = m33; +} + +void Matrix3D::setRotate(const Vec3& axis, float degrees) { + if (auto len = axis.length(); len > 0 && (len * 0 == 0)) { + this->setRotateUnit(axis * (1.f / len), degrees); + } else { + this->setIdentity(); + } +} + +void Matrix3D::setRotateUnit(const Vec3& axis, float degrees) { + auto radians = DegreesToRadians(degrees); + this->setRotateUnitSinCos(axis, sin(radians), cos(radians)); +} + +void Matrix3D::setRotateUnitSinCos(const Vec3& axis, float sinAngle, float cosAngle) { + const float x = axis.x; + const float y = axis.y; + const float z = axis.z; + const float c = cosAngle; + const float s = sinAngle; + const float t = 1 - c; + + setAll(t * x * x + c, t * x * y + s * z, t * x * z - s * y, 0, t * x * y - s * z, t * y * y + c, + t * y * z + s * x, 0, t * x * z + s * y, t * y * z - s * x, t * z * z + c, 0, 0, 0, 0, 1); +} + +bool Matrix3D::operator==(const Matrix3D& other) const { + if (this == &other) { + return true; + } + + auto a0 = getCol(0); + auto a1 = getCol(1); + auto a2 = getCol(2); + auto a3 = getCol(3); + + auto b0 = other.getCol(0); + auto b1 = other.getCol(1); + auto b2 = other.getCol(2); + auto b3 = other.getCol(3); + + return ((a0 == b0) && (a1 == b1) && (a2 == b2) && (a3 == b3)); +} + +} // namespace tgfx diff --git a/src/core/Vec.cpp b/src/core/Vec.cpp new file mode 100644 index 000000000..7cd45f072 --- /dev/null +++ b/src/core/Vec.cpp @@ -0,0 +1,30 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "tgfx/core/Vec.h" +#include "core/utils/MathExtra.h" +#include "utils/Log.h" + +namespace tgfx { + +Vec2 operator/(const Vec2& v, float s) { + DEBUG_ASSERT(!FloatNearlyZero(s)); + return {v.x / s, v.y / s}; +} + +} // namespace tgfx diff --git a/src/core/filters/Transform3DImageFilter.cpp b/src/core/filters/Transform3DImageFilter.cpp new file mode 100644 index 000000000..a46113d1a --- /dev/null +++ b/src/core/filters/Transform3DImageFilter.cpp @@ -0,0 +1,130 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "Transform3DImageFilter.h" +#include "core/utils/MathExtra.h" +#include "core/utils/PlacementPtr.h" +#include "gpu/DrawingManager.h" +#include "gpu/TPArgs.h" +#include "gpu/ops/Rect3DDrawOp.h" +#include "gpu/processors/TextureEffect.h" +#include "gpu/proxies/RenderTargetProxy.h" +#include "tgfx/core/Matrix3D.h" + +namespace tgfx { + +std::shared_ptr ImageFilter::Transform3D(const Matrix3D& matrix) { + return std::make_shared(matrix); +} + +Transform3DImageFilter::Transform3DImageFilter(const Matrix3D& matrix) : matrix(matrix) { +} + +Rect Transform3DImageFilter::onFilterBounds(const Rect& srcRect) const { + // Align the camera center with the center of the source rect. + auto srcModelRect = Rect::MakeXYWH(-srcRect.width() * 0.5f, -srcRect.height() * 0.5f, + srcRect.width(), srcRect.height()); + auto dstModelRect = matrix.mapRect(srcModelRect); + // The minimum axis-aligned bounding rectangle of srcRect after projection is calculated based on + // its relative position to the standard rectangle. + auto result = Rect::MakeXYWH(dstModelRect.left - srcModelRect.left + srcRect.left, + dstModelRect.top - srcModelRect.top + srcRect.top, + dstModelRect.width(), dstModelRect.height()); + return result; +} + +std::shared_ptr Transform3DImageFilter::lockTextureProxy( + std::shared_ptr source, const Rect& renderBounds, const TPArgs& args) const { + float dstDrawWidth = renderBounds.width(); + float dstDrawHeight = renderBounds.height(); + DEBUG_ASSERT(args.drawScale > 0.f); + if (!FloatNearlyEqual(args.drawScale, 1.f)) { + dstDrawWidth = dstDrawWidth * args.drawScale; + dstDrawHeight = dstDrawHeight * args.drawScale; + } + dstDrawWidth = std::ceil(dstDrawWidth); + dstDrawHeight = std::ceil(dstDrawHeight); + const float drawScaleX = dstDrawWidth / renderBounds.width(); + const float drawScaleY = dstDrawHeight / renderBounds.height(); + + auto renderTarget = RenderTargetProxy::MakeFallback( + args.context, static_cast(dstDrawWidth), static_cast(dstDrawHeight), + 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()); + // Align the camera center with the initial position center of the source model. + auto srcModelRect = Rect::MakeXYWH(-srcW * 0.5f, -srcH * 0.5f, srcW, srcH); + auto dstModelRect = matrix.mapRect(srcModelRect); + // SrcProjectRect is the result of projecting srcRect onto the canvas. RenderBounds describes a + // subregion that needs to be drawn within it. + auto srcProjectRect = + Rect::MakeXYWH(dstModelRect.left - srcModelRect.left, dstModelRect.top - srcModelRect.top, + dstModelRect.width(), dstModelRect.height()); + // 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. + // NdcScale first maps the projected coordinates to the NDC region [-1, 1], then scales them so + // that the required drawing area exactly fills the [-1, 1] clip region. The scaling formula is: + // ((2 / srcProjectRect) * (srcProjectRect / renderBounds)) + // Scaling the original image with drawScale does not affect the calculation result of ndcScale. + const Vec2 ndcScale(2.0f / renderBounds.width(), 2.0f / renderBounds.height()); + // ndcOffset translates the NDC coordinates so that the local area to be drawn aligns exactly with + // the (-1, -1) point. + auto ndcRectScaled = + Rect::MakeXYWH(dstModelRect.left * ndcScale.x, dstModelRect.top * ndcScale.y, + dstModelRect.width() * ndcScale.x, dstModelRect.height() * ndcScale.y); + const Vec2 renderBoundsLTNDCScaled((renderBounds.left - srcProjectRect.left) * ndcScale.x, + (renderBounds.top - srcProjectRect.top) * ndcScale.y); + const Vec2 ndcOffset(-1.f - ndcRectScaled.left - renderBoundsLTNDCScaled.x, + -1.f - ndcRectScaled.top - renderBoundsLTNDCScaled.y); + + auto drawingManager = args.context->drawingManager(); + auto drawingBuffer = args.context->drawingBuffer(); + auto vertexProvider = + RectsVertexProvider::MakeFrom(drawingBuffer, srcModelRect, AAType::Coverage); + const Size viewportSize(static_cast(renderTarget->width()), + static_cast(renderTarget->height())); + const Rect3DDrawArgs drawArgs{matrix, ndcScale, ndcOffset, viewportSize}; + auto drawOp = + Rect3DDrawOp::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] + auto uvMatrix = Matrix::MakeTrans(-srcModelRect.left, -srcModelRect.top); + // The Size obtained from Source is the original size, while the texture size generated by Source + // is the size after applying DrawScale. Texture sampling requires corresponding scaling. + uvMatrix.postScale(drawScaleX, drawScaleY); + auto fragmentProcessor = + TextureEffect::Make(std::move(sourceTextureProxy), samplingArgs, &uvMatrix); + drawOp->addColorFP(std::move(fragmentProcessor)); + std::vector> drawOps; + drawOps.emplace_back(std::move(drawOp)); + auto drawOpArray = drawingBuffer->makeArray(std::move(drawOps)); + drawingManager->addOpsRenderTask(renderTarget, std::move(drawOpArray), std::nullopt); + + return renderTarget->asTextureProxy(); +} + +PlacementPtr Transform3DImageFilter::asFragmentProcessor( + std::shared_ptr source, const FPArgs& args, const SamplingOptions& sampling, + SrcRectConstraint constraint, const Matrix* uvMatrix) const { + return makeFPFromTextureProxy(source, args, sampling, constraint, uvMatrix); +} + +} // namespace tgfx \ No newline at end of file diff --git a/src/core/filters/Transform3DImageFilter.h b/src/core/filters/Transform3DImageFilter.h new file mode 100644 index 000000000..6512094c9 --- /dev/null +++ b/src/core/filters/Transform3DImageFilter.h @@ -0,0 +1,63 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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/core/ImageFilter.h" +#include "tgfx/core/Matrix3D.h" + +namespace tgfx { + +/** + * Transform3DImageFilter is an image filter that applies a perspective transformation to the input + * image. + */ +class Transform3DImageFilter final : public ImageFilter { + public: + /** + * Creates a Transform3DImageFilter 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. + */ + explicit Transform3DImageFilter(const Matrix3D& matrix); + + private: + Type type() const override { + return Type::Transform3D; + } + + Rect onFilterBounds(const Rect& srcRect) const override; + + std::shared_ptr lockTextureProxy(std::shared_ptr source, + const Rect& renderBounds, + const TPArgs& args) const override; + + PlacementPtr asFragmentProcessor(std::shared_ptr source, + const FPArgs& args, + const SamplingOptions& sampling, + SrcRectConstraint constraint, + const Matrix* uvMatrix) const override; + + /** + * 3D transformation matrix used to convert model coordinates to clip space. + */ + Matrix3D matrix = Matrix3D::I(); +}; + +} // namespace tgfx diff --git a/src/gpu/DrawingManager.cpp b/src/gpu/DrawingManager.cpp index 94ee3fab7..630259672 100644 --- a/src/gpu/DrawingManager.cpp +++ b/src/gpu/DrawingManager.cpp @@ -28,7 +28,6 @@ #include "gpu/tasks/RuntimeDrawTask.h" #include "gpu/tasks/SemaphoreWaitTask.h" #include "inspect/InspectorMark.h" -#include "tgfx/core/RenderFlags.h" namespace tgfx { static ColorType GetAtlasColorType(bool isAlphaOnly) { diff --git a/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.cpp b/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.cpp new file mode 100644 index 000000000..94bc89209 --- /dev/null +++ b/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.cpp @@ -0,0 +1,86 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "GLSLQuadPerEdgeAA3DGeometryProcessor.h" + +namespace tgfx { + +static constexpr char UniformTransformMatrixName[] = "transformMatrix"; +static constexpr char UniformNdcScaleName[] = "ndcScale"; +static constexpr char UniformNdcOffsetName[] = "ndcOffset"; + +PlacementPtr Transform3DGeometryProcessor::Make( + BlockBuffer* buffer, AAType aa, const Matrix3D& matrix, const Vec2& ndcScale, + const Vec2& ndcOffset) { + return buffer->make(aa, matrix, ndcScale, ndcOffset); +} + +GLSLQuadPerEdgeAA3DGeometryProcessor::GLSLQuadPerEdgeAA3DGeometryProcessor(AAType aa, + const Matrix3D& matrix, + const Vec2& ndcScale, + const Vec2& ndcOffset) + : Transform3DGeometryProcessor(aa, matrix, ndcScale, ndcOffset) { +} + +void GLSLQuadPerEdgeAA3DGeometryProcessor::emitCode(EmitArgs& args) const { + auto vertBuilder = args.vertBuilder; + auto fragBuilder = args.fragBuilder; + auto varyingHandler = args.varyingHandler; + auto uniformHandler = args.uniformHandler; + + varyingHandler->emitAttributes(*this); + emitTransforms(args, vertBuilder, varyingHandler, uniformHandler, ShaderVar(position)); + + if (aa == AAType::Coverage) { + auto coverageVar = varyingHandler->addVarying("Coverage", SLType::Float); + vertBuilder->codeAppendf("%s = %s;", coverageVar.vsOut().c_str(), coverage.name().c_str()); + fragBuilder->codeAppendf("%s = vec4(%s);", args.outputCoverage.c_str(), + coverageVar.fsIn().c_str()); + } else { + 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()); + auto transformMatrixName = uniformHandler->addUniform( + UniformTransformMatrixName, UniformFormat::Float4x4, ShaderStage::Vertex); + args.vertBuilder->codeAppendf("vec4 clipPoint = %s * vec4(%s, 0.0, 1.0);", + transformMatrixName.c_str(), position.name().c_str()); + auto ndcScaleName = + uniformHandler->addUniform(UniformNdcScaleName, UniformFormat::Float2, ShaderStage::Vertex); + args.vertBuilder->codeAppendf("vec4 clipScale = vec4(%s.xy, 1.0, 1.0);", ndcScaleName.c_str()); + auto ndcOffsetName = + uniformHandler->addUniform(UniformNdcOffsetName, UniformFormat::Float2, ShaderStage::Vertex); + args.vertBuilder->codeAppendf("vec4 clipOffset = vec4((%s * clipPoint.w).xy, 0.0, 0.0);", + ndcOffsetName.c_str()); + args.vertBuilder->codeAppend("gl_Position = clipPoint * clipScale + clipOffset;"); +} + +void GLSLQuadPerEdgeAA3DGeometryProcessor::setData(UniformBuffer* vertexUniformBuffer, + UniformBuffer* fragmentUniformBuffer, + FPCoordTransformIter* transformIter) const { + setTransformDataHelper(Matrix::I(), vertexUniformBuffer, transformIter); + fragmentUniformBuffer->setData("Color", defaultColor); + vertexUniformBuffer->setData("transformMatrix", matrix); + vertexUniformBuffer->setData("ndcScale", ndcScale); + vertexUniformBuffer->setData("ndcOffset", ndcOffset); +} + +} // namespace tgfx \ No newline at end of file diff --git a/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.h b/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.h new file mode 100644 index 000000000..77babe93e --- /dev/null +++ b/src/gpu/glsl/processors/GLSLQuadPerEdgeAA3DGeometryProcessor.h @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "gpu/processors/Transform3DGeometryProcessor.h" + +namespace tgfx { + +/** + * The implementation of QuadPerEdgeAA3DGeometryProcessor using GLSL. + */ +class GLSLQuadPerEdgeAA3DGeometryProcessor final : public Transform3DGeometryProcessor { + public: + /** + * Creates a GLSLQuadPerEdgeAA3DGeometryProcessor instance with the specified parameters. + */ + explicit GLSLQuadPerEdgeAA3DGeometryProcessor(AAType aa, const Matrix3D& matrix, + const Vec2& ndcScale, const Vec2& ndcOffset); + + void emitCode(EmitArgs& args) const override; + + void setData(UniformBuffer* vertexUniformBuffer, UniformBuffer* fragmentUniformBuffer, + FPCoordTransformIter* transformIter) const override; + + private: + Color defaultColor = Color::White(); +}; + +} // namespace tgfx \ No newline at end of file diff --git a/src/gpu/ops/DrawOp.h b/src/gpu/ops/DrawOp.h index 63be6ebae..fa18758de 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 }; + enum class Type { RectDrawOp, RRectDrawOp, ShapeDrawOp, AtlasTextOp, Rect3DDrawOp }; virtual ~DrawOp() = default; diff --git a/src/gpu/ops/Rect3DDrawOp.cpp b/src/gpu/ops/Rect3DDrawOp.cpp new file mode 100644 index 000000000..d93dee041 --- /dev/null +++ b/src/gpu/ops/Rect3DDrawOp.cpp @@ -0,0 +1,116 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "Rect3DDrawOp.h" +#include "core/utils/MathExtra.h" +#include "gpu/GlobalCache.h" +#include "gpu/ProxyProvider.h" +#include "gpu/processors/Transform3DGeometryProcessor.h" +#include "tgfx/core/RenderFlags.h" + +namespace tgfx { + +// The maximum number of vertices per non-AA quad. +static constexpr uint32_t IndicesPerNonAAQuad = 6; +// The maximum number of vertices per AA quad. +static constexpr uint32_t IndicesPerAAQuad = 30; + +PlacementPtr Rect3DDrawOp::Make(Context* context, + PlacementPtr provider, + uint32_t renderFlags, + const Rect3DDrawArgs& drawArgs) { + if (provider == nullptr) { + return nullptr; + } + auto drawOp = context->drawingBuffer()->make(provider.get(), drawArgs); + if (provider->aaType() == AAType::Coverage || provider->rectCount() > 1) { + drawOp->indexBufferProxy = + context->globalCache()->getRectIndexBuffer(provider->aaType() == AAType::Coverage); + } + if (provider->rectCount() <= 1) { + // If we only have one rect, it is not worth the async task overhead. + renderFlags |= RenderFlags::DisableAsyncTask; + } + drawOp->vertexBufferProxyView = + context->proxyProvider()->createVertexBufferProxy(std::move(provider), renderFlags); + return drawOp; +} + +Rect3DDrawOp::Rect3DDrawOp(RectsVertexProvider* provider, const Rect3DDrawArgs& drawArgs) + : DrawOp(provider->aaType()), drawArgs(drawArgs), rectCount(provider->rectCount()) { + if (!provider->hasUVCoord()) { + auto matrix = provider->firstMatrix(); + matrix.invert(&matrix); + uvMatrix = matrix; + } + if (!provider->hasColor()) { + commonColor = provider->firstColor(); + } + hasSubset = provider->hasSubset(); +} + +PlacementPtr Rect3DDrawOp::onMakeGeometryProcessor(RenderTarget* renderTarget) { + auto drawingBuffer = renderTarget->getContext()->drawingBuffer(); + // 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. + // + // NDC_Point is the projected vertex coordinate in NDC space, and NDC_Point_shifted is the + // adjusted NDC coordinate. scale1 and offset1 are transformation parameters passed externally, + // while scale2 and offset2 map the NDC coordinates from the valid space to the actual space. + // + // NDC_Point_shifted = ((NDC_Point * scale1) + offset1) * scale2 + offset2 + auto renderTargetW = static_cast(renderTarget->width()); + DEBUG_ASSERT(!FloatNearlyZero(renderTargetW)); + auto renderTargetH = static_cast(renderTarget->height()); + DEBUG_ASSERT(!FloatNearlyZero(renderTargetH)); + const Vec2 scale2(drawArgs.viewportSize.width / renderTargetW, + drawArgs.viewportSize.height / renderTargetH); + Vec2 ndcScale = drawArgs.ndcScale * scale2; + Vec2 ndcOffset = drawArgs.ndcOffset * scale2 + scale2 - Vec2(1.f, 1.f); + if (renderTarget->origin() == ImageOrigin::BottomLeft) { + ndcScale.y = -ndcScale.y; + ndcOffset.y = -ndcOffset.y; + } + return Transform3DGeometryProcessor::Make(drawingBuffer, aaType, drawArgs.transformMatrix, + ndcScale, ndcOffset); +} + +void Rect3DDrawOp::onDraw(RenderPass* renderPass) { + std::shared_ptr indexBuffer = nullptr; + if (indexBufferProxy) { + indexBuffer = indexBufferProxy->getBuffer(); + if (indexBuffer == nullptr) { + return; + } + } + auto vertexBuffer = vertexBufferProxyView ? vertexBufferProxyView->getBuffer() : nullptr; + if (vertexBuffer == nullptr) { + return; + } + renderPass->setVertexBuffer(vertexBuffer->gpuBuffer(), vertexBufferProxyView->offset()); + renderPass->setIndexBuffer(indexBuffer ? indexBuffer->gpuBuffer() : nullptr); + if (indexBuffer != nullptr) { + auto numIndicesPerQuad = aaType == AAType::Coverage ? IndicesPerAAQuad : IndicesPerNonAAQuad; + renderPass->drawIndexed(PrimitiveType::Triangles, 0, rectCount * numIndicesPerQuad); + } else { + renderPass->draw(PrimitiveType::TriangleStrip, 0, 4); + } +} + +} // namespace tgfx \ No newline at end of file diff --git a/src/gpu/ops/Rect3DDrawOp.h b/src/gpu/ops/Rect3DDrawOp.h new file mode 100644 index 000000000..e40777fa2 --- /dev/null +++ b/src/gpu/ops/Rect3DDrawOp.h @@ -0,0 +1,88 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "DrawOp.h" +#include "core/utils/PlacementPtr.h" +#include "gpu/RectsVertexProvider.h" +#include "gpu/proxies/IndexBufferProxy.h" +#include "gpu/proxies/VertexBufferProxyView.h" +#include "tgfx/core/Matrix3D.h" +#include "tgfx/gpu/Context.h" + +namespace tgfx { + +/** + * PerspectiveRenderArgs defines arguments for perspective rendering. + */ +struct Rect3DDrawArgs { + /** + * The transformation matrix from local space to clip space. + */ + Matrix3D transformMatrix; + + /** + * The scaling and translation parameters in NDC space. After the projected model's vertex + * coordinates are transformed to NDC, ndcScale is applied for scaling, followed by ndcOffset for + * translation. These two properties allow any rectangular region of the projected model to be + * mapped to any position within the target texture. + */ + Vec2 ndcScale; + Vec2 ndcOffset; + + /** + * Reference viewport size, used to convert NDC coordinates to window coordinates. The external + * transformMatrix, ndcScale, and ndcOffset are all defined based on this viewport size. + */ + Size viewportSize; +}; + +class Rect3DDrawOp : 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); + + private: + Rect3DDrawOp(RectsVertexProvider* provider, const Rect3DDrawArgs& drawArgs); + + PlacementPtr onMakeGeometryProcessor(RenderTarget* renderTarget) override; + + void onDraw(RenderPass* renderPass) override; + + Type type() override { + return Type::Rect3DDrawOp; + } + + Rect3DDrawArgs drawArgs; + + size_t rectCount = 0; + std::optional commonColor = std::nullopt; + std::optional uvMatrix = std::nullopt; + bool hasSubset = false; + + std::shared_ptr indexBufferProxy = nullptr; + std::shared_ptr vertexBufferProxyView = nullptr; + + friend class BlockBuffer; +}; + +} // namespace tgfx \ No newline at end of file diff --git a/src/gpu/processors/Transform3DGeometryProcessor.cpp b/src/gpu/processors/Transform3DGeometryProcessor.cpp new file mode 100644 index 000000000..7855fa845 --- /dev/null +++ b/src/gpu/processors/Transform3DGeometryProcessor.cpp @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// Tencent is pleased to support the open source community by making tgfx available. +// +// Copyright (C) 2023 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 "Transform3DGeometryProcessor.h" + +namespace tgfx { + +Transform3DGeometryProcessor::Transform3DGeometryProcessor(AAType aa, const Matrix3D& transform, + const Vec2& ndcScale, + const Vec2& ndcOffset) + : GeometryProcessor(ClassID()), aa(aa), matrix(transform), ndcScale(ndcScale), + ndcOffset(ndcOffset) { + position = {"aPosition", VertexFormat::Float2}; + if (aa == AAType::Coverage) { + coverage = {"inCoverage", VertexFormat::Float}; + } + setVertexAttributes(&position, 2); +} + +void Transform3DGeometryProcessor::onComputeProcessorKey(BytesKey* bytesKey) const { + uint32_t flags = (aa == AAType::Coverage ? 1 : 0); + bytesKey->write(flags); +} + +} // namespace tgfx diff --git a/src/gpu/processors/Transform3DGeometryProcessor.h b/src/gpu/processors/Transform3DGeometryProcessor.h new file mode 100644 index 000000000..88d8c9ff3 --- /dev/null +++ b/src/gpu/processors/Transform3DGeometryProcessor.h @@ -0,0 +1,73 @@ +///////////////////////////////////////////////////////////////////////////////////////////////// +// +// 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 "GeometryProcessor.h" +#include "gpu/AAType.h" +#include "tgfx/core/Matrix3D.h" + +namespace tgfx { + +/** + * A geometry processor for rendering 3D transformed quads with optional per-edge anti-aliasing. + */ +class Transform3DGeometryProcessor : public GeometryProcessor { + public: + /** + * Creates a Transform3DGeometryProcessor instance with the specified parameters. + */ + static PlacementPtr Make(BlockBuffer* buffer, AAType aa, + const Matrix3D& matrix, + const Vec2& ndcScale, + const Vec2& ndcOffset); + + std::string name() const override { + return "Transform3DGeometryProcessor"; + } + + protected: + DEFINE_PROCESSOR_CLASS_ID + + explicit Transform3DGeometryProcessor(AAType aa, const Matrix3D& transform, const Vec2& ndcScale, + const Vec2& ndcOffset); + + void onComputeProcessorKey(BytesKey* bytesKey) const override; + + Attribute position = {}; + + Attribute coverage = {}; + + AAType aa = AAType::None; + + /** + * The transformation matrix from local space to clip space. + */ + Matrix3D matrix = Matrix3D::I(); + + /** + * The scaling and translation parameters in NDC space. After the projected model's vertex + * coordinates are transformed to NDC, ndcScale is applied for scaling, followed by ndcOffset for + * translation. These two properties allow any rectangular region of the projected model to be + * mapped to any position within the target texture. + */ + Vec2 ndcScale = Vec2(0.f, 0.f); + Vec2 ndcOffset = Vec2(0.f, 0.f); +}; + +} // namespace tgfx \ No newline at end of file diff --git a/test/baseline/version.json b/test/baseline/version.json index 8bfe87684..bc52ba9e0 100644 --- a/test/baseline/version.json +++ b/test/baseline/version.json @@ -141,6 +141,10 @@ "ModeColorFilter": "8bd149a4", "OpacityShadowTest": "ee2cc771", "RuntimeEffect": "aac25365", + "Transform3DImageFilterCSSBasic": "d541f591", + "Transform3DImageFilterStandardClip": "d541f591", + "Transorm3DImageFilterStandardBasic": "d541f591", + "Transorm3DImageFilterStandardScale": "8be82b96", "blur": "3e0f58ab", "blur-large-pixel": "c26ff3d9", "dropShadow": "ee2cc771", diff --git a/test/src/FilterTest.cpp b/test/src/FilterTest.cpp index 2fbc7b84f..15dcf3c60 100644 --- a/test/src/FilterTest.cpp +++ b/test/src/FilterTest.cpp @@ -25,6 +25,7 @@ #include "core/filters/InnerShadowImageFilter.h" #include "core/shaders/GradientShader.h" #include "core/shaders/ImageShader.h" +#include "core/utils/MathExtra.h" #include "gtest/gtest.h" #include "tgfx/core/BlendMode.h" #include "tgfx/core/Color.h" @@ -807,24 +808,22 @@ TGFX_TEST(FilterTest, GaussianBlurImageFilter) { std::make_shared(5.0f, 5.0f, TileMode::Decal); // Divide into 4 equal tiles. - const auto clipRect1 = - Rect::MakeWH(std::floor(static_cast(opaqueImage->width()) * 0.5f), - std::floor(static_cast(opaqueImage->height()) * 0.5f)); + auto clipRect1 = Rect::MakeWH(std::floor(static_cast(opaqueImage->width()) * 0.5f), + std::floor(static_cast(opaqueImage->height()) * 0.5f)); auto image1 = opaqueImage->makeWithFilter(gaussianBlurFilter, nullptr, &clipRect1); canvas->drawImage(image1, 0.0f, 0.0f); - const auto clipRect2 = + auto clipRect2 = Rect(clipRect1.right, 0.0f, static_cast(opaqueImage->width()), clipRect1.bottom); auto image2 = opaqueImage->makeWithFilter(gaussianBlurFilter, nullptr, &clipRect2); canvas->drawImage(image2, static_cast(opaqueImage->width()) * 0.5f, 0.0f); - const auto clipRect3 = + auto clipRect3 = Rect(0.0f, clipRect1.bottom, clipRect1.right, static_cast(opaqueImage->height())); auto image3 = opaqueImage->makeWithFilter(gaussianBlurFilter, nullptr, &clipRect3); canvas->drawImage(image3, 0.0f, static_cast(opaqueImage->height()) * 0.5f); - const auto clipRect4 = - Rect(clipRect2.left, clipRect2.bottom, clipRect2.right, clipRect3.bottom); + auto clipRect4 = Rect(clipRect2.left, clipRect2.bottom, clipRect2.right, clipRect3.bottom); auto image4 = opaqueImage->makeWithFilter(gaussianBlurFilter, nullptr, &clipRect4); canvas->drawImage(image4, static_cast(opaqueImage->width()) * 0.5f, static_cast(opaqueImage->height()) * 0.5f); @@ -833,4 +832,140 @@ TGFX_TEST(FilterTest, GaussianBlurImageFilter) { EXPECT_TRUE(Baseline::Compare(surface, "FilterTest/GaussianBlurImageFilterComplex2D")); } } + +TGFX_TEST(FilterTest, Transform3DImageFilter) { + const ContextScope scope; + Context* context = scope.getContext(); + ASSERT_TRUE(context != nullptr); + auto surface = Surface::Make(context, 200, 200); + ASSERT_TRUE(surface != nullptr); + Canvas* canvas = surface->getCanvas(); + auto image = MakeImage("resources/apitest/imageReplacement.jpg"); + Size imageSize(static_cast(image->width()), static_cast(image->height())); + + // Test basic drawing with css perspective type. + { + canvas->save(); + canvas->clear(); + + auto cssPerspectiveMatrix = Matrix3D::I(); + constexpr float eyeDistance = 1200.f; + constexpr float farZ = -1000.f; + constexpr float shift = 10.f; + const float nearZ = eyeDistance - shift; + const float m22 = (2 - (farZ + nearZ) / eyeDistance) / (farZ - nearZ); + cssPerspectiveMatrix.setRowColumn(2, 2, m22); + const float m23 = -1.f + nearZ / eyeDistance - cssPerspectiveMatrix.getRowColumn(2, 2) * nearZ; + cssPerspectiveMatrix.setRowColumn(2, 3, m23); + cssPerspectiveMatrix.setRowColumn(3, 2, -1.f / eyeDistance); + + auto modelMatrix = Matrix3D::MakeRotate({0.f, 1.f, 0.f}, 45.f); + modelMatrix.postTranslate(0.f, 0.f, -100.f); + auto transform = cssPerspectiveMatrix * modelMatrix; + auto cssTransform3DFilter = ImageFilter::Transform3D(transform); + Paint paint = {}; + paint.setImageFilter(cssTransform3DFilter); + canvas->drawImage(image, 45.f, 45.f, &paint); + + context->flushAndSubmit(); + EXPECT_TRUE(Baseline::Compare(surface, "FilterTest/Transform3DImageFilterCSSBasic")); + canvas->restore(); + } + + const float halfImageW = imageSize.width * 0.5f; + const float halfImageH = imageSize.height * 0.5f; + auto standardvViewportMatrix = Matrix3D::MakeScale(halfImageW, halfImageH, 1.f); + auto invStandardViewportMatrix = Matrix3D::MakeScale(1.f / halfImageW, 1.f / halfImageH, 1.f); + // The field of view (in degrees) for the standard perspective projection model. + constexpr float standardFovYDegress = 45.f; + // The maximum position of the near plane on the Z axis for the standard perspective projection model. + constexpr float standardMaxNearZ = 0.25f; + // The minimum position of the far plane on the Z axis for the standard perspective projection model. + constexpr float standardMinFarZ = 1000.f; + // The target position of the camera for the standard perspective projection model, in pixels. + constexpr Vec3 standardEyeCenter = {0.f, 0.f, 0.f}; + // The up direction unit vector for the camera in the standard perspective projection model. + static constexpr Vec3 StandardEyeUp = {0.f, 1.f, 0.f}; + const float eyePositionZ = 1.f / tanf(DegreesToRadians(standardFovYDegress * 0.5f)); + const Vec3 eyePosition = {0.f, 0.f, eyePositionZ}; + auto viewMatrix = Matrix3D::LookAt(eyePosition, standardEyeCenter, StandardEyeUp); + // Ensure nearZ is not too far away or farZ is not too close to avoid precision issues. For + // example, if the z value of the near plane is less than 0, the projected model will be + // outside the clipping range, or if the far plane is too close, the projected model may + // exceed the clipping range with a slight rotation. + const float nearZ = std::min(standardMaxNearZ, eyePositionZ * 0.1f); + const float farZ = std::max(standardMinFarZ, eyePositionZ * 10.f); + auto perspectiveMatrix = Matrix3D::Perspective( + standardFovYDegress, static_cast(image->width()) / static_cast(image->height()), + nearZ, farZ); + auto modelMatrix = Matrix3D::MakeRotate({0.f, 1.f, 0.f}, 45.f); + modelMatrix.postTranslate(0.f, 0.f, -10.f / imageSize.width); + auto standardTransform = standardvViewportMatrix * perspectiveMatrix * viewMatrix * modelMatrix * + invStandardViewportMatrix; + auto standardTransform3DFilter = ImageFilter::Transform3D(standardTransform); + + // Test scale drawing with standard perspective type. + { + canvas->save(); + canvas->clear(); + + auto filteredImage = image->makeWithFilter(standardTransform3DFilter); + canvas->setMatrix(Matrix::MakeScale(0.5f, 0.5f)); + canvas->drawImage(filteredImage, 45.f, 45.f, {}); + + context->flushAndSubmit(); + EXPECT_TRUE(Baseline::Compare(surface, "FilterTest/Transorm3DImageFilterStandardScale")); + canvas->restore(); + } + + // Test basic drawing with standard perspective type. + { + canvas->save(); + canvas->clear(); + + Paint paint = {}; + paint.setImageFilter(standardTransform3DFilter); + canvas->setMatrix(tgfx::Matrix::MakeRotate(45, 100, 100)); + canvas->drawImage(image, 45.f, 45.f, &paint); + + context->flushAndSubmit(); + EXPECT_TRUE(Baseline::Compare(surface, "FilterTest/Transorm3DImageFilterStandardBasic")); + canvas->restore(); + } + + // Test image clipping drawing with standard perspective type. + { + canvas->save(); + canvas->clear(); + + auto filteredImage = image->makeWithFilter(standardTransform3DFilter); + auto filteredBounds = standardTransform3DFilter->filterBounds( + Rect::MakeWH(static_cast(image->width()), static_cast(image->height()))); + + auto clipRectLT = Rect::MakeXYWH(filteredBounds.left, filteredBounds.top, + filteredBounds.width() * 0.5f, filteredBounds.height() * 0.5f); + auto imageLT = image->makeWithFilter(standardTransform3DFilter, nullptr, &clipRectLT); + canvas->drawImage(imageLT, 0.f, 0.f); + + auto clipRectRT = Rect::MakeXYWH(clipRectLT.right, filteredBounds.top, + filteredBounds.width() * 0.5f, clipRectLT.height()); + auto imageRT = image->makeWithFilter(standardTransform3DFilter, nullptr, &clipRectRT); + canvas->drawImage(imageRT, static_cast(imageLT->width()), 0.f); + + auto clipRectLB = Rect::MakeXYWH(filteredBounds.left, clipRectLT.bottom, clipRectLT.width(), + filteredBounds.height() * 0.5f); + auto imageLB = image->makeWithFilter(standardTransform3DFilter, nullptr, &clipRectLB); + canvas->drawImage(imageLB, 0.f, static_cast(imageLT->height())); + + auto clipRectRB = + Rect::MakeXYWH(clipRectRT.left, clipRectRT.bottom, clipRectRT.width(), clipRectLB.height()); + auto imageRB = image->makeWithFilter(standardTransform3DFilter, nullptr, &clipRectRB); + canvas->drawImage(imageRB, static_cast(imageLT->width()), + static_cast(imageLT->height())); + + context->flushAndSubmit(); + EXPECT_TRUE(Baseline::Compare(surface, "FilterTest/Transform3DImageFilterStandardClip")); + canvas->restore(); + } +} } // namespace tgfx