伪造的 Godot 中 3D 俯视角色
之前,我分享了一个为 Gdquest 创建的俯视角色视频。人们对这个小家伙的内部运作感到好奇,想知道创建这种角色使用了什么技术。
请注意,这里详细介绍的这种技术最适合自上而下的角色,但您可以为所有类型的深度效果做类似处理。
所有文件都可在 GitHub 上找到,您可以查看项目并阅读以了解其精髓 :)
基础知识
整个效果归结于 Godot 为其节点提供了一个默认的父子关系系统,对父节点应用任何变换都将被其子节点继承。技巧在于玩转 2D 节点的父子关系系统,以获得放置精灵的点,并给我们的角色在 3D 中的错觉。让我们构建一个简单的例子来展示这个想法,这将是我们后来创建更复杂角色所需的所有内容。
让我们使用这个简单的树结构创建一个新的场景:
- SceneRoot (Node2D)
- Visual (Node2D)
- PointSprite (Sprite2D)
- Rig (Node2D)
- PointAnchor (Node2D)
- PlaneResult (RemoteTransform2D: Targets the PointSprite node)
我总是为了清晰起见将角色视觉(精灵组)和绑定(应用变换到精灵上的点组)分开,但你也可以根据自己的结构来做。
让我们回顾 Rig 节点及其子节点,了解它们的作用,这个组决定了精灵在屏幕上的位置。
Rig 节点包含 rig 结构中的所有节点。因为它是该结构中所有节点的父节点,所以它的变换将影响所有子节点。对其 Y 分量应用缩放表示我们想象中的相机角度;数字越小,相机角度越钝,角色看起来越扁平。
点锚节点表示我们点的海拔(改变其 Y 位置分量)
The PlaneResult 节点是我们的最终转换点,我们将使用这个位置来放置 Sprite 节点。因为它相对于其 PointAnchor 父节点,改变它的位置将改变相对于 PointAnchor 节点的最终点位置在平面上。
对于这个节点来说,它只更新其远程节点位置,作为 RemoteTransform2D 是很重要的,这样我们就可以只将最终位置应用于 PointSprite 节点,而不影响缩放和倾斜。当我们对节点的最终变换满意时,我们将使用 RemoteTransform2D 节点来绕过 Godot 的父子系统。
现在一切就绪,我们可以用这些参数来控制我们的设备:
- Y 轴组件用于修改 Rig 节点的 PlaneResult 倾斜。
- 点锚节点的角度值,用于围绕它旋转平面结果节点。
- 点锚节点 Y 位置组件,用于控制平面结果节点的海拔高度。
- 并且更改 PlaneResult 节点的位置,以相对于 PointAnchor 节点在平面上改变其位置。
所以,有了所有这些... “鼓声隆隆” 一个围绕其原点旋转的点... 这可能有点令人失望,但这正是我们构建整个字符所需的一切。
设计我们的角色
现在我们了解了底层系统,让我们设计一个角色来应用我们的新知识。我们的目标是设计一个可以分解成部分(精灵)的角色(称为 2D 木偶)。
此效果最适合简单、风格化的字符。圆形表面效果良好,因为无论从哪个角度观看,它们都能在一定程度上保持形状。最好避免使用具有锐角的物体,如盒子,因为我们实际上并不是在创建 3D 模型,而是在创造一种错觉。
对于这个教程,我选择创建一只鸭子,它比 Gdquest 中设计的角色包含的移动部件更少。这一步相当直接;我通常的工作流程是使用矢量图形软件(如 Figma 或 Inkscape)绘制所需角色的草图。
模型通常从上方面对相机(考虑到它需要可分解且足够简单以便使用这种技术)。一旦我对设计的整体感觉满意,我就可以开始分解它并将每个部分保存为图像(.png)。
导入到 Godot
本节中,我将不描述鸭身体各部分的工作原理,因为它们都遵循早期示例中的相同逻辑(请参考项目以继续学习)。
首先,我们需要设置场景和资产,让我们创建一个文件夹,该文件夹将包含一个专门用于我们角色的场景和一个包含所有鸭子视觉部分的子文件夹。您可以在项目中的 top_down_character 文件夹中找到这个场景。
- top_down_character
- parts
- beak_sprite.png
- body_sprite.png
- etc...
- duck.tscn
- duck.gd
您可能已经注意到,我没有使用颈部精灵,也没有为这个角色制作腿部精灵。为了提高灵活性,我使用 Line2D 节点动态绘制这些部分,并将 limb_line_2d.gd 附加到它上面。此脚本将在一系列 node2D 节点之间绘制线条,这使得这些身体部分可以根据角色动作拉伸和变形。
过程相当直接,就像早期示例中,我们将角色的视觉和逻辑分为两个子节点(视觉和绑定)。这将使我们能够分离关注点,更容易地绘制事物,无需处理绑定组的结构和绘制顺序。
- Duck (Node2D)
- Visual (Node2D)
- Sprites... (Sprite2D)
- Rig (Node2D)
- Anchor (Node2D)
- PlaneResult (RemoteTransform2D: Targets a sprite node)
我们接着描述每个身体部位的连续平面,以便它们可以在我们的想象 3D 空间中单独移动。(注意每个脚和身体都是同样的设置)。这里唯一的例外是头部部分。因为它使用的是 NeckRoot 的最终位置副本,而 NeckRoot 本身是从 BodyPlaneOffset 继承的。在这里使用 RemoteTransform2D 节点的原因是为了摆脱父子关系系统,并重置变换,只保留我们需要的位置。
一旦进入 NeckPositionCopy,我们可以通过改变 HeadHeightOffset 的 Y 位置组件来设置头部的高度。从那里,喙和眼睛部分遵循与身体和脚相同的规则。
从这里,我们计算了放置精灵所需的所有位置,如您所注意到的,它们都在 Rig 组内部以某种方式通过 RemoteTransform2D 节点相互链接。还有一个问题尚未解决……我们还没有旋转角色!让我们通过创建一个脚本来为角色添加最终逻辑。
# Export a range displayed in degrees, but which uses radians behind the curtains
# Adding a setter to this variable allows us to update everything we need to display the character properly at each angle
@export_range(0.0, 360.0, 0.1, "radians") var direction : float = 0.0 : set = _set_direction
# Declare all the sprites
@onready var foot_sprite_l = %FootSpriteL
@onready var foot_sprite_r = %FootSpriteR
@onready var body_sprite = %BodySprite
@onready var beak_sprite = %BeakSprite
# Declare all the nodes needing rotations
@onready var anchors = [%FootAnchorL, %FootAnchorR, %BodyAnchor, %EyesAnchor, %BeakAnchor, body_sprite, beak_sprite]
func _set_direction(value : float):
if !is_inside_tree(): return
direction = value
# Wrap the direction value to an angle value which will always be between 0.0 and a full rotation (TAU)
var angle = wrapf(direction, 0.0, TAU)
# Apply the rotation value to all 2D nodes needing it (anchors and some sprites)
for anchor in anchors:
anchor.rotation = angle
# The sprites for the feet are a bit of an outlier
# because we also want to add a slight rotation offset so there are not fully alligned with the body
# so let set it up directly in code while we are at it :)
foot_sprite_l.rotation = angle + 0.5
foot_sprite_r.rotation = angle - 0.5
从那里,当我们更改方向值时,所有部分都会相应旋转,将精灵定位和旋转到期望的位置。仍然有一个未解决的问题!当脸部和颈部应该在头部和身体后面时,它们没有正确渲染。
为了解决这个问题,我们只需要在_set_direction 函数中创建一个检查角度值的逻辑,并相应地设置节点的 z_index。
var z_index_value = -1 if angle > HALF_PI and angle < PI + HALF_PI else 1
face_sprite_group.z_index = z_index_value
body_sprite.z_index = 1.0 - z_index_value
这里就是了!一只可爱的小鸭子在俯视视角中嘎嘎叫着,摇摇摆摆地走来走去。