各位同学,大家好!今天我们将深入探讨现代图形渲染中的一个核心概念:层级结构(Layer Tree)及其几何转换。特别是,我们将聚焦于 PictureLayer 和 TransformLayer 这两种关键层类型,以及它们如何通过矩阵运算的堆叠来实现复杂的视觉效果。理解这一机制,是掌握高性能、高复杂度用户界面和图形渲染的关键。
1. 视觉合成与层级结构:构建数字世界的基石
在现代图形用户界面(GUI)和游戏引擎中,我们所看到的每一个像素,从最简单的按钮到复杂的3D模型,通常都不是一次性绘制在屏幕上的。相反,它们被组织成一个层级结构,或者说“层树”(Layer Tree)。这种结构带来了巨大的优势:
- 性能优化: 当只有部分内容发生变化时,我们无需重绘整个屏幕。只需更新受影响的层,然后将它们重新合成。这对于动画和交互式应用至关重要。
- 复杂性管理: 将一个复杂的场景分解为独立的、可管理的层,每个层负责绘制其自身的内容或组织其子层。这大大简化了开发和维护。
- 几何转换与动画: 层级结构天然支持对独立元素进行平移、旋转、缩放等几何转换。这些转换可以独立应用于单个层,或通过父层级联传递给子层,从而实现复杂的动画效果。
- 离屏渲染与缓存: 某些层的内容可以被渲染到离屏缓冲区(offscreen buffer)中并缓存起来,直到需要时才与主屏幕合成,进一步提升性能。
在Web浏览器、操作系统UI框架(如Core Animation、Qt Quick)以及许多游戏引擎中,这种层级结构都是其渲染管线的核心。PictureLayer 和 TransformLayer 是这种层级结构中的典型代表,它们各自承担着不同的职责。
PictureLayer(或类似概念,如ContentLayer,DrawableLayer): 负责承载并绘制实际的视觉内容。它可以是一张图片、一段文本、一个矢量图形路径,或者更复杂的像素数据。每个PictureLayer都有自己的局部坐标系,其内容在这个坐标系内进行绘制。TransformLayer(或类似概念,如ContainerLayer,GroupLayer): 不直接绘制任何内容。它的主要作用是提供一个几何转换的上下文,并将这个转换应用到其所有的子层上。它是一个纯粹的结构性元素,用于组织和分组其他层,从而对一组层统一进行变换。
今天,我们将深入探讨这些层如何通过矩阵运算,一步步地将局部坐标系中的内容转换为最终呈现在屏幕上的全局坐标。
2. 几何转换的基础:矩阵数学回顾
要理解层级结构中的几何转换,我们必须先回顾一下矩阵数学。矩阵是表示和组合几何转换的强大工具,它能以统一的方式处理平移、旋转、缩放、斜切等所有线性变换。
2.1 为什么选择矩阵?
- 统一性: 所有的线性几何变换(旋转、缩放、斜切)都可以表示为矩阵乘法。平移(Translation)虽然不是严格的线性变换,但通过引入齐次坐标,也可以被统一为矩阵乘法。
- 组合性: 多个变换可以简单地通过矩阵乘法组合成一个单一的变换矩阵。这意味着无论一个物体经历了多少次平移、旋转、缩放,最终只需要一个矩阵就能描述它从原始状态到最终状态的完整变换。
- 可逆性: 大多数有用的变换矩阵都有逆矩阵,这使得我们可以轻松地“撤销”一个变换,例如,将屏幕坐标转换回局部坐标(用于事件处理)。
2.2 2D 变换矩阵 (3×3) 与 3D 变换矩阵 (4×4)
在2D图形中,我们通常使用3×3的矩阵来表示变换。而在3D图形,或者为了与3D系统兼容,我们则使用4×4的矩阵。尽管我们可能主要关注2D场景,但使用4×4矩阵来表示2D变换也是常见的做法,因为它可以轻松地扩展到3D,并且在许多图形API中,4×4矩阵是标准。
我们以4×4矩阵为例进行讲解,因为它更通用。一个4×4的变换矩阵通常看起来像这样:
$$
M = begin{pmatrix}
m{00} & m{01} & m{02} & m{03}
m{10} & m{11} & m{12} & m{13}
m{20} & m{21} & m{22} & m{23}
m{30} & m{31} & m{32} & m{33}
end{pmatrix}
$$
其中:
- 左上角的 3×3 子矩阵通常负责旋转、缩放、斜切。
- 第四列的前三个元素 ($m{03}, m{13}, m_{23}$) 负责平移。
- 第四行的元素 ($m{30}, m{31}, m_{32}$) 在透视投影中起作用,而在仿射变换中通常是 (0, 0, 0)。
- 右下角的元素 ($m_{33}$) 在仿射变换中通常是 1。
2.3 齐次坐标的必要性
为了将平移操作也统一为矩阵乘法,我们引入了齐次坐标。对于一个2D点 $(x, y)$,它的齐次坐标表示为 $(x, y, 1)$。对于一个3D点 $(x, y, z)$,它的齐次坐标表示为 $(x, y, z, 1)$。将点表示为向量,并与变换矩阵相乘:
$$
begin{pmatrix}
x’
y’
z’
w’
end{pmatrix} = M times begin{pmatrix}
x
y
z
1
end{pmatrix}
$$
最终的笛卡尔坐标通过除以 $w’$ 得到:$(x’/w’, y’/w’, z’/w’)$. 在仿射变换中,$w’$ 通常保持为 1。
2.4 矩阵乘法的顺序与非交换性
矩阵乘法是非交换的,这意味着 $A times B neq B times A$。变换的顺序至关重要。通常,我们按照“从右到左”的顺序应用变换,即一个点向量与一系列矩阵相乘时,最右边的矩阵(最靠近向量的矩阵)代表第一个应用的变换。
例如,要先旋转再平移一个点 $P$,我们会这样写:$P’ = T times R times P$,其中 $R$ 是旋转矩阵,$T$ 是平移矩阵。
2.5 常见的变换矩阵
我们将使用4×4矩阵来表示这些变换。
1. 恒等矩阵 (Identity Matrix):
不改变任何点的矩阵。
$$
I = begin{pmatrix}
1 & 0 & 0 & 0
0 & 1 & 0 & 0
0 & 0 & 1 & 0
0 & 0 & 0 & 1
end{pmatrix}
$$
2. 平移矩阵 (Translation Matrix):
将点沿 $x, y, z$ 轴分别移动 $t_x, t_y, t_z$。
$$
T(t_x, t_y, t_z) = begin{pmatrix}
1 & 0 & 0 & t_x
0 & 1 & 0 & t_y
0 & 0 & 1 & t_z
0 & 0 & 0 & 1
end{pmatrix}
$$
3. 缩放矩阵 (Scaling Matrix):
将点沿 $x, y, z$ 轴分别缩放 $s_x, s_y, s_z$ 倍。
$$
S(s_x, s_y, s_z) = begin{pmatrix}
s_x & 0 & 0 & 0
0 & s_y & 0 & 0
0 & 0 & s_z & 0
0 & 0 & 0 & 1
end{pmatrix}
$$
4. 旋转矩阵 (Rotation Matrix):
旋转相对复杂。我们以绕Z轴旋转 $theta$ 角为例 (2D中最常用)。
$$
R_z(theta) = begin{pmatrix}
costheta & -sintheta & 0 & 0
sintheta & costheta & 0 & 0
0 & 0 & 1 & 0
0 & 0 & 0 & 1
end{pmatrix}
$$
绕X轴和Y轴的旋转矩阵也类似,分别对应不同的三角函数组合。
5. 斜切矩阵 (Shear Matrix):
沿一个轴倾斜另一个轴。例如,沿X轴斜切,使Y值影响X坐标。
$$
Sh_{xy}(s_x, s_y) = begin{pmatrix}
1 & s_x & 0 & 0
s_y & 1 & 0 & 0
0 & 0 & 1 & 0
0 & 0 & 0 & 1
end{pmatrix}
$$
2.6 矩阵逆运算
一个变换矩阵的逆矩阵可以“撤销”该变换。如果 $M$ 将点 $P$ 变换到 $P’$ ($P’ = M times P$),那么 $M^{-1}$ 将 $P’$ 变换回 $P$ ($P = M^{-1} times P’$)。在图形系统中,逆矩阵对于事件处理(hit-testing,将屏幕坐标转换为局部坐标)和相机变换等场景至关重要。
2.7 代码示例:一个简化的 Matrix4x4 类
为了更好地理解,我们先定义一个基础的 Matrix4x4 类,它将包含基本的初始化、乘法和创建各种变换矩阵的方法。这里我们使用Python风格的伪代码,方便理解。
import math
class Matrix4x4:
def __init__(self, elements=None):
if elements is None:
# Initialize as identity matrix
self.elements = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]
elif len(elements) == 16:
self.elements = list(elements)
else:
raise ValueError("Matrix4x4 requires 16 elements or None for identity.")
@staticmethod
def identity():
return Matrix4x4()
@staticmethod
def translation(tx, ty, tz):
return Matrix4x4([
1.0, 0.0, 0.0, tx,
0.0, 1.0, 0.0, ty,
0.0, 0.0, 1.0, tz,
0.0, 0.0, 0.0, 1.0
])
@staticmethod
def scaling(sx, sy, sz):
return Matrix4x4([
sx, 0.0, 0.0, 0.0,
0.0, sy, 0.0, 0.0,
0.0, 0.0, sz, 0.0,
0.0, 0.0, 0.0, 1.0
])
@staticmethod
def rotation_z(angle_radians):
c = math.cos(angle_radians)
s = math.sin(angle_radians)
return Matrix4x4([
c, -s, 0.0, 0.0,
s, c, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
])
# ... Other rotation methods (rotation_x, rotation_y) can be added
def __mul__(self, other):
if not isinstance(other, Matrix4x4):
# Handle vector multiplication later if needed, for now focus on matrix-matrix
raise TypeError("Can only multiply Matrix4x4 by another Matrix4x4.")
result_elements = [0.0] * 16
a = self.elements
b = other.elements
# Standard matrix multiplication (row-major order assumed here)
# result[i][j] = sum(a[i][k] * b[k][j])
# Using 1D array for 4x4 matrix, index mapping: M[row][col] -> elements[row*4 + col]
for i in range(4): # row
for j in range(4): # col
val = 0.0
for k in range(4):
val += a[i*4 + k] * b[k*4 + j]
result_elements[i*4 + j] = val
return Matrix4x4(result_elements)
def transform_point(self, x, y, z, w=1.0):
"""Transforms a 3D point (x, y, z) or 4D vector (x, y, z, w)"""
p = [x, y, z, w]
transformed_p = [0.0] * 4
m = self.elements
for i in range(4):
val = 0.0
for j in range(4):
val += m[i*4 + j] * p[j]
transformed_p[i] = val
# Return as a tuple, handle perspective division if w != 1.0
if transformed_p[3] != 0 and transformed_p[3] != 1.0:
return (transformed_p[0] / transformed_p[3],
transformed_p[1] / transformed_p[3],
transformed_p[2] / transformed_p[3])
return (transformed_p[0], transformed_p[1], transformed_p[2])
def __str__(self):
s = "[n"
for i in range(4):
s += " [" + ", ".join(f"{self.elements[i*4 + j]:.4f}" for j in range(4)) + "]n"
s += "]"
return s
# Example usage:
m_translate = Matrix4x4.translation(10, 20, 0)
m_rotate = Matrix4x4.rotation_z(math.radians(45))
m_scale = Matrix4x4.scaling(2, 0.5, 1)
# Combined transformation: scale, then rotate, then translate
# Note: order of multiplication is T * R * S * P
# So, S is applied first, then R, then T.
m_combined = m_translate * m_rotate * m_scale
print("Combined Matrix:")
print(m_combined)
point = (1, 1, 0)
transformed_point = m_combined.transform_point(*point)
print(f"Original point: {point}")
print(f"Transformed point: {transformed_point}")
这个 Matrix4x4 类是我们后续讨论的基础。它提供了创建基本变换矩阵和将它们组合起来的能力。
3. PictureLayer:内容的几何表示与局部坐标系
PictureLayer 是层级结构中负责绘制实际视觉内容的层。它的核心职责是管理其内部的绘制指令和像素数据。
3.1 局部坐标系
每个 PictureLayer 都有一个自己的局部坐标系(Local Coordinate System)。这个坐标系通常以层的左上角为原点 $(0,0)$,X轴向右,Y轴向下(对于大多数2D GUI系统)。层内的所有绘制操作,如绘制矩形、文本、图片,都是在这个局部坐标系中进行的。
例如,一个宽度为100,高度为50的 PictureLayer,它的内容将在 (0,0) 到 (100,50) 的矩形区域内绘制。
3.2 PictureLayer 的 transform 属性
除了绘制内容,PictureLayer 自身也可以拥有一个几何转换。这个转换由其 transform 属性(通常是一个 Matrix4x4 对象)表示。这个 transform 定义了该层的内容在其父层坐标系中的位置、方向和大小。
想象一下:
- 你有一个
PictureLayer,它内部绘制了一个红色的方块,其顶点在局部坐标系中是(0,0), (50,0), (50,50), (0,50)。 - 这个
PictureLayer有一个transform属性,比如是一个平移矩阵T(100, 50, 0)。 - 这意味着,这个红色的方块,在
PictureLayer的父层看来,它的左上角将位于(100, 50)。
这个 transform 属性将应用于 PictureLayer 内所有绘制的内容。它将 PictureLayer 的局部坐标系中的点转换到其父层的坐标系中。
3.3 代码示例:一个简化的 PictureLayer
class PictureLayer:
def __init__(self, name, width, height, parent=None):
self.name = name
self.width = width
self.height = height
self.parent = parent
self.children = [] # PictureLayer can have children, but they are nested, not "drawn" by it directly in this context
self._local_transform = Matrix4x4.identity() # Transform relative to its parent
self.content_color = "red" # Example content property
if parent:
parent.add_child(self)
def set_transform(self, matrix):
"""Sets the layer's local transform matrix."""
self._local_transform = matrix
def get_local_transform(self):
"""Returns the transform matrix from its local coordinate system to its parent's."""
return self._local_transform
def draw_content(self):
"""
Simulates drawing the layer's internal content.
In a real system, this would involve GPU commands or CPU pixel manipulation.
Here, we just print a description.
"""
print(f" Drawing content for {self.name} (W:{self.width}, H:{self.height}) with color {self.content_color} in its local coordinate system.")
# Example content: a rectangle from (0,0) to (width, height)
# If we were to draw a point at (10,10) in its local system,
# its parent-relative position would be determined by self._local_transform.transform_point(10,10,0)
def add_child(self, child_layer):
self.children.append(child_layer)
child_layer.parent = self
# For demonstration, we might add a placeholder for world transform calculation here later
# def get_world_transform(self): ...
4. TransformLayer:结构化变换的载体
TransformLayer 是层级结构中的另一个关键类型。与 PictureLayer 不同,TransformLayer 不直接承载或绘制任何视觉内容。它的唯一职责是管理一个几何转换,并将其应用到其所有的子层上。
4.1 TransformLayer 的核心职责
TransformLayer 可以被视为一个“变换容器”或“分组变换”。它拥有自己的 transform 属性,这个属性定义了它在父层坐标系中的变换。当它拥有子层时,这个 transform 会级联地应用于所有子层。
例如:
- 你有一个
TransformLayer,它被旋转了45度。 - 这个
TransformLayer有两个子PictureLayer。 - 那么,这两个子
PictureLayer,连同它们各自的内容,都将相对于它们的父TransformLayer被旋转45度。
TransformLayer 在以下场景中非常有用:
- 分组变换: 当你需要同时平移、旋转或缩放一组 UI 元素时,可以将它们都作为同一个
TransformLayer的子层。只需修改TransformLayer的transform,所有子层都会随之变化。 - 隔离变换: 有时你希望在一个区域内应用一个复杂的变换(例如透视变换),而这个变换不应该影响区域外的其他元素。
TransformLayer可以有效地隔离这种变换。 - 动画: 它是实现复杂层级动画的基础。例如,一个角色模型可能由多个
TransformLayer组成,每个骨骼对应一个TransformLayer,通过改变它们的transform来实现骨骼动画。
4.2 代码示例:一个简化的 TransformLayer
class TransformLayer:
def __init__(self, name, parent=None):
self.name = name
self.parent = parent
self.children = []
self._local_transform = Matrix4x4.identity() # Transform relative to its parent
if parent:
parent.add_child(self)
def set_transform(self, matrix):
"""Sets the layer's local transform matrix."""
self._local_transform = matrix
def get_local_transform(self):
"""Returns the transform matrix from its local coordinate system to its parent's."""
return self._local_transform
def add_child(self, child_layer):
self.children.append(child_layer)
child_layer.parent = self
# TransformLayer does not draw content itself
def draw_content(self):
pass # No content to draw
# Placeholder for world transform calculation
# def get_world_transform(self): ...
5. Layer 树的几何转换堆叠:矩阵的级联
现在我们已经了解了 PictureLayer 和 TransformLayer 的基本概念,以及它们各自的局部变换。真正的魔力在于它们如何协同工作,通过矩阵乘法将这些局部变换堆叠起来,最终将每个层的内容从其局部坐标系转换到最终的屏幕坐标系(也称为世界坐标系或全局坐标系)。
5.1 从局部坐标系到世界坐标系:变换矩阵的乘法链
一个层在屏幕上的最终位置和方向,取决于它自身的局部变换,以及所有祖先层(父层、祖父层等,直到根层)的变换。这个过程就是通过矩阵乘法链来实现的。
对于树中的任何一个层 L,其世界变换矩阵 M_world_L 是从根层到 L 的路径上所有局部变换矩阵的乘积。
假设我们有一个层级结构:RootLayer -> ParentLayer -> CurrentLayer。
RootLayer的local_transform定义了它在世界坐标系中的变换(通常是恒等矩阵,如果世界坐标系就是RootLayer的坐标系)。ParentLayer的local_transform定义了它在RootLayer坐标系中的变换。CurrentLayer的local_transform定义了它在ParentLayer坐标系中的变换。
那么,CurrentLayer 的世界变换矩阵 M_world_CurrentLayer 将是:
M_world_CurrentLayer = M_world_RootLayer * M_local_ParentLayer * M_local_CurrentLayer
或者,如果我们假设 M_world_RootLayer 是恒等矩阵(即 RootLayer 本身就处于世界坐标系的原点),那么:
M_world_CurrentLayer = M_local_ParentLayer * M_local_CurrentLayer
重要提示: 矩阵乘法的顺序是关键。从左到右,我们沿着层级树从根到子层进行乘法。这意味着,一个点首先被 CurrentLayer 的局部变换转换到 ParentLayer 的坐标系,然后被 ParentLayer 的局部变换转换到 RootLayer 的坐标系(也就是世界坐标系)。
这是一个从“世界到局部”的思考方式。如果从“局部到世界”来看,一个点 $P{local}$ 在 CurrentLayer 的局部坐标系中,它在世界坐标系中的位置 $P{world}$ 将是:
$P{world} = (M{local_parent} times M{local_current}) times P{local}$
这里的 $M{local_parent}$ 是 ParentLayer 相对于 RootLayer 的变换,而 $M{local_current}$ 是 CurrentLayer 相对于 ParentLayer 的变换。
5.2 如何遍历 Layer 树来计算世界变换
要计算任何一个层 L 的世界变换矩阵,我们需要从该层向上遍历到根层,收集所有祖先的局部变换矩阵,然后按照正确的顺序将它们相乘。
算法步骤:
- 初始化一个
current_world_transform为恒等矩阵。 - 从当前层
L开始,向上遍历到根层。 - 在每次遍历中,获取当前层的
local_transform。 - 将该
local_transform乘以current_world_transform。由于矩阵乘法的非交换性,我们需要确保乘法的顺序是“父在前,子在后”。current_world_transform = parent_local_transform * current_world_transform- 或者,如果我们从根开始向下乘,则是
current_world_transform = current_world_transform * child_local_transform
在实际实现中,通常是从根层向下递归或迭代地计算每个子层的世界变换。
# Extending the base Layer class to handle common properties
class BaseLayer:
def __init__(self, name, parent=None):
self.name = name
self.parent = parent
self.children = []
self._local_transform = Matrix4x4.identity() # Transform relative to its parent
self._world_transform = Matrix4x4.identity() # Cached world transform
if parent:
parent.add_child(self)
def set_transform(self, matrix):
self._local_transform = matrix
self._invalidate_world_transform() # Mark as needing re-calculation
def get_local_transform(self):
return self._local_transform
def add_child(self, child_layer):
self.children.append(child_layer)
child_layer.parent = self
self._invalidate_world_transform() # Child added, might affect parent's rendering context
def _invalidate_world_transform(self):
# In a real system, this would trigger a re-calculation on next render cycle
# For simplicity, we'll recalculate on demand in get_world_transform
pass
def get_world_transform(self):
"""
Calculates and returns the layer's world transformation matrix.
This matrix transforms points from the layer's local coordinate system
to the global (root) coordinate system.
"""
if self.parent:
# Multiply parent's world transform by this layer's local transform
# This order (parent_world * local_self) means local_self's transform
# is applied *after* the parent's world transform.
# So, a point in self's local system first transformed to parent's,
# then parent's to world.
self._world_transform = self.parent.get_world_transform() * self._local_transform
else:
# Root layer's world transform is just its local transform (relative to world origin)
self._world_transform = self._local_transform
return self._world_transform
def __str__(self):
return f"Layer(name='{self.name}')"
# Update PictureLayer and TransformLayer to inherit from BaseLayer
class PictureLayer(BaseLayer):
def __init__(self, name, width, height, parent=None):
super().__init__(name, parent)
self.width = width
self.height = height
self.content_color = "red"
def draw_content(self, renderer):
"""
Simulates drawing. In a real system, 'renderer' would be a graphics context.
The renderer would use self.get_world_transform() to position and transform
the content before drawing.
"""
world_matrix = self.get_world_transform()
print(f" [{self.name}] Drawing content (W:{self.width}, H:{self.height}, Color:{self.content_color}) at world transform:n{world_matrix}")
# Example: Transform a local point (0,0) to world
world_origin = world_matrix.transform_point(0,0,0)
print(f" Local origin (0,0) -> World origin: {world_origin}")
# In a real renderer, vertex data (e.g., for a rectangle) would be multiplied
# by world_matrix before being sent to the GPU.
class TransformLayer(BaseLayer):
def __init__(self, name, parent=None):
super().__init__(name, parent)
def draw_content(self, renderer):
# TransformLayer does not draw content itself, but its children do.
pass
5.3 anchorPoint / transformOrigin 的概念
在许多图形系统中(如CSS transform-origin,Core Animation anchorPoint),变换并不仅仅围绕层的局部原点 (0,0) 进行。而是可以指定一个锚点(anchorPoint 或 transformOrigin),所有旋转和缩放都将围绕这个点进行。
anchorPoint 通常以相对坐标表示(例如,{0.5, 0.5} 表示层的中心)。在内部,这实际上是通过三个矩阵操作来实现的:
- 平移到锚点: 将层的坐标系平移,使锚点成为新的原点。
T(-anchor.x * width, -anchor.y * height, 0) - 应用实际变换: 应用我们设置的旋转、缩放等变换。
R_S(rotation, scaling matrix) - 反向平移: 将坐标系平移回原来的位置。
T(anchor.x * width, anchor.y * height, 0)
所以,一个带有 anchorPoint 的局部变换矩阵 M_local_anchored 实际上是:
M_local_anchored = T(anchor_offset_x, anchor_offset_y, 0) * M_raw_transform * T(-anchor_offset_x, -anchor_offset_y, 0)
其中 M_raw_transform 是我们直接设置的(不考虑锚点的)平移、旋转、缩放矩阵。
anchor_offset_x = anchor.x * layer.width
anchor_offset_y = anchor.y * layer.height
将这个逻辑集成到 BaseLayer 中:
class BaseLayer:
# ... (previous __init__ and other methods)
def __init__(self, name, parent=None):
# ... (existing init code)
self._local_transform = Matrix4x4.identity()
self._anchor_point = (0.0, 0.0, 0.0) # Relative anchor point (0..1, 0..1, 0..1)
self._position = (0.0, 0.0, 0.0) # Position relative to parent, applied AFTER anchor point logic
self._rotation_matrix = Matrix4x4.identity()
self._scale_matrix = Matrix4x4.identity()
def set_position(self, x, y, z=0):
self._position = (x, y, z)
self._invalidate_world_transform()
def set_anchor_point(self, x_ratio, y_ratio, z_ratio=0):
self._anchor_point = (x_ratio, y_ratio, z_ratio)
self._invalidate_world_transform()
def set_rotation_z(self, angle_radians):
self._rotation_matrix = Matrix4x4.rotation_z(angle_radians)
self._invalidate_world_transform()
def set_scale(self, sx, sy, sz=1):
self._scale_matrix = Matrix4x4.scaling(sx, sy, sz)
self._invalidate_world_transform()
def get_local_transform(self):
"""
Calculates the effective local transform matrix including anchor point,
rotation, scale, and position.
The order of operations is:
1. Translate to anchor point (pre-translation)
2. Apply rotation
3. Apply scale
4. Translate back from anchor point (post-translation)
5. Apply explicit position translation
"""
# Note: self.width and self.height are properties of PictureLayer,
# so BaseLayer needs to know about them or assume 0 for TransformLayer.
# For simplicity, let's assume PictureLayer overrides this or passes dimensions.
# Here, we'll pass dimensions to the calculation if available.
layer_width = getattr(self, 'width', 0)
layer_height = getattr(self, 'height', 0)
layer_depth = getattr(self, 'depth', 0) # For 3D layers
# 1. Translate to anchor point
anchor_tx = -self._anchor_point[0] * layer_width
anchor_ty = -self._anchor_point[1] * layer_height
anchor_tz = -self._anchor_point[2] * layer_depth # If 3D
translate_to_anchor = Matrix4x4.translation(anchor_tx, anchor_ty, anchor_tz)
# 2. Apply rotation and scale around the anchor
# Order: scale then rotate (common for CSS transforms)
transform_around_anchor = self._rotation_matrix * self._scale_matrix
# 3. Translate back from anchor point
translate_from_anchor = Matrix4x4.translation(-anchor_tx, -anchor_ty, -anchor_tz)
# 4. Apply explicit position translation (relative to parent)
position_translation = Matrix4x4.translation(self._position[0], self._position[1], self._position[2])
# Combine all parts: T_pos * T_from_anchor * R_S * T_to_anchor
# The actual transform set by the user (rotation, scale) is between the anchor point translations.
# The position is applied last.
effective_transform = position_translation * translate_from_anchor * transform_around_anchor * translate_to_anchor
return effective_transform
# ... (rest of BaseLayer methods)
# Now, PictureLayer and TransformLayer just need to set their properties.
# For PictureLayer, width and height are meaningful for anchor point calculation.
# For TransformLayer, width/height might be 0 or derived from its bounding box of children.
# For this example, we'll assume a TransformLayer doesn't have a 'size' for anchor point calculations,
# or its anchor point is always (0,0) effectively.
# Or, we can add a 'size' property to BaseLayer that subclasses can set.
这里我们拆分了 _local_transform 为 _position, _rotation_matrix, _scale_matrix 来演示 anchorPoint 的计算过程。在实际系统中,这些可能会被组合成一个单一的 Matrix4x4 属性,但其内部逻辑是相似的。
5.4 动画中的变换
动画本质上是随着时间改变层的变换属性。这通常通过在关键帧之间插值(interpolation)变换矩阵或其分量(平移、旋转、缩放)来实现。
- 分量插值: 分别插值平移向量、缩放向量和旋转四元数(Quaternion,用于避免万向节锁),然后重新组合成矩阵。这是更常见和健壮的方法。
- 矩阵插值: 直接在两个矩阵之间进行插值(如线性插值
lerp),但这种方法可能会导致不自然的动画效果,尤其是在旋转时。
在层级结构中,动画可以独立应用于任何一个层。父层动画会影响所有子层,而子层动画则在其父层动画的基础上叠加。
6. 实际应用场景与高级主题
6.1 视口变换与投影矩阵
我们目前讨论的“世界坐标系”通常是一个抽象的3D或2D空间。要将这个世界坐标系中的内容最终呈现在2D屏幕上,还需要经过视口变换(Viewport Transform)和投影变换(Projection Transform)。
- 视图矩阵 (View Matrix) / 相机矩阵: 它将世界坐标系中的物体转换到相机坐标系(或称观察者坐标系)中。可以理解为移动和旋转整个世界,使得相机处于原点并看向Z轴负方向。它是相机位置和方向的逆变换。
- 投影矩阵 (Projection Matrix): 它将相机坐标系中的3D点转换到2D裁剪空间(Clip Space)。
- 正交投影 (Orthographic Projection): 适用于2D游戏或UI,所有物体无论远近,大小保持不变。它将一个立方体区域映射到裁剪空间。
- 透视投影 (Perspective Projection): 模拟人眼观察世界的真实感,远处的物体看起来更小。它将一个视锥体(frustum)区域映射到裁剪空间。
- 视口变换 (Viewport Transform): 最后,裁剪空间中的坐标(通常是 $[-1, 1]$ 范围)被映射到屏幕上的实际像素坐标。
整个渲染管线中的最终变换链是:
屏幕坐标 = 视口矩阵 * 投影矩阵 * 视图矩阵 * 世界变换矩阵 * 局部坐标
在2D UI系统中,通常简化为正交投影,并且视图矩阵可能被省略或合并到根层的世界变换中。
6.2 Hit-Testing / 事件处理
当用户点击屏幕上的一个点时,我们需要确定哪个层被点击了。这个过程称为命中测试(Hit-Testing)。它要求我们将屏幕坐标反向转换到每个潜在被点击层的局部坐标系中。
这正是逆矩阵大显身手的地方。
如果 P_screen = M_total * P_local,其中 M_total 是从 Layer 的局部坐标系到屏幕坐标系的完整变换矩阵。
那么,P_local = M_total_inverse * P_screen。
我们可以计算每个层的世界变换矩阵 M_world_L,然后计算它的逆矩阵 M_world_L_inverse。将屏幕点击点 P_screen 转换到 L 的局部坐标系:P_local_L = M_world_L_inverse * P_screen。
然后检查 P_local_L 是否落在层 L 的边界框内。
# Extending Matrix4x4 with inverse calculation (complex, simplified here for affine)
class Matrix4x4:
# ... (previous methods)
def inverse(self):
# Full 4x4 inverse is complex, typically implemented with LU decomposition or similar.
# For affine transformations (no perspective), a simpler inverse can be derived.
# Assuming last row is [0,0,0,1] and m33 is 1.
# This is a placeholder; a real implementation would use a robust algorithm.
# Simplified inverse for common affine transformations (rotation, scale, translation)
# 1. Invert the 3x3 rotation/scale part
# 2. Compute new translation based on inverted R/S and original translation
m = self.elements
# Extract 3x3 rotation/scale part
R_S = [
m[0], m[1], m[2],
m[4], m[5], m[6],
m[8], m[9], m[10]
]
# Calculate inverse of 3x3 (e.g., using adjugate/determinant or numpy.linalg.inv)
# This is a simplification. A robust inv_3x3 is needed.
# For this example, let's assume we have a helper to inverse a 3x3 matrix.
# If it's pure rotation/scale, inverse is transpose for rotation, 1/scale for scale.
# Placeholder for 3x3 inverse (very simplified, not robust for all cases)
det_rs = (R_S[0]*(R_S[4]*R_S[8]-R_S[5]*R_S[7]) -
R_S[1]*(R_S[3]*R_S[8]-R_S[5]*R_S[6]) +
R_S[2]*(R_S[3]*R_S[7]-R_S[4]*R_S[6]))
if abs(det_rs) < 1e-6:
# Matrix is singular, cannot invert
raise ValueError("Matrix is singular and cannot be inverted.")
inv_det_rs = 1.0 / det_rs
inv_rs_elements = [0.0] * 9
inv_rs_elements[0] = (R_S[4]*R_S[8]-R_S[5]*R_S[7]) * inv_det_rs
inv_rs_elements[1] = (R_S[2]*R_S[7]-R_S[1]*R_S[8]) * inv_det_rs
inv_rs_elements[2] = (R_S[1]*R_S[5]-R_S[2]*R_S[4]) * inv_det_rs
inv_rs_elements[3] = (R_S[5]*R_S[6]-R_S[3]*R_S[8]) * inv_det_rs
inv_rs_elements[4] = (R_S[0]*R_S[8]-R_S[2]*R_S[6]) * inv_det_rs
inv_rs_elements[5] = (R_S[2]*R_S[3]-R_S[0]*R_S[5]) * inv_det_rs
inv_rs_elements[6] = (R_S[3]*R_S[7]-R_S[4]*R_S[6]) * inv_det_rs
inv_rs_elements[7] = (R_S[1]*R_S[6]-R_S[0]*R_S[7]) * inv_det_rs
inv_rs_elements[8] = (R_S[0]*R_S[4]-R_S[1]*R_S[3]) * inv_det_rs
# Original translation vector
tx, ty, tz = m[3], m[7], m[11]
# New translation vector after inverse rotation/scale
inv_tx = -(inv_rs_elements[0]*tx + inv_rs_elements[1]*ty + inv_rs_elements[2]*tz)
inv_ty = -(inv_rs_elements[3]*tx + inv_rs_elements[4]*ty + inv_rs_elements[5]*tz)
inv_tz = -(inv_rs_elements[6]*tx + inv_rs_elements[7]*ty + inv_rs_elements[8]*tz)
return Matrix4x4([
inv_rs_elements[0], inv_rs_elements[1], inv_rs_elements[2], inv_tx,
inv_rs_elements[3], inv_rs_elements[4], inv_rs_elements[5], inv_ty,
inv_rs_elements[6], inv_rs_elements[7], inv_rs_elements[8], inv_tz,
0.0, 0.0, 0.0, 1.0
])
# ... (rest of Matrix4x4 class)
class Renderer:
def __init__(self, root_layer):
self.root_layer = root_layer
self.layers_for_hit_testing = [] # Store drawable layers
def render(self):
self.layers_for_hit_testing = []
print("n--- Rendering Cycle ---")
self._render_recursive(self.root_layer)
print("--- Render Cycle End ---n")
def _render_recursive(self, layer):
if isinstance(layer, PictureLayer):
layer.draw_content(self)
self.layers_for_hit_testing.append(layer)
for child in layer.children:
self._render_recursive(child)
def hit_test(self, screen_x, screen_y):
print(f"n--- Hit Testing at ({screen_x}, {screen_y}) ---")
hit_layers = []
# Iterate in reverse order of drawing (front-to-back) for Z-order
for layer in reversed(self.layers_for_hit_testing):
try:
world_to_layer_matrix = layer.get_world_transform().inverse()
local_point = world_to_layer_matrix.transform_point(screen_x, screen_y, 0)
local_x, local_y, _ = local_point
# Check if the point is within the layer's local bounds
if 0 <= local_x < layer.width and 0 <= local_y < layer.height:
hit_layers.append(layer)
print(f" Hit layer: {layer.name} at local ({local_x:.2f}, {local_y:.2f})")
# In many UIs, we only care about the topmost hit.
return hit_layers[0] # Return the first (topmost) hit
except ValueError as e:
print(f" Could not invert matrix for {layer.name}: {e}")
print(" No layer hit.")
return None
6.3 性能优化
- 矩阵缓存: 每次
get_world_transform()调用都重新计算整个矩阵链是低效的。一个更好的策略是缓存每个层的世界变换矩阵,并在其局部变换或任何祖先的局部变换发生变化时,将该层及其所有子层的缓存标记为“脏”(dirty),然后在下次需要时才重新计算。 - 合并变换: 在某些情况下,可以通过预乘矩阵来减少运行时乘法次数。例如,如果一个层及其父层都没有改变,它们的组合变换可以被预先计算并缓存。
- GPU 加速: 现代图形渲染将大部分矩阵乘法卸载到图形处理单元(GPU)上。变换矩阵作为 Uniforms 传递给顶点着色器(Vertex Shader),由着色器对每个顶点进行并行变换。这比CPU上的矩阵运算快得多。
6.4 CSS Transforms 与 Layer 树
Web浏览器是 Layer 树和矩阵堆叠的绝佳例子。CSS transform 属性直接映射到 Layer 树中的局部变换。
transform: translate(x, y)-> 平移矩阵transform: rotate(angle)-> 旋转矩阵transform: scale(sx, sy)-> 缩放矩阵transform-origin属性直接对应于我们上面讨论的anchorPoint。perspective属性通常应用于一个TransformLayer或根层,它引入了透视投影效果,使得子层具有3D深度感。
浏览器引擎(如Chromium的Compositor)会构建一个包含所有渲染元素的 Layer 树。当CSS transform 改变时,只需要更新相应的层矩阵,然后GPU可以直接使用这些矩阵重新合成,而无需重新绘制像素内容,从而实现高性能动画。
7. 深入代码实现:一个简化的渲染引擎骨架
让我们将上述概念整合到一个更完整的、但仍是简化的渲染引擎骨架中。这个骨架将演示如何构建 Layer 树、设置变换以及模拟渲染和命中测试。
import math
# --- Matrix4x4 Class (as defined previously) ---
class Matrix4x4:
def __init__(self, elements=None):
if elements is None:
self.elements = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]
elif len(elements) == 16:
self.elements = list(elements)
else:
raise ValueError("Matrix4x4 requires 16 elements or None for identity.")
@staticmethod
def identity():
return Matrix4x4()
@staticmethod
def translation(tx, ty, tz):
return Matrix4x4([
1.0, 0.0, 0.0, tx,
0.0, 1.0, 0.0, ty,
0.0, 0.0, 1.0, tz,
0.0, 0.0, 0.0, 1.0
])
@staticmethod
def scaling(sx, sy, sz):
return Matrix4x4([
sx, 0.0, 0.0, 0.0,
0.0, sy, 0.0, 0.0,
0.0, 0.0, sz, 0.0,
0.0, 0.0, 0.0, 1.0
])
@staticmethod
def rotation_z(angle_radians):
c = math.cos(angle_radians)
s = math.sin(angle_radians)
return Matrix4x4([
c, -s, 0.0, 0.0,
s, c, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
])
# ... (add rotation_x, rotation_y if needed)
def __mul__(self, other):
if not isinstance(other, Matrix4x4):
raise TypeError("Can only multiply Matrix4x4 by another Matrix4x4.")
result_elements = [0.0] * 16
a = self.elements
b = other.elements
for i in range(4):
for j in range(4):
val = 0.0
for k in range(4):
val += a[i*4 + k] * b[k*4 + j]
result_elements[i*4 + j] = val
return Matrix4x4(result_elements)
def transform_point(self, x, y, z, w=1.0):
p = [x, y, z, w]
transformed_p = [0.0] * 4
m = self.elements
for i in range(4):
val = 0.0
for j in range(4):
val += m[i*4 + j] * p[j]
transformed_p[i] = val
if transformed_p[3] != 0 and transformed_p[3] != 1.0:
return (transformed_p[0] / transformed_p[3],
transformed_p[1] / transformed_p[3],
transformed_p[2] / transformed_p[3])
return (transformed_p[0], transformed_p[1], transformed_p[2])
def inverse(self):
# Full 4x4 inverse is complex. For simplicity, we assume affine transformation.
# This implementation is a simplified placeholder and might not be numerically robust
# for all 4x4 matrices (e.g., non-invertible or perspective matrices).
# For a real engine, use a dedicated linear algebra library (e.g., NumPy, GLM).
m = self.elements
# Extract 3x3 rotation/scale part
R_S_3x3 = [
m[0], m[1], m[2],
m[4], m[5], m[6],
m[8], m[9], m[10]
]
# Calculate inverse of 3x3 R_S (using cofactor method for simplicity, not efficiency)
det_rs = (R_S_3x3[0]*(R_S_3x3[4]*R_S_3x3[8]-R_S_3x3[5]*R_S_3x3[7]) -
R_S_3x3[1]*(R_S_3x3[3]*R_S_3x3[8]-R_S_3x3[5]*R_S_3x3[6]) +
R_S_3x3[2]*(R_S_3x3[3]*R_S_3x3[7]-R_S_3x3[4]*R_S_3x3[6]))
if abs(det_rs) < 1e-9: # Check for singularity
raise ValueError("Matrix is singular and cannot be inverted.")
inv_det_rs = 1.0 / det_rs
# Adjugate matrix (transpose of cofactor matrix)
adj_rs = [
(R_S_3x3[4]*R_S_3x3[8]-R_S_3x3[5]*R_S_3x3[7]), (R_S_3x3[2]*R_S_3x3[7]-R_S_3x3[1]*R_S_3x3[8]), (R_S_3x3[1]*R_S_3x3[5]-R_S_3x3[2]*R_S_3x3[4]),
(R_S_3x3[5]*R_S_3x3[6]-R_S_3x3[3]*R_S_3x3[8]), (R_S_3x3[0]*R_S_3x3[8]-R_S_3x3[2]*R_S_3x3[6]), (R_S_3x3[2]*R_S_3x3[3]-R_S_3x3[0]*R_S_3x3[5]),
(R_S_3x3[3]*R_S_3x3[7]-R_S_3x3[4]*R_S_3x3[6]), (R_S_3x3[1]*R_S_3x3[6]-R_S_3x3[0]*R_S_3x3[7]), (R_S_3x3[0]*R_S_3x3[4]-R_S_3x3[1]*R_S_3x3[3])
]
inv_rs_elements = [x * inv_det_rs for x in adj_rs]
# Original translation vector
tx, ty, tz = m[3], m[7], m[11]
# New translation vector based on inverse rotation/scale and original translation
inv_tx = -(inv_rs_elements[0]*tx + inv_rs_elements[1]*ty + inv_rs_elements[2]*tz)
inv_ty = -(inv_rs_elements[3]*tx + inv_rs_elements[4]*ty + inv_rs_elements[5]*tz)
inv_tz = -(inv_rs_elements[6]*tx + inv_rs_elements[7]*ty + inv_rs_elements[8]*tz)
return Matrix4x4([
inv_rs_elements[0], inv_rs_elements[1], inv_rs_elements[2], inv_tx,
inv_rs_elements[3], inv_rs_elements[4], inv_rs_elements[5], inv_ty,
inv_rs_elements[6], inv_rs_elements[7], inv_rs_elements[8], inv_tz,
0.0, 0.0, 0.0, 1.0
])
def __str__(self):
s = "[n"
for i in range(4):
s += " [" + ", ".join(f"{self.elements[i*4 + j]:.4f}" for j in range(4)) + "]n"
s += "]"
return s
# --- BaseLayer Class ---
class BaseLayer:
def __init__(self, name, parent=None):
self.name = name
self.parent = parent
self.children = []
self._is_dirty = True # Flag for caching world transform
self._cached_world_transform = Matrix4x4.identity()
# Transformation components
self._position = (0.0, 0.0, 0.0)
self._rotation_z_rad = 0.0
self._scale = (1.0, 1.0, 1.0)
self._anchor_point = (0.0, 0.0, 0.0) # Relative anchor for rotation/scale
if parent:
parent.add_child(self)
def set_position(self, x, y, z=0):
self._position = (x, y, z)
self._mark_dirty()
def set_rotation_z(self, angle_degrees):
self._rotation_z_rad = math.radians(angle_degrees)
self._mark_dirty()
def set_scale(self, sx, sy, sz=1):
self._scale = (sx, sy, sz)
self._mark_dirty()
def set_anchor_point(self, x_ratio, y_ratio, z_ratio=0):
self._anchor_point = (x_ratio, y_ratio, z_ratio)
self._mark_dirty()
def add_child(self, child_layer):
self.children.append(child_layer)
child_layer.parent = self
self._mark_dirty() # Parent's transform might affect child, or child bounding box affects parent.
def _mark_dirty(self):
self._is_dirty = True
# Propagate dirty state up the tree to parents if needed, or just recompute on demand.
# For simplicity, we'll recompute on demand when get_world_transform is called.
def get_local_transform(self):
"""
Computes the layer's local transform including position, rotation, scale, and anchor point.
"""
# Get dimensions for anchor point calculation. Default to 0 if not a PictureLayer.
width = getattr(self, 'width', 0)
height = getattr(self, 'height', 0)
depth = getattr(self, 'depth', 0) # Assuming 3D depth can be 0 for 2D layers
# 1. Translate to anchor point (pre-translation for rotation/scale)
anchor_tx = -self._anchor_point[0] * width
anchor_ty = -self._anchor_point[1] * height
anchor_tz = -self._anchor_point[2] * depth
translate_to_anchor_mat = Matrix4x4.translation(anchor_tx, anchor_ty, anchor_tz)
# 2. Apply rotation and scale
rotation_mat = Matrix4x4.rotation_z(self._rotation_z_rad)
scale_mat = Matrix4x4.scaling(self._scale[0], self._scale[1], self._scale[2])
# Order: scale then rotate (common convention for CSS)
transform_around_anchor_mat = rotation_mat * scale_mat
# 3. Translate back from anchor point (post-translation)
translate_from_anchor_mat = Matrix4x4.translation(-anchor_tx, -anchor_ty, -anchor_tz)
# 4. Apply explicit position translation
position_mat = Matrix4x4.translation(self._position[0], self._position[1], self._position[2])
# Combine all: position * translate_from_anchor * transform_around_anchor * translate_to_anchor
# This order applies transformations from right to left:
# 1. Translate to anchor
# 2. Rotate/Scale around (what is now) the origin
# 3. Translate back from anchor
# 4. Apply final position offset
return position_mat * translate_from_anchor_mat * transform_around_anchor_mat * translate_to_anchor_mat
def get_world_transform(self):
"""
Calculates and returns the layer's world transformation matrix, using caching.
This matrix transforms points from the layer's local coordinate system
to the global (root) coordinate system.
"""
if self._is_dirty:
if self.parent:
self._cached_world_transform = self.parent.get_world_transform() * self.get_local_transform()
else:
self._cached_world_transform = self.get_local_transform()
self._is_dirty = False
return self._cached_world_transform
def __str__(self):
return f"Layer(name='{self.name}')"
# --- PictureLayer Class ---
class PictureLayer(BaseLayer):
def __init__(self, name, width, height, parent=None):
super().__init__(name, parent)
self.width = width
self.height = height
self.content_color = "red"
def draw_content(self, renderer):
world_matrix = self.get_world_transform()
print(f" [{self.name}] Drawing content (W:{self.width}, H:{self.height}, Color:{self.content_color})")
print(f" World Matrix:n{world_matrix}")
# Simulate drawing boundary points
local_points = [
(0, 0, 0), (self.width, 0, 0),
(self.width, self.height, 0), (0, self.height, 0)
]
world_points = [world_matrix.transform_point(x, y, z) for x, y, z in local_points]
print(f" Local Corners: {local_points}")
print(f" World Corners: {[tuple(f'{p:.2f}' for p in pt) for pt in world_points]}")
# --- TransformLayer Class ---
class TransformLayer(BaseLayer):
def __init__(self, name, parent=None):
super().__init__(name, parent)
# TransformLayer doesn't have intrinsic content dimensions,
# so anchor point calculations might be less relevant or assume 0 size.
# For simplicity, we'll let BaseLayer handle it with default 0 dimensions.
def draw_content(self, renderer):
# TransformLayer itself doesn't draw, but its children will be drawn.
pass
# --- Renderer Class ---
class Renderer:
def __init__(self, root_layer):
self.root_layer = root_layer
self.drawable_layers = [] # Stores layers that actually draw content
def render_frame(self):
self.drawable_layers = []
print("n=== Rendering Frame ===")
self._traverse_and_draw(self.root_layer)
print("=== Frame Rendered ===n")
def _traverse_and_draw(self, layer):
# In a real renderer, this is where we'd pass the world matrix to GPU for drawing.
if isinstance(layer, PictureLayer):
layer.draw_content(self)
self.drawable_layers.append(layer) # Add to list for hit testing
for child in layer.children:
self._traverse_and_draw(child)
def hit_test(self, screen_x, screen_y):
print(f"n--- Hit Testing at Screen ({screen_x}, {screen_y}) ---")
hit_layer = None
# Iterate in reverse order of drawing (front-to-back) for correct Z-order hit
# This assumes layers added last are drawn on top. For a real Z-buffer, order matters less.
for layer in reversed(self.drawable_layers):
try:
world_to_layer_matrix = layer.get_world_transform().inverse()
local_point = world_to_layer_matrix.transform_point(screen_x, screen_y, 0)
local_x, local_y, _ = local_point
# Check if the point is within the layer's local bounds
if 0 <= local_x < layer.width and 0 <= local_y < layer.height:
hit_layer = layer
print(f" Hit! Layer: {layer.name} at local coordinates ({local_x:.2f}, {local_y:.2f})")
break # Found the topmost layer
except ValueError as e:
print(f" Warning: Could not invert matrix for {layer.name}: {e}")
if hit_layer:
print(f"--- Hit Test Result: {hit_layer.name} ---")
else:
print("--- No Layer Hit ---")
return hit_layer
# --- Example Scene Setup ---
print("Setting up the scene...")
root = TransformLayer("Root")
# Layer 1: A simple picture layer
layer1 = PictureLayer("Layer1", width=100, height=50, parent=root)
layer1.set_position(50, 50)
layer1.content_color = "blue"
# Layer 2: A transform layer with children
transform_group = TransformLayer("TransformGroup", parent=root)
transform_group.set_position(200, 100)
transform_group.set_rotation_z(30) # Rotate 30 degrees
transform_group.set_scale(1.5, 1.5)
transform_group.set_anchor_point(0.5, 0.5) # Rotate/scale around its center
# Layer 2.1: Child of TransformGroup
layer2_1 = PictureLayer("Layer2.1", width=80, height=40, parent=transform_group)
layer2_1.set_position(-40, -20) # Position relative to TransformGroup's local (0,0)
layer2_1.content_color = "green"
layer2_1.set_anchor_point(0.5, 0.5) # Rotate around its own center
layer2_1.set_rotation_z(45) # Further rotation on top of parent's rotation
# Layer 2.2: Another child of TransformGroup
layer2_2 = PictureLayer("Layer2.2", width=60, height=60, parent=transform_group)
layer2_2.set_position(40, 40) # Position relative to TransformGroup's local (0,0)
layer2_2.content_color = "purple"
# Layer 3: A layer that moves and scales itself
layer3 = PictureLayer("Layer3", width=120, height=80, parent=root)
layer3.set_position(300, 200)
layer3.set_scale(0.8, 1.2)
layer3.set_rotation_z(-15)
layer3.set_anchor_point(0.5, 0.5)
layer3.content_color = "orange"
# --- Simulation ---
renderer = Renderer(root)
# Render the scene
renderer.render_frame()
# Simulate a click on Layer1
renderer.hit_test(70, 70) # Should hit Layer1
# Simulate a click on Layer2.1 (which is rotated and scaled by its parent)
# Approx. center of transform_group is at (200,100)
# Layer2.1 is positioned at (-40,-20) relative to transform_group's origin (center)
# The whole group is rotated 30 deg and scaled 1.5x
# This will be tricky to guess exact world coordinates without drawing, but let's try a point
# near what should be Layer2.1's world position.
# A point like (180, 80) might be within the rotated/scaled green box.
renderer.hit_test(180, 80)
# Simulate a click on Layer2.2
# Layer2.2 is positioned at (40,40) relative to transform_group's origin (center)
renderer.hit_test(260, 160) # Near where Layer2.2 might be after parent transforms
# Simulate a click on Layer3
renderer.hit_test(320, 250)
# Simulate a click on empty space
renderer.hit_test(10, 10)
代码输出分析:
运行上述代码,你将看到每个 PictureLayer 的世界变换矩阵以及它在世界坐标系中的角点。这直观地展示了矩阵堆叠的效果。命中测试的输出则进一步验证了逆矩阵在将屏幕坐标转换回局部坐标方面的作用。
总结与展望
层级结构与矩阵运算的结合,是现代图形渲染中管理复杂场景和实现动态交互的核心范式。PictureLayer 负责内容的绘制,TransformLayer 则专注于几何变换的组织与级联。通过矩阵乘法的堆叠,我们可以将局部坐标系中的元素精确地定位到世界坐标系中,并利用逆矩阵实现高效的事件处理。理解这些基础机制,是构建高性能、可扩展的图形用户界面和沉浸式体验的关键一步。