读Godot4.5文档手册
本文参考Godot4.5中文文档中
手册的内容进行了总结改进,去除了个人感觉无用的部分
手册
最佳实践
在 Godot 中应用面向对象原则
Godot 引擎主要提供了两种创建可复用对象的方式:脚本和场景。
虽没有真正在底层定义类但面向对象的编程原则仍可应用。
脚本
引擎提供了内置的类,如Node。
使用脚本扩展类来创建派生类型。
脚本严格来说并不是类,而是一种资源告知引擎在内置类的基础上的初始化操作。
Godot 的内部类可以将一个类的数据注册进一个名为ClassDB的数据库
可以在运行时访问类的信息(例:属性 方法 常量 信号)。
当对象在执行访问属性或调用方法等操作时,就会检查ClassDB中对象和对象基类的记录,确定对象是否支持该操作。
脚本可以扩展ClassDB中该对象的方法、属性和信号。
备注
脚本即使没有使用 extends 关键字,也会隐式继承引擎的基础 RefCounted 类。
因此,你可以从代码中实例化不使用 extends 关键字的脚本。
不过由于扩展的是 RefCounted,无法把它们附加到 Node 上。
场景
场景是可复用、可实例化、可继承的节点组。
创建场景类似用脚本去创建一些节点,并使用add_child()将其添加为子节点。
常为场景搭配一个带有脚本的根节点,并在脚本中使用这个场景下的节点。
场景的内容有助于定义:
- 脚本可使用哪些节点。
- 如何组织,初始化。
- 彼此之间有什么信号连接。
问题
为什么这些对组织场景很重要?
因为场景的实例都是对象。
因此,许多适用于面向对象原则也适用于场景:单一职责、封装等。
场景组织
如何有效地建立关系?
- 应尽可能设计没有依赖的场景。(可独立运行)
- 如果场景必须与外部环境交互,建议使用依赖注入。
- 依赖注入涉及使高级 API 提供低级 API 的依赖关系。
- 为什么?依赖于其外部环境的类可能会无意中触发 Bug 和意外行为。
✅ 核心目标:松耦合 + 高内聚
“一个类不应依赖外部环境来完成其核心功能。”
要做到这一点,就必须暴露数据,然后依靠父级上下文对其进行初始化。
五种方法:
- 连接信号。
这样做极其安全,但只能用于“响应”,而不是启动。
按惯例,信号名称通常是过去式动词。
1 | # Parent |
- 调用方法。
用于启动行为。
1 | # Parent |
- 初始化 Callable 属性。
比调用方法更安全,因为不需要拥有这个方法的所有权。用于启动行为。
1 | # Parent |
- 初始化 Node 或其他 Object 的引用。
1 | # Parent |
- 初始化 NodePath。
1 | # Parent |
完整示例
1 | # Parent.gd |
1 | # Child.gd |
这些选项隐藏了子节点的访问点。这反过来又使子节点与环境保持松耦合 (loosely coupled)。
人们可以在另外一个上下文中重新使用它,而不需要对API做任何额外的改变。
警告
应倾向于将数据保存在场景内部,尽管它对外部内容有一个依赖关系,甚至是一个松散耦合的依赖,仍然意味着节点将期望其环境中的某些内容为真。
项目的设计理念应避免这种情况的发生。如果不这样做,代码的继承关系将迫使开发人员使用文档, 以在微观尺度上跟踪对象关系;这就是所谓的开发地狱。
通常情况下,编写依赖于外部文档才能安全使用的代码,是很容易出错的。
为了避免创建和维护此类文档,可以将依赖节点(上面的子级)转换为工具脚本,该脚本实现 _get_configuration_warnings()。
从中返回的一个非空字符串紧缩数组(PackedStringArray)将使场景停靠面板生成警告图标,其中包含上述字符串作为节点的工具提示。
这个警告图标和没有定义 CollisionShape2D 子节点时 Area2D 节点旁出现的图标是一样的。这样,编辑器通过脚本代码自记录(self-document)场景,也就不需要在文档里记录一些与之重复的内容了。
这样的GUI可以更好地通知项目用户有关节点的关键信息. 它具有外部依赖性吗?这些依赖性是否得到满足?
其他程序员, 尤其是设计师和作家, 将需要消息中的明确指示, 告诉他们如何进行配置.
选择节点树结构
构造节点树的方法有无数种。
对于没把握的人而言,这份有用的指南可以给他们一个不错的结构样本作为开始。
- “Main”节点(main.gd 作为游戏主要控制器)
- Node2D/Node3D 游戏“世界”(game_world.gd)
- Control “GUI” 管理项目所需的各种菜单和部件(gui.gd)
当变更关卡时,可以稍后换出“World”节点的子级。手动更换场景让用户完全控制他们的游戏世界如何过渡。
下一步是考虑项目需要什么样的游戏系统:跟踪所有内部数据,全局可访问,独立存在。
创建一个自动加载“单例”节点
备注
对于较小的游戏,一个更简单少控制的做法是使用一个“Game”单例
调用 SceneTree.change_scene_to_file() 方法,用于置换出主场景的内容。
这种结构多少保留了“World”作为主要游戏节点。
任一 GUI 也需要是一个单例;作为 "World" 的临时部分,或被手动添加到根节点作为其直接子节点。否则 GUI 节点也会在场景转换时自行删除。
如果一个系统需要修改另一个系统的数据,那么就应该分别定义成单独的脚本或者场景,不应该使用自动加载。
游戏中的每个子系统都应该在SceneTree中占有自己的一席之地。
只有在节点确实是父节点中的元素时才应当使用父子关系。
如果移除父节点的话,同时将这些子节点移除是否说得通?说不通的话,就应该在层级结构中单独列出,两者成为兄弟节点或者其他关系。
备注
某些情况下,我们仍然会需要让这些单独的节点进行相对定位。
此时可以使用 RemoteTransform / RemoteTransform2D 节点。让目标节点有条件地从 Remote* 节点继承选定的变换元素。
要分配 target 的 NodePath,请使用以下方法之一:
1. 一个可靠的第三方, 可能是一个父节点, 来协调分配任务.
2. 一个分组, 轻松提取对所需节点的引用(假设只有一个目标).
什么时候你该这样做?当你必须精细管理,且一个节点必须在场景树上来回移动以保留自己时,就会出现两难的局面。例如……
添加一个“玩家”节点到一个“房间”节点。
需要改变房间了,所以必须删除当前房间节点。
在房间能被删除前,你必须保留玩家并/或将其移走。
- 如果不关心内存,你可以……
- 创建新的房间节点。
- 将玩家节点移动到新的房间节点。
- 删除旧房间。
- 如果比较关注内存情况,那么就需要这样……
- 将玩家节点移动到节点树的其他地方。
- 删除房间节点。
- 实例化并添加新的房间节点。
- 重新添加玩家节点到新房间中。
问题在于这里的角色是一种“特殊情况”;开发者必须知道需要以这种方式处理项目中的角色。
因此,在团队中可靠地分享这些信息的唯一方法就是写文档。
然而,在文档中记录实现细节是很危险的,会成为一种维护负担,使代码可读性下降,不必要地膨胀项目的知识内容。
在拥有更多的资产的,更复杂的游戏中,将整个玩家节点保留在 SceneTree 中的其他地方会更好。这样的好处是:
- 一致性更高。
- 没有“特殊情况”,不必写入文档也不必进行维护。
- 因为不需要考虑这些细节,所以也没有出错的机会。
- 相比之下,如果需要子节点不继承父节点的变换,那么就有以下选项:
声明式解决方案:在它们之间放置一个 Node。作为没有变换的节点,Node 不会将这些信息传递给其子节点。
命令式解决方案:对 CanvasItem 或者 Node3D 节点使用 top_level 属性。这样就会让该节点忽略其继承的变换(transform)。
备注
如果构建的是网络游戏,请记住哪些节点和游戏系统与所有玩家相关,哪些只与权威服务器相关。
例如,用户并不需要所有人都拥有每个玩家的“PlayerController”逻辑的副本。相反,他们只需要自己的。
将它们保持在从“世界”分离的独立的分支中,可以帮助简化游戏连接等的管理。
场景组织的关键是用关系树而不是空间树来考虑 SceneTree。
节点是否依赖于其父节点的存在?
如果不是,那么它们可以自己在别的地方茁壮成长。
如果它们是依赖性的,那么理所当然它们应该是父节点的子节点。
问题
这是否意味着节点本身就是组件?
并不是这样。Godot 的节点树形成的是聚合关系,不是组合关系。
虽然依旧可以灵活地移动节点,但在默认情况下是没有进行移动的必要的。
何时使用场景与脚本
匿名类型
单独使用脚本可以完全定义场景的内容.
但是, 选择哪个来使用, 可能是一个两难问题. 创建脚本实例与创建引擎类相同, 而处理场景需要更改API:
1 | const MyNode = preload("my_node.gd") |
此外, 由于引擎和脚本代码之间的速度差异, 脚本的运行速度将比场景慢一些. 节点越大和越复杂, 将它构建为场景的理由就越多.
命名的类型(注册新类型)
脚本可以在编辑器中被注册为一个新类型。
这样,用户就可以更加便捷地使用脚本,而不是必须…
- 了解他们想要使用的脚本的基本类型.
- 创建一个该基本类型的实例.
- 将脚本添加到节点.
用于注册类型的系统有两种:
自定义类型
仅限编辑器. 类型名称在运行时中不可访问.
不支持继承的自定义类型.
一个初始化工具. 使用脚本创建节点.
编辑器没有对该脚本的类型感知, 或其与其他引擎类型或脚本的关系.
允许用户定义一个图标.
设置使用 EditorPlugin.add_custom_type.Script 类
编辑器和运行时均可访问.
显示全部继承关系.
使用脚本创建节点, 但也可以从编辑器更改或扩展类型.
编辑器知道脚本, 脚本类和引擎c++类之间的继承关系.
允许用户定义一个图标.
引擎开发人员必须手动添加对语言的支持(名称公开和运行时可访问性两者).
编辑器扫描项目文件夹, 并为所有脚本语言注册任何公开的名称. 为公开此信息, 每种脚本语言都必须实现自己的支持.
这两种方法都向创建对话框添加名称, 特别是脚本类, 还允许用户在不加载脚本资源的情况下访问类别名称. 在任何地方都可以创建实例, 和访问常量或静态方法.
有了这些功能, 由于它赋予用户易用性, 人们可能希望它们的类型是没有场景的脚本. 那些正在开发的插件或创建供设计人员使用的内部工具, 将以这种方式使事情变得更轻松.
不足之处在于, 这也意味着很大程度上必须使用命令式编程.
Script 与 PackedScene 的性能
在选择场景和脚本时, 最后一个需要考虑的方面是执行速度.
随着对象内容的增加, 脚本创建和初始化所需的内容也会大大增加. 创建节点层次结构就说明了这一点. 每个Node的逻辑可能有几百行代码.
下面的代码示例创建一个新的 Node, 更改名称, 分配脚本, 将其未来的父级设置为其所有者, 以便保存到磁盘中, 最后将其添加为 “主” 节点的子级:
1 | # main.gd |
这样的脚本代码比引擎端的C++代码要慢很多. 每条指令都要调用脚本API, 导致后端要进行多次 “查找”, 以找到要执行的逻辑.
场景有助于避免这个性能问题。PackedScene(场景包)是场景继承的基础类型,定义了使用序列化数据创建对象的资源。
引擎可以在后端批量处理场景,并提供比脚本好得多的性能。
总结
最好的方法是考虑以下几点:
有一个自定义名称/图标.
场景比脚本更容易跟踪/编辑, 并提供更多的安全性.
命名场景通过声明一个脚本类并给它一个场景作为常量来实现这一点。实际上,该脚本变成了一个命名空间:
1 | # game.gd |
自动加载与常规节点
Godot 提供了一个在项目根节点自动加载节点的功能,允许你在全局范围内访问它们,从而完成单例作用 单例(自动加载) 。
当你在代码中使用 SceneTree.change_scene_to_file 更改场景时,这些自动加载的节点不会被释放。
在本指南中, 你将学习到何时使用自动加载功能, 以及避免使用该功能的方法.
切割音频问题
其他引擎可能鼓励使用创建管理类, 单例将很多功能组织到一个全局可访问的对象中.
由于节点树和信号,Godot提供了许多避免全局状态的方法.
例如, 假设我们正在构建一个平台游戏, 并希望收集能够播放声音效果的硬币, 那么就有一个节点 AudioStreamPlayer.
如果在 AudioStreamPlayer 已经在播放声音时调用它, 新的声音就会打断第一个声音.
一种解决方案是写一个全局的、自动加载的音效管理器类。它会生成一个 AudioStreamPlayer 的节点池,每当一个新的音效请求出现时,它就会在这个节点池中找到可用的节点来播放。我们不妨就把该类命名为Sound ,你可以通过Sound.play("coin_pickup.ogg")从你项目中的任何位置使用它。这在短期内解决了问题但是却造成了更多的麻烦:
- 全局状态 : 一个对象现在负责所有对象的数据. 如果音效有错误, 或没有一个可用的 AudioStreamPlayer , 一切都会崩溃.
- 全局访问 : 意味着任何对象都可以从任何地方调用 Sound.play(sound_path) , 便不容易找到错误的来源了.
- 全局资源分配 : 由于从一开始就存储了一个 AudioStreamPlayer 节点池, 如果数量太少会遇到bug, 而数量太多则会占用更多的内存.
备注
全局访问的问题在于,任何地方的代码都可能将错误的数据传递给我们例子中的 Sound 自动加载。
因此,为了修复这个 bug,你需要检索的区域涵盖了整个项目。当你将代码保存在场景中时, 音频可能仅涉及一个或两个脚本.
与之形成对比的是, 每个场景在其内部, 保留尽可能多的 AudioStreamPlayer 节点, 所有这些问题都会消失:
- 每个场景管理自己的状态信息. 如果数据有问题, 则只会在该场景中引起问题.
- 每个场景只访问自己的节点. 那么如果有一个bug, 很容易找到哪个节点有问题.
- 每个场景只分配所需数量的资源.
管理共享功能或数据
使用自动加载的另一个原因可能是你希望在许多场景中重复使用相同的方法或数据.
对于函数,可以使用 GDScript 中的 class_name 关键字创建一种新的 Node 类型,为单个场景提供该功能。
当涉及到数据时, 你可以:
- 创建一个新类型的 Resource 来共享数据.
- 将数据存储在每个节点可以访问的对象中, 例如使用 owner 属性来访问场景的根节点.
何时应使用自动加载
GDScript 支持使用 static func 创建 static (静态) 函数,与 class_name 结合使用时还可以创建辅助函数库,无需创建实例来调用这些函数。 静态函数也有一些限制:不能引用成员变量、非静态(non-static)函数或 self。
从 Godot 4.1 开始,GDScript 还支持使用 static var 的 static (静态)变量,意味着你现在可以在类的实例之间共享变量,而无需创建单独的自动加载节点或脚本。
尽管如此,对于那些涵盖范围广泛的系统来说,使用自动加载的节点仍然可以简化你的代码。如果自动加载的节点管理自己的信息并且不侵入其他对象的数据,那么这就是一个创建处理广泛任务的系统(例如,任务或对话系统)的好方法。
备注
自动加载不完全是一个单例。没有什么可以阻止你实例化自动加载的节点的副本。
它只是一个使节点作为场景树的根的子节点自动加载的工具,而与游戏的节点结构或运行哪个场景(比如通过按 F6 键运行当前场景)无关。
因此,你可以通过调用 get_node("/root/Sound") 来获取名为 Sound 的自动加载节点。
何时以及如何避免为任何事情使用节点
- Object:终极轻量级对象,原始的 Object 必须使用手动内存管理。
尽管如此,创建自己的自定义数据结构——甚至是节点结构——也并不难,并且比 Node 类更轻量。
示例:参见 Tree 节点。它支持对具有任意行数和列数的内容表,进行高级定制。用来生成可视化的数据实际上是 TreeItem 对象的树。
优势: 将 API 简化为较小范围的对象,有助于提高其可访问性、改善迭代时间。与其使用整个 Node 库,不如创建一组简略的 Object,节点可以从这些 Object 中生成和管理相应的子节点。
备注
处理它们时要小心. 可以将 Object 存储到变量中, 但是这些引用可能在没有警告的情况下失效.
例如, 如果对象的创建者决定删除它, 这将在下一次访问时, 触发错误状态.
RefCounted:只比 Object 稍微复杂一点。它们会记录对自己的引用,只有当对自己没有另外的引用存在时,才会删除加载的内存。在大多数需要在自定义类中存取数据的情况下,很有用。
示例:见 FileAccess 对象。它的功能就像普通 Object 一样,只是不需要人为删除。
优势: 与 Object 相同.Resource :只比 RefCounted 稍微复杂一点。它们天然具有将其对象属性序列化(即保存)到 Godot 资源文件,或从 Godot 资源文件中反序列化(即加载)的能力。
示例 : 脚本, PackedScene (用于场景文件), 以及其他类型, 比如 AudioEffect 类. 每一个都可以保存和加载, 因此它们均是从 Resource 继承而来的.
优势:关于 Resource 与传统数据存储方法相比的优势已经说了 很多。然而,在使用 Resource 替代 Node 的情境下,Resource 的主要优点是与检查器的兼容性。虽然几乎和 Object/Reference 一样轻量,它们仍然可以在检查器中显示并导出属性。这使它们在易用性上,可以媲美使用子节点的方式,而且如果有人计划在其场景中包含许多这类 Resource/Node,它们还可以提高性能。
Godot 接口
脚本常常需要依赖其他对象来获取功能。这个过程分为两部分:
- 获取对可能具有这些功能的对象的引用。
- 从对象访问数据或逻辑。
获取对象引用
对所有 Object 来说,获得引用的最基础的方法,是通过另一个已获得引用的对象。
1 | var obj = node.object |
同样的原则也适用于 RefCounted 对象。虽然用户经常以这种方式访问 Node 和 Resource,但还有其他方法可用。
除了访问属性和方法,也可以通过加载来获得 Resource。
1 | # 如果你需要一个“导出的常量变量”(export const var,实际不存在), |
请注意以下几点:
- 有许多加载这些资源的方法。
- 在设计对象如何访问数据时,不要忘记,还可以将资源作为引用传递。
- 请记住,加载资源时只会获取引擎维护的缓存资源实例。如果要获取一个新对象,必须 复制 一个现有引用,或者使用 new() 从头实例化一个对象。
节点同样也有另一种访问方式:场景树。
1 | extends Node |
从对象访问数据或逻辑
Godot 的脚本 API 是鸭子类型(duck-typed)的。
这意味着,当脚本执行某项操作时,Godot 不会通过类型来验证其是否支持该操作。相反,它会检查对象是否实现了这个被调用的方法。
例如,CanvasItem 类具有 visible` 属性。暴露给脚本 API 的所有属性实际上都是与名称绑定的 setter 和 getter 对。如果有人尝试访问 CanvasItem.visible,那么 Godot 将按顺序执行以下检查:
- 如果对象附加了脚本,它将尝试通过脚本设置属性。这使得脚本有机会通过覆盖属性的 setter 方法来覆盖在基础对象上定义的属性。
- 如果脚本没有该属性,它会在 ClassDB 中针对 CanvasItem 类及其所有继承类型执行 HashMap 查找以查找“visible”属性。如果找到,它会调用绑定的 setter 或 getter。有关 HashMap 的更多信息,请参阅《数据偏好》文档。
- 如果没有找到, 它会进行显式检查, 以查看用户是否要访问 script 或 meta 属性.
- 如果没有, 它将在 CanvasItem 及其继承的类型中检查 _set/_get 实现(取决于访问类型). 这些方法可以执行逻辑, 从而给人一种对象具有属性的印象. _get_property_list 方法也是如此.
- 请注意,即使对于不合法的符号名称也会发生这种情况,例如以数字开头或包含斜杠(/)的名称。
因此,这个鸭子类型的系统可以在脚本、对象的类,或对象继承的任何类中定位属性,但仅限于扩展 Object 的对象。
Godot 提供了多种选项,来对这些访问执行运行时检查:
- 鸭子类型属性的访问。Godot 将像上文所述的那样对它进行属性检查。如果对象不支持该操作,则执行将停止。
1 | # 所有对象都有类似鸭子类型(duck-typed)的 get、set 和 call 包装方法 |
方法检查。
在 CanvasItem.visible 的例子中,我们可以像访问任何其他方法一样,访问 set_visible 和 is_visible。
1 | # 获取第一个子节点 |
将访问权限外包给 Callable。当需要最大程度地摆脱依赖时,这种方法可能很有用。在这种情况下,人们依赖外部上下文来设置该方法。
1 | # child.gd |
Godot 通知
Godot 中的每个对象都实现了 _notification 方法。
其目的是允许对象响应可能与之相关的各种引擎级回调。
例如,如果引擎告诉 CanvasItem 去“绘制”,则它将调用 _notification(NOTIFICATION_DRAW)。
在所有这些通知之中,有很多类似“绘制”这样经常需要在脚本中去覆盖的通知,多到 Godot 要提供专用函数的地步:
_ready(): NOTIFICATION_READY
_enter_tree(): NOTIFICATION_ENTER_TREE
_exit_tree(): NOTIFICATION_EXIT_TREE
_process(delta): NOTIFICATION_PROCESS
_physics_process(delta): NOTIFICATION_PHYSICS_PROCESS
_draw(): NOTIFICATION_DRAW
用户可能不会意识到 Node 之外的类型也有通知,例如:
Object::NOTIFICATION_POSTINITIALIZE:在对象初始化期间触发的回调。脚本无法访问。
Object::NOTIFICATION_PREDELETE:在引擎删除 Object 之前触发的回调,即析构函数。
并且 Node 中存在的许多回调没有任何专用的方法,但仍然非常有用。
Node::NOTIFICATION_PARENTED: 将子节点添加到另一个节点时,会触发此回调。
Node::NOTIFICATION_UNPARENTED: 将子节点从另一个节点下删除时,会触发此回调。
你可以在通用的 _notification() 方法中访问所有这些自定义通知。
备注
文档中被标记为“virtual”的方法(即虚方法)可以被脚本覆盖重写。
一个经典的例子是 Object 中的 _init 方法。
虽然它没有等效的 NOTIFICATION_* 通知,但是引擎仍然会调用该方法。大多数语言(C#除外)都将其用作构造函数。
对比 _process、_physics_process、*_input
当需要使用“依赖于帧速率的 delta 时间增量”时,请使用 _process。
如果需要尽可能频繁地更新对象数据,也应该在这里处理。
频繁执行的逻辑检查和数据缓存操作,大多数都在这里执行。
但也需要注意执行频率,如果不需要每帧都执行,则可以选择用定时器循环来替代。
1 | # Allows for recurring operations that don't trigger script logic |
当需要与帧速率无关的时间增量时,请使用 _physics_process。
如果代码需要随着时间的推移进行一致的更新,不管时间推进速度是快还是慢,那么就应该在这里执行代码。
频繁执行的运动学和对象变换操作,应在此处执行。
为了获得最佳性能,应尽可能避免在这些回调期间进行输入检查。
_process 和 _physics_process 每次都会触发(默认情况下这些更新回调不会 “休眠”)。
相反,*_input 回调仅在引擎实际检测到输入的帧上触发。
在 input 回调中同样可以检查输入动作。如果要使用增量时间,则可以使用相关的增量时间获取方法来获取。
1 | # Called every frame, even when the engine detects no input. |
对比 _init、初始化、导出
如果脚本初始化它自己的没有场景的节点子树,则该代码将会在 _init() 中执行。
其他属性或独立于 SceneTree 的初始化也应在此处运行。
_init()在脚本创建并初始化其属性之后,_enter_tree() 或 _ready() 之前触发
实例化场景时,属性值将按照以下顺序设置:
初始值赋值:为属性赋初始值,未指定初始值时赋默认值。Setter 函数即便存在也不会使用。_init()赋值:在 _init() 中通过各种赋值改变属性的取值,会触发 setter 函数。
导出值赋值:如果在“检查器”中修改了导出属性的值,就会再次修改该属性的值,会触发 setter 函数。
1 | # test is initialized to "one", without triggering the setter. |
因此,选择实例化脚本还是实例化场景,对初始化和引擎调用 setter 的次数都会产生影响。
对比 _ready、_enter_tree、NOTIFICATION_PARENTED
将场景实例化并首次添加到运行的场景树时,Godot 会沿着场景树从上至下实例化节点(调用 _init() 函数),再从根节点出发从上至下构建场景树。因此 _enter_tree() 是按照树的顺序从上至下一级一级调用的。场景树构建完成后,所有叶节点就会调用 _ready。一个节点的所有子节点都调用完该方法后,就会轮到该节点自己调用。此时就是逆着树的顺序从下至上一级一级调用的,最终到达根节点。
当实例化脚本或独立的场景时,节点不会在创建时被添加到 SceneTree 中,所以未触发 _enter_tree 回调。而只有 _init 调用发生。当场景被添加到 SceneTree 时,才会调用 _enter_tree 和 _ready。
如果需要触发作为节点设置父级到另一个节点而发生的行为, 无论它是否作为在主要/活动场景中的部分发生, 都可以使用 PARENTED 通知. 例如, 这有一个将节点方法连接到其父节点上自定义信号, 而不会失败的代码段。对可能在运行时创建并以数据为中心的节点很有用。
1 | extends Node |
数据偏好
数组、字典、对象
Godot 把脚本 API 中的所有变量都存储在Variant(存储兼容数据结构)中。
例如 Array(数组)、 Dictionary(字典)、 Object(对象)。
Godot 使用 Vector
将数组内容存储在一段连续的内存之中,也就是说,元素与元素之间是相邻的。
这里的 Vector 是传统 C++ STL 库中数组对象的名称,是个“模板”类型,即它只能存储特定类型的数据(用尖括号表示)。
例如,PackedStringArray 其实就类似于 Vector。
因为是在内存中连续存储,所以执行各种操作的性能如下:
迭代:最快,非常适合循环。
- 操作:把计数器加一即可获取下一个元素。
插入、删除、移动:与位置相关,一般较慢。
- 操作:元素的添加、删除、移动需要移动与之相邻的元素(腾出地方或者填充空缺)。
- 在末尾添加、删除很快。
- 在任意位置添加、删除较慢。
- 在开头添加、删除最慢。
- 如果需要在开头执行多次插入、删除操作,那么……
- 反转数组。
- 通过循环在末尾执行数组更改。
- 再把数组反转回来。
这样就只复制了两次数组(虽然比较慢,但还是常数时间),否则就得把平均大概一半的数组复制 N 遍(线性时间)。
取值、设值:因为是按位置存取的,所以最快。例如你可以请求第 0 个、第 2 个、第 10 个等等的元素,但不能按照元素的值来请求。
- 操作:把起始位置做一次加法,得到所需的索引。
查找:最慢。根据值获取索引,也就是位置。
操作:必须遍历数组,一个个元素做比较,直到找到匹配的为止。
- 性能同时也取决于是否需要查遍整个数组才能找到目标。
如果数组能够保持一定的顺序,自定义搜索操作可以缩短到对数时间(相对而言很快)。不过外行用户不会对此感到满意。做法是每次编辑后都重新对 Array 进行排序,编写利用已排序特性的搜索算法。
Godot 将 Dictionary 实现为 HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator> .
引擎存储一个键值对的小数组(初始化为 2^3 或 8 条记录)。
当人们尝试访问一个值时,他们会为其提供一个密钥。然后它对密钥进行哈希处理 ,即将它转换为一个数字。
“哈希”用于计算数组中的索引。作为一个数组,HM 在映射到值的键的“表”中进行快速查找。
当 HashMap 变得太满时,它会增加到 2 的下一个幂(因此,16 条记录,然后是 32 条记录,依此类推)并重建结构。
使用哈希是为了减少键的冲突几率。发生冲突时,哈希表必须重新计算索引号,将占据原有位置的值纳入考虑范围。
总之,这样做就能够以牺牲内存和一些较小的操作效率为代价,让所有记录的访问都达到常数时间。
对每个键进行任意次哈希。
哈希操作是常量时间的,因此即使某个算法必须执行多次,只要哈希的计算次数与表的密度没有什么大关系,那么就能够保持较快的速度。这样……保持不断增长的表规模.
HashMaps为了减少哈希冲突, 并保持访问速度, 在表中保留了未使用的内存的间隙. 这就是为什么它总是会以2幂为倍数扩展其容量.
如大家所知,字典擅长的任务是数组所不擅长的。其操作细节概述如下:
- 迭代 : 快速.
- 操作: 遍历映射的内部散列向量. 返回每个键. 之后, 用户使用该键跳转到并返回所需的值.
- 插入, 删除, 移动 : 最快.
- 操作: 散列给定的键. 执行1个加法操作来查找适当的值(数组开始+偏移量). 移动其中的两个(一个插入, 一个擦除). 映射必须进行一些维护, 以保留其功能:
- 更新记录的有序列表.
- 确定列表密度, 是否需要扩展列表容量.
- 字典会记住用户插入键的顺序. 这使它能够执行可靠的迭代.
- 操作: 散列给定的键. 执行1个加法操作来查找适当的值(数组开始+偏移量). 移动其中的两个(一个插入, 一个擦除). 映射必须进行一些维护, 以保留其功能:
- 取值, 设值 : 最快. 和 根据键 查找相同.
- 操作: 和插入/删除/移动类似.
- 查找 : 最慢. 标识值的键.
- 操作: 必须遍历记录并比较该值, 直到找到匹配的为止.
- 请注意,Godot并未开箱即用地提供此功能(因为它们并非用于此任务).
Godot用愚蠢, 但动态的方式容纳数据容器实现对象. 提出问题时, 对象将查询数据源.
例如, 要回答”你是否有一个名为 position 的属性?”的问题, 它可能会询问其 script 或 ClassDB.
这里重要的细节是对象任务的复杂性. 每次执行这些多源查询时, 它运行 几个 迭代循环和哈希表查找. 此外, 查询是线性时间操作, 依赖于对象的继承层次结构大小. 如果 Object 查询的类(当前类)什么都没有找到, 则该请求将一直推迟到下一个基类, 一直到原始 Object 类为止. 虽然这些都是单独的快速操作, 但它必须进行如此多的检查, 于是这一事实使得它们比查找数据的两种方法都要慢.
当开发人员提到脚本API有多慢时, 所引用的正是这一系列查询. 与编译后的, 应用程序知道在哪里可以找到任何东西的,C++代码相比, 不可避免的是, 脚本API操作将花费更长的时间. 他们必须定位任何相关数据的来源, 然后才能尝试访问它.
GDScript 很慢的原因是, 它执行的每个操作都要经过这个系统.
C#可以通过更优化的字节码, 以更快的速度处理一些内容. 但是, 如果C#脚本调用引擎类的内容, 或者脚本试图访问它的外部内容, 它会通过这个管道.
NativeScript C++甚至更进一步, 默认将所有内容都保持在内部. 对外部结构的调用将通过脚本API进行. 在NativeScript C++中, 注册方法以将其公开给脚本API是一项手动任务. 至此, 外部非C++类将使用API来查找它们.
因此, 假设从引用扩展到创建数据结构, 比如一个 Array 或 Dictionary, 为什么选择一个 Object 而不是其他两个选项?
控件 : 对象能够创建更复杂的结构. 可以在数据上分层抽象, 以确保外部API不会响应内部数据结构的更改. 更重要的是, 对象可以有信号, 允许响应式行为. 对象带来了创建更复杂结构的能力.
清晰 : 当涉及到脚本和引擎类为对象定义的数据时, 对象是一个可靠的数据源. 属性可能不包含期望的值, 但是无需担心这个属性是否首先存在.
便利 : 如果已经有了类似的数据结构, 之后从现有类扩展, 可以使构建数据结构的任务变得容易得多. 相比之下, 数组和字典不能满足所有的用例.
对象还让用户有机会创建更专门化的数据结构。有了它,一个人可以设计自己的列表、二叉搜索树、堆、散列树、图、不相交集,以及其他选择。
“为什么不在树结构中使用节点?” 有人可能会问. 节点类包含与自定义数据结构无关的内容. 因此在构建树结构时, 构造自己的节点类型是很有帮助的.
1 | extends Object |
这里开始, 然后就可以创建具有特定功能的结构, 只会受到他们想象力的限制.
枚举:整数 VS 字符串
大多数语言都提供了枚举类型,GDScript 也不例外。
但与其他大多数语言不同的是,GDScript 的枚举允许开发者使用整数或字符串作为枚举值(后者只有在 GDScript 中使用 @export_enum 注解时才可以使用)。
那么问题来了:“该用哪一种枚举?” “觉得哪个更舒服就选哪个。”
这是 GDScript 特有的特性,并非(C++、C#等)一般的 Godot 脚本所特有的特性;该语言将可用性置于性能之上。
在技术层面上,整数比较(常量时间)比字符串比较(线性时间)更快,若想保持其他语言中使用枚举的习惯,则应使用整数来表示枚举值。
当你想要 打印 枚举值时,使用整数的主要问题就出现了:
尝试直接打印以 int 型保存的枚举 MY_ENUM 会打印 5 之类的东西,而不是像 MyEnum 这样的字符。
若要打印以 int 型保存的枚举。必须编写一个字典来映射每个枚举所对应的字符串值。
如果开发者使用枚举的主要目的是打印值,并希望将它们作为相关概念组合在一起,那么将枚举作为字符串使用是有意义的。
这样一来,也就不需要在打印上执行单独的数据结构了。
AnimatedTexture vs. AnimatedSprite2D vs. AnimationPlayer vs. AnimationTree
在什么情况下应该使用Godot的各种动画类?对于Godot的新用户来说, 可能不是马上清楚答案.
AnimatedTexture 是引擎绘制一个动画循环, 而不是一个静态图像的纹理. 用户可以进行如下操作:
- 它在纹理的每个部分移动的速率(FPS)。
- 纹理中包含的区域数(帧).
Godot 的 RenderingServer 会按照规定的速度依次绘制区块。好处是不涉及引擎部分额外的逻辑。坏处是用户几乎没有控制权。
另外请注意,AnimatedTexture 是一种 Resource,与此处讨论的其他 Node 对象不同。可以创建 Sprite2D 节点,使用 AnimatedTexture 作为其纹理。或者(仅在其他方法无法满足要求时)可以将 AnimatedTexture 作为图块添加到 TileSet 中并将其与 TileMapLayer 集成到一起,从而获得自动动画化的背景。使用此方法时所有的渲染将在单个批处理内绘制调用。
AnimatedSprite2D 节点可以与 SpriteFrames 资源结合使用,使用户可以通过精灵表创建各种动画序列、在动画之间切换并控制它们的速度、区域偏移量和方向。
这使得它们非常适合控制基于二维的帧动画。
若需要触发与动画更改相关的其他效果,例如创建粒子效果、调用函数或操作与帧动画无关的其他外围元素,则需要将一个 AnimationPlayer 节点与 AnimatedSprite2D 关联。
如果你想设计更复杂的二维动画系统,AnimationPlayer 也是你的必备工具,例如:
- 剪纸动画:在运行时编辑精灵的变换。
- 二维网格动画:为精灵的纹理划分一个区域,并将骨架绑定在上面。然后动画化其中的骨骼,使骨骼按照彼此之间的关系,成比例地拉伸和弯曲纹理。
虽然我们需要一个 AnimationPlayer, 来为游戏设计每个独立的动画序列, 它也可以用来混合复合动画, 也就是说, 在这些动画之间实现平滑的转换. 在为对象规划的动画之间, 也可能存在一个层次结构. 在这些情况下使用 AnimationTree 效果很出色. 可以在 这里 找到关于使用 AnimationTree 的深入指南.
逻辑偏好
先添加节点还是先修改属性?
运行时使用脚本初始化节点时,你可能需要对节点的名称、位置等属性进行修改。
常见的纠结点在于,你应该什么时候去修改?
最佳实践是在节点加入场景树之前修改取值。
部分属性的 setter 代码会更新其他对应的值,可能会比较慢!
大多数情况下,这样的代码不会对游戏的性能产生影响,但对于程序式生成之类的重型使用场景,就可能让游戏卡成 PPT。
综上,最佳的做法就是先为节点设置初始值,然后再把它添加到场景树中。
有值在被加入场景树之前不能被设置的例外情况,比如设置世界坐标的时候。
加载 VS 预加载
在 GDScript 中,存在全局 preload 方法。它尽可能早地加载资源,以便提前进行“加载”操作,并避免在执行性能敏感的代码时加载资源。
其对应的 load 方法只有在执行 load 语句时才会加载资源。也就是说,它将立即加载资源。所以,在敏感进程中加载资源会造成速度减慢。
load() 函数是可以被所有脚本语言访问的 ResourceLoader.load(path) 的别名。
那么, 预加载和加载到底在什么时候发生, 又应该什么时候使用这两种方法呢?我们来看一个例子:
1 | # my_buildings.gd |
预加载允许脚本在加载脚本时处理所有加载. 预加载是有用的, 但也有一些时候, 人们并不希望这样.
为了区分这些情况, 我们可以考虑以下几点:
- 如果无法确定何时可以加载脚本, 则预加载资源, 尤其是场景或脚本, 可能会导致进一步加载, 这是人们所不希望的. 这可能会导致无意中, 在原始脚本的加载操作之上的可变长度加载时间. 在原始脚本的加载操作之上, 这可能导致意外的, 可变长度的加载时间.
- 如果其他东西可以代替该值(例如场景导出的初始化), 则预加载该值没有任何意义. 如果打算总是自己创建脚本, 那么这一点并不是重要因素.
- 如果只希望“导入”另一个类资源(脚本或者场景),那么最好的解决方法就是使用预加载常量(Preloaded Constant)。不过也有例外的情况:
- 如果“导入”的类有可能发生变化,那么就应该是属性,使用 @export 或 load() 初始化(或者甚至更晚一些才初始化)。
- 如果脚本需要大量依赖关系,又不想消耗太多内存,则可以在环境变化时动态地加载或卸载各种依赖关系。如果将资源预加载为常量,则卸载这些资源的唯一方法是卸载整个脚本。如果改为加载属性,则可以将它们设置为 null 并完全删除对资源的所有引用(扩展自 RefCounted 的类型会在指向其的所有引用均已消失时自动释放内存)。
大型关卡:静态 VS 动态
如果正在创建一个大型关卡, 哪种情况是最合适的?
他们应该将关卡创建为一个静态空间吗?
还是他们应该分阶段加载关卡, 并根据需要改变世界的内容?
答案很简单,“当性能需要的时候”。与这两种选择有关的困境是一种古老的编程选择:优化内存还是速度?
最简单的方法是使用静态关卡, 它可以一次加载所有内容. 但是, 这取决于项目, 这可能会消耗大量内存. 浪费用户的运行内存会导致程序运行缓慢, 或者计算机在同一时间尝试做的所有其他事情都会崩溃.
无论如何,应该将较大的场景分解为较小的场景(以利于资产重用)。然后,开发人员可以设计一个节点,该节点实时管理资源和节点的创建/加载和删除/卸载。具有大型多样环境或程序生成的元素的游戏,通常会实行这些策略,以避免浪费内存。
另一方面, 对动态系统进行编码更复杂, 即, 使用更多的编程逻辑, 这会导致出现错误和bug的机会. 如果不小心的话, 开发的系统, 会增加应用程序的技术成本.
因此, 最好的选择是…
- 在小型游戏中使用静态关卡.
- 在开发中型/大型游戏时, 可以去创建一个可以对节点和资源的管理进行编码的库或插件.
如果随着时间的流逝而改进, 以提高可用性和稳定性, 那么它可能会演变成跨项目的可靠工具. - 为一款中/大型游戏编写动态逻辑代码, 因为你拥有编程技能, 但却没有时间或资源去完善代码(必须要完成游戏). 以后可能会进行重构, 将代码外包到插件中.
项目组织
组织
有两种种方法来组织项目:
- 根据场景分别放置场景所需要的资源。(推荐)
- 创建一个专门的文件夹,并在其中放置所有资源。
风格指南
为了项目之间的一致性,我们建议遵循以下规范:
- 使用 snake_case 风格为文件夹和文件命名(除了c#脚本).
这避免了在 Windows 上导出项目时可能出现的大小写敏感问题.C# 脚本是这个规则的一个例外, 因为按照惯例是用类名来对它们命名, 而类名应该是 PascalCase 风格. - 使用 PascalCase 风格对节点进行命名, 这与内置的节点大小写风格一致.
- 通常, 将第三方资源放在顶级的 addons/ 文件夹中, 即使它们不是编辑器插件. 这样更加容易跟踪哪些文件是第三方文件.
当然这个规则也有一些例外: 如果你要使用第三方游戏资源创建角色, 将这些资源和角色场景及脚本放在同一文件夹下会更好.
导入
因此,现在可以从项目文件夹中透明地导入资产。
忽略具体文件夹
为防止 Godot 导入特定文件夹中的文件, 请在文件夹中创建一个名为 .gdignore 的空文件(以 . 号开头). 这对于加快初始项目导入非常有用.
要在 Windows 上创建文件名以点开头的文件,请在文件名的前后都写一个点(“.gdignore.”)。确认之后 Windows 会自动移除末尾的点。
你也可以使用文本编辑器来创建它,例如记事本。或在命令提示符中输入以下命令:type nul > .gdignore
一旦文件夹被忽略,其中资源就不能再使用 load() 和 preload() 方法加载。被忽略文件夹会从文件系统栏目中隐藏,从而减少混乱。
请注意 .gdignore 文件的内容会被忽略,因此该文件应当为空。它不像 .gitignore 文件一样支持模式匹配。
大小写敏感
Windows 和最近版本的 macOS 默认使用不区分大小写的文件系统,而 Linux 发行版默认使用区分大小写的文件系统。
由于 Godot 的 PCK 虚拟文件系统区分大小写,因此在导出项目后可能会导致问题。
为了避免这种情况,建议对项目中的所有文件都使用 snake_case 蛇形命名法(一般使用小写)。
版本控制系统
版本控制插件
Godot 旨在对版本控制系统(Version Control System,VCS)友好,并尽量生成可读且可合并的文件。
Godot 支持通过插件在编辑器本身中使用 VCS。可以在编辑器中的项目> 版本控制下设置或关闭 VCS。
截至 2023 年 7 月,尚且只有一个 Git 插件可用,但社区可能会创建其他的 VCS 插件。
官方 Git 插件
可以在GitHub找到最新的版本。
最新的更新、文档和源代码可以在 Godot iOS 插件库找到Godot iOS plugins repository。
从 VCS 中排除的文件
当第一次在编辑器中打开项目时,Godot 会自动创建一些文件和文件夹。 为了避免生成的数据使版本控制仓库膨胀,你应该将它们添加到 VCS 忽略中:
- .godot/:此文件夹存储各种项目缓存数据。
- *.translation:这些文件是从 CSV 文件生成的导入后的的二进制翻译文件。
你可以选择在 Godot 项目管理器创建项目时自动生成版本控制元数据。
当选择 Git 选项时,将在项目根目录中创建 .gitignore 和 .gitattributes 文件:
在现有的项目中,选择编辑器顶部的 项目 菜单,然后选择 版本控制 > 生成版本控制元数据。
这将与在项目管理器中执行的操作一样创建相同的文件。
在 Windows 上使用 Git
大多数 Git for Windows 客户端都将 core.autocrlf 设置为 true。
可能会导致部分文件错误地被 Git 标记为已修改,因为这些文件的行尾被自动从 LF 转换成了 CRLF。
最好将此选项设置为:git config --global core.autocrlf input
使用项目管理器或编辑器创建版本控制元数据时,会使用 .gitattributes 文件自动强制使用 LF 行尾,因此无需更改 Git 配置。
Git LFS
Git LFS(大文件存储)是一个 Git 扩展,允许管理存储库中的大文件。
用 Git 中的文本指针替换大文件,同时将文件内容存储在远程服务器上。
这对于管理大型资产(例如纹理、音频文件和 3D 模型)非常有用,而不会使 Git 存储库膨胀。
备注
使用 Git LFS 时,您需要确保在将任何文件提交到存储库之前已设置它。
如果您已经将文件提交到存储库,则需要将它们从存储库中删除,并在设置 Git LFS 后重新添加它们。
可以使用 git lfs migrate 来转换存储库中的现有文件,但这更深入,需要对 Git 有很好的了解。
一种常见的方法是使用 Git LFS(和适当的 .gitattributes)设置一个新存储库,然后将文件从旧存储库复制到新存储库。
这样,您可以确保所有文件从一开始就被 LFS 跟踪。
当添加或修改 LFS 跟踪的文件时,Git 会自动将它们存储在 LFS 中,而不是常规的 Git 历史记录中。
可以像常规 Git 文件一样推送和拉取 LFS 文件,但请记住,LFS 文件与 Git 历史记录的其余部分分开存储。
这意味着可能需要在将存储库克隆到的任何计算机上安装 Git LFS 才能访问 LFS 文件。
下面是一个示例 .gitattributes 文件,可以将其用作 Git LFS 的起点。
之所以选择这些文件类型,是因为它们常用,但您可以修改列表以包含项目中可能拥有的任何二进制类型。
1 | # Normalize EOL for all files that Git considers text files. |
有关 Git LFS 的更多信息,请查看官方文档: https://git-lfs.github.com/ 和 https://docs.github.com/en/repositories/working-with-files/managing-large-files。
故障排除
- 编辑器运行缓慢,占用所有的 CPU 和 GPU 资源
尤其是在 macOS 上,因为大多数 Mac 都有 Retina 显示屏。
由于 Retina 显示器的像素密度更高,因此所有内容都必须以更高的分辨率渲染。会增加 GPU 上的负载并降低感知性能。
有几种衡量性能和电池续航的方法:
- 在 3D 模式下,单击左上角的透视按钮并启用半分辨率。现在 3D 视口就会以半分辨率渲染,速度最多可以提高到原来的 4 倍。
- 打开编辑器设置并将低处理器模式睡眠(微秒)的值增加到 33000(30 FPS)。该值决定了渲染每帧画面之间所间隔的时间(微秒单位)。 较高的值将会使编辑器操作起来没有那么跟手,但可显著降低 CPU 和 GPU 使用率。
- 如果有某个节点导致编辑器连续重新绘制(例如粒子),请将其隐藏并在脚本中使用 _ready() 方法显示它。这样,它将隐藏在编辑器中,但仍在正在运行的项目中可见。
- 编辑器在可变刷新率显示器(G-Sync/FreeSync)上出现卡顿和闪烁的情况?
可变刷新率显示器需要不断调整其伽玛曲线,以便随着时间的推移发出一致的光量。
当刷新率变化很大时,这可能会导致图像的黑暗区域出现闪烁,这是因为 Godot 编辑器仅在必要时重绘。
这有几种解决办法:
- 在编辑器设置中启用界面 > 编辑器 > 持续更新。即使画面没有变化,编辑器也将不断渲染。请注意,这会增加功耗、加大热量和噪音排放。
为了缓解这种情况,你可以在编辑器设置中将低处理器模式睡眠(微秒)增加到 33000(30 FPS)。
该值决定了渲染每帧画面之间所间隔的时间(微秒单位)。
较高的值将会使编辑器操作起来没有那么跟手,但可显著降低 CPU 和 GPU 使用率。 - 在显示器或图形驱动程序中禁用可变刷新率。
- VRR 闪烁这个问题在某些显示器上,可以通过你的显示器的 OSD 中的 VRR 控制或微调暗区选项来减少。这些选项可能会增加输入延迟或导致黑色失真。
- 如果使用 OLED 显示器,可以在编辑器设置中使用 Black (OLED) 编辑器主题预设。因为 OLED 显示器的出色的黑阶表现,这可以隐藏 VRR 闪烁。
- 编辑器或项目花了很长时间才启动?
使用基于 Vulkan 的渲染器(Forward+ 或 Mobile)时,首次启动将会花费较长的时间。
这是因为着色器需要先编译才能进行缓存。更新 Godot、更新显卡驱动或切换显卡后,着色器也需要重新缓存。
如果这个问题在首次启动后依然存在,那么这是 Windows 上的一个已知错误,当你连接了特定的 USB 外设时就会出现 。
特别是,海盗船的 iCUE 软件似乎引起了该错误。尝试将 USB 外设的驱动程序更新为最新版本。
如果错误仍然存在,则需要在打开编辑器之前断开故障外围设备的连接。然后,你可以再次连接外围设备。
Portmaster 等防火墙软件可能会屏蔽调试端口,导致项目启动时间变长,并且无法在编辑器中使用调试功能(例如查看 print() 的输出)。
变通方法是在“编辑器设置”中修改项目所使用的调试端口(网络 > 调试 > 远程端口)。默认值是 6007;可以尝试设成大于 1024 的值,比如 7007。
在 Windows 上,当首次加载项目时,如果电脑刚开机,Windows Defender 会导致项目启动时文件系统缓存验证耗时显著增加。
对于文件较多的项目尤为明显。考虑通过以下步骤将项目文件夹添加到排除列表中:病毒与威胁防护 > 病毒与威胁防护设置 > 添加或删除排除项。
点击系统控制台后 Godot 编辑器没有响应
在启用了系统控制台的 Windows 上运行 Godot 时,你可以通过在命令窗口中单击来意外启用选择模式。
Windows 的这种特定行为会暂停应用程序,以便你在系统控制台内选择文本。Godot 无法覆盖此系统特定的行为。
要解决此问题,请选择系统控制台窗口,然后按 Enter 退出选择模式。手动移动 Godot 编辑器的 macOS Dock 图标之后出现多余的编辑器图标
如果你打开 Godot 编辑器并手动改变 dock 图标的位置,然后重启编辑器,你会在 dock 的最右边看到一个重复的 dock 图标。
这是由于 macOS dock 的设计限制造成的。解决这个问题的唯一已知方法是将项目管理器和编辑器合并为一个进程,这意味着项目管理器在启动编辑器时不再产生一个单独的进程。虽然使用单一进程实例会带来一些好处,但由于任务的复杂性,完成这个功能没有列入我们近期的工作计划。
为了避免这个问题,保持 Godot 编辑器的 dock 图标在 macOS 创建的默认位置。在项目管理器和编辑器窗口的左上角出现“NO DC”之类的文本
这是由于 NVIDIA 显卡驱动程序注入了覆盖显示信息造成的。
要在 Windows 上禁用此覆盖,请在 NVIDIA 控制面板中将图形驱动程序设置恢复为默认值。
要在 Linux 上禁用此覆盖,请打开 nvidia-settings,转到 X Screen 0 > OpenGL Settings,然后取消选中 Enable Graphics API Visual Indicator。在项目管理器和编辑器窗口右下角出现一个麦克风或刷新的图标
这是由于 NVIDIA 图形驱动程序注入覆盖以显示 ShadowPlay 录制的即时重播信息造成的。此覆盖只能在 Windows 上看到,因为 Linux 不支持 ShadowPlay。
要禁用此覆盖,请按 Alt + Z(NVIDIA 覆盖的默认快捷方式)并在 NVIDIA 覆盖中禁用设置 > HUD 布局 > 状态指示器。
你也可以选择安装取代 GeForce Experience 的新的 NVIDIA 程序 https://www.nvidia.com/en-us/software/nvidia-app/,这样就不会遇到这个问题。与 GeForce Experience 不同的是,NVIDIA 程序会在屏幕的角落而不是每个窗口的角落绘制回放指示器。编辑器或项目显示得过于锐利或模糊

可能是由于你的图形驱动程序强制对所有 Vulkan 或 OpenGL 应用程序进行图像锐化。
你可以在图形驱动程序的控制面板中禁用此行为:
NVIDIA(Windows):打开开始菜单,选择 NVIDIA 控制面板。打开左侧的管理 3D 设置选项卡。在中间的列表中,滚动到图像锐化,并将其设置为关闭锐化。
AMD(Windows):打开开始菜单,选择 AMD 软件。点击右上角的设置 “齿轮 “图标。转到图形选项卡,然后禁用 Radeon 图像锐化。
如果编辑器或者项目看起来过于模糊,这可能是由于 FXAA 被你的显卡驱动强制应用到所有的 Vulkan 或者 OpenGL 应用程序上。
NVIDIA(Windows):打开开始菜单并选择 NVIDIA 控制面板。打开左侧的管理 3D 设置选项卡。在中间的列表中, 滚动到平滑设置 - FXAA 并将其设置为应用程序控制的。
NVIDIA(Linux):打开应用程序菜单,选择 NVIDIA X 服务器设置。在左侧选择 Antialiasing Settings,取消对 Enable FXAA 的勾选。
AMD(Windows):打开开始菜单并选择 AMD Software。点击设置右上角的“齿轮”图标。转到图形选项卡, 滚动到底部并点击高级以展开其设置。禁用形态抗锯齿。
像是 vkBasalt 这种第三方开发的供应的工具可能会强迫所有的 Vulkan 应用程序开启锐化或者 FXAA。你可能也需要检查他们的设置。
当你变更过了显卡驱动和第三方工具中的设置后,重启 Godot 去应用这些设置。
如果你仍然希望在其他应用程序上强制锐化或 FXAA,建议你使用显卡驱动控制面板提供的应用程序配置系统,针对每个应用程序的进行设置。
- 此编辑器或项目看起来颜色很淡
在 Windows 上,这通常是由不正确的操作系统或显示器设置引起的,因为 Godot 目前不支持 HDR 输出(即使它可能在内部以 HDR 渲染)。
由于大多数显示器并非被设计为在 HDR 模式下显示 SDR 内容 https://tftcentral.co.uk/articles/heres-why-you-should-only-enable-hdr-mode-on-your-pc-when-you-are-viewing-hdr-content,建议在未运行使用 HDR 输出的应用程序时在 Windows 设置中禁用 HDR。在 Windows 11 上,可以通过按 Windows + Alt + B 来完成该操作(该快捷方式是 Xbox Game Bar 应用程序的一部分)。要根据当前正在运行的应用程序自动切换 HDR,你可以使用 AutoActions。
如果你坚持启用 HDR,可以通过确保显示器配置为使用 HGIG 色调映射(而不是 DTM),然后使用 Windows HDR 校准应用程序 <https://support.microsoft.com/en-us/windows/calibrate-your-hdr-display-using-the-windows-hdr-calibration-app-f30f4809-3369-43e4-9b02-9eabebd23f19>__ 来稍微改善结果。强烈建议在使用 HDR 时使用 Windows 11 而不是 Windows 10。不过,最终结果可能仍不如在显示器上禁用 HDR。
计划在未来版本中支持 HDR 导出。
- 从挂起状态恢复 PC 后,编辑器/项目冻结或显示出现故障
当在 Linux 上使用 NVIDIA 的专用图形驱动程序时便会出现这个已知的问题。
目前还没有明确的解决方案,因为当涉及 OpenGL 或 Vulkan 时,Linux + NVIDIA 上的挂起通常会出现问题。
与 Forward+ 和 Mobile 渲染方法(使用 Vulkan )相比,兼容性渲染方法(采用 OpenGL )通常不太容易出现挂起相关问题。
NVIDIA驱动程序提供了一个实验性选项可在挂起后保护视频内存用以解决这个问题。
据报道,该选项与较新的 NVIDIA 驱动程序版本配合使用效果更好。
为了避免丢失工作内容,请在使电脑进入睡眠状态之前将场景保存在编辑器中。
- 项目在编辑器中正常运行,但在导出后无法加载部分文件
这通常是由于忘记在导出对话框中指定非资源文件过滤器而导致的。
默认情况下,Godot 只会将实际的资源包含到 PCK 文件中。一些常用的文件,例如 JSON 文件,不会被视为资源。
例如,如果你在导出的项目中加载 test.json,则需要在非资源导出过滤器中指定 *.json。有关更多信息,请参阅 资源选项。
另外,请注意,导出的项目永远不会包含名字以点开头的文件和文件夹。这是为了防止将 .git 等版本控制文件夹包含在导出的 PCK 文件中。
在 Windows 上,也可能是大小写敏感性的问题。如果你在脚本里引用资源时所使用的大小写与文件系统中的不符,在导出项目后就会载入失败。这是因为虚拟 PCK 文件系统是大小写敏感的,而 Windows 的文件系统是大小写不敏感的。
- 项目在从项目管理器打开后频繁崩溃或立即崩溃
这可能是由多个因素引起的,比如编辑器插件、GDExtension 插件或其他原因。
建议以恢复模式打开项目,并尝试找到并修复导致崩溃的原因。
编辑器简介
编辑器的界面
项目管理器
使用项目管理器不需要什么教程,自己实际体验就可以更好掌握
- 项目的使用
- 创建、删除、导入或运行游戏项目
- 下载演示和模板
- 从资产库下载开源项目模板和演示程序
- 用标签管理项目
- 创建、删除、编辑标签
- 恢复模式
- 从项目列表中选择项目,点击编辑按钮旁边的下拉箭头,然后选择“在恢复模式下编辑”。
检查器面板
如果不可见,可以通过导航到 编辑器 > 编辑器设置 > 编辑器停靠 > 检查器
项目设置
可以在 ProjectSettings 类中查看完整的设置列表。
有三种方式来修改项目设置:
- 通过项目设置窗口
- 代码
使用set_setting()来修改设置的值然而,许多项目设置都只会在游戏启动时读取一次。1
2ProjectSettings.set_setting("application/run/max_fps", 60)
ProjectSettings.set_setting("display/window/size/mode", DisplayServer.WINDOW_MODE_WINDOWED)
在此之后,使用 set_setting() 更改设置就不会产生效果了。
不过大多数设置在 Engine、DisplayServer 等运行时类上都有相应的属性或方法:通常,项目设置会在运行时复制到以下类中:1
2Engine.max_fps = 60
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
Engine、PhysicsServer2D、PhysicsServer3D、RenderingServer、Viewport、Window。 - 编辑project.godot
在内部,Godot 将项目的设置存储在一个 project.godot 文件中(INI 格式的纯文本文件)
一般来说,建议使用项目设置窗口,不建议手动编辑 project.godot。
读取项目设置
使用 get_setting() 或 get_setting_with_override() 来读取项目设置:
1 | var max_fps = ProjectSettings.get_setting("application/run/max_fps") |
由于许多项目设置仅在启动时读取一次,项目设置中的值可能不再准确。
在这种情况下,最好从运行时的等效属性或方法中读取值:
1 | var max_fps = Engine.max_fps |
脚本编辑器
以下为文本编辑器的部分关键特性:
完整集成 GDScript。
支持 GDScript 和 JSON 文件的代码高亮,语法检查。
支持书签,断点,自动缩进,代码折叠。
自定义主题。
多光标,可以通过按下 Alt + 单击左键 来启用。
自动补全变量、函数、常量等。
选中符号后使用 Ctrl + D 进行内联重构。
跨项目文件进行批量查找和替换。
编辑器默认快捷键
https://docs.godotengine.org/zh-cn/4.x/tutorials/editor/default_key_mapping.html
自定义界面
编辑器布局会保存在一个在配置的路径 编辑器数据路径 中一个叫 editor_layouts.cfg 的文件中。
XR 编辑器
专为 XR 设备原生运行而设计的 Godot 编辑器版本
目前 Godot XR 编辑器仅适用于运行 Meta Horizon OS v69 或更高版本的以下 Meta Quest 设备:
Meta Quest 3
Meta Quest 3s
Meta Quest Pro
未研究
Android 编辑器
https://godotengine.org/download/android/
未研究
Web 编辑器
https://editor.godotengine.org/releases/latest/
未研究
高级功能
命令行教程
未研究
使用外部的文本编辑器
未研究
管理编辑器功能
限制 Godot 编辑器的功能用“功能配置文件”设置。
编辑器 > 管理编辑器功能。默认没有配置。
请点击创建配置文档并为其命名。然后你就会看到 Godot 编辑器中所有功能的列表了。
要在编辑器之间分享配置,请点击导出按钮。请将自定义配置保存为 .profile 文件。
要在其他编辑器中使用,请打开其管理编辑器配置窗口并点击导入,然后选择该 .profile 文件。
如果大量电脑都需要自定义配置,这个过程可能会比较繁琐。
另一种办法是启用 Godot 的自包含模式,可以将所有编辑器配置放在与编辑器二进制文件同一文件夹中。
2D
2D 简介
CanvasItem 是 2D 的基础节点。Node2D 是 2D游戏对象的基础节点。Control 是 所有 GUI 的基础节点。
可以在 2D 屏幕上显示 3D 场景。
画布层
画布项CanvasItem是所有 2D 节点的基类。
可以把画布项组织成树。
每个项目都会继承父节点的变换:父节点移动,子项也会移动。
CanvasItem节点及其派生节点通过视口节点Viewport来显示,是这个视口的直接或间接子节点。Viewport的Viewport.canvas_transform属性能够对它所包含的CanvasItem层级结构施加一个自定义的Transform2D变换。Camera2D等节点的工作原理就是修改这个变换。
像滚动这样的效果最好是通过操纵画布的变换transform属性来实现。
通常情况下,并不希望游戏或应用程序中的所有东西都受到画布变换的约束。比如:
- 视差背景:比场景其他部分移动得慢的背景。
- UI:用户界面(UI)或平视显示系统(HUD)叠加在游戏世界的视图上。
希望生命计数器、分数显示和其他元素能够保持其屏幕位置,即使游戏世界的视角发生变化。 - 转场:用于转场的效果(淡入淡出、混合)也保持在固定的位置。
CanvasLayer
画布层CanvasLayer节点,可以为所有后代添加一个单独的 2D 渲染层。Viewport的子节点默认在图层“0”处绘制,而 CanvasLayer在任何数字层处绘制。
数字较大的图层将绘制在数字较小的图层之上。(栈)CanvasLayer也有自己的变换,不受其他层的影响。
这使得当我们对游戏世界的观察发生变化时,UI 可以固定在屏幕空间中。
创建视差背景(Parallax Background)。
通过层为“-1”的 CanvasLayer 完成。
带有分数、生命计数器和暂停按钮的屏幕也可以创建在编号为“1”的层中。
CanvasLayer独立于树顺序,仅依赖于层数,因此可以只在需要时实例化。
备注
控制节点的绘制顺序并不一定要用`CanvasLayer`。
确保节点被正确绘制在“前面”或“后面”的标准方法是调整场景面板中节点的顺序。
在视口中,场景面板中较上面的节点会被画在较下面的节点的后面。
2D 节点的`CanvasItem.z_index`属性也能够控制绘图顺序。
视口变换与画布变换
如何为提供给 Input 的输入事件在正确的坐标系中确定位置
画布变换
每个CanvasItem节点将驻留在 Canvas Layer 中。
每个 Canvas Layer 都有一个变换(平移、旋转、缩放等),可以作为 Transform2D 进行访问。
节点默认是在 0 层上绘制,即内置的画布。
如果要把节点放在不同的层中,可以使用CanvasLayer节点。全局变换
Viewport还具有全局画布变换(Transform2D)主要用于 Godot 的CanvasItem编辑器.拉伸变换
Viewport拉伸变换,处理窗口大小变化、分辨率适配时的缩放(比如全屏放大)。
为了方便将InputEvent的坐标转换到CanvasItem局部坐标,添加了CanvasItem.make_input_local()函数。窗口变换
根视口是一个Window。为了能够像多分辨率 一样将窗口的内容进行缩放和移动,每个Window都包含了窗口变换。
例如在Viewport使用固定长宽比显示时,负责窗口边缘的黑框。变换顺序
要将 CanvasItem 本地坐标转换为实际屏幕坐标,必须应用以下变换链:
变换函数
上图显示了一些可用的变换函数。所有变换都是从右向左的.
将一个变换与一个坐标相乘会得到一个更靠左的坐标系,
将一个变换的 affine_inverse() 相乘会得到一个更靠右的坐标系:
1 | # 继承自 CanvasItem |
要将CanvasItem的本地坐标转换为屏幕坐标,只需按以下顺序相乘:
1 | # 屏幕坐标 = 屏幕变换 × 全局变换 × 局部坐标 |
但请记住,通常情况最好不要使用屏幕坐标.
推荐使用画布坐标CanvasItem.get_global_transform(),以保证自动分辨率调整能正常工作.
- 提供自定义输入事件
通常需要将自定义输入事件提供给场景树:
1 | var local_pos = Vector2(10, 20) # 定义一个局部坐标 local_pos,表示相对于某个 Control 或 Node2D 节点的本地坐标系中的点 (10, 20)。 |
渲染
2D 灯光和阴影
Godot 提供了可以使用实时 2D 照明和阴影的功能
完整的 2D 光照设置涉及多个节点:
- CanvasModulate(用于使场景变暗)
- 用于指定一种颜色作为“环境”基色,使场景变暗。这是任何 2D 灯光都无法到达区域的最终照明颜色。
如果没有 CanvasModulate 节点,由于 2D 灯光只会照亮现有的无阴影外观,最终场景看起来会过于明亮。
- 用于指定一种颜色作为“环境”基色,使场景变暗。这是任何 2D 灯光都无法到达区域的最终照明颜色。
- PointLight2D(用于全向或点光源)
- 用于点亮场景。灯光的常见工作方式是在场景的其余部分添加选定的纹理来模拟光照。
- 阴影只会出现在 PointLight2D 覆盖的区域上,方向基于 Light 的中心。
- DirectionalLight2D(用于日光或月光)
- LightOccluder2D(用于投射灯光阴影)
- 用于告诉着色器场景的哪些部分会投射阴影。这些遮挡物可以作为独立节点放置,也可以作为 TileMapLayer 节点的一部分。
- 其他可接收光照的 2D 节点,例如 Sprite2D 和 TileMapLayer。
- Sprite2D 用于显示灯泡、背景和阴影投射器的纹理。

- Sprite2D 用于显示灯泡、背景和阴影投射器的纹理。
备注
背景色不接受任何光照。如果要在背景上投射灯光,则需要为背景添加可视化表示,例如 Sprite2D。
Sprite2D 的 Region 属性有助于快速创建重复的背景纹理,但要记得在 Sprite2D 属性中将 Texture > Repeat 设置为 Enabled。
点光源(也称位置光源)
可用于表示火把、火、射弹等发出的光。PointLight2D 提供了以下属性,可在检查器中进行调整:
- 纹理: 光源的纹理。纹理大小决定光源大小。
纹理可以有一个 alpha 通道,在使用Light2D的 Mix 混合模式时有用。 - 偏移量: 灯光纹理的偏移量。与移动灯光节点不同,改变偏移量不会导致阴影移动。
- 纹理缩放: 灯光大小的乘法器。数值越大,灯光越亮。
- 高度: 灯光在法线贴图中的虚拟高度。默认灯光与接收灯光的表面非常接近。
如果使用法线贴图,这将使灯光几乎不可见,因此可以考虑增加此值。
只有在使用法线贴图的表面上,调整灯光的高度才会产生明显的不同。
如果没有预制纹理可用于灯光,可以使用这种 “中性 ”点光源纹理:
如果需要不同的渐变,在纹理属性上分配 新建 GradientTexture2D 来程序化地创建纹理。
展开其 Fill 部分并将填充模式设置为 Radial 。
然后调整渐变本身,使其从不透明的白色开始到透明的白色,并将其起始位置移动到中心。
平行光
用于表现阳光或月光。DirectionalLight2D 提供以下的属性:
- 高度: 灯光在法线贴图中的虚拟高度( 0.0 = 平行于曲面,1.0 = 垂直于曲面)。
默认情况下,灯光与接收灯光的表面完全平行。
如果使用法线贴图,这将使灯光几乎不可见,因此可以考虑增大此值。
调整灯光的高度只会对使用法线贴图的表面产生视觉差异。 高度不会影响阴影的外观。 - 最大距离: 物体距离摄像机中心的最大距离(单位:像素)。
减小该值可以防止对位于摄像机外的物体投射阴影(同时还能提高性能)。
最大距离 不考虑 Camera2D 的缩放,这意味着在较高的缩放值下,当缩放至给定点时,阴影会更快消失。
备注
无论 Height 属性的值是多少,定向阴影看起来总是无限长。这是 Godot 中用于 2D 灯光的阴影渲染方法的限制。
不想获得无限长的定向阴影,应禁用 DirectionalLight2D 中的阴影,并使用自定义着色器来读取 2D 带符号距离场。
该距离场从场景中的 LightOccluder2D 节点自动生成。
常用灯光属性PointLight2D 和 DirectionalLight2D 都提供共同的属性,这些属性是 Light2D 基类的一部分:
- 启用: 允许切换灯光的可见性。与隐藏灯光节点不同,禁用此属性不会隐藏灯光的子节点。
- 仅编辑器:如果启用,灯光仅在编辑器中可见。在运行的项目中将自动禁用。
- 颜色: 灯光的颜色。
- 能量: 灯光强度乘数。数值越大,光线越亮。
- 混合模式: 用于光线计算的混合公式:
添加(Add)适合大多数使用情况。
减(Subtract)可用于负光,负光在物理上并不精确,但可用于特殊效果。
混合(Mix)模式通过线性插值将灯光纹理对应的像素值与灯光下方的像素值混合。 - 范围 -> Z 下限(Z Min): 受光线影响的最小 Z 值。
- 范围 -> Z 上限(Z Max): 受光线影响的最大 Z 值。
- 范围 -> 层下限(Layer Min): 受光线影响的最小层数值。
- 范围 -> 层上限(Layer Max):受光线影响的最大层数值。
- 范围 -> 对象遮罩(Item Cull Mask): 根据其他节点的可视层选项Occluder Light Mask(遮挡掩膜),控制那些节点接收到来自这个节点的光线。
通过这种方式可以让某些物体不被光线照射。
设置阴影
启用 PointLight2D 或者 DirectionalLight2D 节点的 Shadow > Enabled 属性之后,你将看不到任何变化。
这是因为在你的场景中还没有任何节点拥有投射阴影需要使用的遮挡器。
场景中显示阴影添加 LightOccluder2D 节点。具有与精灵轮廓相匹配的遮光多边形。
除了多边形资源(必须设置多边形资源才能产生视觉效果)之外,LightOccluder2D 节点还有两个属性:
- SDF碰撞:如果启用,则遮挡器将成为可在自定义着色器中使用的实时生成的 签名距离字段 (signed distance field)的一部分。
当不使用从此 SDF 中读取的自定义着色器时,启用这个功能不会带来视觉上的差异,并且没有性能成本,因此默认情况下为方便起见它是启用的。 - 遮挡器光照蒙版: 这与
PointLight2D和DirectionalLight2D的 Shadow > Item Cull Mask 属性一起使用,以控制哪些对象为每个光源投射阴影。
这可用于防止特定对象投射阴影。
有两种方法可以来创建光线遮挡器:
自动生成光遮蔽器
遮挡器可以自动地Sprite2D节点上创建,需要选中节点,单击顶部工具栏的Sprite2D菜单,然后选择创建LightOccluder2D同级节点。
可以调整扩展(像素)和收缩(像素),然后点击更新预览。手动绘制光遮蔽器
创建一个LightOccluder2D节点,然后选择该节点单击顶部工具栏的新图标。
可以单击创建新点来开始绘制遮挡多边形。
可以右键单击点来删除,拖动线创建新点。
启用阴影的 2D 灯光能够调整以下属性:
- Color:阴影区域的颜色。
默认情况下,阴影区域是全黑的,但这可以出于艺术目的而改变。颜色的 alpha 通道控制的是阴影被指定颜色着色的程度。 - Filter:阴影所使用的过滤模式。
默认值为 None,渲染速度最快,并且非常适合像素艺术风格的游戏(因为它具有“方块”视觉效果)。如果你想要柔和的阴影,请使用 PCF5。PCF13 则更柔和,但渲染需求更高。由于渲染成本较高,PCF13 只应用于少量光源同时存在的情况下。 - Filter Smooth:过滤平滑。
控制的是当 Filter 为 PCF5 或 PCF13 时,应用于阴影的柔化程度。
较高的值会导致阴影更加柔和,但可能会出现带状伪影(特别是使用 PCF5 时)。 - Item Cull Mask:项目剔除遮罩。
控制的是哪些 LightOccluder2D 节点能够投射阴影,取决于对应的 Occluder Light Mask(遮挡器灯光遮罩)属性。
法线和镜面贴图
大大提升 2D 光照的立体感。
与 3D 渲染类似,法线贴图可以根据接收光线的表面方向来改变光线的强度,从而使照明效果不再平面化(按像素进行调整)。
镜面贴图通过让一部分光线反射回观察者来进一步改善视觉效果。PointLight2D 和 DirectionalLight2D 都支持法线贴图和镜面贴图。
法线贴图和镜面贴图可分配给任何 2D 元素,包括继承自 Node2D 或 Control 的节点。
法线贴图表示每个像素“指向”的方向。
引擎会利用这些信息,以物理上合理的方式将光照正确应用到 2D 表面。
法线贴图通常由手绘的高度贴图创建,但也可以由其他纹理自动生成。
镜面贴图定义了每个像素对光线的反射程度(如果镜面贴图包含颜色,则定义反射的颜色)。
亮度值越高,纹理上指定位置的反射就越亮。
镜面贴图通常以漫反射纹理为基础,通过手动编辑创建。
如果在你的精灵中没有使用法线贴图或者镜面贴图,使用免费开源工具 Laigter 来生成。
要在 2D 节点上设置法线贴图和/或镜面贴图,请为绘制节点纹理的属性创建一个新的 CanvasTexture 资源。
展开新创建的资源。你可以找到需要调整的几个属性:
- Diffuse > Texture:(漫反射 > 纹理)基础的颜色贴图。
在这个属性中,加载你将使用在精灵本身的纹理。 - Normal Map > Texture:(法线贴图 > 纹理)法线贴图的纹理。
在这个属性中,你可以加载从高度图生成的法线贴图纹理(见上面的提示)。 - Specular > Texture:(镜面反射 > 纹理)镜面贴图纹理,可以控制漫反射纹理上每个像素的镜面反射强度。
镜面贴图通常使用灰度反射,但是它也可以包含色彩来增强反射的颜色。在这个属性中,加载一个已创建的镜面贴图纹理(见上面的提示)。 - Specular > Color:(镜面反射 > 颜色)镜面反射的颜色乘数。
- Specular > Shininess:(镜面反射 > 光泽度)用于镜面反射的高光指数。
值越低,反射的明亮度和扩散性会增加,而值越高,反射会更加局部化。较高的值适用于湿润表面。 - Texture > Filter:(纹理 > 过滤器)可以设置为覆盖纹理过滤模式
无论节点属性设置如何(或渲染 > 纹理 > 画布纹理 > 默认纹理过滤项目设置)。 - Texture > Repeat:(纹理 > 重复)可以设置为覆盖纹理过滤模式
无论节点的属性如何设置(或者渲染 > 纹理 > 画布纹理 > 默认纹理重复项目设置)。
启用法线贴图后,你可能会注意到灯光会显得较弱:
- 增加
PointLight2D和DirectionalLight2D节点上的 Height 属性。 - 增加灯光的 Energy 属性,以接近启用法线贴图之前的照明强度。
使用添加式精灵作为 2D 灯光的快速替代品
如果在使用 2D 灯光时遇到性能问题,不妨将其中一些节点替换为使用叠加混合的 Sprite2D 节点。
这尤其适用于短暂的动态效果,如子弹或爆炸。
添加式精灵的渲染速度要快得多,因为它们不需要通过单独的渲染管道。
此外,这种方法还可以与 AnimatedSprite2D(或 Sprite2D + AnimationPlayer)一起使用,这样就可以创建动画二维 “灯光”。
不过,与 2D 灯光相比,添加式精灵有一些缺点:
- 与 “实际 ”二维光照相比,混合公式并不准确。
这在光线充足的区域通常不是问题,但这会妨碍添加精灵去正确照亮那些完全黑暗的区域。 - 添加式精灵不能投射阴影,因为它们不是灯光。
- 添加式精灵会忽略其他精灵上使用的法线贴图和镜面贴图。
要显示一个具有加法混合的精灵,请创建一个 Sprite2D 节点并为其分配一个纹理。
在检查器中,向下滚动到 CanvasItem > Material 部分,展开它并点击 Material 属性旁边的下拉菜单。
选择 New CanvasItemMaterial,点击新创建的材质进行编辑,然后将 Blend Mode 设置为 Add。
2D 网格
在2D中网格不常见,因为图片使用得更频繁。
Godot 的2D部分是一个纯2D引擎,因此它不能直接显示3D网格(尽管可以通过 Viewport 和 ViewportTexture 来实现)。
如果你对在二维视口上显示三维网格感兴趣,请参见 使用
SubViewport作为纹理教程。
2D 网格可以尝试使用代码中的 SurfaceTool 创建它们,并在 MeshInstance2D 节点中显示它们。
目前,在编辑器中生成 2D 网格的唯一方法是导入 OBJ 文件作为网格,或者从 Sprite2D 转换而来。
优化绘制的像素
转换成网格能确保不绘制透明部分从而节省性能,尤其是在移动设备上。
将 Sprite2D 转换为 2D 网格来利用这种优化。
从边缘有大量透明的图片开始,放到一个 Sprite2D 中,从菜单选择“转换为 2D 网格”
默认值对于许多情况来说已经足够好了,但你可以根据需要更改扩展和简化。
2D 精灵动画
使用 AnimatedSprite2D 类和 AnimationPlayer 创建 2D 动画角色
- 首先,我们将使用
AnimatedSprite2D对单个图像集合进行动画处理。 - 然后我们将使用此类对精灵表进行动画处理。
- 最后,我们将学习另一种使用
AnimationPlayer和Sprite2D的Animation属性来制作精灵表动画的方法。
控制动画AnimateSprite2D 与精灵表AnimationPlayer 与精灵表
根节点也可以是 Area2D 或 RigidBody2D。动画仍然会以同样的方式制作。
一旦动画完成,你就可以为 CollisionShape2D 形状分配一个形状。
可在 Godot 中用于 2D 动画的两个类。AnimationPlayer 比 AnimatedSprite2D 稍微复杂一些,但它提供了额外的功能,因为你还可以为其他属性(如位置或比例)设置动画。
类 AnimationPlayer 也可以与 AnimatedSprite2D 一起使用。
备注
如果同时更新一个动画和一个其他的属性(比如说,平台跳跃游戏可能会更新精灵的 h_flip/v_flip 属性然后同时开始一个转身动画“turning”),要记住 play() 不是即时生效的。它会在下次 AnimationPlayer 被处理时生效。也就是说可能要到下一帧才行,导致现在这一帧变成“问题”帧——应用了属性的变化,但动画还没有开始。如果这会造成麻烦的话,在调用 play() 后,你可以调用 advance(0) :ref:`AnimationPlayer
2D 粒子系统
粒子系统用于模拟复杂的物理效果,例如火花、火焰、魔法粒子、烟雾、薄雾等。
这个想法是以固定的间隔发射具有固定的寿命的 “粒子”。
在其生命周期中,每个粒子都具有相同的基本行为。
设置基本物理参数,添加随机性。
Godot 为 2D 粒子提供了两个不同的节点:
GPUParticles2D更先进,使用 GPU 来处子理粒效果。CPUParticles2D是 CPU 驱动的选项,其功能与GPUParticles2D几乎相同,但在使用大量粒子时性能较低。在低端系统或 GPU 瓶颈情况下可能表现更好。
虽然 GPUParticles2D 是通过 ParticleProcessMaterial(还可以使用自定义着色器)进行配置的
不过匹配的选项是通过 CPUParticles2D 中的节点属性提供的(除了轨迹设置)。
GPUParticles2D 节点可转换为 CPUParticles2D 节点。
也可以将 GPUParticles2D 节点转换为 CPUParticles2D 节点,但如果你使用了仅 GPU 支持的功能,可能会出现一些问题。
今后没有计划向 CPUParticles2D 添加新功能,但将接受添加 GPUParticles2D 中已有功能的拉取请求。
因此,我们建议使用 GPUParticles2D,除非你有明确的理由不这样做。
使用步骤:
GPUParticles2D节点添加到场景中。- 添加材质到粒子节点,转到检查器面板中的 Process Material。
- 新建
ParticleProcessMaterial。 GPUParticles2D节点现在可以向下发射白点了。
粒子系统可以使用单个纹理或动画翻页(filpbook)。
翻页是一种纹理,其中包含可以回放或在发射期间随机选择的多个动画帧。翻页相当于粒子的精灵表。
粒子翻页适合再现复杂的效果,如烟雾、火焰、爆炸。
可以通过使每个粒子使用不同的纹理(纹理通过 Texture 属性设置),来引入随机纹理变化。
可以在线寻找现成的粒子翻页图,或使用外部工具预渲染它们,例如 Blender 。
相比起单个纹理,使用动画翻页需要额外的配置,必须在 GPUParticles2D(或 CPUParticles2D)节点的 Material 部分中创建一个新的 CanvasItemMaterial,启用 Particle Animation ,并将 H Frames 和 V Frames 分别设置为翻页纹理中的列数和行数:
完成此操作后,ParticleProcessMaterial(对于 GPUParticles2D)或 CPUParticles2D 检查器中的 动画部分将生效。
如果你的翻页纹理是黑色背景而不是透明背景,你还需要将混合模式设置为 Add 而不是 Mix 才能使它正确地显示。
或者,你也可以修改纹理以使它在图像编辑器中有透明背景。在 GIMP 中,可以使用 Color > Color to Alpha 菜单来完成此操作。
时间参数
- 生命期(Lifetime)
每个粒子存活的时间,单位为秒。当生命期结束时,将创建一个新粒子来替换它。 - 单次(One Shot)
启用后,GPUParticles2D 节点将一次发射其所有粒子,之后将不再发射。 - 预处理(Preprocess)
粒子系统从没有粒子被发射开始,然后开始发射。 当加载场景如火炬,雾等系统时可能会带来不便,因为它会在进入场景的那一刻开始发射。 预处理用于让系统在第一次实际绘制之前处理给定的秒数。 - 速度缩放(Speed Scale)
速度比例具有默认值 1 ,用于调整粒子系统的速度。 降低值会使粒子变慢,而增加值会使粒子更快。 - 爆炸性(Explosiveness)
如果有10个寿命为 1 的粒子,则意味着粒子将每0.1秒发射一次. 爆炸性参数改变了这一点,并迫使粒子一起发射. 范围是:
0: 定期发射粒子(默认值)。
1: 同时发射所有粒子。
中间的值也是允许的。 - 随机性(Randomness)
所有物理参数都可以随机化。 随机值范围从 0 到 1。 随机化参数的公式为:
initial_value = param_value + param_value * randomness - 固定 FPS(Fixed FPS)
此设置可用于将粒子系统设置为以固定的帧率渲染。 例如,将值更改为 2 将使粒子以每秒2帧的速度渲染。 请注意,这不会减慢粒子系统本身的速度。Godot 4.3 目前不支持 2D 粒子的物理插值。作为临时解决方案,你可以在检查器底部将粒子节点的 Node > Physics Interpolation > Mode 设置为禁用,以关闭物理插值功能。
- Fract Delta
将Fract Delta设置为 true 会启用分数增量计算,这会使粒子显示效果更加平滑。
这种平滑性的提升源于更高的计算精度。在具有高度随机性或快速移动粒子的系统中,这种差异会更加明显。
有助于保持粒子系统的视觉一致性,确保每个粒子的运动与其实际生命周期保持一致。
如果不启用此选项,当粒子在帧内的某个时间点发射时,可能会出现粒子跳跃或移动超出应有范围的现象。
更高的精度会带来性能上的折衷,特别是在粒子数量较多的系统中。
绘图参数
- 可见矩形范围(Visibility Rect)
可见性矩形控制粒子在屏幕上的可见性。如果位于视口之外,则引擎将不会在屏幕上渲染粒子。
可以使用工具栏让 Godot 自动生成可见性矩形。
为此,请选择 GPUParticles2D 节点并单击 粒子 > 生成可见性矩形。
Godot 将模拟Particles2D节点发射粒子几秒钟,并设置矩形以适合粒子所占据的表面。
可以使用 Generation Time (sec) 选项控制发射持续时间。 最大值为25秒。
如果需要更多时间让粒子移动,你可以暂时更改Particles2D节点上的preprocess时间。 - 局部坐标(Local Coords)
默认情况启用,移动节点,则所有粒子会跟随移动:
禁用,移动节点,则已发射的粒子不会受到影响: - 绘制顺序(Draw Order)
这可以控制绘制单个粒子的顺序。Index 表示粒子根据它们的发射顺序被绘制(默认)。Lifetime表示它们按照剩余寿命的顺序被绘制。
ParticleProcessMaterial 2D 用法
处理材质属性
在这个材质中的属性控制粒子在其生命周期中的行为和变化。
它们中的许多拥 Min 、 Max 和 Curve 值,允许对其行为进行微调。
值之间的关系是:当一个粒子被生成时,属性会被设置为 Min 和 Max 之间的一个随机值。
如果 Min 和 Max 是相同的,那么每个粒子的值将始终是相同的。
如果也设置了 Curve ,属性的值将会乘以粒子生命周期当前点上曲线的值。
使用曲线来改变粒子生命周期中的属性。这种方式可以表达非常复杂的行为。
下面为ParticleProcessMaterial 2D的属性:
- 随机性的生命周期
Lifetime Randomness属性控制应用于每个粒子生命周期的随机性程度。
值为 0 表示完全没有随机性,由Lifetime属性设置的所有粒子的存活时间相同。
值为 1 表示粒子的生命周期在 [0.0, Lifetime] 范围内完全随机。 - 粒子标志 Particle Flags
控制粒子的变幻特效。 - 生成 Spawn
- 位置 (Position)
- 发射色彩
Capture from Pixel会使粒子在其产生点处继承遮挡材质的颜色。
在单击“确定”后,将生成遮罩并将其设置为 ParticleProcessMaterial,位于 Spawn 下,然后是 Position
本节中的所有值均由“加载发射遮罩”菜单自动生成,因此通常应将它们保留。
不应将图像直接添加到点纹理(Point Texture)或颜色纹理(Color Texture)中。应始终使用“加载发射遮罩”菜单。
- 发射色彩
- 角度(Angle)
确定粒子的初始角度(以度为单位)。 该参数通常在随机化后会有用。 - 速度(Velocity)
- 继承速度比例 (Inherited Velocity Ratio)
- 速度轴转 (Velocity Pivot)
- 方向 (Direction)
这是粒子发射的基础方向。 默认值是 Vector3(1,0,0) ,它使粒子向右发射。
然而,在默认的重力设置下,粒子会直线下降。
为了让这个属性作用更明显,你需要一个大于 0 的初始速度(initial velocity)。 - 发散(Spread)
此参数是以度为单位的角度,它会被随机加减到基础 Direction 上。
180 的铺开角度将向所有方向发射(+/- 180)。 - 扁平度(Flatness)
这个属性只对3D粒子有用。 - 初速度(Initial Velocity)
初始速度是粒子发射的速度(单位为像素/秒)。
以后可以通过重力或其他加速度来修改速度(后述)。
- 位置 (Position)
- 动画速度 Animation Velocity
- 角速度(Angular Velocity)
角速度是粒子围绕其中心转动的速度(以度/秒为单位)。 - 方向速度(Direction Velocity)
- 环绕速度(Orbit Velocity)
环绕速度速度用于使粒子绕它们的中心转动。 - 径向速度(Radial Velocity)
- 速度限制(Velocity Limit)
- 角速度(Angular Velocity)
- 加速度 Accelerations
- 重力(Gravity)
应用于每个粒子上的重力。 - 线性加速度(Linear Acceleration)
应用于每个粒子的线性加速度。 - 径向加速度(Radial Acceleration)
如果此加速度为正,则粒子会向远离发射中心加速。 如果是负的,他们会被加速吸进去。 - 切向加速度(Tangential Acceleration)
该加速度会使用从粒子到中心点的切向量,结合径向加速度可以做出很酷炫的效果。 - 阻尼(Damping)
阻尼选项会对颗粒施加摩擦力,迫使它们停止。
它特别适用于火花或爆炸,火花或爆炸通常以高线速度开始,然后在他们隐去时停下来。
- 重力(Gravity)
- 显示 Display
- 缩放(Scale)
确定粒子的初始大小。 - 随速度缩放(Scale Over Velocity)
- 颜色曲线 (Color Curve)
用于改变发射出来的粒子颜色。 - 色相变化 (Hue Variation)
Variation 值设置的是应用于每个粒子的初始色调变化。
Variation Rand 值控制色调变化的随机性比率。 - 动画 (Animation)
仅当GPUParticles2D或CPUParticles2D节点上使用的CanvasItemMaterial已进行相应配置时,粒子翻页动画才有效。
如要将粒子翻页设置为线性播放,请将Speed Min和Speed Max值设置为 1:
默认情况下,循环功能是禁用的。如果粒子在其生命周期结束之前播放完毕,则粒子将继续使用翻页的最后一帧(根据翻页纹理的设计方式,这一帧可能是完全透明的)。如果启用循环,粒子的动画将循环回到第一帧并重复播放。
根据精灵表包含的图像数量以及粒子的存活时间情况,你的动画可能看起来会并不流畅。粒子的存活时间、动画速度和精灵表中图像数量之间的关系是这样的:当动画速度为 1.0 时,动画将在粒子生命周期结束时,播放到序列中的最后一个图像。
如果你希望将粒子翻页用作每个粒子的随机粒子纹理源,请将速度值保持为 0 ,并将 Offset Max 设置为 1
请注意,GPUParticles2D节点的 Fixed FPS 也会影响动画播放。为了动画播放流畅,建议将其设置为 0,以便在每个渲染帧上模拟粒子。如果这个设置不适合你的用例,请将 Fixed FPS 设置为等于翻页动画使用的有效帧速率(请参阅上面的公式)。
- 缩放(Scale)
- 扰动 Turbulence
- 碰撞 Collision
- 子发射器 Sub Emitters
- 资源 Resources
发射形状ParticleProcessMaterials 允许你设置发射蒙版,它决定发射粒子的区域和方向。 这些可以从项目中的纹理生成。
设置 ParticleProcessMaterial,并选择了GPUParticles2D 节点。 工具栏中就会出现“粒子”菜单
打开它并选择“加载发射遮罩”,然后选择你想要用作遮挡的纹理,会出现一个具有多个设置的对话框。
- 发射遮罩
纹理可以生成三种类型的发射遮挡:
- 实体像素(Solid Pixels):粒子将从纹理的任意区域产生,透明区域除外。
- 边界像素(Border Pixels):粒子将从纹理的外边缘产生。
- 有向边界像素(Directed Border Pixels):与边界像素类似,但为遮罩添加了额外信息,使粒子能够从边界发射出去。
请注意,需要设置初速度(Initial Velocity)才能使用该功能。
2D 抗锯齿
由于分辨率有限,以 2D 渲染的场景可能会出现锯齿现象。
使用诸如 Line2D、 Polygon2D 或 TextureProgressBar 等节点时最为明显。
2D 中的自定义绘图 对于不支持抗锯齿的方法也可能会出现锯齿现象。
为了解决这个问题,Godot 支持多种在 2D 渲染中启用抗锯齿的方法。
Line2D和自定义绘图中的反锯齿属性(推荐)
Line2D 具有Antialiased(抗锯齿)属性,可以在检查器中启用。
此外,2D 中的自定义绘图中的一些方法支持提供可选的 antialiased 参数,可以在调用函数时设置为 true。
这些方法不需要启用 MSAA,这使得它们的 基准 性能成本很低。
换句话说,如果你在某个阶段没有绘制任何抗锯齿几何图形,则不会产生永久性的额外性能消耗。
这些抗锯齿方法的缺点是需要生成额外的几何图形。
如果要生成每帧都需要更新的复杂 2D 几何图形,这可能会成为程序的性能瓶颈。
此外,Polygon2D、TextureProgressBar 和几种自定义绘图方法都不具备抗锯齿属性。
对于这些节点,可以使用 2D 多采样抗锯齿来代替。
- 多重采样抗锯齿(MSAA)
该功能仅适用于集群 Forward+ 和 Forward 移动后端,不适用于兼容性渲染器。
在 2D 中启用 MSAA 之前,必须先了解 MSAA 的操作对象。
2D 中的 MSAA 遵循与 3D 中类似的限制。
虽然它不会带来任何模糊,但其应用范围是有限的。2D MSAA 的主要应用包括:
- 几何边缘,如直线和多边形绘图。
- 精灵边缘,仅限于与纹理边缘接触的像素。
这适用于线性过滤和最近邻插值过滤。使用图像透明度创建的精灵边缘不受 MSAA 影响。
MSAA 的缺点是它只对边缘起作用。
这是因为 MSAA 增加了覆盖采样的数量,但没有增加颜色采样的数量。
但是,由于颜色采样的数量没有增加,因此片段着色器仍然只为每个像素运行一次。
因此,MSAA 不会影响以下类型的锯齿:
- 最近邻过滤纹理中的锯齿(像素艺术)。
- 自定义 2D 着色器造成的锯齿。
- 使用 Light2D 时的镜面反射锯齿。
- 文字渲染中的锯齿。
可以通过更改项目设置中 渲染 > 抗锯齿 > 质量 > MSAA 2D Rendering > Anti Aliasing > Quality > MSAA 2D 的值来启用 MSAA。
请注意,要更改的是 MSAA 2D 的值,不是 MSAA 3D 的值,这两个是不同的设置项。
2D 中的自定义绘图
可以通过自定义命令在屏幕上绘制任何 2D 节点(例如,基于 Control 或 Node2D )。
2D 节点中的自定义绘制非常有用。下面是一些用例:
- 绘制现有节点类型无法完成的形状或逻辑,例如带有尾迹或特殊动态多边形的图像。
- 绘制大量简单的对象,例如 2D 游戏中的一个栅格或一个面板。自定义绘制避免了使用大量节点的开销,能降低内存占用,并提高性能。
- 制作自定义的 UI 控件,以满足很多可用的控件之外的特别需求。
添加脚本到 CanvasItem 的派生节点(Control 或 Node2D),重载 _draw() 函数。
1 | extends Node2D |
绘制命令在 CanvasItem 的类参考中有所描述,绘制命令的数量很多下面是一些示例。
_draw 函数只调用一次,然后绘制命令被缓存并记住,因此不需要进一步调用。
如果因为状态或其他方面的变化而需要重新绘制,在当前节点中调用 CanvasItem.queue_redraw ,触发新的 _draw() 调用。
这是一个稍微复杂一点的例子,我们有一个可以被随时修改的纹理变量,并且使用一个 setter,它在纹理被修改时强制一次该纹理的重绘:
1 | extends Node2D |
为直观地看到这一功能,你可以通过将 icon.svg 拖放到检查器上的 纹理 属性。
当先前脚本运行时更改 纹理 属性值,纹理也将自动更改。
1 | # 在某些情况下,需要绘制每一帧。 |
坐标和线宽对齐
绘图 API 使用的是 CanvasItem 的坐标系,不一定是像素坐标。
这意味着 _draw() 使用的是应用 CanvasItem 的变换后创建的坐标空间。
此外,你还可以使用 draw_set_transform 或 draw_set_transform_matrix 在其上方应用自定义变换。
使用 draw_line 时应该考虑线的宽度。
mn奇数宽度时,为了使线保持居中,起点和终点的位置应该偏移 0.5 ,如下所示。
1 | func _draw(): |
与使用 filled = false 的 draw_rect 方法相同。
1 | func _draw(): |
抗锯齿绘图
Godot 在 draw_line 方法中提供参数来启用抗锯齿功能,但并非都提供**抗锯齿(antialiased)**参数。
对于不提供抗锯齿参数的方法,可以启用 2D MSAA,这会影响整个视口的渲染。
这个功能(2D MSAA)提供了高质量的抗锯齿,但性能成本更高,而且只适用于特定元素。详情见 2D 抗锯齿。
分别启用 antialiased=false 、 antialiased=true 以及 antialiased=false 搭配 2D MSAA 2x、4x 和 8x 抗锯齿。
工具
编辑时显示绘制
运行编辑器时,也可能需要绘制自己的节点。
这可以用于预览或可视化某些特性或行为。
为此可以使用工具注解 @tool。
1 | @tool |
每当你添加或移除 @tool 注解时,需要保存场景,重新构建项目(仅限 C#),
并且选择菜单选项 场景 > 重载已保存场景 来手动重载当前场景,才能刷新 2D 视图中的节点。
示例 1:绘制自定义形状
用 Godot 引擎的自定义绘制功能使用绘图函数来绘制 Godot 标志。
备注
以下说明使用了一组固定坐标,该坐标对于高分辨率屏幕(大于 1080p)可能太小。
如果是这种情况,并且绘图太小,请考虑在 项目设置 > 显示/窗口/拉伸/缩放Display > Window > Stretch > Scale 中增加窗口缩放比例
以将项目调整到更高的分辨率(2 或 4 倍缩放通常效果良好)。
虽然有专用节点 (Polygon2D),但在本例使用更底层的绘制函数。
1 | extends Node2D |
为了模拟平滑曲线,可以向数组中添加更多点,或者使用数学函数来插值曲线以从代码中创建平滑形状(参见 示例 2)。
多边形为了形成封闭的形状,总是将最后一个定义的点连接到第一个点。
绘制连接线
绘制一系列不封闭成多边形的连接线与之前的方法非常相似。
首先,我们将定义构成嘴巴形状的坐标列表,如下所示:
1 | var coords_mouth = [ |
与 draw_polygon() 不同,折线(polyline)的所有点只能有一个唯一的颜色(第二个参数)。
有 2 个附加参数:线的宽度(默认情况下尽可能小),和启用或禁用抗锯齿(默认情况下禁用)。
绘制圆
绘制圆形使用 draw_circle 方法根据其圆的中心定位它。
第一个参数是以 Vector2 形式写下的中心坐标,第二个参数是其半径,第三个参数是其颜色:
1 | var default_font : Font = ThemeDB.fallback_font; # 默认字体 |
对于部分未填充的圆弧(某些任意角度之间的圆形部分),可以使用方法 draw_arc。
请注意,如果要同时绘制多条未连接的线,可以通过使用 draw_multiline 方法在一次调用中绘制所有线,来获得额外的性能提升。
其他参数以及其他和文本字符相关的方法都可以在 CanvasItem 的类参考中找到。
动画
如果希望自制图形在运行时改变形状,便需要修改执行时调用的方法或者参数,或者应用一个变换。
举个例子,如果想让我们刚刚设计的自制形状旋转,那么可以先 _ready 和_process 方法中添加如下变量和代码:
1 | extends Node2D |
在 _draw() 中调用属性而动画化,必须调用 queue_redraw() 来强制刷新,否则不会在屏幕上更新内容。
例如,可以通过改变嘴巴线条的宽度,使机器人“张嘴”和“闭嘴”,宽度的变化遵循正弦 (sin) 曲线
1 | var _mouth_width : float = 4.4 |
注意_mouth_width 与任何其他属性一样,都是自定义的属性。
任何属性都可以使用更标准的高级方法进行动画处理,例如 Tween 或 AnimationPlayer 节点。
唯一区别是需要调用 queue_redraw() 应用更改,使内容在屏幕上显示。
示例 2:绘制动态线条
在两点之间绘制直线
第一个点固定在屏幕左上角 (0, 0) ,第二个点由屏幕上的光标位置决定。
1 | extends Node2D |
使用 get_mouse_position 方法获取鼠标在默认视口中的位置。
如果位置与上次绘制请求时相比发生了变化(小优化,避免在每一帧都重新绘制),而进行一次重新绘制。_draw() 方法只有一行代码:绘制一条绿色线,宽度为 10 像素,左上角和获取到的坐标之间。
绘制两点之间的弧线
用除直线以外的形状或函数连接这两个点。
尝试在两个点之间创建一个弧线(圆周的一部分)。
通过将线段的起始点、段数、宽度、颜色和抗锯齿属性导出为变量,从而可在编辑器检查器面板中方便修改这些属性:
1 | extends Node2D |
起始角度和结束角度将分别为点 1 到点 2 的向量角度以及点 2 到点 1 的向量角度。
注意,需要将 end_angle 归一化为正值,因为如果 end_angle 小于 start_angle ,则弧线将逆时针绘制,在这里,并不想要该效果 (弧线会上下颠倒)。
2D 视差
Godot 提供 Parallax2D 节点来实现视差(一种视觉效果,通过让图像纹理相对于相机以不同速度移动来模拟纵深)。
滚动缩放
视差效果的核心是 scroll_scale 属性(滚动速度系数)
设为 1 时视差节点与相机同速。
远,请使用小于 1 的值,设为 0 就会完全停止。
近,请使用大于 1 的值,这样图像滚动得就会更快。
使用方式:确保使用的纹理的左上角位于 (0, 0) 交叉点,如下图所示。
上面这个场景由五个图层构成,对应的 scroll_scale 可以设置为:
(0.7, 1) - 森林
(0.5, 1) - 山丘
(0.3, 1) - 低处的云
(0.2, 1) - 高处的云
(0.1, 1) - 天空
无限重复
相机移动时,repeat_size 可以让节点的位置根据这个值向前或向后吸附。
这个效果的原理是让所有子级画布项重复一次并使用这个值进行偏移。
大小问题
为了实现无限重复效果,图像本身就需要能无缝拼接,并且需要图像的大小在设置 repeat_size 之前至少要和视口大小一致。
如果我们把 repeat_size 设置为图像的尺寸,无限重复效果在滚动时就会出问题,因为原始纹理没有覆盖整个视口。
如果我们把 repeat_size 设置为视口的尺寸,那么就会有一个很大的间隙。那该怎么办呢?
- 把视口调小
- 放大 Parallax2D 设置
Parallax2D的 scale 属性 - 缩放子节点 将 Sprite2D 节点放大到能够覆盖住屏幕
请注意,Parallax2D.repeat_size 和 Sprite2D.region_rect 等设置并不会考虑缩放,因此这些值也需要根据缩放进行调整。 - 重复纹理
你也可以通过提前准备子节点来确保从一开始就走上正确的道路。如果你有一个希望重复的 Sprite2D,但它的尺寸太小,就可以按照以下步骤来重复它:
将texture_repeat设置为 CanvasItem.TEXTURE_REPEAT_ENABLED
将region_enabled设置为 true
将region_rect设置为纹理大小的倍数,让纹理足够覆盖住视口。
位置问题
应当尽量避免把所有纹理都设成在 (0,0) 居中,应当从 (0,0) 开始,向右下扩展至 repeat_size 的大小值。
滚动偏移
希望从不同的位置开始,Parallax2D 附带一个 scroll_offset 属性用于偏移无限重复画布的开始位置。
例如,如果你的图像是 288x208,将 scroll_offset 设置为 (-144,0) 或 (144,0) 可使其从图像的中间位置开始。
把 repeat_times 调大有用吗?
从技术上来说,增加 repeat_times 在某些情况下将是可行的,但这是一种暴力的解决方案,而不是被设计用于解决的问题。
更好的解决方法是理解重复效果的工作原理,并在开始时就适当地设置视差纹理。
检查是否有任何纹理溢出到画布的负轴向。
确保在视差节点中使用的纹理都位于从 (0,0) 开始的“无限重复画布”内。
重复次数
当通过将 Camera2D.zoom 设置为 (0.5, 0.5) 进行缩小时,会出现问题:
尽管在默认缩放级别下,视口的所有内容都已正确设置,但缩小后会使它小于该视口,从而破坏无限重复效果。
这时 repeat_times 就可以提供帮助。将值设置为 3(前后各一个额外的重复),现在它就足够大,可以容纳无限重复的效果了。
如果这些纹理需要垂直地重复,我们应该为 repeat_size 指定一个 y 值。repeat_times 也会自动在上方和下方添加重复。
这只是一个水平视差,因此它会在图像上方和下方留下一个空白块。
我们如何解决这个问题?在这个例子中,我们将天空拉高,将草精灵拉低。纹理现在支持正常缩放级别以及缩小到一半大小。
分屏
大多数用 Godot 制作分屏游戏的教程一开始都是要写一个简单的脚本.
把第一个 SubViewport 的 Viewport.world_2d 赋值给第二个 SubViewport,从而实现屏幕的共享。
存在多个相机时,很显然就会出现问题,因为同一个纹理不可能同时出现在两个不同的地方!
解决方法也是有的,把视差节点往第二个(或者第三第四个) SubViewport 里复制一份就好了。
当然,现在两个背景都会在两个 SubViewport 中显示。
我们希望的是每个视差背景只在其对应的视口中显示。你可以通过以下方式实现这一点:
将所有视差节点的 visibility_layer 保留为其默认值 1 。
- 将第一个 SubViewport 的
canvas_cull_mask设置为仅 1 号图层和 2 号图层。
视差节点提供一个共同的父节点,并将其 visibility_layer 设置为 2 - 对第二个 SubViewport 执行相同的操作,但使用图层 1 和 图层 3。
视差节点执行相同的操作,但使用 3 号图层。
这是如何工作的?
如果一个画布项的 visibility_layer 与 SubViewport 的 canvas_cull_mask 不匹配,它将隐藏所有子节点,即使它们匹配也是如此。
我们利用这一点,让 SubViewport 停止那些父节点没有匹配的 visibility_layer 的视差节点的渲染。
在编辑器中预览
4.3 版本之前推荐的是把每个层都放到各自的 ParallaxBackground 下面,然后启用 follow_viewport_enabled 属性,再对各个层进行缩放。这个方法要用对还挺难的,用 CanvasLayer 代替 ParallaxBackground 也能达到想要的效果。
另外推荐 KoBeWi 的“Parallax2D Preview”插件。这个插件提供了很多预览模式,挺好用的!
物理与移动
2D 运动概述
使用 CharacterBody2D
也适用于其他节点类型(如 Area2D、RigidBody2D)。
- 八向移动
1 | extends CharacterBody2D |
get_input() 使用 Input 的 get_vector() 检查四个按键事件。
将长度为 1 的方向矢量乘以所需的速度来设定速度。
- 旋转+移动
类似于经典街机游戏”Asteroids式运动”。
按左/右旋转角色,而按上/下使得角色在面向的方向上向前或向后。
1 | extends CharacterBody2D |
这里我们添加了两个变量来跟踪我们的旋转方向和速度。旋转直接应用于主体的 rotation 属性。
要设置速度,我们使用物体的 transform.x ,这是一个指向物体 “前进” 方向的矢量,然后乘以速度。
- 旋转+移动(鼠标)
这种运动方式是前一种运动方式的变体。角色将始终“看向”鼠标指针。前进/后退输入保持不变。
1 | extends CharacterBody2D |
这里我们用到 Node2D 中的 look_at() 方法,使玩家朝向鼠标的位置。如果没有此功能,可以通过如下设置角度以获得相同的效果:
1 | rotation = get_global_mouse_position().angle_to_point(position) |
- 点击并移动
最后一个示例仅使用鼠标来控制角色。 单击屏幕将使游戏角色移动到目标位置。
1 | extends CharacterBody2D |
注意我们在移动之前做的 distance_to() 检查,没有这个检查,物体在到达目标位置时会 “抖动”,
因为它稍微移过该位置时就会试图向后移动,只是每次移动步长都会有点远从而导致来回重复移动。
如果你喜欢,取消注释的 rotation 代码可以使物体转向其运动方向。
小技巧:
该技术也可以用到“跟随”的游戏角色中。
target 目标位置可以是任何你想移动到的对象的位置。
工具
使用 TileSet
使用 TileMapLayer 节点设计关卡有很多好处。
首先,在绘制布局时可以直接把图块“画”到栅格上,比放置 Sprite2D 节点要快很多。
其次,由于图块地图针对大量图块的绘制进行了优化,支持更大的关卡。
最后,可以添加更强大的功能,如碰撞、遮挡、导航等形状。
用法:
- 创建 TileSet(图块集)
- 添加图块表(Tilesheet)
默认图块形状是 Square(正方形),也可以选择 Isometric(等轴)、Half-Offset Square(半偏移正方形)、Hexagon(六边形)。
使用了 Square 以外的图块形状可能需要调整 Tile Layout(图块布局)和 Tile Offset Axis(图块偏移轴)。
要让图块被图块坐标裁剪,还可以启用 Rendering > UV Clipping(渲染 > UV 裁剪)属性。
这样就能够保证图块无法绘制到它们在图块表上分配的区域之外。
如果依赖于自动图块创建,必须在创建图集前设置图块大小。
图集将确定哪些从图块表中而来的图块可以添加到 TileMap 节点(因为并不是图像的每一个部分都是有效的图块)。
如果想在一个 TileSet 中使用好几张图块表来选择图块,新建几个图块集,并为每个图块集配置纹理再继续。
也可以用这种方法把单张图片作为一个图块来使用(但是用图块表会方便得多)。
可以在图集上调整以下属性:
- ID: 标识符(在该 TileSet中每个图块集的标识符是唯一的),用于排序。
- 名称: 在此处使用描述性的名称来方便管理(例如“地形”,“装饰”等)。
- 边距: 图像边缘上的边距不应选择为图块(以像素为单位)。当你下载的图块表图像的边缘有边距(例如,用于表明归属),则增加这个值可能会很有用。
- 间距: 以像素为单位的地图集上的每个图块之间的间距。如果你使用的图块表图像包含辅助线(例如每个图块之间的轮廓),则增加间距可能会很有用。
- 纹理区域大小:以像素为单位的图集上的每个图块的大小。在大多数情况下,这应该与 TileMapLayer 属性中定义的图块大小相匹配(尽管并不是必需的)。
- 使用纹理内边距: 选用,则在每个图块周围添加一个1像素的透明的边缘,以防止启用过滤时纹理渗出(bleeding)。建议默认将其启用。
请注意,更改纹理边距,间距和区域大小都有可能会导致图块丢失(其中一些将位于图集图片的坐标之外)。
如要自动从图块表中再生图块,请使用图块集编辑器顶部的三个垂直点菜单按钮,然后选择在不透明纹理区域创建图块 :
使用场景合集
可以将实际的场景放置为图块。允许将任何节点集合用作图块。
比如说,可以使用场景图块来放置游戏元素,例如玩家可以与之互动的商店。
还可以使用场景图块来放置 AudioStreamPlayer2D (用于环境声音)、粒子效果等。
警告
与图集相比,场景图块具有更大的性能开销,因为每个场景都是为每个放置的图块单独实例化的。
建议仅在必要时使用场景图块。要在没有任何高级操作的图块中绘制精灵,请使用图集代替。
对于本例,将创建一个包含 CPUParticles2D 根节点的场景。
将此场景保存到场景文件(与包含 TileMapLayer 的场景分离)
然后切换到包含 TileMapLayer 节点的场景,创建一个新的场景集合。
为场景合集输入描述名称,创建一个新的场景槽。
将若干图集合并为单个图集
必须在TileSet资源处创建了不止一个图集(atlas)。
在图集(atlas)列表下找到“三个垂直排列的点”的菜单按钮,选择打开图集合并工具
通过按住 Shift 或 Ctrl 键并同时点击多个元素来选择多个图集
选择合并合并成一个图集图片。
小技巧
TileSet 具有一个 图块代理 系统。图块代理是一张映射表,它允许通知使用了给定 TileSet 的 TileMap,以便将一组给定的图块标识符替换为另一组图块标识符。
合并不同图集时会自动设置图块代理,但也可以通过 管理图块代理 对话框手动设置,你可以使用上面提到的“三个竖点”菜单访问该对话框。
当你更改了图集 ID 或想用另一个图集的图块替换一个图集的所有图块时,手动创建图块代理可能会很有用。请注意,编辑 TileMap 时,你可以用相应的映射值替换所有网格。
向 TileSet 添加碰撞、导航和遮挡
首先为 TileSet 资源创建物理层、导航层或遮挡层。
- 请选择
TileMapLayer节点,单击检查器中的TileSet属性值进行编辑,然后展开 物理层 并选择 添加元素: - 导航辅助,可以创建导航层
- 光照多边形遮挡器,可以创建遮挡层
本教程的后续步骤专门用于创建碰撞多边形,但导航和遮挡的步骤非常相似。
唯一需要注意的是,图块的遮挡多边形属性是图集检查器中渲染分节的一部分。请确保展开该部分,以便编辑多边形。
创建物理层后访问 TileSet 检查器中选择的物理层部分。
小技巧
你可以在聚焦 TileSet 编辑器时按 F 快速创建矩形碰撞形状。
如果你有一个大的图块集,为每个图块单独指定碰撞可能会花费很多时间。
要快速将类似的碰撞形状应用到多个图块,请使用<一次为多个图块指定属性的功能>。
为 TileSet 的图块分配自定义元数据
使用自定义数据层用来存储游戏过程中所需的特定信息。
例如玩家接触该图块时应该受到的伤害,以及是否能够使用物理将图块摧毁。
数据是在 TileSet 中与图块进行关联的:放置的所有图块使用的都是相同的自定义数据。
如果你需要创建拥有不同自定义数据的变体图块,可以通过创建备选图块并为该备选图块更改自定义数据来实现。
改变自定义数据的顺序不会损坏现有的元数据:TileSet 编辑器会在自定义数据属性的顺序发生改变后自动进行更新。
创建地形集(自动图块)
备注
这个功能和 Godot 3.x 中的自动图块使用了不同的实现方式。
地形系统能够完全替代自动图块,功能也更强大。与自动图块不同,地形系统支持不同地形之间的过渡,这样就可以为同一个图定义多个地形。
与以前不同的是,自动图块是一种特定类型的图块,而地形只是分配给图集图块的一组属性。
使用专门的 TileMap 绘制模式时就会用到这些属性,该模式能够对带有地形数据的图块进行智能选择。
这意味着地形图块既可以作为地形绘制,也可以像其他图块一样作为单个图块绘制。
Godot 提供的地形系统就能够自动进行图块的连接。这样就可以自动使用“正确”的图块变体。
- 地形按照地形集分组。
- 地形集有固定的模式,包括 Match Corners and Sides、Match Corners、Match sides(匹配角落和边缘、匹配角落、匹配边缘)。
- 模式决定了地形集中的地形如何相互匹配。
备注
上述模式在 Godot 3.x 中对应的自动图块位掩码模式为 2×2、3×3、3×3 Minimal。
也和 Tiled 编辑器中使用的模式类似。
选中 TileMapLayer 节点,转到检查器的 TileSet 资源中创建一个新的地形集:
创建地形集后,你必须在地形集中再创建若干地形:
在 TileSet 编辑器中,切换到“选择”模式并单击图块。
在中间一栏展开地形部分,为图块分配地形集 ID 和地形 ID。
-1 表示“没有地形集”和“没有地形”,因此你必须先将地形集设置为大于等于 0 的值,然后才能将地形设置为大于 0 的值。
地形集 ID 和地形 ID 互相独立。从 0 开始,不从 1 开始。
完成此操作后,现在可以配置地形邻接值(Terrain Peering Bits),该部分在中间一列中可见。
邻接值决定了根据相邻图块的情况放置哪个图块。-1 是一个特殊值,表示空白空间。
例如,如果一个地砖的所有位都设置为 0 或更大,那么只有在 所有 8个相邻图块都使用具有相同地形 ID 的图块时,它才会出现。
如果图块的位设置为“0”或更大,但左上、上和右上的位设置为“-1”,则只有在其顶部(包括对角线)有空位时才会出现。
一次为多个图块指定属性
有两种方法可以同时为多个图块分配属性。根据你的使用情况,一种方法可能比另一种方法更快:
- 使用多个图块选择
如果希望一次在多个图块上配置多种属性,请选择 TileSet 编辑器顶部的 选择 模式:- 选择多个图块。
- 使用 TileSet 编辑器中间一列的检查器分配属性。只有在此更改的属性才会应用到所有选定的图块。
- 使用图块属性绘制
如果要一次将单个属性应用于多个图块,则可以使用属性绘制模式来实现此目的。
在中间一列配置要绘制的属性,然后点击右列中的图块(或按住鼠标左键)以将属性“绘制”到图块上。
对于手动设置耗时的属性(例如碰撞形状),图块属性绘制特别有用
创建备选图块
使用单一的图块图像(在图集中只能找到一次),但要以不同的方式进行配置。
例如,使用相同的图块图像,但对它进行旋转、翻转或调制成不同的颜色。这就可以使用备选图块来实现。
小技巧
从 Godot 4.2 开始,你将不再必须通过创建替代图块来旋转或翻转图块。
你可以使用 TileMap 编辑器工具栏中的旋转/翻转按钮,在任何图块放置进 TileMap 编辑器中时旋转它。
创建备选图块,右键基本图块:
- 备用 ID: 此可选图块的唯一数字标识符。更改它会破坏现有的瓦片地图,所以要小心!此 ID 还控制在编辑器中显示的可选图块列表中的排序。
- 渲染 > 水平翻转
- 渲染 > 垂直翻转
- 渲染 > 转置
- 渲染 > 纹理原点:绘制图块时使用的原点。可以用来将图块进行相对于基础图块的视觉偏移。
- 渲染 > 调制:渲染图块时的颜色乘数。
- 渲染 > 材质:当前图块使用的材质。可以使用此选项为单个图块应用不同的混合模式或自定义着色器。
- Z 索引: 当前图块的排序。数值大的图块会渲染在同一层中的其他图块之上。
- Y 排序原点(Y Sort Origin):基于其 Y 坐标(以像素为单位)进行图块排序时要使用的垂直偏移量。
这使得可以将图层视为在不同高度上用于俯视角的游戏。
调整此值可以帮助缓解某些图块排序的问题。仅当 CanvasItem > Ordering 下的 TileMapLayer 图层的 Y Sort Enabled 为启用时才有效。可以通过点击在备选图块旁的大 “+” 图标来创建额外的 备选图块变体。这相当于选择基本图块并右键点击它以再次选择创建备选图块。
备注
当创建一个备选图块时,其基本图块的任何属性将不会被继承下来。如果你希望备选图块与基本图块的属性相同,那么你需要在备选图块中重新设置这些属性。
使用 TileMap
在 TileMapLayer 中指定 TileSet
如果你已经进行了使用 TileSet 所述的步骤,你现在应该有一个内嵌进 TileMapLayer 节点的 TileSet 资源。
这样的做法适合制作原型,但在实际项目当中,你一般会有多个关卡复用同一个图块集。
要在多个 TileMapLayer 节点中复用同一个 TileSet ,建议将 TileSet 保存到外部资源。
为此,请点击 TileSet 资源旁边的下拉菜单 ,然后选择 保存 :
多 TileMapLayer 及相关设置
使用图块地图时,适当情况下通常建议使用多个TileMapLayer节点。
你可以每个图层放置一个图块在给定位置,如果你有不止一层就能实现多个图块的叠加。
每个TileMapLayer节点都有几个可以调整的属性:
- Enabled:true,当前图层在编辑器中和运行项目时可见。
- TileSet:图块集。
- 渲染 (Rendering)
- Y Sort Origin: 每个图块在Y 轴排序时使用的垂直偏移量(以像素为单位)。仅在CanvasItem设置下 Y Sort Enabled 为 true 时有效。
- X轴绘制顺序反转:反转X轴上图块的绘制顺序。要求在CanvasItem设置中“Y轴排序启用”为真。
- Rendering Quadrant Size 象限是为了优化目的而绘制在单个 CanvasItem 上的一组图块。此设置定义了地图坐标系中正方形边的长度。该象限大小不适用于 Y 排序的 TileMapLayer,因为在这种情况下图块是按 Y 位置分组的。
- 物理 (Physics)
- Collision Enabled 启用或禁用碰撞。
- Use Kinematic Bodies 当为 true 时,TileMapLayer 的碰撞形状将实例化为运动学刚体。
- Collision Visibility Mode TileMapLayer的碰撞形状是否可见。如果设置为默认,则取决于显示碰撞的调试设置。
- 导航 (Navigation)
- Navigation Enabled(导航启用)是否启用导航区域。
- Navigation Visible TileMapLayer 的导航网格是否可见。如果设置为默认,则取决于显示导航的调试设置。
小技巧
TileMap 内置导航存在许多实际限制,导致寻路性能和路径跟随质量较差。
在设计完 TileMap 后,考虑将其烘焙成一个更优化的导航网格(并禁用TileMap的NavigationLayer)
使用 NavigationRegion2D 或 NavigationServer2D。查看 使用导航网格 以获取更多信息。
绘制模式和工具
TileMap 编辑器顶部的工具栏从左到右,可以选择的绘制模式和工具是:
- 选择
在2D编辑器中,可以通过单击单个图块或按住鼠标左键框选多个图块来选择。
请注意,无法选择空白区域:如果创建了一个矩形选择,只会选择非空图块。
要追加到当前选择,按 Shift 键,然后选择一个图块。要从当前选择中删除,请按 Ctrl 键,然后选择一个图块。
然后可以在任何其他绘画模式下使用该选择来快速创建已放置模式的副本。
可以通过按 Del ,从 TileMap 中删除选定的图块。
在绘制模式下,可以通过按住 Ctrl 键然后进行选择来暂时切换到此模式。
可以复制并粘贴已经放置的图块:选择图块,按 Ctrl+C 键,然后按 Ctrl+V 键,也可以通过左键单击将所选内容粘贴到图中。还可以按 Ctrl+V 键再次进行复制,右键单击或按 Escape 键可取消粘贴。 - 绘制
- 线段
- 矩形
- 油漆桶填充
- 拾取器
- 橡皮
- 使用散布随机绘图
使用模式保存和加载预制的图块放置
要创建图案,切换到“选择”模式,选择已经在2D场景绘制的图案 Ctrl+C。
单击“图案”选项卡中的空白区域 Ctrl+V。
要使用现有图案,请在图案选项卡中单击其图像,切换到绘画模式。
备注
尽管图案是在 TileMap 编辑器中编辑的,但它存储在 TileSet 资源中。
这样允许在加载保存到外部文件的 TileSet 资源后,在不同的 TileMapLayer 节点中复用图案。
自动处理地形的图块连接
要使用地形,TileMapLayer 节点必须至少包含一个地形集和该地形集中的一个地形。如果尚未为 TileSet 创建地形集,请参考 创建地形集(自动图块) 。
地形连接有 3 种绘图模式可选:
- 连接 ,图块与相同 TileMapLayer 上的周围图块相连。
- 路径,图块会与相同笔画绘制出的图块相连(直到松开鼠标按键)。
- 图块特定的覆盖以解决冲突或处理地形系统未涵盖的情况。
“连接”模式更容易使用,但“路径”更灵活,因为它允许艺术家在绘制过程中进行更多控制。
例如,“路径”可以让道路直接相邻而不相互连接,而“连接”则会强制两条道路相连。
最后,在某些情况下,你可以从地形中选择特定的图块来解决冲突。
任何至少有一个位(bit)设置为对应地形 ID 的值的图块将出现在可供选择的图块列表中。
处理缺失图块
如果删除 TileMap 所引用 TileSet 中的图块,TileMap 将显示一个占位符,表示放置的图块 ID 无效:
这些占位符在运行的项目中不可见,但图块数据仍会持久保存在磁盘中。这样你就可以安全地关闭和重新打开此类场景。
重新添加具有匹配 ID 的图块后,图块将以新图块的外观出现。
备注
在选择 TileMapLayer 节点并打开 TileMap 编辑器之前,可能无法看到缺失的图块占位符。
3D
3D 简介
基础操作
坐标系
Godot 在 3D 中使用公制,1 个单位等于 1 米。
使用 3D 资产时,最好始终使用正确的比例(在 3D 建模软件中将单位设置为公制)。
Godot 允许在导入后缩放,尽管在大多数时都没问题,但在极少数情况下,会在渲染或物理等敏感区域带来浮点精度问题(从而导致故障或伪影)。
确保你的艺术家始终在正确的比例下进行创作!
X 是两边
Y 是上/下
Z 是前/后
一些有用的键盘绑定:
要吸附放置或旋转,在移动、缩放或旋转时按 Ctrl 键。
要将视图居中到所选对象上,请按 F。
在 3D 环境中导航
默认的3D场景导航控制类似于 Blender
在编辑器设置中也包含了自定义鼠标按钮和行为的选项,就仿佛使用其他工具那样。
打开 编辑器设置>各编辑器>3D 。然后在导航下,找到导航方案 。
使用 Blender 风格的变换快捷键
从 Godot 4.2 开始,可以启用 Blender 风格的快捷键来平移、旋转和缩放节点。
在 Blender 中,这些快捷键分别是:
- G 用于平移
- R 用于旋转
- S 用于缩放
小技巧
在聚焦于 3D 编辑器视口的同时,按下快捷键,可以移动鼠标或输入数字以将选定节点移动指定的 3D 单位量。
你可以通过用字母指定特定的轴,然后指定距离(如果使用键盘来输入值),以此来将选中节点的平移限制到特定的轴上。
例如,要将选中物体向上移动 2.5 个单位,请按顺序输入以下序列
G-Y-2-.-5-Enter
在 Godot 中使用 Blender 风格的变换快捷键
编辑器设置的快捷键选项卡,然后在 Spatial Editor 部分中进行以下调整:
- 将开始平移变换绑定到 G。
- 将开始旋转变换绑定到 R。
- 将开始缩放变换绑定到 S。
- 取消缩放模式快捷键的绑定,避免冲突。
Node3D 节点
使用专业的 3D 工具(通常称为数字内容创建工具 DCC)来创建3D资源
然后导出到某种交换文件格式,才能被 Godot 导入。
- 手动制作的模型(使用 3D 建模软件)
可以导入外部工具创建的3D模型。
具体取决于格式导入整个场景,包括其中的动画、骨骼绑定、混合形状,也可以简单地作为资源使用。 - 生成的几何体(静态)
- 可以直接使用 ArrayMesh 资源创建自定义几何体。
只需创建数组并使用 ArrayMesh.add_surface_from_arrays() 函数即可。 - 也可以使用辅助类 SurfaceTool,它提供了更直接的 API 和辅助工具,用于索引、生成法线、切线等。
用于生成静态几何体,因为创建顶点数组并将它们提交给3D API具有显著的性能开销。
- 可以直接使用 ArrayMesh 资源创建自定义几何体。
- 即时几何体(经常更新)
提供了一种特殊的 ImmediateMesh 资源,它可以在 MeshInstance3D 节点中使用。
这提供了 OpenGL 1.x 风格的即时模式 API 来创建点、线、三角形等。 - 3D 中的 2D
通过使用不旋转的固定相机(正交或透视)
可以使用诸如 Sprite3D 和 AnimatedSprite3D 等节点来创建混合了具有3D背景,更逼真的视差,灯光/阴影效果等的2D游戏。
缺点在于与2D相比增加了复杂性并降低了性能,以及缺乏进行像素工作时的参考。 - 环境
Godot提供了一个 WorldEnvironment 节点,该节点允许更改背景颜色。
模式(就像放一个天空盒时那样)以及应用多种内置处理后效果。 环境可以在Camera中被覆写。 - 预览环境和灯光
默认情况下,如果 3D 场景中没有 WorldEnvironment 或者 DirectionalLight3D 节点,就会打开对应预览项为场景布光。
图标右侧画着三个点的下拉菜单中可以调整预览灯光和预览环境的属性。
同一个项目中,不同场景使用相同的预览太阳和预览环境。因此,在这里作出的调整应该适合所有需要预览灯光和预览环境的场景。 - 相机
相机可以在正交或透视投影中工作。
摄像机与父视口或其祖先视口相关联,且仅显示到他们上面。
由于场景树的根是一个视口,默认情况下会在其上显示摄像机,但如果需要子视口(作为渲染目标或画中画),则需要自己的子摄像头才能显示。
处理多台摄像机时,每个视口都遵循以下规则:
- 如果场景树中没有摄像机,则第一个进入的摄像机将成为活跃摄像机。进入场景的其他摄像机将被忽略(除非它们被设置为 current)。
- 如果相机设置了“current”属性,则无论场景中是否有其他相机,都会使用它。如果该属性已设置,它将变为活动状态,取代之前的摄像机。
- 如果活动摄像机离开了场景树,则按树形顺序排列的第一台摄像机将取代它。
使用 3D 变换
对欧拉角说不
- 旋转顺序
第一人称控制器(例如FPS游戏)。
实现希望的效果,必须先在 Y 轴上应用旋转,然后在 X 轴上旋转。
如果先在 X 轴上应用旋转,然后再在 Y 轴上应用旋转,则效果会不理想。
在X,Y和Z中应用旋转是不够的: 你还需要旋转顺序。 - 插值
设想你想在两个不同的相机或敌人位置(包括旋转)之间转换。
解决这个问题的一个合乎逻辑的方法是从一个位置插值到下一个位置。
相机实际上旋转去了相反的方向!
这可能有几个原因:
- 旋转不会线性映射到方向,因此它们插值并不总是会形成最短路径(即从 270 到 0 的度数与从 270 开始到 360 的度数不同,即使角度是相同的)。
- 万向节锁死正在发挥作用(第一个和最后一个旋转的轴对齐,因此失去了一个自由度)

所有这些的结论是,不应该在游戏中使用 Node3D 节点的 rotation 属性。
它主要用在编辑器中,为了与2D引擎一致,并且用于简单的旋转(通常只有一个轴,或者,在有限的情况下,两个)。
尽管你可能会受到诱惑,但不要使用它。
相反,有一个更好的方法来解决你的旋转问题。(变换)
变换的介绍
方向使用 Transform3D 数据类型。
每个 Node3D 节点都包含一个与父级变换相关的 transform 属性(如果父级是 Node3D 派生类型)。
也可以通过 global_transform 属性访问世界坐标变换。
变换拥有一个基 Basis(transform.basis 子属性)由三个 Vector3 向量组成。
这些向量可以通过 transform.basis 属性访问,也可以使用 transform.basis.x、transform.basis.y、transform.basis.z 直接访问。
每个向量指向它的轴被旋转的方向,因此它们可以有效地描述节点的总旋转。
比例(只要它三个轴长度是一致的)也可以从轴的长度推断出来。一个基也可以被解释为一个 3x3 矩阵并像 transform.basis[x][y] 这样使用。
默认的基(未经修改)类似于:
1 | var basis = Basis() |
这类似于一个 3x3 单位矩阵。
变换除了基以外还有一个原点。这是一个 Vector3,用于指定该变换距离实际原点 (0, 0, 0) 有多远。
变换是基与原点的组合,可以有效地表示空间中特定的平移、旋转和缩放。
可视化变换的一种方法是在“本地空间”模式下查看该对象的 3D 小工具。
操作变换
当然,变换并不像角度那样容易控制,并且有它自己的问题。
可以对变换进行旋转,方法是将基与另一个基相乘(称作累加),或者使用其旋转方法。
1 | var axis = Vector3(1, 0, 0) # Or Vector3.RIGHT |
简化方法
1 | # 将变换绕X轴旋转0.1弧度。 |
这会相对于父节点来旋转节点。要相对于对象空间旋转(节点自己的变换),请使用下面的方法:
1 | # 将变换绕X轴旋转0.1弧度。 |
轴应该定义在物体的局部坐标系中。
例如,要围绕物体的局部 X、Y 或 Z 轴旋转,可以使用 Vector3.RIGHT 表示 X 轴, Vector3.UP 表示 Y 轴, Vector3.FORWARD 表示 Z 轴。
度误差
对变换执行连续的操作将导致由于浮点错误导致的精度损失.
这意味着每个轴的比例可能不再精确地为 1.0 ,并且它们可能不完全相互为 90 度.
如果一个变换每帧旋转一次,最终会随着时间的推移开始变形。 不可避免。
有两种不同的方法来处理这个问题。
- 首先是在一段时间后对变换进行**正交归一化(orthonormalize)**处理(如果每帧修改一次,则可能每帧一次):
transform = transform.orthonormalized()
这将使所有的轴再次拥有有 1.0 的长度并且彼此成 90 度角. 但是,应用于变换的任何缩放都将丢失.
建议不要缩放将要操作的节点;而是缩放其子节点(例如 MeshInstance3D)。如果你绝对必须要缩放节点,请在最后重新应用它:
1 | transform = transform.orthonormalized() |
获取信息
怎么从变换中获得角度?没有必要。你必须尽最大努力停止用角度思考。
游戏角色面对的方向射击子弹,只需使用向前的轴(通常为 Z 或 -Z )。
1 | bullet.transform = transform |
敌人在看着游戏角色吗? 为此判断你可以使用点积
1 | # 获取从玩家指向敌人的方向向量 |
所有常见的行为和逻辑都可以用向量来完成。
设置信息
当然,有些情况下你想要将一些信息赋予到变换上。
想象一下第一人称控制器或环绕旋转的摄像机。
那些肯定是用角度来完成的,因为你确实希望变换以特定的顺序进行。
对于这种情况,请保证角度和旋转在变换外部 ,并在每帧设置他们。
不要尝试获取并重新使用它们,因为变换是不应该以这种方式使用的。
环顾四周,FPS风格的示例:
1 | # accumulators |
如你所见,在这种情况下,保持外部旋转更为简单,然后使用变换作为最后的方向。
用四元数插值
四元数能有效率地完成两个变换之间的插值。
在实际应用中,了解它们的主要用途是做最短路插值就足够了。
同样,如果你有两个旋转,四元数将平滑地使用最近的轴在它们之间进行插值。
将旋转转换为四元数很简单:
1 | # 将基(Basis)转换为四元数,需注意缩放(Scale)信息会丢失。 |
Quaternion 类型参考包含有关数据类型的更多信息(它还可以进行变换累积、变换点等,尽管使用较少)。
如果你多次对四元数进行插值或应用运算,请记住它们最终需要归一化。否则,会带来数值精度误差。
四元数在处理相机/路径/等东西的移动轨迹时很有用。 插值的结果总会是正确且平滑的。
程序式几何体
网格(Mesh)
由一个或多个表面(Surface)组成。
表面是由多个子数组组成的数组,包含顶点、法线、UV 等。
在 Godot 中,几何体用 Mesh(网格)来表示。
Godot 中很多东西的名称里都含有“Mesh”:
- MeshInstance3D 节点绘制 Mesh 和 ArrayMesh 资源。代表的是某个网格在场景中的实例。
可以在多个 MeshInstance3D 中重复使用同一个网格,用不同的材质或变换(缩放、旋转、位置等)在场景的不同部分绘制它。 - MultiMesh 与 MultiMeshInstance3D 结合使用多次绘制同一个对象。
MultiMeshInstance3D 可以以非常低的性能成本绘制数千次网格,利用的是硬件实例化的优势。
使用 MultiMeshInstance3D 的缺点是所有网格的表面都只能使用同一种材质。
它使用一个实例数组为每个实例存储不同的颜色和变换,但所有实例的表面使用的都是相同的材质。
表面
每个表面都有自己的材质。
使用 MeshInstance3D 时,你也可以使用 material_override 属性来覆盖 Mesh 中所有表面的材质。
表面数组
长度为 ArrayMesh.ARRAY_MAX 的数组。
数组中的每个位置都填充了一个包含每个顶点信息的子数组。
例如,位于 ArrayMesh.ARRAY_NORMAL 处的数组是一个顶点法线的 PackedVector3Array。有关更多信息,请参阅 Mesh.ArrayType。
表面数组可以是有索引的,也可以是非索引的。
创建非索引数组就像在索引 ArrayMesh.ARRAY_INDEX 处不分配数组一样简单。
非索引数组为每个三角形存储唯一的顶点信息,也就是说,当两个三角形共用一个顶点时,顶点在数组中是重复的。
有索引的曲面数组只存储每个唯一顶点的顶点信息,然后还存储一个索引数组,它映射出如何从顶点数组构造三角形。
一般来说,使用索引数组的速度更快,但这意味着你必须在三角形之间共享顶点数据,这并不总是需要的(例如,当你想要每面法线时)。
工具
Godot 提供了不同的访问和处理几何体的方法.
- ArrayMesh (阵列网格体)
扩展了 Mesh,增加了一些不同的便捷函数,最重要的是,可以通过脚本构建 Mesh 表面。 - MeshDataTool (网格数据工具)
将Mesh数据转换为顶点,面和边的数组的资源,可以在运行时进行修改. - SurfaceTool (表面工具)
允许使用OpenGL 1.x即时模式风格的接口创建网格. - ImmediateMesh (即时网格体)
使用立即模式风格的接口绘制对象的网格(像 SurfaceTool 一样)。
ImmediateMesh 和 SurfaceTool 的区别在于,ImmediateMesh 是直接用代码动态绘制的,而 SurfaceTool 则是用来生成一个 Mesh,你可以用它做任何你想做的事。
ImmediateMesh 因为其直接的 API 而对原型设计很有用,但它的速度很慢,因为每次进行修改时都要重建几何体。
最有用的是快速添加简单的几何体来进行可视化调试(例如,通过绘制线条来可视化物理光线投射等)。
使用 ArrayMesh
使用函数 add_surface_from_arrays() 最多需要五个参数,前两个必须,后三个可选:
- 第一个参数是 PrimitiveType(图元类型)OpenGL 中的概念
用于指示 GPU 如何根据给定的顶点来排列图元,即它们是否代表三角形、线条、点等。有关可用选项,请参阅 Mesh.PrimitiveType。 - 第二个参数 arrays 是存储网格信息的实际 Array
该数组是一个普通的 Godot 数组,用空括号 [] 构造。它为每一种类型的信息存储一个 Packed**Array(如 PackedVector3Array、PackedInt32Array等),用于构建表面。
arrays 的常见元素列出如下,还有必须在 arrays 中包含位置信息。有关完整列表,另请参阅 Mesh.ArrayType。索引 Mesh.ArrayType 枚举 数组类型 0 ARRAY_VERTEXPackedVector3Array 或 PackedVector2Array 1 ARRAY_NORMALPackedVector3Array 2 ARRAY_TANGENTPackedFloat32Array 或 PackedFloat64Array 4 个浮点数组。前 3 个浮点数确定切线,最后一个浮点数确定副法线方向,即 -1 或 1。 3 ARRAY_COLORPackedColorArray 4 ARRAY_TEX_UVPackedVector2Array 或 PackedVector3Array 5 ARRAY_TEX_UV2PackedVector2Array 或 PackedVector3Array 10 ARRAY_BONES4 个 float 一组的 PackedFloat32Array 或 4 个 int 一组的 PackedInt32Array。每一组都列出了影响给定顶点的 4 根骨骼的索引。 11 ARRAY_WEIGHTS4 个 float 一组的 PackedFloat32Array 或 PackedFloat64Array。每个 float 都列出了给定顶点对 ARRAY_BONES 中特定骨骼的权重。 12 ARRAY_INDEXPackedInt32Array
在创建网格的大部分情况中,我们通过顶点位置来定义网格。
因此(位于索引 0 处的)顶点数组通常是必需的。
而(位于索引 12 处的)索引数组是可选的,只有在它被包含时才会使用。
也可以只用索引数组而不用顶点数组来创建网格,但这不在本教程的内容范围之中。
其他所有数组包含的都是关于顶点的信息。
它们也是可选的,只有在包含时才会用到。
有些数组(例如 ARRAY_COLOR`)用每个顶点一个元素的形式来提供额外的顶点信息。
它们的大小必须与顶点数组一致。
另一些数组(例如 ARRAY_TANGENT)用四个元素来描述一个顶点。它们必须正好是顶点数组的四倍。
正常的使用场景下,add_surface_from_arrays() 的最后三个参数通常都是留空的。
设置 ArrayMesh
创建一个 MeshInstance3D ,并在检查器中为其添加一个 ArrayMesh。
通常,在编辑器里添加 ArrayMesh 没什么用,但这里可以让我们免去用代码创建的麻烦,直接使用这个 ArrayMesh。
接下来,在 MeshInstance3D 上添加一个脚本。
在 _ready() 下创建一个新的数组。var surface_array = []
这将是保存表面信息的数组——将保存表面需要的所有数据数组。Godot 希望它的大小是 Mesh.ARRAY_MAX,所以要相应地调整。
1 | var surface_array = [] |
接下来,为你将使用的每种数据类型创建数组.
1 | var verts = PackedVector3Array() |
一旦你用几何体填充了你的数据数组,就可以通过将每个数组添加到 surface_array ,然后提交到网格中来创建网格.
1 | surface_array[Mesh.ARRAY_VERTEX] = verts |
在这个例子中,使用了 Mesh.PRIMITIVE_TRIANGLES,但你也可以使用网格所提供的任何图元类型。
把这些放到一起,完整的代码是这样的:
1 | extends MeshInstance3D |
中间可以放你想要的任何代码。下面我们会给出一些示例代码,用于生成球体。
1 | extends MeshInstance3D |
保存
最后,我们可以使用 ResourceSaver 类来保存该 ArrayMesh。当你想生成一个网格,然后在以后使用它而不需要重新生成时,这个方法很有用。
1 | # 保存网格到 .tres 文件,并启用压缩。 |
使用 MeshDataTool
MeshDataTool 不是用来生成几何体的,但它对动态改变几何体很有帮助,例如,如果你想写一个脚本来分割,简化或变形网格.
MeshDataTool不像直接使用ArrayMesh改变数组那么快.
但是,它提供了比ArrayMesh更多的信息和工具来处理网格.
当使用MeshDataTool时,它会计算ArrayMeshes中没有的网格数据,如面和边,这些数据对于某些网格算法来说是必要的.
如果你不需要这些额外的信息,那么使用 ArrayMesh 可能会更好.
MeshDataTool 只能用于使用 Mesh.PRIMITIVE_TRIANGLES PrimitiveType 的网格。
我们通过调用create_from_surface()来使用 ArrayMesh 初始化 MeshDataTool。
如果该 MeshDataTool 中已经有初始化的数据了,调用 create_from_surface() 会为你将其清除。或者你可以在重用 MeshDataTool 之前自己调用clear()。
下面的例子中,假定已经创建了一个名叫 mesh 的 ArrayMesh。网格生成的示例见 ArrayMesh 教程。
1 | var mdt = MeshDataTool.new() |
create_from_surface() 使用 ArrayMesh 中的顶点数组来计算另外两个数组,一个是边、一个是面,总计三个数组。
边缘是任意两个顶点之间的连接. 边缘数组中的每一条边缘都包含了对它所组成的两个顶点的引用,以及它所包含的最多的两个面.
面是由三个顶点和三条对应的边组成的三角形. 面数组中的每个面都包含了它所组成的三个三角形和三条边的参考.
顶点数组包含与每个顶点相连的边、面、法线、颜色、切线、uv、uv2、骨骼和权重信息。
为了从这些数组中获取信息,你可以使用 get_ **** () 的函数:
1 | mdt.get_vertex_count() # 返回顶点数组中的顶点数量 |
你选择用这些函数做什么取决于你。一个常见的用例是对所有顶点进行迭代,并以某种方式对它们进行转换:
1 | for i in range(get_vertex_count): |
这些修改不是在 ArrayMesh 上直接进行的。如果你要动态更新现有的 ArrayMesh,请在添加新表面前使用 commit_to_surface() 来删除已有表面:
1 | mesh.clear_surfaces() # 删除所有的网格表面. |
下面是一个完整的示例,将一个叫做 mesh 的球体网格变成随机变形的块状,并更新了法线和顶点颜色。如何生成基础网格见 ArrayMesh 教程。
1 | extends MeshInstance3D |
使用 SurfaceTool
SurfaceTool 提供了一个用于构造几何体的有用接口。
该接口类似于 ImmediateMesh 类。你设置每个顶点的属性(例如法线、uv、颜色),然后当你添加顶点时,它就会捕获这些属性。
SurfaceTool 还提供了一些有用的辅助函数,如 index() 和 generate_normals()。
属性是在添加每个顶点之前添加的:
1 | st.set_normal() # 被下面的法线设置覆盖,无效。 |
当使用 SurfaceTool 完成生成几何体后,调用 commit() 完成生成网格。
如果将一个 ArrayMesh 传递给了 commit(),那么它就会在这个 ArrayMesh 的末尾附加一个新的表面。
而如果没有传递任何信息,commit() 则返回一个 ArrayMesh。
1 | st.commit(mesh) |
代码创建一个有索引的三角形
1 | var st = SurfaceTool.new() |
你可以选择添加一个索引数组,可以通过调用 add_index() 将顶点添加到索引数组中,也可以通过调用 index() 将顶点数组缩小以删除重复的顶点.
1 | # 继续之前的代码,先添加第四个顶点 |
同样,如果你有一个索引数组,但希望每个顶点都是唯一的(例如,因为想在每个面而不是每个顶点使用唯一的法线或颜色),可以调用 deindex() .st.deindex()
如果你不想自行添加自定义法线,那么可以使用 generate_normals() 来添加,调用时机应该是在生成几何体之后、使用 commit() 或 commit_to_arrays() 提交网格之前。调用 generate_normals(true) 会将最终的法线翻转。另外请注意,generate_normals() 只有在图元类型为 Mesh.PRIMITIVE_TRIANGLES 时有效。
你可能发现了,在生成的网格上,法线贴图或者其他一些材质属性看上去不对劲。这是因为对法线贴图而言,必需的是切线,这和法线是两码事。有两种解决方法,手动添加切线信息,或者使用 generate_tangents() 自动生成。这个方法要求每个顶点都已经具有 UV 和法线。
1 | st.generate_normals() |
默认情况下,当生成法线时,它们将以每个面为基础进行计算. 如果想要平滑的顶点法线,在添加顶点时,调用 add_smooth_group() . add_smooth_group() 需要在建立几何体时调用,例如在调用 add_vertex() (如果没有索引)或 add_index() (如果有索引)之前.
使用 ImmediateMesh
ImmediateMesh 是一个使用 OpenGL 1.x 风格的 API 创建动态几何体的便捷工具。
这使得它对于需要每帧更新的网格来说,既易于使用又高效。
使用这个工具生成复杂的几何体(几千个顶点)效率很低,即使只做一次。相反,它的设计是为了生成每一帧变化的简单几何体。
首先,你需要创建一个 MeshInstance3D 并在检查器中向其添加一个 ImmediateMesh。
接下来,将脚本添加到 MeshInstance3D 上。如果你希望 ImmediateMesh 每帧都更新,则应该把 ImmediateMesh 的代码放在 _process() 函数中;如果你想创建一次网格体而后不再更新它,则代码应放在 ready() 函数中。如果仅生成一次表面,则 ImmediateMesh 与任何其他类型的网格一样高效,因为生成的网格会被缓存并重用。
必须调用 surface_begin() 才能开始生成几何体 。surface_begin() 将一个 PrimitiveType 作为参数。PrimitiveType(图元类型)指示 GPU 如何根据给定的顶点来安排图元,可以是三角形、线、点等。完整的列表可以在 Mesh 的类参考页面中找到。
一旦你调用了 surface_begin() ,就可以开始添加顶点了。每次添加一个顶点,首先使用 surface_set****() (例如 surface_set_normal() )添加顶点的特定属性,如法线或 UV。然后调用 surface_add_vertex() 来添加一个带有这些属性的顶点。例如:
1 | # 添加一个带有法线和UV坐标的顶点 |
只有在调用 surface_add_vertex() 之前添加的属性才会被包含在该顶点中。如果在调用 surface_add_vertex() 之前添加属性两次,则仅第二次调用才会被使用。
最后,当添加了所有的顶点后,调用 surface_end() 来表示已经完成了网格的生成。你可以多次调用 surface_begin() 和 surface_end() 来为网格生成多个表面。
下面的示例代码在 _ready() 函数中绘制了一个三角形。
1 | extends MeshInstance3D |
ImmediateMesh 也可以跨帧使用。每次调用 surface_begin() 和 surface_end() 时,你都会向 ImmediateMesh 添加一个新表面。如果你想在每一帧从头开始重新创建网格,请在调用 surface_begin() 之前先调用 clear_surfaces()。
1 | extends MeshInstance3D |
上面的代码将在每个帧里动态地创建并绘制一个表面。
应该使用哪一个?
SurfaceTool和ArrayMesh都是生成静态几何体(网格)的绝佳工具.
使用 ArrayMesh 比使用 SurfaceTool 稍快一些,但 API 的难度更大一些。另外,SurfaceTool 还有一些便捷的方法,比如 generate_normals() 和 index()。
ImmediateMesh 比 ArrayMesh 和 SurfaceTool 受到更多限制。
但是,如果你本来就需要每一帧都改变几何体,它提供的接口更简单,甚至可能比每一帧生成一个 ArrayMesh 更快。
MeshDataTool 的速度并不快,但它可以让你访问网格的各种属性,而这些属性是其他工具无法获得的(边、面等)。当你需要根据这类数据来变换网格时,它是非常有用的,但如果不需要这些信息,就不适合使用。如果你要使用需要访问面数组或边数组的算法,最好使用 MeshDataTool。
3D 文本
有三种使用方法:
- Label3D 节点
优势
- 生成速度比 TextMesh 快。
- 可以使用位图字体和动态字体(带或不带 MSDF 或 mipmap)。
限制 - 默认情况下,Label3D 与 3D 环境的交互有限。着色标志启用时能够接受光照、被光源着色。
但是,即使在 Label3D 的 GeometryInstance3D 属性中将阴影投射设置为开启 ,它也不会投射阴影。
这是因为该节点内部生成具有透明纹理的四边形网格(每个四边形一个字形),并且具有与 Sprite3D 相同的限制。
当多个 Label3D 重叠,尤其是当它们具有轮廓时,透明度排序问题也会变得明显。
这可以将 Label3D 的透明度模式设置为 Alpha Cut 来缓解 ,但代价是文字渲染不够流畅。
Opaque Pre-Pass 透明度模式可以保持文本的流畅性,同时允许 Label3D 投射阴影,但仍会存在一些透明度排序问题。 - 从远处看Label3D时,文本渲染质量也会受影响。想提升文本渲染质量可参考这些方法,对字体使用mipmap or 切换成MSDF 字体渲染.
- MeshInstance3D 节点的 TextMesh 资源
TextMesh 生成的不是透明四边形,而是代表字形轮廓的 3D 网格,具有和网格一样的属性。
因此,TextMesh 默认是开启着色的,会自动在环境中投射阴影。TextMesh 也可以设置材质(包括自定义着色器)。
优势
相对于 Label3D 而言,TextMesh 有以下优点:
- TextMesh 可以使用纹理来修改文本各个面的颜色。
- TextMesh 几何体具有深度,字形看上去是 3D 的。
- TextMesh 可以使用自定义的着色器,而 Label3D 无法使用。
限制
TextMesh 的局限性有: - 没有内置的轮廓支持,而 Label3D 支持。但是可以使用自定义着色器模拟。
- 仅支持动态字体(.ttf、.otf、.woff、.woff2)。不支持 .fnt 和 .font 格式的位图字体。
- 无法正确渲染轮廓自相交的字体。如果使用从 Google Fonts 等处下载到的字体时出现渲染问题,请尝试改为从作者的官方网站下载。
- 对文本进行抗锯齿,需要启用全场景抗锯齿,比如:MSAA,FXAA,Temporal Antialiasing(TAA)。如果未启动抗锯齿,文本会产生颗粒状,尤其是远距离观察时颗粒状更为明显。参考 3D 抗锯齿 。
- 投影 Label 节点(或者其他 Control 节点)
做法是在脚本的 _process() 函数中使用 Camera3D 节点的 unproject_position 的返回值。
使用这个返回值来设置 Control 节点的 position 属性。
优势
- Label、RichTextLabel 等任何 Control 节点,甚至 Button 这样的节点都可以用这种方法。这样就能够实现强大的格式和 GUI 交互。
- 基于脚本的做法能够在定位方面做到最大的自由度。例如,这样就能够在超出屏幕范围后将 Control 吸附到屏幕的边缘(用于在游戏中实现 3D 标记)。
- Control 主题仍然有效。这样实现自定义项目全局的设置就更方便。
限制 - 投影的 Control 无法以任何形式被 3D 几何体遮挡。目标位置被遮挡时,你可以借助 RayCast 将该控件完全隐藏,但是无法实现位于墙壁后面时只隐藏部分区域的效果。
- 可以根据距离调整 Control 的 scale 属性,从而调整文本的大小,但是需要手动缩放。Label3D 和 TextMesh 会自动处理,但是灵活性不足(无法设置最小/最大的文本像素大小)。
- 必须在脚本中考虑到分辨率和纵横比的变化,这可能具有挑战性。
渲染
3D 渲染的局限性
出于对性能的要求,实时渲染引擎有很多局限性。
纹理尺寸限制
PC上,旧设备可能不支持大于 8192×8192 的纹理。在 GPUinfo.org 上检查目标 GPU 的限制。
移动端 GPU 通常限制为 4096×4096 纹理。此外,某些移动端 GPU 不支持非 2 的幂大小的纹理的重复操作。
因此,如果想纹理在所有平台上都能正确显示,则应避免使用大于 4096×4096 的纹理,如果纹理需要重复,则大小应为 2 的幂。
要限制可能因为尺寸太大而无法渲染的特定纹理的大小,你可以将 Process > Size Limit 导入选项设置为大于 0 的值。
这将减少导入时纹理的尺寸(保留纵横比),而不影响源文件。带状颜色
使用 Forward+ 或 Mobile 移动渲染方法时,Godot 的 3D 引擎在内部以 HDR 进行渲染。
但是渲染输出会经过色调映射到低动态范围,以便在屏幕上显示。这可能会导致可见的条带效应,尤其是在使用无纹理的材质时。
出于性能原因,使用 Mobile 移动渲染方法时颜色精度也比使用 Forward+ 时要低。
使用兼容性渲染方法时,不使用 HDR,并且颜色精度是所有渲染方法中最低的。这也适用于 2D 渲染,在 2D 渲染中,使用平滑渐变纹理时可以看到条带。
- 有两个主要的方法来缓解条带:
- 如果使用 Forward+ 或 Forward 移动渲染方法,请在 项目设置 > 渲染 > 抗锯齿 中启用 Use Debanding 。 这会应用全屏去色带着色器作为后期处理效果,并且非常经济合算。
- 将一些噪点烘焙到纹理中。这主要在 2D 中有效,例如用于渐晕(vignetting)效果。在 3D 中,你还可以使用一个自定义去色带着色器应用于你的材质。即使你的项目以低颜色精度渲染,该技术也能发挥作用,这意味着它在使用移动端和兼容性渲染方法时也能发挥作用。
深度缓冲精度
为了在 3D 空间中排序对象,渲染引擎使用了深度缓冲区(也称为 Z 缓冲区)。
这个缓冲区具有有限的精度:在桌面平台上是 24 位,在移动平台上有时是 16 位(出于性能原因)。
如果两个不同的对象最终具有相同的缓冲值,那么就会发生 Z 冲突(Z-fighting),此时移动或旋转相机,将观察到纹理来回闪烁。
为了使深度缓冲在渲染区域上更精确,你应该增加摄像机节点的 Near 属性。
但是要小心,如果你设置得太高,玩家就会看穿附近的几何体。
同时,还应该减少摄像机节点的 Far 属性到你用例允许的最低值,尽管它不会像 Near 属性那样影响精度。
如果你仅在玩家能够看得远时才需要高精度,则可以根据游戏条件动态更改它。
例如,如果玩家进入飞机,则可以暂时增加 Near 属性以避免远处的 Z 冲突( Z-fighting)。玩家离开飞机,就可以减少它。
根据场景和玩家视野条件,你还可以在玩家不会看出差异的情况下将产生z冲突的对象移得更远。透明度排序
在 Godot 中,透明材质是在不透明材质之后绘制的。
透明对象在绘制之前会从后向前排序,排序依据是该 Node3D 的位置,而不是世界空间中顶点的位置。
因此, 互相有重叠的对象可能会出现排序错误的情况。
要修复排序不当的对象,可以调整材质的 渲染优先级 属性,或节点的 排序偏置 属性。
渲染优先级将强制特定材质出现在其他透明材质的前面或后面,而排序偏置将向前或向后移动对象以进行排序。即便如此,这可能也并不总是能解决问题。
一些渲染引擎会使用顺序无关的透明技术来缓解这个问题,但这类技术对于 GPU 而言开销很大。
- Godot 目前没有提供这个功能,但仍然有几种方法可以避免这个问题:
- 只有在你真正需要的时候才让材质透明。
如果一种材质只有一个很小的透明部分,请考虑将它分割成一个单独的材质。
这将允许不透明部分投射阴影,也可以提高性能。 - 如果你的纹理大部分都是完全不透明和完全透明的区域,则可以使用 Alpha 测试而不是 Alpha 混合。
这种透明模式渲染速度更快,并且不会出现透明度问题。
在 StandardMaterial3D 中启用 Transparency > Transparency 至 Alpha Scissor ,并根据需要相应调整 Transparency > Alpha Scissor Threshold 。
请注意,除非在材质属性中启用了 alpha 抗锯齿,否则 MSAA 不会对纹理边缘进行抗锯齿。
但是,无论材质上是否启用了 alpha 抗锯齿功能,FXAA、TAA 和超级采样都能够对纹理边缘进行抗锯齿处理。 - 如果你需要渲染纹理上的半透明区域,Alpha Scissor 就不适用了。
将 StandardMaterial3D 的 Transparency > Transparency 属性设置为 Depth Pre-Pass 有时会有作用(以性能为代价)。
你还可以尝试 Alpha Hash 模式。 - 如果你想让材质随着距离增加而淡出
使用 StandardMaterial3D 的距离淡出模式(distance fade mode)的 Pixel Dither 或 Object Dither 来代替 PixelAlpha。
这将使材质不透明,还可以加快渲染速度。
- 只有在你真正需要的时候才让材质透明。
标准 3D 材质与 ORM 3D 材质
StandardMaterial3D 和 ORMMaterial3D (遮挡、粗糙度、金属)
旨在提供大部分功能无需编写着色器代码,但如果需要附加功能也可以转换为着色器代码。
添加材质到对象有 4 种方法:
- 可以在网格的
Material(材质)属性中添加。该网格每次被使用时,它都会具有该材质。 - 可以在使用该网格的节点(比如 MeshInstance3D 节点)的 Material 属性中添加,该材质将仅由该节点使用,它还将覆盖网格的材质属性。
- 可以使用该网格的节点的 Material Override(材质覆盖)属性中添加,该材质将仅由该节点使用。它还将覆盖节点的常规材质属性和网格的材质属性。
- 可以是 Material Overlay(材质覆盖层)属性中。会在该网格所使用的当前材质上方再渲染一个材质。例如,可以用来在网格上放置半透明护盾效果。
BaseMaterial 3D 设置
StandardMaterial3D 有许多设置来决定材质的外观。所有这些设置属性都属于 BaseMaterial3D 类别
ORM 材质的设置几乎完全相同,只有一处不同:没有单独的环境光遮蔽、粗糙度和金属度贴图设置,而是使用单个 ORM 纹理。
该纹理的不同颜色通道用来对应这三个参数。
像 Substance Painter 和 Armor Paint 这样的程序将为你提供以此格式导出的选项,对于这两个程序,它们具有虚幻引擎的导出预设,该引擎也使用 ORM 纹理。
Transparency(透明)
默认情况下,Godot 中的材质是不透明的。
这样的设置下渲染速度很快,但这也意味着即使在 Albedo > Texture 属性中使用透明纹理(或将 Albedo > Color 设置为透明颜色),材质在视觉上也不是透明的。
为了能让视线透过材质,材质需要设置成透明的。
Godot 提供了几种透明度模式:
- Disabled: 材质不透明。这个设置的渲染速度最快,支持所有渲染功能。
- Alpha: 材质是透明的。半透明区域是通过混合绘制的。这个设置渲染速度很慢,但它允许半透明。
此外,使用 Alpha 混合的材质无法投射阴影,并且在屏幕空间反射中不可见。- Alpha 非常适合粒子效果和 VFX。
- Alpha Scissor(Alpha 裁剪): 材质是透明的。
不透明度低于 Alpha Scissor Threshold(Alpha 裁剪阈值)的半透明区域不会被绘制(高于此阈值的区域则绘制为不透明)。
这个设置比 Alpha 渲染速度更快,并且不会出现透明度排序问题。
它的缺点是会导致“要么有或要么无”的透明度情况,不存在中间值。使用 Alpha 裁剪的材质可以投射阴影。- Alpha Scissor 非常适合树叶和栅栏,因为它们具有硬边,需要正确地区分才能看上去表现不错。
- Alpha Hash: (Alpha 哈希)材质是透明的。
使用颜色抖动绘制半透明区域。这种也是“要么有或要么没有”的透明度,但颜色抖动有助于以有限的精度表示部分不透明的区域,具体取决于视口分辨率。
使用 alpha 哈希的材质可以投射阴影。- Alpha Hash 适合逼真效果的头发,而风格化的头发使用 alpha 裁剪可能效果更好。
- Depth Pre-Pass: (深度预通过)首先通过不透明管道渲染对象的完全不透明像素,然后通过 Alpha 混合渲染其余部分。
这可让透明度排序大部分正确(尽管不完全如此,因为部分透明区域可能仍然表现出不正确的排序)。使用深度预通道的材质可以投射阴影。
备注
如果满足 任何 这些条件,Godot 将自动强制材质通过 alpha 混合变得透明:
将透明度模式设置为 Alpha (如此处所述)。
设置除默认 Mix 之外的混合模式
启用 Refraction 、 Proximity Fade 或 Distance Fade 。
Alpha 混合透明有一些限制:
- Alpha 混合材质的渲染速度明显较慢,尤其是当它们重叠时。
- 当透明表面相互重叠时,Alpha 混合材料可能会出现排序问题。这意味着表面可能会以错误的顺序渲染,后面的表面看起来位于实际上更靠近相机的表面的前面。
- Alpha 混合材质虽然可以接收阴影,但不会投射阴影。
- Alpha 混合材质不会出现在任何反射中(反射探针除外)。
- 屏幕空间反射和锐利的 SDFGI 反射不会出现在 Alpha 混合材质上。启用 SDFGI 后,无论材质粗糙度如何,都会使用粗糙反射作为后备。
在使用 Alpha 透明度模式之前,请始终先考虑其他透明度模式是否更适合你的需求。
Alpha 抗锯齿
这个属性仅当透明度模式为 Alpha Scissor 或 Alpha Hash 时才可见。
虽然 Alpha 裁剪和 Alpha 哈希材质的渲染速度比 Alpha 混合材质更快,但它们会在不透明和透明区域之间渲染出硬边缘。虽然可以使用基于后处理的 抗锯齿技术(例如 FXAA 和 TAA),但这并不总是理想的,因为这些技术往往会使最终结果看起来更模糊或出现重影伪影。
有 3 种 Alpha 抗锯齿模式可用:
- Disabled: 无 alpha 抗锯齿功能。除非使用基于后处理的抗锯齿解决方案,否则透明材质的边缘将出现锯齿。
- Alpha Edge Blend: (Alpha 边缘混合)使不透明和透明区域之间平滑过渡。也称为“alpha to coverage”(alpha 覆盖)。
- Alpha Edge Clip: (Alpha 边缘剪辑)在不透明和透明区域之间产生清晰但仍然有抗锯齿的过渡。也称为“alpha to coverage + alpha to one”(alpha 覆盖 + alpha 到 1)。
当 Alpha 抗锯齿模式设置为 Alpha Edge Blend 或 Alpha Edge Clip 时,新的 Alpha Antialiasing Edge (alpha 抗锯齿边缘值)属性在检查器下方可见。这个属性用于控制阈值,低于该阈值的像素应变为透明。虽然你已经定义了 Alpha 裁剪阈值(仅当使用 Alpha Scissor 时),但此附加阈值用于在不透明和透明像素之间平滑过渡。 Alpha Antialiasing Edge 必须 始终 设置为严格低于Alpha 裁剪阈值的值。当 alpha 裁剪阈值为 0.5 ,alpha 抗锯齿边缘值的默认值 0.3 是比较合理的。但请记住,在修改 alpha 裁剪阈值时,必须调整此调整此 alpha 抗锯齿边缘值。
如果你发现抗锯齿效果不佳,请尝试增加 Alpha Antialiasing Edge ,同时确保其低于 Alpha Scissor Threshold (如果材质使用了 Alpha 裁剪)。另一方面,如果你注意到,随着相机靠近材质,纹理的外观发生明显变化,请尝试减小 Alpha Antialiasing Edge 。
为了获得最佳效果,当使用 Alpha 抗锯齿功能时,应在“项目设置”中将 MSAA 3D 设置为至少 2x。这是因为,此 Alpha 抗锯齿功能依赖于 Alpha 覆盖,而 Alpha 覆盖是 MSAA 提供的功能。如果没有使用 MSAA,则会在材质的边缘应用固定的抖动模式,这对于平滑边缘并不是很有效(尽管也有一点用)。
Blend Mode(混合模式)
控制材质的混合模式。请记住,Mix 以外的任何模式都会强制对象通过透明管道。
- Mix:(混合)默认混合模式,Alpha 控制对象可见的程度。
- Add:(添加)对象的最终颜色会添加到屏幕的颜色中,非常适合耀斑或类似火焰的效果。
- Sub:(减去)从屏幕颜色中减去对象的最终颜色。
- Mul:(相乘) 对象的最终颜色与屏幕的颜色相乘。
- 预乘 Alpha: 对象的颜色应该已经乘以 alpha。当 alpha 为 0.0 时,其行为类似于 Add (完全透明),并且在 alpha 为 1.0(不透明)时类似于混合 。
Cull Mode(剔除模式)
确定渲染背面时不绘制对象的哪一侧:
Back: 当不可见时,对象的背面被剔除(默认)。
Front: 当不可见时,物体的正面被剔除。
Disabled: 用于双面对象(不进行剔除)。
备注
默认情况下,Blender 在材质上禁用背面剔除,导出材质时匹配其在 Blender 中的渲染方式。
这意味着 Godot 中的材质会将其剔除模式设置为 Disabled。
这会降低性能,因为背面将被渲染,即使它们不可见。
要解决此问题,请在 Blender 的材质选项卡中启用 Backface Culling ,然后再次将场景导出为 glTF。
深度绘制模式(Depth Draw Mode)
指定何时必须进行深度渲染。
- 仅不透明(默认,Opaque Only):仅为不透明对象绘制深度。
- 始终(Always):为不透明和透明物体深度绘制。
- 从不(Never):不进行深度绘制(不要将其与下面的无深度测试选项混淆)。
- 深度预处理(Depth Pre-Pass):对于透明物体,首先对不透明部分进行不透明处理,然后在上面绘制透明度。对透明草或树叶使用该选项。
无深度测试(No Depth Test)
为了使近距离物体出现在远处的物体上,进行深度测试。 禁用它会导致对象出现在其他所有内容之上(或之下)。
禁用此选项对于在世界空间中绘制指标最有意义,并且与Material的 Render Priority 属性一起效果很好(请参阅本页底部)。
着色
着色模式
材质支持三种着色模式:逐像素、逐顶点和无着色。
- 逐像素(Per-Pixel)着色模式会为每个像素计算光照,适用于大多数使用场景。但是,在某些情况下,你可能希望通过使用其他着色模式来提高性能。
- 逐顶点(Per-Vertex)着色模式,通常称为”顶点着色”或”顶点光照”,该模式会为每个顶点计算一次光照,然后在像素之间对结果进行插值。
在低端或移动设备上,使用逐顶点光照可以显著提升渲染性能。渲染多层透明度时(例如使用粒子系统时),使用逐顶点着色可以提升性能,尤其是在相机贴近粒子时。
你也可以使用逐顶点光照来实现复古风格的外观。 - 无着色(Unshaded)着色模式完全不会计算光照。直接输出 Albedo (反照率)颜色。光源不会对材质产生任何影响,无光照材质通常会显得比有光照材质亮很多。
无光照渲染在某些特定的视觉效果中非常有用。如果需要最大性能,它也可以用于粒子效果、低端设备或移动设备。
漫反射模式(Diffuse Mode)
指定光线照射到物体时发生漫散射所使用的算法:
- Burley:默认模式,原始的 Disney Principled PBS 漫反射算法。
- Lambert:不受粗糙度的影响。
- Lambert Wrap:当粗糙度增加时,将 Lambert 拓展至覆盖 90 度以上。适用于头发和模拟廉价的次表面散射。这种实现是节能的。
- Toon: 为照明提供硬边缘,光滑度受粗糙度的影响。
建议你从环境的环境光设置中禁用 sky contribution(天空补偿),或在 StandardMaterial3D 中禁用环境光以获得更好的效果。
镜面反射模式(Specular Mode)
指定镜面反射斑点的呈现方式。镜面反射斑点是在对象中反射的光源的形状。
ShlickGGX: 现在 PBR 3D引擎使用的最常见的斑点。
Toon: 创建一个toon blob,根据粗糙度改变大小。
禁用: 有时候blob很烦人。 消失吧!
禁用环境光(Disable Ambient Light)
使物体不会接收任何会照亮它的环境光。
禁用雾
使对象不受基于深度或体积雾的影响。
这对于粒子或其他添加混合的材质很有用,否则它们会显示网格的形状(即使在没有雾的情况下看不见的地方)。
禁用镜面遮挡
使对象在通常被遮挡的地方不减少反射。
顶点颜色(Vertex Color)
此设置允许选择默认情况下如何处理来自 3D 建模应用程序的顶点颜色。默认情况下,它们会被忽略。
Use as Albedo(用作反照率)
选择此选项意味着用顶点颜色作为反射颜色。
是 sRGB(Is sRGB)
大多数 3D 建模软件可能会将顶点颜色导出为 sRGB,因此切换此选项将有助于使它们看起来正确。
Albedo(反照率)
Albedo 是材质的基色,所有其他设置都在其上运行. 设置为 Unshaded 时,这是唯一可见的颜色. 在以前版本的Godot中,这个通道被命名为 Diffuse . 名称的改变主要是因为在PBR(Physically Based Rendering,基于物理渲染)中,这种颜色影响的计算远不止漫射光照路径.
反照率颜色(Albedo Color)可以和纹理一起使用,因为它们会被相乘。
反照率颜色和纹理的 Alpha通道 也用于对象透明度. 如果你使用带 alpha通道 的颜色或纹理,请确保启用透明度或 alpha scissoring 以使其正常工作.
Metallic(金属度)
Godot 使用了金属模型,因为它比别的模型简单得多。该参数定义了材质的反射程度。反射性越高,漫射/环境光对材质的影响就越小,反射的光就越多。这种模型被称为“能量守恒”。
Specular 参数是反射率的一般数量(与 Metallic 不同,能量不守恒,因此请将其保留为 0.5 并且除非你需要,否则不要碰它)。
最小的内部反射率是 0.04,因此不可能使材质完全不产生反射,就像在现实生活中一样。
Roughness(粗糙度)
粗糙度 会影响反射的发生方式. 值 0 使其成为完美的镜子,而 1 的值完全模糊了反射(模拟自然微表面).
最常见的材质类型可以通过 Metallic 和 Roughness 的正确组合来实现.
Emission(自发光)
Emission 指定材质发出的光量(请记住,这不包括环绕几何体的光, 除非使用 VoxelGI 或 SDFGI)。
此值将添加到生成的最终图像中,并且不受场景中其他光照的影响。
法线贴图
法线贴图允许你设置一个代表更精细形状细节的纹理,这不会修改几何体,只会修改光的入射角.
在Godot中,为了更好的压缩和更广泛的兼容性,只使用了法线贴图的红色和绿色通道.
备注
Godot 需要法线贴图使用 X+、Y+、Z+ 坐标,即 OpenGL 风格。如果你导入了用于其他引擎的材质,它可能使用的是 DirectX 风格,那么就需要对法线贴图的进行转换,翻转 Y 轴。
弯曲法线贴图
弯曲法线贴图描述环境光照的平均方向。
与常规法线贴图不同,这用于改进材质对光照的反应方式,而不是添加表面细节。
这可以通过两种方式实现:
- 间接漫反射照明旨在更紧密地匹配全局照明。
- 如果启用了镜面反射遮挡,则使用弯曲法线和环境光遮挡来计算,而不仅仅是根据环境光进行计算。这包括屏幕空间环境光遮挡 (SSAO) 和其他环境光遮挡源。
Godot 仅使用弯曲法线贴图的红色和绿色通道,以获得更好的压缩和更广泛的兼容性。
创建弯曲法线贴图时,它需要满足三件事才能在 Godot 中正常工作:
- 烘烤时必须使用射线的余弦分布 。
- 纹理必须在切线空间中创建。
- 弯曲的法线贴图需要使用 X+、Y+ 和 Z+ 坐标,这称为 OpenGL 样式。如果您导入了用于其他引擎的材质,则它可能是 DirectX 风格,在这种情况下,需要转换弯曲的法线贴图,以便翻转其 Y 轴。这可以通过将 通道重新映射(Channel Remap) 部分下的绿色通道设置为 导入停靠栏中的倒置绿色 。
弯曲法线贴图不同于常规法线贴图。两者不可互换。
Rim(边缘)
一些织物具有小的微毛,导致光在其周围散射.
Godot使用 Rim 参数模拟它.
与仅使用发射通道的其他边缘照明实施方式不同,这实际上考虑了光(没有光意味着没有边缘). 这使得效果显著地更加可信.
边缘大小取决于粗糙度,并且有一个特殊参数来指定它必须如何着色。如果染色(Tint)为 0,则使用光的颜色作为边缘。如果染色(Tint)为 1,则使用材质的反照色。使用中间的值通常效果最好。
Clearcoat(清漆)
Clearcoat 参数用于为材质添加辅助的透明涂层。
这在汽车油漆和玩具中很常见。在实践中,它是在现有材质之上添加的较小的镜面反射斑点。
Anisotropy(各向异性)
这会更改镜面反射斑点的形状并将其与切线空间对齐。各向异性通常与头发一起使用,或使诸如拉丝铝之类的材质更加逼真。与流向贴图结合使用时效果特别好。
Ambient Occlusion(环境光遮蔽)
可以指定烘焙的环境遮挡贴图. 此贴图会影响有多少环境光到达物体每个表面(默认情况下它不会影响直接光). 虽然可以使用屏幕空间环境遮挡(Screen-Space Ambient Occlusion,SSAO)来生成环境遮挡,但没有什么能比良好烘焙的AO贴图的质量更好. 建议尽可能烘焙环境遮挡.
Height(高度)
在材质上设置高度贴图会做一个光线步进搜索,以模拟沿视图方向的凹陷的正确位移。这不会增加真正的几何体,而是创建一种深度的幻觉——想了解用于物理碰撞(如地形)的高度贴图形状,请查看 HeightMapShape3D。它可能不适用于复杂的物体,但会为纹理产生逼真的深度效果。为获得最佳效果,Height 应与法线贴图一起使用。
Subsurface Scattering(次表面散射)
该功能仅适用于 Forward+ 渲染器,不适用于 Mobile 或 Compatibility 渲染器。
此效果模拟穿透物体表面,散射然后散出的光. 创造逼真的皮肤,大理石,有色液体等有用.
Back Lighting(背光照明)
这可以控制有多少光从被点亮的一侧(正对灯光)传输到暗侧(背对灯光). 这适用于植物叶子,草,人耳等薄物体.
Refraction(折射)
当启用折射时,Godot 会尝试从正在渲染的对象后面获取信息。这允许以类似于现实生活中折射的方式扭曲透明度。
记住使用透明的反照率纹理(或减少反照率颜色的 alpha 通道)使折射可见,因为折射依赖于透明度才能产生可见效果。
折射也会考虑材质的粗糙度。粗糙度越高,受折射影响的物体看起来越模糊,这模拟了现实生活中的行为。如果开启了折射且降低了Albedo的透明度后,依然看不见物体后方,可通过降低材质的粗糙度解决。
可以选择在 Refraction Texture 属性中指定法线贴图,以便在每个像素的基础上扭曲折射方向。
备注
折射效果是作为屏幕空间效果实现的,并强制材料透明。这使得效果相对较快,但也造成了一些限制:
透明度排序 可能会出现问题。
折射材料不能折射到自身或其他透明材料上。在另一种透明材料后面的折射材料是不可见的。
屏幕外的物体无法出现在折射效果中。这在折射强度值较高时最为明显。
折射材料前面的不透明材料将看起来具有 "折射 "边缘,尽管它们不应该出现这种边缘。
细节(Detail)
Godot允许使用辅助反射和法线贴图生成细节纹理,可以通过多种方式进行混合. 通过将其与二级UV或三平面模式相结合,可以实现许多有趣的纹理.
有几种设置可以控制细节的使用方式.
- 遮罩(Mask):细节遮罩是一张黑白图像,用于控制纹理上的混合位置。白色用于细节纹理,黑色用于常规材质纹理,不同深浅的灰色用于材质纹理和细节纹理的部分混合。
- 混合模式: 有四种模式控制纹理的混合方式.
- 融合: 合并两个纹理的像素值. 黑色时,仅显示材质纹理;白色时,仅显示细节纹理. 灰色的值在两者之间创建一个平滑的混合.
- 相加: 将一个纹理与另一个纹理的像素值相加. 与融合模式不同的是,两个纹理在蒙板的白色部分而不是灰色部分完全混合. 原始纹理在黑色部分基本没有变化
- 相减: 将一个纹理的像素值与另一个纹理的像素值相减. 第二种纹理在蒙版的白色部分被完全减去,在黑色部分只被减去一点,灰色部分根据具体实际纹理减去不同的程度.
- 相乘: 将上方纹理中每个像素的 RGB 通道数与下方纹理中相应像素的值相乘.
- Albedo: 在此处放置要混合的反射纹理. 如果此插槽中没有任何内容,则默认情况下将其解释为纯白.
- 法线: 在此处放置需要混合的法线纹理. 如果这个槽中没有任何东西,它将被解释为一个平坦的法线贴图. 即使材质未启用法线贴图也可以使用这个槽.
UV1 和 UV2
Godot每种材质支持两个UV通道. 二级UV通常可用于环境遮挡或发射(烘焙的光照).
UV可以缩放和偏移,这在使用重复纹理时很有用.
三平面映射(Triplanar Mapping)
UV1 和 UV2 都支持三平面映射。这是获得纹理坐标的另一种方法,有时称为“自动纹理”。纹理在 X、Y、Z 中采样,通过法线混合。可以在世界空间或对象空间中执行三平面映射。
在下图中,你可以看到所有图元如何与世界三平面共享相同的材质,因此砖纹理在它们之间平滑地继续。
世界三平面(World Triplanar)
使用三平面映射时(见下文,在 UV1 和 UV2 设置中),它是在对象局部空间中计算的。此选项使三平面映射使用世界空间。
采样
过滤器
材质使用的纹理过滤方法。请参阅 this page 获取完整的选项列表及其说明。
重复
材质使用的纹理是否重复,以及重复的方式。请参阅 this page 获取完整的选项列表及其说明。
阴影
不接受阴影(Do Not Receive Shadows)
使对象不会接收任何可能会被投射到其上的阴影。
使用阴影到不透明度(Use Shadow to Opacity)
光照会改变alpha值,阴影部分是不透明的,而没有阴影的地方是透明的。 对于AR中将阴影堆叠到一个照相机反馈中很有用。
公告板
公告板模式(Billboard Mode)
启用公告板模式来绘制材质。这控制对象如何面向相机:
已禁用(Disabled):公告板模式已被禁用。
已启用(Enabled):公告板模式已启用。对象的 -Z 轴将始终面向相机的观察平面。
Y 公告板(Y-Billboard):物体的 X 轴将始终与相机的观察平面对齐。
粒子公告板(Particle Billboard):最适合粒子系统,因为它允许指定翻页动画。
仅当公告板模式为粒子公告板(Particle Billboard)时,粒子动画(Particles Anim)部分才可见。
公告板保持比例(Billboard Keep Scale)
启用在公告板模式下缩放网格。
生长(Grow)
沿法线指向的方向增长对象顶点:
这通常用于创建廉价的轮廓. 添加第二个material pass,使其变为黑色,无阴影(unshaded),反向剔除(Cull Front),并添加一些增长:
要使 Grow 功能按预期工作,网格必须具有共享顶点的连接面,即”平滑着色”。如果网格具有独立顶点的断开面,即”平面着色”,使用 Grow 时网格会出现缝隙。
Transform
固定大小(Fixed Size)
这使得无论距离如何,对象都以相同的大小呈现。 这主要用于指示物(无深度测试和高渲染优先级)和某些类型的广告牌。
使用点大小(Use Point Size)
此选项仅在渲染的几何体由点组成时有效(通常从3D 建模软件中导入时由三角形组成)。如果是这个情况,那么这些点可以被调整大小(见下文)。
点大小(Point Size)
绘制点时,指定点的大小,单位为像素。
使用粒子轨迹 (Particle Trails)
如果为 true,则启用 GPUParticles3D 轨迹所需的着色器部分功能。这还需要使用具有适当蒙皮的网格体,例如 RibbonTrailMesh 或 TubeTrailMesh。在 GPUParticles3D 网格中使用的材质之外启用此功能将中断材质渲染。
使用 Z Clip Scale
将渲染的对象缩放到摄像机上,以避免夹入墙壁等物体。这旨在用于相对于摄像机固定的对象,例如玩家手臂、工具等。
调整此设置后,光照和阴影将继续正常工作,但 SSAO 和 SSR 等屏幕空间效果可能会因比例较低而中断。因此,尽量保持此设置尽可能接近 1.0。
使用 FOV Override
覆盖 Camera3D 的视场角(以度为单位)。
这就像在 Camera3D 上设置视野一样,使用 Camera3D.keep_aspect 设置为 Camera3D.KEEP_HEIGHT。此外,在忽略视野设置的非透视相机上,它可能看起来不正确。
Proximity and Distance Fade(邻近和距离淡入淡出)
Godot允许材质通过彼此接近以及取决于与观察者的距离而隐去。邻近淡入淡出(Proximity Fade)对于诸如软粒子或大量水平滑地过渡到海岸很有效。
距离渐变适用于在一定距离后才出现的光轴或指示器。
请注意,使用 Pixel Alpha 模式启用近距离渐变或远距离渐变时,会启用 Alpha 混合。Alpha 混合对 GPU 的要求较高,可能会导致透明度排序问题。Alpha 混合也会禁用许多材质功能,例如阴影投射。
当角色距离相机太近时,想隐藏角色可考虑使用 Pixel Dither 或更好的 Object Dithe (比Pixel Dither更快).
Pixel Alpha 模式:对象身上像素的透明度会随相机距离而改变。效果最好,但会直接跳过常规渲染管线,强制把材质送入透明处理阶段 (这会导致一些问题,比如:物体无法产生阴影).
像素抖动模式:该模式通过仅渲染一小部分像素来近似透明度。
对象抖动模式:与前一种模式类似,但计算出的透明度在整个对象的表面是相同的。
材质设置
渲染优先级(Render priority)
可以更改对象的渲染顺序,尽管这对于透明对象有用(或执行深度绘制但没有颜色绘制的不透明对象,例如地板上的裂缝).
物体首先按照不透明/透明队列进行排序,然后根据 render_priority,优先级越高绘制越晚。透明物体还会根据深度进行排序。
深度测试优先于渲染优先级。仅靠优先级无法强制不透明物体相互覆盖绘制。
Next Pass(下一阶段)
在材质上设置 next_pass 将导致物体使用该下一个材质再次渲染。
材质会根据不透明/透明队列进行排序,然后按 render_priority 排序,优先级越高绘制越晚。
除非使用了 grow 设置或其他顶点变换,否则两种材质之间的深度测试将相等。多个透明通道应使用 render_priority 来确保正确排序。
3D 灯光和阴影
场景可以有很多不同类型的光源:
- 来自材质本身的自发光颜色(但是无法影响附近的对象,除非进行了烘焙,或者启用了屏幕空间间接光照)。自发光是材质的属性。
- 灯光节点:DirectionalLight3D、OmniLight3D、SpotLight3D。
- Environment 或 反射探针 中的环境光。
- 全局光照(LightmapGI、VoxelGI、SDFGI)。
灯光节点
灯光节点有三种:
DirectionalLight3D、OmniLight3D、SpotLight3D。
让我们来看看灯光的通用参数:
- Color:发光的基础颜色。
- Energy:能量乘数。这对于使灯光饱和或使用 高动态范围光照 非常有用。
- Indirect Energy:间接能量。用于间接光(反弹的光)的次级乘数。适用于 使用光照贴图全局照明、VoxelGI 和 SDFGI。
- Volumetric Fog Energy:体积雾能量。用于体积雾的次级乘数。仅在启用体积雾时有效。
- Negative: (减色)光变为减色而不是添加。对于手动补偿一些黑暗角落有时很有用。
- Specular:镜面反射。影响受此光影响的物体中镜面反射斑点的强度。值为零时,该光变为纯漫反射光。
- Bake Mode:设置灯光的烘焙模式。见 使用光照贴图全局照明。
- Cull Mask: (剔除遮罩)在下面选定的图层中的物体将受到此光的影响。请注意,通过这个剔除遮罩禁用的对象仍然会投射阴影。
如果你不希望被禁用的物体投射阴影,请将 GeometryInstance3D上的 Cast Shadow 属性调整为所需的值。如果你希望使用真实世界的单位来配置灯光的强度和色温,请参阅 物理灯光和相机单位。
灯光数量限制
- 使用 Forward+ 渲染器时,Godot 使用集群方法进行实时光照。可以添加任意数量的灯光(只要性能允许)。
但是,当前相机视图中可以存在的集群元素的默认上限仍为 512 个。
集群元素是指全向灯、聚光灯、贴花或反射探针。
可以通过调整项目设置 > 渲染 > 限制 > 集群构建器的 最大集群元素数 来增加该上限。 - 使用 Mobile 渲染器时,每个网格资源有 8 个 OmniLight 加 8 个 SpotLight 的限制。
此外,在当前相机视图中可渲染的 OmniLight 和 SpotLight 数量限制为 256 个。这些限制目前无法更改。 - 使用兼容渲染器时,每个网格资源最多可渲染 8 个 OmniLight 加 8 个 SpotLight。
此限制可以在 渲染 > 限制 > OpenGL 的高级项目设置里调整 最大可渲染元素数 和/或 单对象最大光源数 来提升,但会牺牲性能和延长着色器编译时间。
可以通过减少此限制来减少着色器编译时间并略微提升性能。
在所有的渲染方法中,最多可以同时显示 8 个 DirectionalLight。
但是,每增加一个启用阴影的 DirectionalLight,都会降低每个 DirectionalLight 的有效阴影分辨率。这是因为所有灯光共享方向阴影图集。
如果超过了渲染限制,灯光就会在摄像机移动过程中跳进跳出,这可能会分散注意力。
在灯光节点上启用 Distance Fade 有助于减少这一问题,同时还能提高性能。将网格分割成更小的部分也会有所帮助,尤其是关卡几何体(这也能提高剔除效率)。
如果你需要渲染的灯光数量超过了给定渲染器所能提供的数量,请考虑使用 烘焙光照贴图,并将灯光的烘焙模式设置为 静态 。
这样就可以完全烘焙光照,从而加快渲染速度。你也可以使用任何全局光照技术的自发光材质来替代在大范围内发光的灯光节点。
阴影贴图
灯光可以可选地投射阴影. 这使它们具有更好的真实感(光线不会照到被遮挡的区域),但它会带来更大的性能开销.
有一个通用阴影参数列表,每个参数也有一个特定的功能:
- Enabled: 启用此灯光下的阴影贴图。
- Opacity: (不透明度)被遮挡的区域会因该不透明度系数而变暗。
默认情况下,阴影是完全不透明的,但是可以更改此设置,以使阴影对于给定的光线来说是半透明的。 - Bias:(偏置)当此参数太小时,阴影会打在物体自己身上。当太大时,阴影会与物体本体分开。请调整到最适合你的状态。
- Normal Bias:当此参数太小时,阴影会打在物体自己身上。当太大时,阴影会与物体本体分开。请调整到最适合你的状态。
- Transmittance Bias:(透射率偏置)当此参数太低时,启用透射率的材质上,阴影会打在物体自己身上。
如果太高,阴影将不会影响始终启用透射率的材质。请调整到最适合你的状态。 - Reverse Cull Face:反转表面剔除,当阴影贴图使用反转表面剔除渲染时,在某些场景表现更好。
- Blur(模糊):倍增该灯光的阴影模糊半径。
这适用于传统阴影贴图和接触硬化阴影(角度距离(Angular Distance)或大小(Size)大于 0.0 的灯光)。
数值越大,阴影越柔和,对于移动的物体来说,阴影在时间上也会显得更加稳定。
增加阴影模糊的缺点是,它会让用于滤波的颗粒图案更加明显。另请参阅 阴影过滤模式。 - Caster Mask(投射遮罩): 只有在这些图层中的对象才能投射阴影。注意,这个遮罩不会影响阴影投射到哪些对象上。
调整阴影偏置
下图是调整偏置的图像。默认值适用于大多数情况,但通常来说,它取决于几何的大小和复杂程度。
如果给定灯光的 Shadow Bias 或 Shadow Normal Bias 设置得太低,阴影就会 “涂抹 “到物体上。
这将导致光线的预期外观变暗,称为 阴影失真 (shadow acne):
另一方面,如果给定光线的 Shadow Bias 或 Shadow Normal Bias 设置得太高,阴影可能看起来与物体脱节。这被称为阴影悬浮(peter-panning):
一般来说,增加 Shadow Normal Bias 比增加 Shadow Bias 更可取。
增大 Shadow Normal Bias 不会像增大 Shadow Bias 那样导致更多的阴影悬浮,但仍能有效解决大多数阴影失真问题。
增加 Shadow Normal Bias 的缺点是会使某些物体的阴影看起来更薄。
任何偏置问题都可以通过 提高阴影贴图分辨率 来解决,尽管这可能会导致性能下降。
外观更改注意事项:在光源上启用阴影时,请注意,与在兼容性渲染器中渲染无阴影时相比,光源的外观可能会发生变化。
由于旧移动设备的限制,阴影是使用多通道渲染方法实现的,因此带有阴影的光源在 sRGB 空间而不是线性空间中渲染。
渲染空间的这种变化有时会极大地改变灯光的外观。要获得与无阴影光源相似的外观,您可能需要调整光源的能量设置。
平行光
计算中最便宜的光,应该尽可能使用(虽然它不是计算起来最便宜的阴影贴图,但这点稍后再说).
平行光模拟覆盖整个场景的无限数量的平行光线。
平行光节点由指示光线方向的大箭头表示。但是,节点的位置根本不会影响照明,它可以在任何地方。
每个表面的正面被光线照射,而其他部分则保持黑暗。与大多数其他类型的光不同,平行光没有特定的参数。
定向光源还提供 角距离(Angular Distance) 属性,该属性确定光源的角度大小(以度为单位)。将其增加到 0.0 以上 会在距离施法者较远的地方使阴影更柔和,同时还 影响程序化天空材质中太阳的外观。这称为 接触硬化阴影(也称为 PCSS)。
作为参考,从地球看太阳的角距离约为 0.5。这种阴影的性能消耗资源较高,因此如果在启用阴影的灯光上将此值设置为高于 0.0,请查看 PCSS 建议 中的建议。
方向光阴影贴图
为了计算阴影贴图,从覆盖整个场景(或最大距离)的正交角度渲染场景(仅深度)。
但是,这种方法存在一个问题,因为靠近相机的物体接收到的低分辨率阴影可能看起来是块状的。
为了解决这个问题,我们使用了一种名为平行分割阴影贴图(PSSM,Parallel Split Shadow Maps)的技术。
这将视锥体分割成 2 个或 4 个区域。每个区域都有自己的阴影贴图。这使得靠近观察者的小区域可以具有与远处巨大区域相同的阴影分辨率。
当为 DirectionalLight3D 启用阴影时,默认阴影模式为具有 4 个分割的 PSSM。
在对象大到足以出现在所有四个分割区域中的情况下,它会导致绘制调用增加。
具体来说,这样的对象将被总共渲染五次:四个阴影分割各渲染一次,最终场景渲染一次。
这可能会影响性能,理解该行为对于优化场景和管理性能预期非常重要。
有了它,阴影变得更加详细:
为了控制PSSM,暴露了许多参数:
每个分割距离都是相对于相机最远处进行控制的(如果大于 0 ,则为阴影 Max Distance(最大距离) )。0.0 是眼睛位置,1.0 是阴影在一定距离处结束的位置。分割介于两者之间。默认值通常效果很好,但一般会调整第一个分割数值,以便为近处对象提供更多细节(比如第三人称游戏中的角色)。
请务必根据场景需要设置阴影的 Max Distance 。最大距离越小,阴影效果越好,性能也越高,因为阴影渲染中需要包含的物体越少。你还可以调整 Fade Start 来控制远处阴影淡出距离的强度。对于 Max Distance 完全覆盖任何给定摄像机位置的场景,可以将 Fade Start 增加到 1.0 ,以防止阴影在远处渐变。在 Max Distance 没有完全覆盖场景的场景中,不应该这样做,因为阴影会在远处突然消失。
有时,一个分割与下一个之间的过渡看起来很糟糕。要解决此问题,可以打开 Blend Splits (混合分割)选项,牺牲细节和性能以换取更平滑的过渡:
Shadow > Normal Bias 参数可用于修复当对象垂直于光线时自阴影的特殊情况。唯一的缺点是它会使阴影变得更薄。在大多数情况下,在增加 Shadow > Bias 之前,请考虑增加 Shadow > Normal Bias 。
最后,如果未细分网格的大型对象出现了阴影缺失的情况,可以调整 Pancake Size(压平区大小)属性来修复。只有在发现阴影缺失与阴影偏置问题无关时,才可以更改此值。
全向光
全向光是一种点光源,可在所有方向上发射光,直至给定的半径。
在现实生活中,光衰减是个和距离成反比的函数,这意味着全向光没有半径。这是一个问题,因为这意味着计算几个全向光会变得很困难。
为了解决这个问题,引入了 Range (范围)参数和衰减函数。
这两个参数允许调整其在视觉上的工作方式,以便找到美学上令人愉悦的结果.
OmniLight3D 中还提供了“ 大小” 参数。增加此值将使光源淡出速度变慢,并且当远离施法者时阴影显得更模糊。这可用于在一定程度上模拟区域光。这称为接触硬化阴影(也称为 PCSS)。这种影子是 价格昂贵,因此请查看 PCSS 建议如果将此值设置为上述值 0.0 在启用了阴影的光源上。
全向光阴影贴图
全向光的阴影贴图相对简单。需要考虑的主要问题是用于渲染它的算法。
全向阴影可以渲染为双抛物面或立方体映射。 双抛物面渲染速度快,但会导致变形,而立方体 更正确,但速度较慢。
默认值为 立方体(Cube),但对于视觉上没有太大差异的光源,请考虑将其更改为 双抛物面(Dual Paraboloid)。
如果渲染的对象大部分是不规则且细分的,那么 Dual Paraboloid (双抛物线)通常就足够了。
无论怎么说,由于这些阴影被缓存在阴影图集中(后面会详细介绍), 对于大多数场景而言,它可能不会对性能产生影响。
启用阴影功能的全向灯光可以使用投影。投影纹理会将灯光的颜色乘以纹理上给定点的颜色。
因此,一旦分配了投影纹理,灯光通常会显得更暗;你可以增加 Energy 来弥补这一点。
全方位光投影纹理需要特殊的 360° 全景贴图,类似于 PanoramaSkyMaterial 纹理。
通过下面的投影纹理,可以得到以下结果:
小技巧
如果你已获得立方体贴图形式的全方位投影图像,你可以使用 这个基于网络的转换工具 将它们转换为单一的全景图像。
聚光
聚光与全向光类似,但是它们只发光到锥形(或“截断”)中。
用于模拟手电筒、车灯、反射器、聚光灯等。这种类型的光也会向其指向的相反方向衰减。
聚光和OmniLight3D共用相同的 Range(范围)、 Attenuation(衰减)和 Size(大小),并添加了两个额外参数:
- Angle(角度)光线的光圈角度。
- Angle Attenuation(角度衰减)锥形衰减,有助于柔化锥形边界。
聚光灯阴影贴图
光斑具有与阴影贴图全向光源相同的参数。
与全向光源相比,渲染点阴影贴图的速度要快得多,因为只需要渲染一个阴影纹理(而不是渲染 6 个面,或在双抛物面模式下渲染 2 个面)。
启用阴影功能的光线可以使用投影。投影纹理会将灯光的颜色乘以纹理上给定点的颜色。因此,一旦分配了投影纹理,灯光通常会显得更暗;你可以增加 Energy 来弥补这一点。
与全向光投影不同,聚光灯投影纹理不需要遵循特殊格式就能看起来正确无误。它的映射方式类似于贴花。
通过下面的投影纹理,可以得到以下结果:
广角聚光灯的阴影质量会低于窄角聚光,因为阴影贴图会分布在更大的表面上。角度大于 89 度时,聚光阴影将完全停止工作。如果你需要更宽的灯光阴影,请使用全向光。
阴影图集
与具有自己的阴影纹理的平行光不同,全向光和聚光被分配了阴影图集的槽位。该图集可以在高级项目设置( 渲染 > 灯光和阴影 > 位置阴影 )中进行配置。
这个分辨率适用于整个阴影图集。该图集分为四个象限:
每个象限可以细分,分配任意数量的阴影贴图。以下是默认细分方式:
阴影图集分配空间如下:
最大阴影贴图尺寸(未使用细分时)代表屏幕尺寸(或更大)的灯光。
细分(较小的贴图)表示距离视图较远并且比例较小的灯光的阴影。
每一帧,以下过程被应用于所有光:
检查灯光是否在正确大小的插槽上. 如果没有,重新渲染它并将其移动到更大/更小的插槽.
检查影响阴影贴图的任何对象是否已更改. 如果是的话,重新渲染光线.
如果上述情况均未发生,则不执行任何操作,阴影保持不变.
如果一个象限中的槽位满了,光线会被推回到更小的槽位中,取决于大小和距离。如果所有象限中的所有槽位都已满,则某些灯光即使启用了阴影,也将无法渲染阴影。
默认的阴影分配策略最多可以渲染 88 盏灯,并在相机锥体中启用阴影(4 + 4 + 16 + 64):
第一象限也是最精细的象限,可以存储 4 个阴影。
第二象限可存储 4 个其他阴影。
第三象限可存储 16 个阴影,但细节较少。
第四象限也是细节最少的象限,可存储 64 个阴影,但细节更少。
每个象限使用较多的阴影数量可以支持启用阴影的更多灯光,同时还能提高性能(因为阴影将以较低的分辨率为每个灯光渲染)。不过,增加每个象限的阴影数量的代价是降低阴影质量。
在某些情况下,你可能想要使用不同的分配策略。例如,在自上而下看的游戏中,所有灯光的大小都大致相同,你可能希望将所有象限设置为具有相同的细分,以便所有灯光都具有相似质量级别的阴影。
平衡性能与质量
阴影渲染是 3D 渲染性能方面的重要议题。做出正确的选择非常重要,这样才能避免制造出瓶颈。
平行光的阴影质量设置可以在运行时通过调用合适的 RenderingServer 方法进行更改。
位置光(全向光/聚光)的阴影质量设置可以在运行时通过根 Viewport 进行更改。
阴影贴图大小
高阴影分辨率会带来更清晰的阴影,但性能会大幅下降。还应注意,更清晰的阴影并不总是更逼真。在大多数情况下,应将其保持为默认值 4096 或针对低端 GPU 降低至 2048。
如果在减小阴影贴图大小后,位置阴影变得过于模糊,可以通过调整 shadow atlas 象限以包含较少的阴影来解决。这样就能以更高的分辨率渲染每个阴影。
阴影过滤模式
这里可以选择多种阴影贴图质量设置。默认的 Soft Low 在性能和质量之间取得了很好的平衡,适用于有细节纹理的场景,因为纹理细节有助于使颜色抖动的纹路不那么明显。
不过,在纹理细节较少的项目中,颜色抖动的纹路可能会更加明显。要隐藏这种纹路,可以启用 时间抗锯齿(TAA)、AMD FidelityFX Super Resolution 2.2 (FSR2)、快速近似抗锯齿(FXAA) 或将阴影滤镜质量提高到 Soft Medium 或更高。
Soft Very Low 设置会自动减少阴影模糊,使低采样数产生的伪影不那么明显。相反,Soft High 和 Soft Ultra 设置会自动增加阴影模糊,以更好地利用增加的样本数。
16 位与 32 位
默认情况下,Godot 使用 16 位深度纹理进行阴影贴图渲染。在大多数情况下,我们都建议使用这种方式,因为它的性能更好,而且质量也不会有明显差异。
如果禁用 16 Bits,则将使用 32 位色深的纹理。这样做可以减少大型场景和启用阴影的大型灯光中的伪影。不过,这种差异通常几乎不明显,但却会带来显著的性能损失。
灯光/阴影的距离淡出
OmniLight3D 和 SpotLight3D 提供了一些能够隐藏远距离灯光的属性。如果是在大型场景中,并且存在几十盏灯,就能够显著提升性能。
Enabled:(启用)控制是否启用距离淡入淡出( LOD 的一种形式)。 光线将在 Begin + Length 内淡出,之后它将被剔除并且根本不会发送到着色器。 使用它可以减少场景中活动灯光的数量,从而提高性能。
Begin:(开始)光线开始消失时距相机的距离(以 3D 单位表示)。
Shadow:(阴影)阴影开始消失时距相机的距离(以 3D 单位表示)。 与光线相比,这可用于更快地淡出阴影,从而进一步提高性能。 仅当为灯光启用阴影时才可用。
Length: (长度)光线和阴影淡出的距离(以 3D 单位表示)。 光线在这段距离内慢慢变得更加透明,最后完全不可见。 值越高,淡出过渡越平滑,这样的配置在相机快速移动时更合适。
PCSS 建议
百分比接近软阴影 (Percentage-closer soft shadows,PCSS) 会提供更真实的阴影贴图外观,半影( penumbra )大小根据光源(caster)与接收阴影的表面之间的距离而变化。 这会带来很高的性能成本,特别是对于平行光而言。
为了避免性能问题,建议:
仅在给定时间里使用少量启用了 PCSS 阴影的灯光。 这种效果通常在大而明亮的灯光下最为明显。 较微弱的辅助光源通常不会从使用 PCSS 阴影中获益。
为用户提供禁用 PCSS 阴影的设置。 在平行光上,这可以通过在脚本中将 DirectionalLight3D 的 light_angular_distance 属性设置为 0.0 来完成。 对于位置光源,这可以通过在脚本中将 OmniLight3D 或 SpotLight3D 的 light_size 属性设置为 0.0 来完成。
投影器过滤模式
投影的渲染方式也会对性能产生影响。 渲染> 纹理 > 光投影器 > 过滤 高级项目设置允许你控制投影纹理如何过滤。 Nearest/Linear 不使用 mipmap,这使得渲染速度更快。 然而,投影在远处看起来会有颗粒感。 Nearest/Linear Mipmaps 在远处看起来会更平滑,但从倾斜角度观看时投影看起来会模糊。 这可以通过使用 Nearest/Linear Mipmaps Anisotropic 来解决,这是最高质量的模式,但也是消耗最高的。
如果你的项目具有像素艺术风格,请考虑将过滤器设置为 Nearest 的值之一,以便投影使用最近邻过滤(nearest-neighbor filtering)。 否则,请继续使用 Linear 。
使用贴花
贴花仅在 Forward 和 Forward Mobile 渲染器中受支持,在兼容性渲染器中则不受支持。
如果使用兼容性渲染器,请考虑使用 Sprite3D 作为将贴花投影到(大部分)平坦表面上。
贴花是应用于 3D 不透明或透明表面的投影纹理。
该投影是实时生成的,不依赖于网格生成。 这允许你在每一帧移动贴花,即使在复杂的网格上应用时,也只会对性能产生很小的影响。
虽然贴花无法将实际的几何细节添加到投影表面上,但贴花仍然可以利用基于物理的渲染来提供与完整的 PBR 材质类似的属性。
使用案例:
- 静态装饰
- 动态游戏元素
- 片面阴影
在编辑器中创建贴花
在 3D 编辑器中创建贴花节点。
在检查器中,展开 Textures 部分并在 Textures > Albedo 中加载纹理。
将贴花节点移向对象,然后旋转它以使贴花可见(并且方向正确)。 如果贴花看起来是镜像的,请尝试将其旋转 180 度。 你可以通过将 Parameters > Normal Fade 增加到 0.5 来仔细检查其方向是否正确。 这将防止贴花投影到不面向贴花的表面上。
如果你的贴花仅影响静态对象,请将其配置为防止影响动态对象(反之亦然)。 为此,请更改贴花的 Cull Mask (剔除蒙版)属性以排除某些图层。 执行此操作后,修改动态对象的 MeshInstance3D 节点以更改其可见性层。 例如,你可以将它们从第 1 层移动到第 2 层,然后在贴花的 Cull Mask 属性中禁用第 2 层。
贴花节点属性:
Extents:(范围)贴花的大小。 Y 轴决定贴花投影的长度。 保持投影长度尽可能短,以增加剔除机会,从而提高性能。
- 纹理
- Albedo:(反照率)用于贴花的反照率(漫反射/颜色)贴图。
在大多数情况下,这是你要首先设置的纹理。 如果使用法线或 ORM 贴图,则 必须 设置反照率贴图以提供 Alpha 通道。
该 Alpha 通道将用作遮罩,以确定法线/ORM 贴图对底层表面的影响程度。 - Normal:(法线)用于贴花的法线贴图。 这可用于通过修改光对其的反应方式来增加贴花上的感知细节。 该纹理的影响会乘以反照率纹理的 Alpha 通道(但不是 Albedo Mix )。
- ORM:用于贴花的遮挡(Occlusion)/粗糙度(Roughness)/金属贴图(Metallic map)。 这是用于存储 PBR 材质贴图的优化格式。 环境光遮挡贴图存储在红色通道中,粗糙度贴图存储在绿色通道中,金属贴图存储在蓝色通道中。 该纹理的影响会乘以反照率纹理的 Alpha 通道(但不是 Albedo Mix )。
- Emission:(自发光)用于贴花的自发光纹理。与 Albedo 不同,此纹理看起来会在黑暗中发光。
- Albedo:(反照率)用于贴花的反照率(漫反射/颜色)贴图。
- 参数
- Emission Energy:(自发光能量)自发光纹理的亮度。
- Modulate:(调制)将反射率和自发光贴图的颜色相乘。通过这种方式对贴花进行着色(例如,对于绘画贴花)或通过随机化每个贴花的调制来增加多样性。
- Albedo Mix:(反照率混合)反照率纹理的不透明度。与使用具有更透明 alpha 通道的反照率纹理不同,将该值降低到 1.0 以下 不会 减少法线/ORM 纹理对下表面的影响。在创建仅普通/ORM 贴花(如足迹或湿水坑)时,将此值设置为 0.0。
- Normal Fade:(法线衰减)当贴花的 AABB (Axis-Aligned Bounding Box, 轴对齐边界框)
与目标表面之间的角度变得过大时,贴花将会淡出。值为 ``0.0时,不管角度如何都会投影出贴花,而值为 0.999 时,贴花将仅限于几乎垂直的表面。由于额外的法线角度计算,将 Normal Fade 设置为大于 0.0 的值会带来一些性能损耗。
- 垂直淡化(Vertical Fade)
- Upper Fade:(上部淡化)随着表面远离 AABB 中心(朝向贴花的投影角),贴花将逐渐淡出的曲线。 只有正值才有效。
- Lower Fade:(下部淡化)随着表面远离 AABB 中心(远离贴花的投影角),贴花将逐渐淡出的曲线。 只有正值才有效。
- 距离淡出
- Enabled(启用):控制是否启用距离淡出(LOD 的一种形式)。 贴花将在 Begin + Length 内淡出,之后它将被剔除并且根本不会发送到着色器。 使用它可以减少场景中活动贴花的数量,从而提高性能。
- Begin:(开始)贴花开始淡出时距相机的距离(以 3D 单位表示)。
- Length:(长度)贴花淡出的距离(以 3D 单位表示)。 贴花在这段距离内逐渐变得透明,最后完全不可见。 值越高,淡出过渡越平滑,这在相机快速移动时更适合。
- Cull Mask(剔除遮罩)
- Cull Mask:(剔除遮罩)指定此贴花将投影到哪些 VisualInstance3D 图层。 默认情况下,贴花会影响所有图层。 使用它可以指定哪些类型的对象接收贴花,哪些类型不接收贴花。这个功能特别有用,你可以确保动态对象不会意外收到针对其下方地形的贴花。
贴花的渲染顺序
默认情况下,贴花的渲染顺序是基于他们的 AABB 和相机的距离排序的。
距离相机更近的AABB会优先被渲染,这意味着如果有些贴花处于相同的位置,那贴花的渲染顺序有时会随相机位置的变化而变化。
为解决贴花重叠导致的闪烁问题,你可以调整贴花 Node 检查器下的 VisualInstance3D 中的 Sorting Offset 属性。这个 offset 不是一个严格的优先级排序,而是一个准则,虽然渲染器会依据该准则决定渲染顺序,但贴花排序方式仍会受到 AABB 大小的影响。因此,offset 值更高的贴花总是会被绘制在 offset 值更低的贴花上。
如果你想让某个贴花总是渲染在其他贴花上,你需要将该贴花的Sorting Offset属性设置成正值,该值要大于可能与其重叠的其他最大贴花的AABB长度。相反,如果想让该贴花绘制在其他贴花后,将Sorting Offset设置为负值即可。
调整性能和质量
贴花渲染性能主要取决于其屏幕覆盖范围及其数量。
一般来说,覆盖大部分屏幕的一些大贴花的渲染消耗,会比散布在各处的许多小贴花的渲染消耗更高。
要提高渲染性能,你可以如上所述启用 Distance Fade 属性。
这将使远处的贴花在远离相机时淡出(并且可能对最终场景渲染几乎没有影响)。 使用节点组,你还可以根据用户配置防止生成非必要的装饰贴花。
贴花的渲染方式也会对性能产生影响。 渲染 > 纹理 > 贴花 > 过滤 高级项目设置可让你控制如何过滤贴花纹理。 Nearest/Linear 不使用 mipmap。 然而,贴花在远处看起来会有颗粒感。 Nearest/Linear Mipmaps 在远处看起来会更平滑,但从倾斜角度观看时贴花会看起来模糊。 这可以通过使用 Nearest/Linear Mipmaps Anisotropic 来解决,它提供最高的质量,但渲染速度也较慢。
如果你的项目具有像素艺术风格,请考虑将过滤设置为 Nearest 值之一(即具有 Nearest 属性的任意一个过滤),以便贴花使用最近邻过滤(nearest-neighbor filtering)。 否则,请继续使用 Linear 。
限制
贴图不能影响除上面列出的材质特性之外的材质特性,例如高度(用于视差贴图)。
出于性能方面的考虑,贴花使用的是完全固定的渲染逻辑,也就是说贴花无法使用自定义着色器。
然而,投影面上的自定义着色器能够读取被贴花覆盖的信息,例如表面的粗糙度和金属性。
使用 Forward+ 渲染器时,Godot 使用集群方法进行贴花渲染。可以添加任意数量的贴花(只要性能允许)。
但是,当前相机视图中可以存在的集群元素的默认上限仍为 512 个。
集群元素是指全向灯、聚光灯、贴花或反射探针。可以通过调整渲染 > 限制 > 集群构建器中的最大集群元素数高级项目设置来增加该上限。
在使用移动渲染器时,每个单独的 Mesh 资源 上只能应用 8 个贴花。如果有更多贴花影响单个网格,并非所有贴花都会在该 Mesh 上渲染。
物理灯光和相机单位
为什么使用物理灯光和相机单位?
Godot 对许多适用于光的物理属性(如颜色、能量、相机视野和曝光)使用任意单位(arbitrary units)。
默认情况下,这些属性使用任意单位,因为使用精确的物理单位会带来一些权衡,这对于许多游戏来说是不值得的。
由于 Godot 在默认情况下注重易用性,因此默认情况下禁用物理光单元。
物理单位的优点
如果你的目标是在项目中实现照片级真实感,那么使用现实世界的单位作为基础可以帮助你更轻松地进行调整。 有关现实世界材质、灯光和场景亮度的参考资料可以在 Physically Based 等网站上找到。
当从其他使用物理光单位(如 Blender)的 3D 软件移植场景时,在 Godot 中使用现实世界单位也会很有用。
物理单位的缺点
使用物理光单位的最大缺点是你必须密切注意在给定时间使用的动态范围。 将非常高的光强度与非常低的光强度混合时,可能会遇到浮点精度错误。
实际上,这意味着你必须手动管理曝光设置,以确保场景不会过度曝光或曝光不足。 自动曝光可以帮助你平衡场景中的光线,使其在正常范围内,但它无法恢复因动态范围过高而损失的精度。
使用物理光和相机单位不会自动使你的项目看起来 更好 。 有时,远离现实主义的表现实际上可以使场景在人眼看来更好。 此外,与非物理单位相比,使用物理单位需要更严格的要求。 只有正确设置物理单位以匹配现实世界的参考,才能获得使用物理单位的大多数好处。
备注
物理光单位仅在 3D 渲染中可用,在 2D 渲染中不可用。
设置物理灯光单元
物理光单位可以与物理相机单位分开启用。
要正确启用物理光单位,需要 4 个步骤:
- 使用项目设置。
- 配置运行。
- 配置环境。
- 配置 Light3D 节点。
由于物理光和相机单位仅需要少量计算来处理单位转换,因此启用它们不会对 CPU 产生任何明显的性能影响。 然而,在 GPU 方面,物理相机单元目前会强制使用景深。 这对性能有中等影响。 为了减轻这种性能影响,可以在高级项目设置中降低景深质量。
启用项目设置
打开项目设置,启用 高级设置 开关,然后启用 渲染>灯光与阴影>使用物理光线单位 。 重新启动编辑器。
配置相机
警告
当物理光单位启用时,如果你的场景中有一个 WorldEnvironment 节点(即编辑器环境被禁用),你 必须 将一个 CameraAttributes 资源分配给 WorldEnvironment 节点。 否则,如果你有可见的 DirectionalLight3D 节点,3D 编辑器视口将显得极其明亮。
在 Camera3D 节点上,你可以将 CameraAttributes 资源添加到其 Attributes 属性。 该资源用于控制相机的景深和曝光。 使用 CameraAttributesPhysical 时,其焦距属性也用于调整相机的视野(field of view)。
启用物理光单位后,CameraAttributesPhysical 的 Exposure 部分中将提供以下附加属性:
Aperture: (光圈)相机光圈的大小,以光圈值(f-stop)为单位测量。 光圈值是相机焦距与光圈直径之间的无单位比率。 高光圈设置将导致较小的光圈,从而导致图像更暗和焦点更清晰。 低光圈会导致大光圈,从而让更多的光线进入,从而产生更亮、聚焦度更低的图像。
Shutter Speed: (快门速度)快门打开和关闭的时间,以 倒数秒 (1/N) 为单位测量。 较低的值将允许更多的光线进入,从而导致图像更亮,而较高的值将允许更少的光线进入,从而导致图像更暗。 当使用脚本获取或设置此属性时,单位为秒,而不是秒的倒数。
Sensitivity: (灵敏度)相机传感器的灵敏度,以 ISO 为单位测量。 灵敏度越高,图像越亮。 当启用自动曝光时,这可以用作曝光补偿的方法。 该值加倍将使曝光值(以 EV100 测量)增加 1 级。
Multiplier: (乘数) 非物理 曝光乘数。 较高的值将增加场景的亮度。 这可用于后期处理调整或制作动画目的。
默认 Aperture 值的16 光圈值适合白天户外使用(即与默认 DirectionalLight3D 一起使用)。 对于室内照明情况,2 到 4 之间的值更合适。
摄影和电影制作中使用的典型快门速度为 1/50(0.02 秒)。 夜间摄影一般使用1/10(0.1秒)左右的快门,而运动摄影则使用1/250(0.004秒)至1/1000(0.001秒)之间的快门速度以减少运动模糊。
在现实生活中,白天户外摄影根据天气情况,感光度通常设置在 50 ISO 到 400 ISO 之间。 较高的值则用于室内或夜间摄影。
备注
与现实生活中的相机不同,Godot 中不会模拟提高 ISO 感光度或降低快门速度(例如可见颗粒或灯光尾迹)后产生的不利影响。
请参阅 设置物理相机单位 以了解 CameraAttributesPhysical 属性的描述,这些属性在 不 使用物理光单位时也可用。
配置环境
警告
默认配置是针对白天户外场景设计的。 夜间和室内场景需要调整 DirectionalLight3D 和 WorldEnvironment 背景强度才能看起来正确。 否则,位置光源(即点光源)在默认强度下几乎不可见。
如果你尚未将 WorldEnvironment 和 Camera3D 节点添加到当前场景,请立即单击 3D 编辑器视口顶部的 3 个垂直点来添加。 单击 将太阳添加到场景 ,再次打开对话框,然后单击 将环境添加到场景 。
启用物理光单位后,可以在 Environment 资源中编辑一个新属性:
Background Intensity:(背景强度)背景天空的强度,单位为尼特(坎德拉每平方米)。 如果环境光和反射光各自的模式设置为 Background ,这也会影响它们。 如果设置了自定义 Background Energy ,则该能量将乘以强度。
配置灯光节点
在启用物理光单位后,Light3D 节点中有 2 个新属性可用:
Intensity:(强度)光的强度,单位为勒克斯(DirectionalLight3D) 或流明(OmniLight3D/SpotLight3D) 。 如果设置了自定义 Energy ,则该能量将乘以强度。
Temperature: (色温)光的 色温 以开尔文(Kelvin)为单位定义。 如果设置了自定义 Color ,则该颜色将乘以色温。
OmniLight3D/SpotLight3D 强度
流明是光通量的度量,是光源每单位时间发出的可见光总量。
对于 SpotLight3D,我们假设可见锥体外部的区域被完美的光吸收材料包围。 因此,锥体区域的表观亮度 不会 随着锥体尺寸的增大和减小而改变。
典型的家用灯泡的亮度范围约为 600 流明至 1200 流明。 一支蜡烛的亮度约为 13 流明,而一盏路灯的亮度约为 60000 流明。
DirectionalLight3D 强度
勒克斯是单位面积光通量的度量,等于每平方米一流明。 勒克斯是在给定时间内照射到表面的光量的度量。
使用 DirectionalLight3D 情况下,在晴朗的晴天,阳光直射下的表面可能会接收到大约 100000 勒克斯。 家中的一个典型房间可能接收到大约 50 勒克斯的亮度,而月光照射下的地面可能接收到大约 0.1 勒克斯的亮度。
色温
6500 开尔文是白色。 较高的值会导致较冷(偏蓝)的颜色,而较低的值会导致较暖(偏橙色)的颜色。
阴天的太阳的色温约为 6500 开尔文。 晴天时,太阳的色温在 5500 至 6000 开尔文之间。 在晴朗的日子里,当日出或日落时,太阳的色温约为 1850 开尔文。
色温图,从 1,000 开尔文(左)到 12,500 开尔文(右)
Energy 和 Color 等其他 Light3D 属性在出于制作动画目的以及偶尔需要创建具有非真实属性的灯光时仍可编辑。
设置物理相机单位
物理相机单位可以与物理光单位分开启用。
在将 CameraAttributesPhysical 资源添加到 Camera3D 节点的 Camera Attributes 属性后,FOV 等属性将不再可编辑。相反,现在这些属性由 CameraAttributesPhysical 的属性来控制,如焦距和光圈。
CameraAttributesPhysical 在其 Frustum(截锥体)部分提供以下属性:
Focus Distance:(焦距距离)相机到将处于焦点的物体的距离,以米为单位测量。内部将被限制为至少比 Focal Length 大 1 毫米。
Focal Length:(焦距)相机镜头与相机光圈之间的距离,以毫米为单位测量。控制视野和景深。较大的焦距将导致较小的视野和较窄的景深,意味着较少的物体会处于焦点。较小的焦距将导致较宽的视野和较大的景深,这意味着更多的物体会处于焦点。此属性会覆盖相机的 FOV 和 Keep Aspect 属性,使它们在检视器中为只读。
Near/Far:以米为单位的近裁剪和远裁剪距离。这些与 Camera3D 具有同名属性的行为相同。较小的 Near 值允许相机显示非常近的物体,但可能会在远处出现精度(Z 冲突)问题。较大的 Far 值允许相机看到更远的距离,但可能会导致远处出现精度(Z 冲突)问题。
默认焦距 35 毫米对应广角镜头。与默认的 75 度 “实用 “垂直视场角相比,它仍然会导致视场角明显变窄。这是因为在电影制作和摄影等非游戏使用情况下,更倾向于使用较窄的视场角,以获得更具电影感的外观。
在电影制作和摄影中常用的焦距数值有:
鱼眼(超广角):低于 15 毫米。几乎看不到景深。
广角:介于 15 毫米到 50 毫米之间。景深减小。
标准:介于 50 毫米到 100 毫米之间。标准景深。
长焦:大于 100 毫米。增加景深。
就像使用 保持高度(Keep Height)纵横比模式时一样,有效视场取决于视口的纵横比,较宽的纵横比将自动导致更宽的水平视场。
在Auto Exposure(自动曝光)部分,还可以根据相机的平均亮度级别启用自动曝光调节,具有以下属性:
最小灵敏度:相机可以达到的最暗亮度,以 EV100 为单位测量。
最大灵敏度:相机可以达到的最亮亮度,以 EV100 为单位测量。
速度:自动曝光效果的速度。影响相机执行自动曝光所需的时间。较高的值可以实现更快的过渡,但根据场景的不同,得到的调整可能会显得分散注意力。
比例:自动曝光效果的比例。影响自动曝光的强度。
EV100 是在 ISO 100 感光度下测量的曝光值(EV)。请参考此表获取在实际生活中常见的 EV100 值。
粒子系统(3D)
在Godot中每个粒子系统由两个主要部分组成:粒子和发射器。
有两种类型的三维粒子系统:
GPUParticles3D
CPU 粒子系统相对不灵活,但适用于更广泛的硬件,为旧设备和手机提供更好的支持。
性能不如 GPU 粒子系统,并且无法渲染出尽可能多的单个粒子。此外,CPU 粒子系统目前不具备 GPU 粒子控制的所有可用选项。CPUParticles3D
GPU粒子系统在GPU上运行,并且可以在现代硬件上渲染成十几万个粒子。你可以为其编写自定义粒子着色器,使其非常灵活。
还可以通过使用吸引子节点和碰撞节点,使它们与环境进行交互。
有三种粒子吸引器节点:GPUParticlesAttractorBox3D、GPUParticlesAttractorSphere3D 和 GPUParticlesAttractorVectorField3D。
吸引器节点对其作用范围内的所有粒子施加力,并根据该力的方向将它们拉近或推开。
有几种粒子碰撞节点:GPUParticlesCollisionBox3D 和 GPUParticlesCollisionSphere3D 是较简单的节点。
用来创建基本形状,以便粒子与其碰撞。另外两个节点提供了更复杂的碰撞行为:GPUParticlesCollisionSDF3D室内场景与粒子发生碰撞,无需手动创建所有单独的盒子和球体碰撞器时。GPUParticlesCollisionHeightField3D 粒子与大型室外场景发生碰撞,会创建一个包含世界和其中对象的高度图,并将其用于大规模粒子碰撞。
创建 3D 粒子系统
添加 GPUParticles3D 节点设置两个参数:
Process Material材质
ParticleProcessMaterial 是一种特殊的材质。
不是用于绘制对象,是在 GPU 上更新粒子数据和行为,而不是 CPU,这可以带来巨大的性能提升。
单击新添加的材质会显示一个长长的属性列表,可以设置这些属性来控制粒子的行为。Draw Pass绘制通道
选择新建 QuadMesh 并将其 Size 的 x 和 y 设为 0.1
每个粒子系统最多可以使用 4 个绘制通道,每个通道可以渲染一个不同的网格,并使用自己独特的材质。
所有绘制通道都使用由处理材质计算的数据,这是一种用于组合复杂效果的高效方法:一次计算粒子的行为,然后将其提供给多个渲染通道。
要在不支持现代图形 API 的较旧设备上发布游戏时,可能有必要将 GPU 粒子转换为 CPU 粒子。
在转换过程中失去的一些显著特性包括:多重绘制通道,湍流,子发射器,尾迹,吸引器,碰撞。
还会丢失以下属性:
数量比(Amount Ratio)
插值至结尾(Interp to End)
阻尼作为摩擦(Damping as Friction)
发射形状偏移(Emission Shape Offset)
发射形状缩放(Emission Shape Scale)
继承速度比(Inherit Velocity Ratio)
速度轴心(Velocity Pivot)
方向速度(Directional Velocity)
径向速度(Radial Velocity)
速度限制(Velocity Limit)
随速度缩放(Scale Over Velocity)
3D 粒子系统属性
- 发射器属性
- Emitting 用于激活和停用粒子系统。
- Amount 属性控制着在任何时间下可见粒子的最大数量。
- Amount Ratio 属性是粒子与要射出的粒子数量的比例。
小于 1.0,则生命周期内发射的粒子数量将为 Ammount * Amount Ratio。
对于制作发射粒子的数量随时间变化的效果来说这个属性很有用。 - Sub Emitter
可以将另一个粒子节点设置为子发射器,它将作为每个粒子的子节点生成。
有关如何将子发射器添加到粒子系统的详细说明,请参阅本手册中的子发射器部分。
- 时间属性(Time)
- Lifetime 属性控制每个粒子存在时间长短,秒为单位。
许多粒子属性都可以在粒子生命周期内设置而变化,从一个值平滑过渡到另一个值。
Lifetime 和 Amount 是相联的,决定粒子发射速率:每秒粒子数 = Amount / Lifetime - Interp to End 属性会使节点中的所有粒子在其生命周期结束时进行插值。
- One Shot 将会发射 amount 数量的粒子,然后自行禁用,会仅运行一次。
一次性粒子非常适合用于单个事件做出反应的特效,例如,物品拾取或子弹击中墙壁时迸发出的碎片。 - Preprocess 如果值为 1 ,则粒子系统开始运行时,将看起来已经运行了一秒钟。用于跳过 “装载” 时间。
- Speed Scale 属性来减慢或加快粒子系统的移动速度速度。
- Explosiveness 属性控制粒子是顺序发射还是同时发射。值为 0 表示粒子一个接一个地发射。值为 1 表示所有 amount 粒子同时发射,从而使效果看起来更具爆炸性。
- Randomness 属性为粒子发射时间添加随机性。当 Explosiveness 设置为 1 时,该属性无效。
- Fixed FPS 属性限制了粒子系统的处理频率,包括属性更新以及碰撞和吸引器。这可以有效提高性能,特别是在大量使用粒子碰撞的场景中。
- Interpolate 属性会在更新之间混合粒子属性,因此即使粒子系统以 10 FPS运行,看起来也像是 60 FPS运行一样平滑。
使用 粒子碰撞 时,如果粒子移动速度快且碰撞器较薄,则可能会发生隧道效应。这可以通过增加固定 FPS(以性能为代价)来解决。
- Lifetime 属性控制每个粒子存在时间长短,秒为单位。
- 碰撞属性(Collision)
- Base Size 属性定义了每个粒子的默认碰撞大小,用于检查粒子当前是否与环境发生碰撞。
通常希望这个大小与粒子的大小大致相同。对于非常小且移动非常快的粒子,增加此值可以防止其穿过碰撞几何体。
- Base Size 属性定义了每个粒子的默认碰撞大小,用于检查粒子当前是否与环境发生碰撞。
设置粒子碰撞需要按照《3D粒子碰撞》中描述的进一步步骤进行。
- 绘制属性(Drawing)
- Visibility AABB 属性定义了一个围绕粒子系统原点的盒子。
只要这个盒子的任意部分在相机的视野内,粒子系统就是可见的,一旦离开相机视野,粒子系统将停止渲染,可以尽可能使盒子较小来提高性能。- Local Coords 所有粒子计算都使用局部坐标系来确定诸如上下、重力和运动方向等。
例如,上下会跟随粒子系统或其父节点而旋转。当未选中该属性时,这些计算将使用全局世界空间:在世界空间中,向下始终为 -Y ,不跟随粒子系统旋转。- Draw Order 属性控制着单个粒子绘制时的顺序。
- Index 代表以发射顺序绘制:即后生成的粒子会绘制在先生成的粒子的顶层。
- Lifetime 代表以剩余生命周期顺序绘制。Reverse Lifetime 则是反转了 Lifetime 的绘制顺序。
- View Depth 代表以与摄像机的距离顺序绘制:距离摄像机更近的粒子绘制在更远的粒子顶层。
- Transform Align 属性控制粒子的默认旋转。
- Disabled 意味着它们不以任何特定的方式对齐,相反,它们的旋转由处理材质设置的值决定。
- Z-Billboard 表示粒子将始终面向相机,类似于 Standard Material 的 Billboard 属性。
- Y to Velocity 表示每个粒子的 Y 轴与其移动方向对齐,这对于子弹或箭矢之类的物体很有帮助,因为你希望粒子一直指向“前方”。
- Z-Billboard + Y to Velocity 结合了前两种模式。每个粒子的 Z 轴将指向相机,而Y 轴与速度对齐。
- 尾迹属性(Trail)
- Enabled 属性控制粒子是否渲染为尾迹。
- Length Secs 属性控制尾迹应该被发射多长。该持续时间越长,尾迹就会越长。
- 处理材质属性(Process Material)
- Lifetime Randomness 属性控制应用于每个粒子生命周期的随机性程度。
- 粒子标志(Particle Flags)
- Align Y 属性将每个粒子的 Y 轴与其速度对齐,与
Transform Align属性设置为 Y to Velocity 相同。- Rotate Y 属性与 Angle 和 Angular Velocity 组中的属性配合使用,以控制粒子旋转。如果要对粒子应用任何旋转,则必须启用 Rotate Y。
例外情况是使用Standard Material 的任何粒子,其中 Billboard 属性设置为 Particle Billboard。没启用 Rotate Y,粒子也会旋转。- Disable Z 属性时,粒子将不会沿 Z 轴移动。将使用局部 Z 轴还是世界 Z 轴由 局部坐标 属性确定。
- Daming as Friction 属性将阻尼行为从恒定减速度更改为基于速度的减速度。
- 出生(Spawn)
- 位置(Position)
- 发射形状(Emission Shape) 粒子可以从空间中的单个点发射,也可以以填充形状的方式发射。
- 角度(Angle)
- Angle 属性控制粒子的起始旋转。必须启用以下两个属性之一:Rotate Y 使粒子围绕粒子系统的 Y 轴旋转。
在 标准材质 中的 Billboard 属性,如果设置为 Particle Billboard ,则使粒子围绕从粒子指向相机的轴旋转。- 速度(Velocity)
- Initial Velocity Ratio 粒子初始速度的百分比。
- Velocity Pivot 粒子的旋转轴。
- 方向(Direction) 设置速度或加速度属性后生效。
- Spread 属性为每个粒子的方向添加了一些变化和随机性。值越高,偏离原始路径的程度就越强。
- Flatness 属性限制沿 Y 轴的扩散。0 的值表示没有限制,1 的值将消除沿 Y 轴的所有粒子移动。粒子将完全“水平”的地展开。
- Initial Velocity 属性控制粒子的初始速度。分为 Velocity Min 和 Velocity Max ,两者默认均设置为 0 ,这就是你最初看不到任何移动的原因。一旦你为这些属性中的任何一个设置了值 如上所述,粒子就开始移动了。方向会被这些值乘以,因此,通过设置负速度,你可以使粒子向相反方向移动。
- 动画速度(Animation Speed)
- 角速度(Angular Velocity) 控制粒子的旋转速度,如上文所述 。你可以通过使用 Velocity Min 或 Velocity Max 的负数值来反转方向。像 Angle 属性一样,只有当设置了 Rotate Y 位标记或在 Standard Material 中选择了 Particle Billboard 模式时,旋转才会可见。
Damping 属性对角速度没有影响。
- 加速度(Acceleration)
- 重力(Gravity)
接下来的几个属性分组紧密协作,控制粒子的运动和旋转。Gravity 将粒子向其所指的方向拉动,默认情况下是直接向下,强度等于地球的重力。重力影响所有粒子的运动。如果你的游戏使用物理,并且世界的重力可以在运行时更改,你可以使用这个属性来保持游戏的重力与粒子重力同步。如果未设置其他运动属性,Gravity 值 (X=0,Y=0,Z=0) 意味着没有粒子将永远移动。- 线性加速度(Linear Accel)
粒子的速度是一个恒定值:一旦设置,它就不会改变,粒子将始终以相同的速度移动。你可以使用 Linear Accel 属性来改变粒子生命周期中的移动速度, 如上文所述 。正值将加快粒子速度,使其移动得更快。负值将减慢其速度,直至停止并开始向反方向移动。
我们必须牢记,当我们改变加速度时,我们并不是直接改变速度,我们改变的是速度的 变化 。加速度曲线上的 0 值并不会停止粒子的运动,它会停止粒子运动的变化。在那一刻,无论它的速度是多少,它将保持那个速度继续移动,直到加速度再次改变。- 径向加速度(Radial Accel)
为所有粒子添加了类似重力的力,该力的起源位于粒子系统当前位置。负值使粒子向中心移动,就像行星对其轨道上物体的重力一样。正值使粒子远离中心。- 切向加速度(Tangential Accel)
这个属性在粒子系统XZ平面上的圆的切线方向上添加了粒子加速度,该圆的原点位于粒子系统的中心,半径是每个粒子当前位置与系统中心的距离投影到该平面上。
圆的切线是与圆 “接触 ”的直线,在接触点与圆的半径成直角。粒子系统XZ平面上的圆是你从上方直接向下看粒子系统时所看到的圆。
始终限制在该平面内,并且不会沿系统的Y轴移动粒子。一个粒子的位置足以定义这样一个圆,如果我们忽略向量的Y分量,到系统中心的距离就是半径。
将使粒子围绕粒子系统的中心轨道运动,但半径会不断增加。从上方看,粒子将从中心向外螺旋移动。负值会反转方向。- 阻尼(Dampin)
逐渐停止所有运动。除非总加速度大于阻尼效果粒子将继续减速直到完全不动。值越大,将粒子完全停止所需的时间就越少。- 吸引器相互作用(Attractor Interaction)
如果你想让粒子系统与 particle attractors 互动,你必须勾选 Enabled 属性。当它被禁用时,粒子系统会忽略所有的粒子吸引器。- 显示(Display)
- 缩放(Scale) 控制粒子的大小。你可以为 Scale Min 和 Scale Max 设置不同的值,以随机化每个粒子的大小。
不允许负值,所以你无法用这个属性翻转粒子。
如果你将粒子作为公告板发射,你的绘制阶段中的 Standard Material 的 Keep Size 属性必须启用,缩放才能生效。- 颜色曲线(Color Curves) 控制粒子的初始颜色。只有在 Material 的 Vertex Color 的 Use As Albedo 属性启用后生效。
这个属性与来自粒子材质自己的 Color 或 Texture 属性的颜色相乘。
- Color Ramp 属性在粒子的生命周期内改变粒子的颜色。会遍历定义的所有颜色范围。
- Color Initial Ramp 属性从颜色坡道上的随机位置选择粒子的初始颜色。
- Alpha Curve 属性控制粒子的透明度。
- Emission Curve 颜色曲线控制粒子的初始颜色。
- 色相变化(Hue Variation)
像 Color 属性一样控制粒子的颜色,但是方式不同。它不是直接设置颜色值,而是通过改变颜色的色相来实现。无法体现颜色有多亮或有多饱和。
将其设为较高的值会导致颜色变化更加剧烈,而较低的值则限制可用颜色为原始的最近邻颜色。- 动画(Animation)
控制粒子的标准材质中精灵表动画的行为。- 湍流(Turblence) 湍流为粒子运动添加了噪声,能够创造出生动有趣的图案。可以用来控制运动速度、噪声图案以及对粒子系统的总体影响。
你可以在粒子湍流章节找到这些属性的详细解释。- 碰撞(Collision) Mode 属性控制发射器如何以及是否与粒子碰撞节点发生碰撞。
- Disabled 以禁用此粒子系统的任何碰撞。
- Hide On Contact ,粒子在碰撞时会立即消失。
- Constant ,粒子碰撞后会反弹。你会在检查器中看到两个新属性。它们控制粒子在碰撞事件期间的行为。
较高的 摩擦 值会减少沿表面的滑动。这在粒子与倾斜表面碰撞时特别有用,如果你希望它们保持在原地而不是滑到底部,比如雪花落在山上。
较高的 反弹 值会使粒子在与表面碰撞时像橡胶球一样反弹。
如果启用了 Use Scale 属性,碰撞基础大小 将乘以粒子的 当前缩放 。你可以使用这个属性来确保渲染大小和碰撞大小对于随机缩放或随时间变化的粒子相匹配。- 子发射器(Sub Emitter) Mode 属性控制子发射器如何以及何时生成。
- Disabled,则永远不会生成子发射器。
- Constant ,子发射器会以恒定的速率持续生成。
- Frequency 属性控制一秒钟内发生的次数。将模式设置为 At End ,子发射器将在父粒子生命周期结束时生成,正好在它被销毁之前。Amount At End 属性控制将生成多少个子发射器。
- At Collision ,粒子与环境碰撞时子发射器会生成。
- Amount At Collision 属性控制将生成多少个子发射器。
- 当启用了保持速度(Keep Velocity)属性时,新生成的子发射器一开始就具有父粒子在子发射器创建时的速度。
![]()
- 设置具有至少一次绘制过程的粒子系统,并为该绘制过程中的网格分配一个 Standard Material。
- 将精灵表指定给 Albedo 中的 Texture 属性
- 将材质的 Billboard 属性设置为 Particle Billboard。这样就可以用材质中的 Particles Anim 分组了。
- 将 H Frames 设置为精灵表中的列数,将 V Frames 设置为精灵表中的行数。
- 如果你希望动画持续重复,请勾选 Loop。
- 处理材质属性(Process Material)中Animation 属性的 Speed 属性控制精灵表动画的速度。
将 Speed Min 和 Speed Max 都设置为 1 ,你应该能看到动画播放了。Offset 属性控制新生成的粒子动画的起始位置。
默认情况下,它总是序列中的第一张图片。你可以通过改变 Offset Min 和 Offset Max 来增加一些变化,以随机化起始位置。
三个不同的粒子系统使用相同的烟雾精灵表
根据精灵表包含的图像数量以及粒子的存活时间情况,你的动画可能看起来会并不流畅。
粒子的存活时间、动画速度和精灵表中图像数量之间的关系是这样的:当动画速度为 1.0 时,动画将在粒子生命周期结束时,播放到序列中的最后一个图像。
如果你的精灵表包含 64 (8x8) 个图像,并且粒子的生命周期设置为 1 秒,则动画在 64 FPS (1 秒 / 64 个图像)下将非常流畅。
如果将生命周期设置为 2 秒,则在 32 FPS 下仍将相当流畅。但是,如果粒子存活 8 秒,则在 8 FPS 下动画将明显卡顿。
为了使动画再次流畅,你需要将动画速度增加到 3 之类的速度以达到可接受的帧速率。
请注意,GPUParticles3D 节点的 Fixed FPS 也会影响动画播放。
为了动画播放流畅,建议将其设置为 0,以便在每个渲染帧上模拟粒子。
如果这个设置不适合你的用例,请将 Fixed FPS 设置为等于翻页动画使用的有效帧速率(请参阅上面的公式)。
粒子子发射器
- 创建一个粒子系统,并将其分配给子发射器。
找到父级上的 Sub Emitter 属性,然后点击旁边的框,分配子发射器。
实例化场景中的粒子系统也可以设置为子发射器,只要在实例化场景中启用了 Editable Children 属性即可。
反之亦然:你可以将子发射器分配给实例化场景中的粒子系统,即使是来自其他实例化场景的粒子系统。 - 发射器模式
当你分配子发射器时,你不会立即看到它生成。默认情况下,发射处于禁用状态,需要先启用。
将 ParticleProcessMaterial 的 Sub Emitter 分组中的 Mode 属性设置为 Disabled 以外的其他属性。
发射器模式还决定产生多少个子发射器粒子。
Constant 以 Frequency 属性设置的频率生成单个粒子。
对于 At End 和 At Collision ,你可以直接使用 Amount At End 和 Amount At Collision 属性设置发射量。 - 限制
要记住的一件事是,来自子发射器的活动粒子总数始终受到子发射器粒子系统上的 Amount 属性的限制。如果你发现从子发射器生成的粒子数量不足,则可能需要增加粒子系统中的数量。当粒子系统作为子发射器生成时,某些发射器属性将被忽略。例如,Explosiveness 属性不起作用。根据发射器模式的不同,粒子要么以固定的间隔依次生成,要么一次爆炸性全部生成。
3D 粒子尾迹
Godot 提供了几种类型的尾迹,你可以将其添加到粒子系统中。
在使用尾迹之前,你需要先设置几个参数:
- 创建一个新的粒子系统并分配一个如前所述的处理材质。
- 勾选粒子系统的 Trails ,并将 Lifetime 设置为 0.8 来增加自发光持续时间。
- 在处理材质上,将 Direction 设置为 (X=0,Y=1.0,Z=0) ,并将其 Initial Velocity 设置为 10.0。
- 唯一仍然缺少的是绘制阶段的网格。你在此处设置的网格类型控制你最终将获得哪种类型的粒子尾迹。
带状尾迹
最简单的粒子尾迹类型是带状尾迹。
- 导航到 Draw Passes 部分,然后从 Pass 1 选项中选择 新建RibbonTrailMesh。
- 为 Material 属性分配一个新的 Standard Material,并在 Transform 属性中启用 Use Particle Trails。
- 带状网格 Shape 参数有两个选项。Cross 会创建两个垂直的四边形,使粒子尾迹更加三维。
只有当你不在 Particle Billboard 模式下绘制尾迹时,这才有意义,并且在从不同角度查看粒子时会有所帮助。
Flat 选项将网格限制为单个四边形,最适合公告板粒子。 - Size 参数控制尾迹的宽度。用它来使尾迹更宽或更窄。
- Sections、Section Length 和 Section Segments 共同用于控制粒子尾迹的平滑程度。当粒子尾迹不沿直线行进时,它所具有的节越多,它在弯曲和旋转时看起来就越平滑。Section Length 控制每个节的长度。将该值乘以节数即可知道该尾迹的总长度。
- Section Segments 参数进一步将每个部分细分为段。不过,它对尾迹部分的平滑度没有影响。相反,它控制粒子尾迹整体形状的平滑度。Curve 属性定义此形状。点击“曲线”旁边的框,然后指定或创建新曲线。尾迹的形状将与曲线一样,曲线的值在尾迹的头部为 1.0 ,曲线的值在尾部为 1.0。
由不同曲线形成的粒子尾迹。尾迹从左向右移动。
根据曲线的复杂程度,当部分数较少时,粒子尾迹的形状看起来不会非常平滑。这就是 Section Segments 属性的用武之地。增加节段的数量会在尾迹的两侧添加更多折点,以便它可以更紧密地跟随曲线。
颗粒尾迹形状平滑度:每节 1 段(顶部),每节 12 段(底部)
管状尾迹
管状尾迹与带状尾迹共享许多属性。它们之间的最大区别在于管状轨迹发出圆柱形网格而不是四边形。
管状尾迹发出圆柱形粒子
要创建管状尾迹,请导航到 Draw Passes 部分,然后从 Pass 1 选项中选择 New TubeTrailMesh。一个 TubeTrailMesh 是一个圆柱体,它被分成几个部分,然后沿着这些部分拉伸和重复。为 Material 属性分配一个新的 Standard Material,并在 Transform 属性分组中启用 Use Particle Trails。粒子现在应该以长圆柱形轨迹发射。
关键管状网格参数
Radius 和 Radial Steps 属性之于管状尾迹,就像 Size 之于带状尾迹一样。Radius 定义管的半径,并增加或减少其整体尺寸。Radial Steps 控制管圆周周围的边数。较高的值会提高管盖部的分辨率。
Sections 和 Section Length 对于管状尾迹和带状尾迹的工作方式相同。它们控制管道轨迹在弯曲和扭曲而不是沿直线移动时看起来有多平滑。增加节的数量将使其看起来更平滑。更改 Section Length 属性可更改每个节的长度以及轨迹的总长度。Section Rings 是带状尾迹的 Section Segments 属性的管状尾迹等效项。它细分了小节,并为管添加了更多的几何形状,以更好地适应 Curve 属性中定义的自定义形状。
你可以用曲线塑造管状尾迹,就像塑造带状尾迹一样。点击 Curve 属性旁边的框,然后指定或创建新曲线。尾迹的形状与曲线相似,曲线的值在尾迹的头部为 0.0 ,曲线的值在尾迹尾部为 1.0。
具有自定义曲线形状的粒子管状尾迹:4 条管盖边、3 个小节、1 个截面环(左),12 条管盖边、9 个小节、3 个截面环(右)
你可能想要设置的一个重要属性是粒子系统的 Drawing 分组中的 Transform Align。如果保持原样,管子将无法保持体积;它们在移动时变平,因为即使它们改变方向,它们的 Y 轴也会一直指向上方。这可能会导致大量渲染伪影。将属性设置为 Y to Velocity ,每个粒子尾迹都沿其运动方向保持其 Y 轴对齐。
粒子湍流
湍流使用噪点纹理为粒子运动添加变化和有趣的图案。
可以与粒子吸引器和碰撞节点结合使用,以创建看起来更加复杂的行为。
- Noise Strength 属性控制图案的对比度,这会影响整体湍流清晰度。
较低的值会创建一个更柔和的模式,其中单个粒子移动路径不会与另一个移动路径明显分开。将此值设置为更高的数字以使这个模式下更加清晰。 - Noise Scale 属性控制模式的频率。它基本上改变了噪点纹理的UV标度,其中较小的值会产生更精细的细节,但重复的图案会更快地变得明显。较大的值会导致整体湍流模式较弱,但在重复开始成为问题之前,粒子系统可以覆盖更大的区域。
- Noise Speed 属性采用向量并控制噪点平移速度和方向。这允许你随时间移动噪点图案,从而为粒子系统添加另一层移动变化。
- Noise Speed Random 属性为噪点平移速度增加了一些随机性。这有助于打破可预见的模式,尤其是在较高的平移速度下,当重复变得明显时。
- 用 Influence Min 设置最小值,使用 Influence Max 设置最大值。
当粒子生成时,影响是从这个范围内随机选择的。你还可以使用 Influence Over Life 属性设置一条曲线,每个粒子的生命周期内依照该曲线修改影响值。
这三个属性共同控制着湍流对粒子系统的影响强度 as described before 。 - 使用 Initial Displacement Min 设置下限,使用 Initial Displacement Max 设置上限。当粒子生成时,从该范围内随机选择位移量并乘以随机方向。
3D 粒子吸引子
吸引器有三种类型:
- GPUParticlesAttractorBox3D 盒型吸引器
- GPUParticlesAttractorSphere3D 球型吸引器
- GPUParticlesAttractorVectorField3D 向量场吸引器
在 ParticleProcessMaterial 上启用 Attractor Interaction 属性。对每个需要对吸引器做出反应的粒子系统执行该操作。
检查器中的 GPUParticlesAttractor3D 属性:
- 强度(Strength)控制吸引器的强度。正值将粒子拉近吸引器中心,而负值则将它们推开。
- 衰减(Attenuation)控制着吸引器影响区域内的强度衰减。每个粒子吸引器都有一个边界。它的强度在边界的边界处最弱,在中心处最强。边界外的粒子完全不受吸引器的影响。衰减曲线控制强度在这段距离上的减弱方式。直线表示强度与距离成正比:如果粒子位于边界和中心的中间,则吸引器的强度将是中心强度的一半。不同的曲线形状会改变粒子向吸引器加速的速度。
- (Directionality) 属性可改变粒子被拉动的方向。值为 0.0 时,没有方向性,这意味着粒子被拉向吸引器的中心。值为 1.0 时,吸引器是完全定向的,这意味着粒子将沿着吸引器的局部 -Z 轴被拉动。你可以通过旋转吸引器来更改全局方向。如果 Strength 为负,则粒子将沿着 +Z 轴被拉动。
无方向性(左)与全方向性(右)。注意粒子如何沿着吸引子的局部Z轴运动。 - Cull Mask 属性根据每个系统的 visibility layer 控制哪些粒子系统会受到吸引器的影响。仅当在吸引器的剔除遮罩中启用了至少一个系统的可见性层时,粒子系统才会受到吸引器的影响。
3D粒子碰撞
粒子碰撞节点共有四种:
- GPUParticlesCollisionBox3D
- GPUParticlesCollisionSphere3D
- GPUParticlesCollisionSDF3D
- GPUParticlesCollisionHeightField3D
通用属性
位于检查器的 GPUParticlesCollision3D 区域中。
Cull Mask 属性根据每个系统的 visibility layers 控制哪些粒子系统会受到碰撞节点的影响。
仅当碰撞器的剔除遮罩中至少启用了一个系统的可见性层时,粒子系统才会与碰撞节点发生碰撞。
- 盒型碰撞
- 球型碰撞
- 高度场碰撞
- 带符号距离场(SDF)碰撞
故障排除
为了使粒子碰撞起作用,粒子的可见性 AABB 必须与碰撞器的 AABB 重叠。如果尽管设置了碰撞器,但碰撞似乎不起作用,请通过选择 GPUParticles3D 节点并在 3D 编辑器视口顶部选择 GPUParticles3D > 生成可见性 AABB… 来生成更新后的可见性 AABB。
如果粒子移动得很快而碰撞体很薄,有两种解决方案:
- 让碰撞体变得更厚。例如,如果粒子不能穿过一个坚实的地面,你可以让代表地面的碰撞体比其实际的视觉表现更厚。
高度图碰撞体(heightfield collider)在设计上自动处理这种情况,因为高度图无法表示“房间之上还有房间”的碰撞情况。 - 增大 GPUParticles3D 节点中 Fixed FPS 的值,以增加粒子碰撞检测的频率。这会产生性能影响,所以应避免设置过大的值。
复杂发射形状
Godot 提供了一种根据任意复杂形状发射粒子的方法。
这些形状由场景中的网格生成,并作为纹理存储在粒子处理材质中。
这是一种用途非常广泛的工作流程,用户可以将粒子系统用于传统用法之外的用途,如树叶或复杂的全息效果。
- 创建一个粒子系统。添加一个网格实例作为粒子发射点的源。
- 选中粒子系统后,导航到视口菜单并选择 GPUParticles3D 条目。从那里,选择从节点创建发射点。
- 此时会弹出一个对话窗口,要求你选择一个节点作为发射源。选择场景中的一个网格实例并确认选择。下一个对话窗口将处理点的数量以及如何生成这些点。
- 发射点 Emission Points 控制将要生成的点的总数。粒子将从这些点中生成,因此在这里输入的内容取决于源网格的大小(需要覆盖的面积)和所需的粒子密度。
Emission Source 为点的生成方式提供了 3 种不同的选项: - 如果只想在网格表面上分布发射点,请选择 表面点(Surface Points)。
- 如果还想生成有关曲面法线的信息,并使粒子沿法线指向的方向移动,请选择此选项 Surface Points + Normal (Directed) 。
- 最后一个选项“ 体积”(Volume)在网格体内部的任何地方创建发射点,而不仅仅是在其表面上。
发射点存储在粒子系统的本地坐标系中,因此你可以移动粒子节点,发射点也会跟着移动。
当你想在多个不同的地方使用同一个粒子系统时,这可能会很有用。另一方面,当你移动粒子系统或源网格时,可能需要重新生成发射点。
发射形状纹理
可用的发射形状纹理
复杂粒子发射形状的所有数据都存储在一组纹理中。具体数量取决于所使用的发射形状类型。如果将粒子处理材质上发射形状(Emission Shape)组中的形状(Shape)属性设置为点(Points),则可以访问 2 个纹理属性,即点纹理(Point Texture)和颜色纹理(Color Texture)。将其设置为定向点(Directed Points),则还有第三个属性,称为法线纹理(Normal Texture)。
点纹理Point Texture属性涵盖了上一步可能生成的所有发射点。每个粒子生成时都会随机选择一个点。如果存在法线纹理Normal Texture属性,它会在点位置提供一个方向向量。如果还设置了颜色纹理Color Texture 属性,它会在点位置进行采样,调和处理材质上设置的其他颜色,为粒子提供颜色。
你还可以通过调用点计数Point Count属性,在创建发射形状后随时更改发射点的数量。这包括在游戏运行时动态更改点数量。
高动态范围光照
那么,所有”HDR”业务在什么时候发挥作用?要理解答案,我们需要查看显示器的行为.
你的显示器输出线性光比率从某个最大强度到某个最小强度.
现代游戏引擎在各自的场景中对线性光值进行复杂的数学计算. 这将会有什么问题呢?
根据显示器类型的不同,显示的强度范围有限.
然而,游戏引擎渲染的强度值范围没有限制.
虽然 “最大强度” 对sRGB显示器来说有一定的意义,但在游戏引擎中却没有任何影响;每帧渲染时可能产生无限宽的强度值范围.
这意味着场景光强,也称为场景对应(scene-referred)的光照比率,需要进行转换和映射以适应所选显示器的特定输出范围。
这就类似通过虚拟摄像机拍摄我们的游戏引擎场景,在这里,我们的虚拟摄像机将对场景数据应用特定的摄像机渲染变换,其输出将以特定的显示类型显示。
Godot目前尚不支持高动态范围 输出. 它只能在HDR中执行照明,并将结果映射为低动态范围的图像.
对于高级用户来说,仍然可以得到一个非色调映射的视图图像与完整的HDR数据,然后可以保存到一个OpenEXR文件.
计算机显示器
几乎所有显示器都需要对发送给它们的代码值进行非线性编码.
显示器又利用其独特的传输特性,将代码值 “解码” 为输出的线性光比,并在每个红色,绿色和蓝色发射点的独特颜色的光中投射出这些比值.
对于大多数计算机显示器来说,显示器的规格是根据 IEC 61966-2-1,即 1996 年的 sRGB 规格,进行概述的。该规范概述了 sRGB 显示器的性能,包括 LED 像素中的灯光颜色以及输入(OETF)和输出(EOTF)的传输特性。
并非所有显示器都使用与计算机显示器相同的OETF和EOTF. 例如,电视广播显示器使用BT.1886 EOTF. 然而,Godot目前只支持sRGB显示器.
sRGB标准是围绕着常见的桌面计算CRT显示器的电流与光输出之间的非线性关系而制定的.
场景参考模型的数学要求我们将场景乘以不同的值,以调整不同光照范围的强度和曝光.
显示器简单的传递函数不能恰当地渲染游戏从引擎场景中输出的更大动态范围. 这需要一种更复杂的编码方法.
场景线性和资源管道
在场景线性 sRGB 中工作并不像按一下开关那样简单。首先,导入的图像资产必须在导入时转换为线性光比例。即使将其线性化,这些资产也可能并不完全适合用作纹理,具体取决于它们的生成方式。
有两种方法可以做到这一点:
- 用于在图像导入时显示线性比率的sRGB传递函数
这是最简单的使用sRGB资源的方法,但不是最理想的. 这样做的一个问题是质量的损失. 每通道使用8位来表示线性光比率,不足以正确量化这些值. 这些纹理以后也可能会被压缩,可能会加剧这个问题. - 硬件sRGB传输函数显示线性转换
GPU 会在使用浮点读取纹素后进行转换。这在 PC 和游戏主机上很好用,但大多数移动设备不支持,或者它们不支持压缩的纹理格式(例如 iOS)。
场景线性到显示参考的非线性
完成所有渲染后,场景线性渲染需要转换为适当的输出,例如sRGB显示.
为此,请在当前的: Environment 中启用 sRGB 转换(更多内容见下文).
请记住,sRGB -> 显示线性和显示线性 -> sRGB 转换必须始终同时启用。
如果未启用其中之一,将导致可怕的视觉效果,仅适用于前卫的实验性独立游戏。
HDR的参数
HDR可以在 Environment 资源中找到.
这些在大多数情况下都可以在 WorldEnvironment 节点中找到或在相机中设置. 更多信息请参阅 环境和后期处理.
全局光照
全局光照由以下几个关键概念组成:
- 间接漫反射光照
种光照不会因为相机的角度而改变。间接漫反射光照有两个主要来源:
- 光弹跳在表面上。这种反射光照会与材质的反照率颜色相乘。
然后,反射的照明可以被其他表面反射,由于光衰减,影响会减小。在现实生活中,光线会反射无数次。
但是出于性能原因,无法在游戏引擎中模拟。相反,退回次数通常限制为 1 或 2(烘焙光照贴图时最多限制为 16)。
更多的反弹将导致阴影区域更真实的光线衰减,而代价是导致性能较低或烘烤时间更长。 - 自发光材质还可以发出可以在表面上反弹的光。这充当 区域照明 的一种形式。
与使用 OmniLight3D 或 SpotLight3D 节点发光不同,确定大小的区域将使用自己的表面发光。
直接漫反射光照已经由灯光节点本身处理,这意味着全局光照算法只尝试表示间接光照。
不同的全局光照技术提供不同级别的精度来表示间接漫反射光照。有关详细信息请参阅本页底部的比较表。
为较小的对象提供更精确的环境光遮蔽,屏幕空间环境光遮蔽(SSAO)可以在环境设置中启用。SSAO 具有较大的性能开销,因此在针对低端硬件时请确保禁用它。
- 镜面反射光效果
镜面反射照明也被称为反射 。这是根据摄像机角度而变化强度的照明。这种镜面照明可以是 直接 或 间接 。
大多数全局光照技术都提供了一种渲染镜面反射光照的方法。然而,不同技术渲染镜面反射光照的精度差异很大。有关详细信息,请参阅本页底部的比较表。
为了给较小的对象提供更准确的反射,可以在环境设置中启用屏幕空间反射(SSR)。SSR 的性能开销很大(甚至比 SSAO 更高),因此在针对低端硬件时一定要禁用它。
<—正在施工—>
环境和后期处理
体积雾和雾体积
3D 抗锯齿
优化
使用 MultiMeshInstance3D
在正常情况下,使用一个 MeshInstance3D 节点来显示 3D网 格,比如主角的人体模型,但在某些情况下,你希望在一个场景中创建同一个网格的多个实例。
你可以多次复制同一个节点,并手动调整变换。这可能是一个乏味的过程,而且结果可能看起来很机械。
此外,这种方式也不利于快速迭代。MultiMeshInstance3D 是此问题的可能解决方案之一。
MultiMeshInstance3D,顾名思义,是在特定网格的表面上创建 MeshInstance 的多个副本。一个示例是树形网格用随机比例和方向的树填充地形网格。
设置节点
基本设置需要三个节点:MultiMeshInstance3D 节点和两个MeshInstance3D 节点。
一个节点用作目标,即要在其上放置多个网格的表面网格。 在树的示例中,这就是地形。
另一个节点是作为源节点,也就是你想复制的网格。在树的情况下,这将是树本身。
在我们的示例中,将使用 Node3D 作为场景的根节点。场景树看起来像这样:
现在你已准备好了一切。选择 MultiMeshInstance3D 节点并查看工具栏,你应该在视图旁边看到一个名为 MultiMesh 的额外按钮。
单击它并在下拉菜单中选择填充表面。将弹出一个名为填充MultiMesh的新窗口。
MultiMesh 设置
以下是选项说明。
目标表面
用来放置源网格副本的目标表面的网格。
源网格
要在目标曲面上复制的网格。
网格向上轴
轴用作源网格的上轴。
随机旋转
随机地围绕源网格的向上轴旋转。
随机砖块
随机化源网格的整体旋转。
随机缩放
随机化源网格的比例。
缩放(Scale)
将放置在目标曲面上的源网格的比例。
数量
放置在目标曲面上的网格实例数量。
选择目标曲面。在树的情况下,这应该是地形节点。源网格应该是树节点。根据你的喜好调整其他参数。按 Populate ,源网格的多个副本将放在目标网格上。 如果对结果满意,可以删除用作源网格的网格实例。
最终结果应如下所示:
要更改结果,请使用不同的参数重复相同的步骤.
网格的细节级别(LOD)
与 遮挡剔除 一样,细节级别(LOD)是优化 3D 项目渲染性能的最重要方法之一。
Godot 提供了一种方法,可以在导入时自动生成细节较少的网格供 LOD 使用,然后在需要时自动使用这些 LOD 网格。
这对用户完全透明。meshoptimizer 库用于幕后的 LOD 网格生成。
网格 LOD 适用于任何绘制 3D 网格的节点。这包括 MeshInstance3D、MultiMeshInstance3D、GPUParticles3D 和 CPUParticles3D。
如果你需要使用艺术家创建的网格手动配置细节层次,请使用 可见范围(HLOD) 而不是自动网格 LOD。
生成网格 LOD
默认情况下,导入的 3D 场景(glTF、.blend、Collada、FBX)会自动生成网格 LOD。
生成 LOD 网格后,将在渲染场景时自动使用它们。你无需手动配置任何内容。
然而,对于导入的 3D 网格(OBJ),网格LOD生成 不会 自动启动。
这是因为 OBJ 文件默认情况下不作为完整的 3D 场景导入,而仅作为单独的网格资源加载到MeshInstance3D 节点(或 GPUParticle3D,CPUParticles3D…)。
为了使OBJ文件具有为其生成的网格LOD,在文件系统面板中选择它,去到导入面板,将其 导入为 选项更改为 场景 ,然后单击 重新导入 :
这需要在单击 重新导入 后重新启动编辑器。
可见范围(HLOD)
遮挡剔除
分辨率缩放
可变速率着色
工具
使用 CSG 设计关卡原型
使用 GridMap
带弹簧臂的第三人称相机
动画
动画功能介绍
AnimationPlayer 节点创建动画,动画数据容器。
一个AnimationPlayer节点可以保存多个动画,这些动画可以自动相互过渡。
动画面板由四部分组成:
- 动画控件(即添加,加载,保存和删除动画)
- 轨道列表
- 带有关键帧的时间轴
- 时间轴和轨道控件

管理动画库
出于复用性,动画被注册在动画库资源列表中(动画按钮)
如果你将动画添加到 AnimationPlayer 而不指定任何特定设置,则该动画将默认注册到 AnimationPlayer 具有的 [Global] 动画库中。
如果有多个动画库并且你尝试添加动画,则会出现一个包含选项的对话框。
属性轨道
动画系统不仅限于位置、旋转和缩放。可以对任何属性进行动画化。
轨道设置
每条属性轨道的末尾有设置面板,设置更新模式、插值模式和循环模式
更新模式告诉 Godot 何时更新属性值:
- 连续:每帧都更新属性
- 离散:仅在位于关键帧时更新属性
- 捕获:如果第一个关键帧的时间大于 0.0,就会记录该属性的当前值,并将其与第一个动画帧混合。
例如,利用“捕获”模式,你可以将处于任意位置的节点移动到特定的位置。
插值模式告诉 Godot 如何计算关键帧之间的帧值:
- 临近:设置为最接近的关键帧的值
- 线性:使用线性函数计算两个关键帧之间的值
- 三次方:使用三次函数计算两个关键帧之间的值
- 线性角(仅出现在旋转属性中):具有最短路径旋转的线性模式
- 三次角(仅出现在旋转属性中):具有最短路径旋转的立方模式

利用三次插值,动画在关键帧处的速度较慢,而在关键帧之间的速度较快,从而使动作更加自然。立体插值常用于角色动画。线性插值以固定的速度进行动画变化,从而产生更加机械的效果。
支持两种循环模式,如果将其设置为循环时,则会影响动画效果:
- 钳制循环插值: 第一个关键帧继承最后一个关键帧的值
- 环绕循环插值: 回归到第一个关键帧
使用 RESET 轨道(默认轨道)
添加名为RESET(大写)的动画。
只在时间为 0 处存在一个关键帧,默认值。
如果 AnimationPlayer 的 Reset On Save(保存时重置)属性为 true,场景在保存时会应用重置动画的效果(相当于寻道到 0.0 时间点的效果)。
只有保存的文件会受到影响——编辑器中的属性轨道还是会保持原样。
如果要在编辑器中重置轨道,请选中 AnimationPlayer 节点,打开 动画 底部面板,然后选择动画按钮 下拉菜单中的 应用重置 。
在检查器中使用属性旁边的关键帧图标时,编辑器会要求你自动创建 RESET 轨道。
RESET 轨道也用作混合的参考值。另请参阅为了更好地混合。
洋葱皮(一种显示功能)
启用单击洋葱图标。此时,在动画对象先前位置将有透明红色副本显示出来。
与洋葱皮按钮相邻的下拉菜单可以用来调整洋葱皮的工作方式,包括在未来的帧中使用洋葱皮的能力。
动画标记 (Animation Markers)
动画标记可用于播放动画的特定部分。
向动画添加标记,右键单击时间轴上方的空间选择插入标记。
所有标记都需要唯一名称。还可以设置标记颜色。
要在两个标记之间播放动画部分使用 play_section_with_markers() 和 play_section_with_markers_backwards() 方法。
预览两个标记之间的动画,请使用 Shift + Click 选择标记。选择两个时,它们之间的空格应以红色突出显示。
动画轨道类型

3D 位置、旋转、缩放轨道
专为从外部3D模型导入的动画而设计。
混合形状轨道
混合形状轨道针对 MeshInstance3D 中的混合形状动画进行了优化。
专为从外部3D模型导入的动画而设计。
方法调用轨道
允许在动画中的特定时间调用函数。
例如,你可以在死亡动画的结束时调用 queue_free() 来删除节点。
为了安全起见,在编辑器中预览动画时,方法调用轨道上的事件不会被执行。
创建轨道时会打开一个窗口让你选择与该轨道关联的节点。
要更改方法调用或其参数,请单击关键帧并转到检查器面板。
如果展开“Args”部分,你将看到一个可编辑的参数列表。
要通过代码创建这样的轨道,请传递一个包含目标方法的名称和参数的字典作为 Animation.track_insert_key() 中 key 的变体。键及其预期值如下:
键 值 "method"方法的名称,类型为 String "args"传递给该函数的所有参数,类型为 Array
1 | # 创建一个调用方法的动画轨道。 |
贝塞尔曲线轨道
类似于属性轨道,允许使用贝塞尔曲线控制属性值。
贝塞尔曲线轨道和属性轨道不能在
AnimationPlayer和AnimationTree中混合。
请单击动画轨道右侧的曲线图标以打开贝塞尔曲线编辑器。
在编辑器中,帧由实心菱形表示,空心菱形通过线控制曲线的形状连接到它们。
小技巧
为了在手动处理曲线时获得更好的精度,你可能需要更改编辑器的缩放级别。
编辑器右下角的滑块可用于放大和缩小时间轴。
也可以使用 Ctrl + Shift + 鼠标滚轮 来执行该操作。使用 Ctrl + Alt + 鼠标滚轮 将放大和缩小 Y 轴。
右键曲线可以选择手柄模式:
- 自由:允许操纵器定向到任何方向,而不影响另一个操纵器的位置。
- 线性:不允许操纵器旋转,绘制线性图形。
- 平衡:使操纵器一起旋转,但关键帧和操纵器之间的距离不镜像。
- 镜像:使一个操纵器的位置完美镜像另一个操纵器,包括它们到关键帧的距离。
音频播放轨道
创建音频播放轨道场景必须具有 AudioStreamPlayer、AudioStreamPlayer2D 或 AudioStreamPlayer3D 节点之一。
混合模式允许你选择在 AnimationTree 中混合时是否调整音频音量。
动画播放轨道
如果在场景中实例化了包含动画播放器的场景,则需要在场景树中启用“子节点可编辑”。
对场景中其他动画播放器节点进行排序。
比如你可以用它为过场动画中的多个角色制作动画。
而后选择要与轨道关联的动画播放器。
选中你刚创建的帧,以在检查器面板中选择动画。
如果动画已经在播放,并且你想提前停止它,可以创建一个帧并将其在检查器中设置为 [STOP] 。
剪纸动画
用于剪纸动画的绑定工具,而且是工作流的理想选择:
- 动画系统与引擎完全集成:这意味着动画可以控制的不仅仅是物体的运动。
纹理、精灵大小、轴心、不透明度、颜色调制等都可以进行动画和混合。 - 混合动画风格 :
AnimatedSprite2D允许将传统赛璐璐动画与剪纸动画一起使用。
在赛璐璐动画中,不同的动画帧使用完全不同的绘图,而不是相同的片段位置不同。
在其他基于剪纸的动画中,赛璐璐动画可以选择性地用于复杂的部件,例如手、脚、改变面部表情等。 - 自定义形状元素 : 可以用
Polygon2D 创建自定义形状,允许UV动画,变形等. - 粒子系统 : 剪纸式动画配件可以与粒子系统相结合,这对魔法效果,喷气背包等很有用.
- 自定义碰撞器:在骨架的不同部位设置碰撞器和影响区域,非常适合 Boss 和格斗游戏。
- 动画树 : 允许在几个动画之间进行复杂的组合和混合,与3D动画的工作方式相同.
- 创建模型
创建一个空的Node2D作为场景的根
模型的第一个节点是臀部,接下来是躯干作为臀部的子级。
可以按 E 进入旋转模式,退出旋转模式按 ESC。
旋转轴心错误,需要调整,有三种方式:
- 可以通过更改 Sprite2D 中的 offset 属性来调整轴心
- 可以将鼠标悬停在所需的轴心点上时,按 V 移动所选 Sprite2D 的轴心。
- 上方工具栏也有类似的功能。
继续添加身体部件,左臂有些问题. 在二维中,子节点出现在父节点的前面:我们希望左臂出现在臀部和躯干的后面.
我们用 RemoteTransform2D 节点来解决这个问题.还可以通过调整从二维节点继承的任何节点的Z属性,来修复深度排序问题.
RemoteTransform2D 节点
可以对层次结构中其他地方的节点进行变换.
- 完成骨架
通过选择节点并旋转可以有效地为前向运动学(FK)设置动画.
对于简单的物体和装配来说已经足够了,但仍然有限制:
- 在复杂的装配中,在主视口中选择精灵会变得很困难. 场景树最终被替代,用来选择部件,这可能会比较慢.
- 反向动力学(Inverse Kinematics、IK)对于手脚等肢体的运动非常有用,目前我们的绑定还无法使用。
为了解决这些问题,我们将会使用 Godot 的骨架。
骨架
有问题IK 链
IK 是反向动力学(Inverse Kinematics)的缩写,给手部、足部以及其它需绑定肢体的动画带来便利。
动画提示
下一节将是创建剪纸动画的技巧集合。
设置关键帧和排除属性
当动画编辑器窗口打开时,特殊的上下文元素会出现在顶部工具栏中:
按键按钮在当前游戏开始位置为选定的对象或骨骼插入位置,旋转和缩放关键帧.
通过切换关键帧按钮左边的 “位置” ,”旋转” 和 “缩放” 按钮,可以修改其功能,允许你指定将为三个属性中的哪一个创建关键帧.
下面是一个例子,说明如何使用其的: 假设你有一个节点,其中已经有两个关键帧只对其缩放进行动画. 你想在同一个节点重叠添加旋转移动. 旋转运动应该在不同的时间开始和结束,与已经设置的缩放变化不同. 在添加新关键帧时,可以使用切换按钮,只添加旋转信息. 这样,你就可以避免添加不需要的缩放关键帧,破坏现有的缩放动画.
创建放松姿势
将放松姿势视为默认姿势,当游戏中没有其他姿势处于活动状态时,应该将其设置为剪纸绑定。创造一个放松姿势如下:
- 确保钻机部件以看起来像“静止”的排列方式放置。
- 创建一个新动画,重命名为 “rest”.
- 选择装配中的所有节点(应该可以框选).
- 确保工具栏中的“loc”、“rot”和“scl”切换按钮均处于活动状态。
- 按按键。将为存储其当前排列的所有选定零件插入键。现在,在游戏中必要时,可以通过播放你创建的“休息”动画来调用此姿势。
只修改旋转
当制作剪纸动画绑定时,通常只需要改变节点的旋转. 很少使用位置和比例.
因此,在插入键时,你可能会发现在大多数时间里只有 “rot” 切换键处于激活状态,会很方便:
这将避免为坐标和缩放创建不必要的动画轨道.
关键帧 IK 链
编辑IK链时,不需要选择整个链来添加关键帧. 选择链的端点并插入关键帧将自动为链的所有其他部分插入关键帧.
视觉上移动父级后面的精灵
有时,在动画过程中,需要让节点相对于其父节点更改其可视深度.
想象一个面对镜头的角色,他从背后拿出一个东西放在面前.
在这个动画过程中,整个手臂和他手中的物体都需要改变相对于角色身体的视觉深度.
为了帮助解决这个问题,在所有 Node2D 的派生节点上都有一个可制作关键帧的“Behind Parent”(在父级之后)属性。
规划绑定时,请考虑它需要执行的动作,并考虑如何使用“Behind Parent”和/或 RemoteTransform2D 节点。它们提供重叠的功能。
为多个关键帧设置缓动曲线
要将同一缓动曲线同时应用于多个关键帧:
选择相关的关键帧.
点击动画面板右下角的铅笔图标. 这将打开过渡编辑器.
在过渡编辑器中,点击所需曲线进行应用.
2D 骨架
在 3D 工作中,骨骼变形是角色和生物的常见功能。
而对于 2D,由于这种功能并不常用,因此很难找到针对这种功能的主流软件。
一种选择是在 Spine 或 Dragonbones 等第三方软件中创建动画。内置也支持此功能。
为什么要在 Godot 中直接处理骨骼动画?答案是这样做有很多好处:
- 能更好地与引擎集成,从而减少使用外部工具导入和编辑的麻烦.
- 能够控制粒子系统,着色器,声音,调用脚本,颜色,透明度等动画.
- Godot内置的骨骼系统非常高效,并且是为性能而设计的.
接下来的教程将讲解 2D 骨架变形。
教程素材:
场景树结构:
Polygon2D 节点设置如下:
教程素材选择并指定纹理:
转到点模式,选择铅笔,在需要的区域绘制一个多边形
小技巧-复制多边形
复制Polygon2D节点改名字.
进入 "UV" 对话框,移动多边形.
创建骨架
创建一个 Skeleton2D 节点作为根节点的子项作为骨架的基础:
创建一个 Bone2D 节点作为骨架的子项。把它放在臀部(通常从这里开始建立骨架)。
所有的骨骼都显示关于缺少放松姿势的警告。
放松姿势是骨骼的默认姿势,你可以随时恢复到这个姿势(这对于动画制作非常方便)。
设置放松姿势,点击场景树中的 skeleton 节点,然后在工具栏中点击 Skeleton2D 按钮选择覆盖放松姿势。
多边形的变形
- 选择之前创建的多边形,并将骨架节点分配 Skeleton 属性。
- 打开多边形编辑器,进入骨骼部分。
- 点击同步骨骼到多边形。
- 此步骤仅需手动执行一次(除非你通过添加/删除/重命名骨骼来修改骨架)。
- 它确保你的绑定信息保存在多边形中,即使骨架节点意外丢失或被修改。按“同步骨骼到多边形”按钮来同步列表。
- 白色点完全受到权重影响,而黑色点完全不受影响。
- 如果在多根骨骼上,绘制了相同的白色点,那么权重的影响将平均分配在这些骨骼之间。
- 所以通常不需要过多使用中间色调,除非你想仔细打磨弯曲效果。
- 绘制完权重后,制作骨骼的动画(不是多边形的动画!)会具有修改和弯曲多边形的预期效果。
但这并不完美。尝试调调骨骼的动画,弯曲的多边形往往会产生意想不到的结果:形变扭曲。
这是因为Godot在绘制多边形时,产生了内部三角形来连接这些点. 它们并不总是按你所期望的方式弯曲.
如果要解决这个问题,你需要在几何图形中设置提示,以明确希望它如何变形.
内部顶点
再次打开每根骨骼的 UV 菜单,进入点部分。
在你期望几何体弯曲的地方添加一些内部顶点:
现在转到 多边形 部分,重新绘制细节更丰富的多边形.
这样,当多边形弯曲时,你需要确保它们变形的可能性最小,慢慢尝试找出正确的设置.
使用 AnimationTree
在3D场景中经常使用AnimationTree
- AnimationTree 节点不包含自己,使用 AnimationPlayer 节点中的动画.
- 当从3D交换格式导入场景时, 它们通常自带动画(要么是多个, 要么是在导入时从一个大的动画中拆分出来).
- 最后, 导入的Godot场景在 AnimationPlayer 节点中包含动画.
很少在Godot中直接使用导入的场景(它们要么实例化, 要么来自继承), 你可以将 AnimationTree 节点放置在包含导入的新场景中.
然后, 将 AnimationTree 节点指向导入场景内创建的 AnimationPlayer 节点.
这是在, 中的设置, 参考下图:

为玩家创建了一个以 CharacterBody3D 为根节点的新场景。这个场景中实例化了原来的 .dae(Collada)文件,并创建 AnimationTree 节点。
创建树
可以在 AnimationTree 中使用三种主要节点类型:
- 动画节点,从链接的 AnimationTree 中引用动画。
- 动画根节点, 用于混合子节点.
- 动画混合节点,在 AnimationNodeBlendTree 中使用,通过多个输入端口进行单图混合。
在 AnimationTree 中设置根节点, 如下几种类型可供选择:
- AnimationNodeAnimation:从列表中选择一个动画并播放它. 这是最简单的根节点, 一般不直接用作根节点.
- AnimationNodeBlendTree:包含许多混合类型的节点,如调配, 混合2, 混合3, 一对一等. 最常用的根节点之一.
- AnimationNodeStateMachine:将多个根节点作为图中的子节点. 每个节点作为一个 状态 使用, 并提供多个函数在状态之间进行切换.
- AnimationNodeBlendSpace2D:允许在二维混合空间中放置根节点. 在二维中控制混合位置以混合多个动画.
- AnimationNodeBlendSpace1D:以上的简化版本(一维)。
混合树(AnimationNodeBlendTree)
可包含用于混合的根节点和常规节点。节点从菜单添加到图中:
所有混合树默认都包含一个 Output(输出)节点,为了让动画播放,必须有个东西与其相连。
以下是可用节点的简短描述:
- 混合2/混合3(add2/add3) 将通过用户指定输入混合值之间进行混合:
对于更复杂的混合, 建议使用混合空间.
混合也可以使用过滤器, 也就是说, 你可以单独控制通过混合功能的轨道. 这对于动画的层叠非常有用. - OneShot 此节点将执行子动画, 并在完成后返回. 可以用于定制淡入淡出时间, 以及过滤器.
在设置时间和改变动画播放后,播放节点会通过将其 request 值设置为 AnimationNodeOneShot.ONE_SHOT_REQUEST_NONE/ 做到在下一个进程帧自动清除请求。
1 | # 播放连接到 "shot" 端口的子动画 |
- 时间缩放(Time Seek) 用来使寻找命令发生在动画图像的任何子代上。
使用这个节点类型可以从 AnimationNodeBlendTree 中的开始或某个位置播放 Animation。
在设置时间和改变动画播放后,寻找节点通过设置其 seek_position 值为 -1.0,在下一个进程帧自动进入睡眠模式。
1 | # 从头开始播放子动画 |
- 时间缩放(TimeScale) 允许通过 scale 参数缩放连接到 in 输入的动画速度(或使其反转)。 将 scale 设置为0会暂停动画。
- 转换(Transition) 非常简单的状态机(当你不想使用 StateMachine 节点时)。
动画可以连接到输出,过渡时间可以指定。
在设置请求和更改动画播放后,过渡节点会通过将其 transition_request 值设置为空字符串 (“”),在下一个进程帧自动清除请求。
1 | # 播放连接到 "state_2" 端口的子动画 |
- 二维混合空间(BlendSpace2D)
BlendSpace2D 是一个在二维空间进行高级混合的节点. 将点添加到一个二维空间, 然后可以控制位置来确定混合:
可以控制X和Y的范围(为方便起见, 还可以标记它们). 默认情况下, 可以在任何位置放置点(只需右键单击坐标系统或使用 添加点 按钮)将自动生成德洛内三角形.
也可以通过禁用 自动三角形 选项来手动绘制三角形, 虽然基本上没必要这么做:
最后, 可能会更改混合模式. 默认情况下, 混合是通过在最近的三角形内插点来实现的. 当处理二维动画(逐帧)时, 你可能希望切换到 离散 模式. 此外, 如果你想在离散动画之间切换时保持当前播放位置, 请使用 进位 模式. 此模式可在 混合 菜单中更改: - 一维混合空间(BlendSpace1D) 这类似于二维混合空间, 但在一维空间中(所以不需要三角形).
- 状态机(StateMachine) 这个节点是一个状态机,根节点都是状态。
根节点可以创建并通过线路连接。状态通过转换连接,它们是具有特殊性质的连接。转换是单向的,但是可以用两个来达到双向连接。
有多种类型的转换:- Immediate(立即):将立即切换到下一个状态。当前状态将结束,并与新状态的开头相混合。
- Sync(同步):立即切换到下一个状态,但会将新状态快进并到旧状态的播放位置。
- At End(末尾):将等待当前状态播放结束,然后切换到下一个状态动画的开头。
过渡也有一些属性。单击任何过渡,它就会显示在“检查器”面板中: - Switch Mode(切换模式)为过渡类型(见上文),可以在此处创建后修改。
- Auto Advance(自动前进)当达到此状态时将自动开启转换。最适合 At End 切换模式。
- Advance Condition(前进条件)会在条件成立时打开自动前进。这是一个可以用变量名填充的自定义文本字段。可以从代码中修改变量(稍后将对此进行详细介绍)。
- Xfade Time(叠化时间)是在这个状态和下一个状态之间交叉渐变的时间。
- Priority(优先级)与代码中的 travel() 函数一起使用(后述)。当从一个状态到另一个状态时,会优先使用优先级较低的过渡。
- Disabled(禁用)允许禁用此转换(它不会在行程或自动前进期间使用)。
为了更好的混合
在 Godot 4.0+ 中,为了使混合结果具有确定性(结果可复现且始终一致),混合属性值必须具有特定的初始值。
例如,在要混合两个动画的情况下,如果一个动画具有属性轨道而另一个动画没有,则计算混合动画时,要好像后一个动画(即本来没有属性轨道的那个)具有初始值的属性轨道一样去处理。
当使用 Skeleton3D 骨骼的 Position/Rotation/Scale 3D 轨道时,初始值为 Bone Rest(骨骼放松姿势)。
对于其他属性而言,初始值是 0 ,并且如果轨道出现在 RESET 动画中,那么则使用它第一个关键帧的值。
例如,下面的 AnimationPlayer 有两个动画,但其中一个缺少 Position 的属性轨道。
这意味着缺少该 Position 的动画会将这些 Position 视为 Vector2(0, 0)。
可以通过将 Position 的 Property 轨道作为初始值添加到 RESET 动画中来解决这个问题。
请注意,RESET 动画的存在是为了在最初加载对象时定义默认姿势。它被假定只有一帧,并且不应使用时间轴进行播放。
另请记住,将“插值类型”设置为“线性角”或“三次角”的“Rotation 3D 轨道”和用于 2D 旋转的“属性”轨道,将阻止从初始值旋转超过 180 度的操作作为混合动画。
这种限制对于 Skeleton3D 非常有用,可以防止骨骼在混合动画时穿透身体。因此,Skeleton3D 的 Bone Rest (骨骼放松姿势)值应尽可能接近可移动范围的中点。 这意味着人形模型最好以 T-pose 导入 。
你可以看到,优先考虑从 Bone Rest 出发的最短旋转路径,而不是动画之间的最短旋转路径。
如果需要通过混合动画将 Skeleton3D 本身旋转 180 度以上,则可以使用 Root Motion。
根骨骼运动
处理 3D 动画时,一种流行的技术是利用根骨骼为其余部分骨骼制作运动动画。
这样处于动画角色的脚步就能够与下方的地板相匹配,并且还能够实现过场动画中与物体的精确交互。
在 Godot 中回放动画时,可以将这根骨骼选作根运动轨道。这会在视觉上取消这根骨骼的变换(在原地播放动画)。
这样做以后,可以通过 AnimationTree API 获取实际的变换:
1 | # 获取根运动的位移增量(本次动画播放的移动量) |
可以将这些值提供给 CharacterBody3D.move_and_slide 等函数,用来控制角色的移动。
还有一个名为 RootMotionView 的工具节点,可以放置在场景中充当角色和动画的自定义地板(这个节点默认在游戏期间禁用)。
使用代码控制
创建树和预览之后,就只剩下一个问题:“这些东西怎么使用代码来控制?”。
要注意动画节点就是资源,因此他们会在所有使用他们的实例之间共享。直接修改节点中的值,将会影响到场景中所有使用这个 AnimationTree 的实例。通常是不希望这样的,不过也有一些不错的用法,比如你可以复制粘贴你的动画树的一部分,或者在不同的动画树中复用具有复杂布局的节点(例如状态机和混合树)。
实际的动画数据包含在 AnimationTree 节点中, 并通过属性访问. 检查 AnimationTree 节点的 “参数” 部分, 查看所有可以实时修改的参数:
这很方便, 因为它可以通过 AnimationPlayer 获得动画效果, 甚至是 AnimationTree 本身, 允许实现非常复杂的动画逻辑.
想要通过代码修改这些值, 必须获得该属性的路径. 这是很容易做到的, 把鼠标悬停在任何参数:
允许设置或读取它们:
1 | animation_tree.set("parameters/eye_blend/blend_amount", 1.0) |
状态机行程StateMachine 实现提供了很多不错的功能,其中之一就是“行程”(Travel)的能力。
可以向图发出指令,让其从当前状态转到另一个状态,所有的中间状态都会被访问到。
这是通过 A* 算法实现的。如果当前状态和目的状态之间不存在任何可达的过渡路径集,图就会立即传送到目的状态。
要使用行程能力, 你应该首先从 AnimationTree 节点中检索 AnimationNodeStateMachinePlayback 对象(其被导出为一个属性).
1 | var state_machine = animation_tree["parameters/playback"] |
一旦检索到, 可以调用它提供的许多函数之一:
1 | state_machine.travel("SomeState") |
状态机必须正在运行才能使用行程能力。确保调用 start() 或选择一个节点以在加载时自动播放。
播放视频
VideoStreamPlayer 节点支持视频的播放。
唯一支持的格式是 Ogg Theora(不要与 Ogg Vorbis 音频混淆)和可选的 Ogg Vorbis 音轨。
扩展可以带来对其他格式的支持。
备注
您可能会找到带有 .ogg 或 .ogx 扩展名的视频,这些扩展名是 Ogg 容器中数据的通用扩展名。
将这些文件扩展名修改为 .ogv可能可以让视频在 Godot 中导入。
不过,并不是所有 .ogg 或 .ogx 扩展名的文件都是视频——有些可能只包含音频。
设置 VideoStreamPlayer
在 Stream 属性中加载 .ogv 文件。
如果视频尚未采用 Ogg Theora 格式,请跳转到 推荐 Theora 编码设置。
在场景加载时立即播放视频,请在检查器中勾选 Autoplay。
可以在脚本中调用 VideoStreamPlayer 节点的 play() 开始播放。
处理大小变化及不同的纵横比
Godot 4.0 中在默认情况下,VideoStreamPlayer 会自动调整到与视频分辨率相匹配的大小。
你可以让它遵循普通的 Control 大小规则,启用 VideoStreamPlayer 节点的 Expand 即可。
要调整 VideoStreamPlayer 节点的大小随窗口大小改变的方式,通过 2D 编辑器视口顶部的布局按钮调整锚点。
不过,这种设置可能不足以处理所有可能的情况,例如全屏播放视频但不造成形变(需要在边界处留白)。
要进行精确控制,可以使用专用的 AspectRatioContainer 节点:
- 确保不是任何其他容器节点的子节点。
- 在 2D 编辑器的顶部将布局设置为整个矩形。
- 将 Ratio(比例)设置为与视频的长宽比匹配的比例。
可以在检查器里直接输入数学公式。请记住要将其中的一个操作数写成浮点形式,否则会得到整数的商。 - 配置好 AspectRatioContainer 之后,将 VideoStreamPlayer 节点调整为该 AspectRatioContainer 节点的子节点。
- 确保禁用了该 VideoPlayer 的 Expand。 现在应该就会自动适应到全屏的大小,不产生变形。
有关在项目中支持多种纵横比的更多技巧,请参阅 多分辨率。
在 3D 表面上显示视频
使用 VideoStreamPlayer 节点作为 SubViewport 节点的子节点,就可以在 3D 表面上显示任何 2D 节点。
可以使用以下步骤实现:
- 创建一个 SubViewport 节点。将其设置为与视频大小相匹配的像素大小。
- 创建一个 VideoStreamPlayer 子节点,并为其指定一个视频的路径。请确保禁用了 Expand,需要时启用 Autoplay。
- 创建一个 MeshInstance3D 节点,将其 Mesh 属性设为 PlaneMesh 或 QuadMesh。将该网格的大小调整到与视频的长宽比一致(否则看上去就会变形)。
- 在 GeometryInstance3D 部分的 Material Override 属性中新建一个 StandardMaterial3D 资源。
- 在该 StandardMaterial3D(底部)的 Resource 部分启用 Local To Scene。这是在 Albedo Texture 属性中使用 ViewportTexture 所必须的。
- 在该 StandardMaterial3D中,将 Albedo > Texture 属性设置为新建 ViewportTexture。
点击编辑这个新的资源,在 Viewport Path 属性中指定指向 SubViewport 节点的路径。 - 在该 StandardMaterial3D 中启用 Albedo Texture Force sRGB,防止颜色变化。
- 如果广告板需要自发光,请将 着色模式 设置为 无阴着色 以提高渲染性能。
更多关于设置的信息,请参阅 使用视口 和 3D GUI 演示。
循环视频
启用 Loop 属性。
请注意,将项目设置 视频延迟补偿 设置为非零的值可能会导致视频循环不再无缝,因为音频和视频的同步发生在每个循环开始时,会导致偶尔丢失帧。
将项目设置中的 视频延迟补偿 设置为 0 以避免丢帧问题。
视频解码条件及推荐分辨率
由于 GPU 在解码 Theora 视频时没有硬件加速,所以视频解码是在 CPU 上执行的。
现代的桌面 CPU 可以以 1440p @ 60 FPS 或更高的速度解码 Ogg Theora 格式的视频,但低端移动 CPU 处理高分辨率视频可能会比较吃力。
为了确保视频在各种硬件上都能够顺利解码:
- 为桌面平台开发游戏时,建议最多编码为 1080p(最好是 30 FPS)。
大多数人还在使用 1080p 或者更低分辨率的显示器,所以编码为更高分辨率的视频可能不值那些增大的文件大小和 CPU 需求。 - 为移动和 Web 平台开发游戏时,建议最多编码为 720p(最好是 30 FPS 或更低)。移动设备上 720p 和 1080p 的视频通常很难看出区别。
播放限制
- 不支持从 URL 播放视频流。
- 仅支持单声道和立体声音频输出。支持具有 4、5.1 和 7.1 音频通道的视频,但向下混合为立体声。
推荐 Theora 编码设置
建议是(在大多数情况下)避免依赖内置的 Ogg Theora 导出器。
你可能想要优先使用外部程序编码视频的原因有 2 个:
- Blender 等程序可以渲染 Ogg Theora。然而,默认的质量预设就如今的标准而言通常是非常低的。
你可能可以在软件里提高质量选项,但输出的质量可能仍然不理想(提升了文件大小)。
这通常意味着那个软件只支持按照固定比特率(CBR)去进行编码,不支持可变比特率(VBR)。
大多数场合应该都优先使用 VBR 编码,因为在相同的文件大小下能够提供更好的质量。 - 有些其他的程序根本无法渲染 Ogg Theora。
在这种情况下,你可以将视频使用高质量格式渲染作为中介(例如高比特率 H.264 视频),然后再重新编码成 Ogg Theora。
理想情况下,你应该使用无损或者未压缩格式作为中介格式,最大化输出 Ogg Theora 视频的质量,不过这样做会需要大量的磁盘空间。
FFmpeg (CLI) 是用于此目的的流行开源工具。FFmpeg 的学习曲线很陡峭,但它是一个强大的工具。
这是将 MP4 视频转换为 Ogg Theora 的 FFmpeg 命令示例。
因为 FFmpeg 支持很多输入格式,几乎任何输入视频格式(AVI、MOV、WebM……)应该都可以使用下面的命令。
请确保你的 FFmpeg 副本是启用 libtheora 和 libvorbis 编译的。检查方法是不带任何参数执行 ffmpeg,然后查看命令输出中的 configuration: 一行。
警告
当前官方 FFmpeg 版本的 Ogg/Theora 多路复用器中存在一些错误。
强烈建议使用最新的静态每日构建之一,或从其主分支构建以获取最新修复。
平衡质量与文件大小
- 视频质量等级(-q:v)必须在 1 和 10 之间。
将质量设为 6 是在质量和文件大小之间的一个不错的妥协。
如果要编码较高的分辨率(例如 1440p 或者 4K),你可能想要把 -q:v 降为 5,把文件大小控制在合理的范围内。
因为 1440p 和 4K 视频的像素密度更高,相较于低分辨率的视频,较低的质量预设看上去的效果是一样甚至更好的。 - 音频质量等级(-q:a)必须在 -1 和 10 之间。
将质量设为 6 是在质量和文件大小之间的一个不错的妥协。
与视频质量不同,提升音频质量并不会显著增加输出文件的大小。
因此,如果你想要尽可能清晰的音频,可以将其设为 9,达到感知上无损的音频。
在你的输入文件使用的已经是有损音频压缩时,这个设置尤其有用。
更高质量的音频确实会增加解码器的 CPU 使用率,因此在系统负载较高的情况下可能会导致音频丢失。
Ogg Vorbis 音频质量预设及其对应的可变比特率表见这个页面。 - **GOP(图片组)**大小 (-g:v) 是关键帧之间的最大间隔。
增加此值可以提高压缩率,而对质量几乎没有影响。
默认大小 (12) 对于大多数类型的内容来说太低,因此建议在降低视频质量之前使用更高的 GOP 值。
不过,随着 GOP 规模的增加,压缩优势将逐渐消失。64 到 512 之间的值通常提供最佳压缩。更高的 GOP 大小将增加最大寻道时间,当从 64 开始超过 2 的幂时会突然增加。GOP 大小的最大寻道时间 65 的长度几乎是 GOP 尺寸 64 的两倍,具体取决于解码速度。
FFmpeg:转换时保持原始视频分辨率
以下命令会在保持原始分辨率的前提下对视频进行转换。
视频和音频的比特率会被设为可变,在最大化质量的同时在不需要高比特率视频/音频的时候节省空间(例如静态场景)。
ffmpeg -i input.mp4 -q:v 6 -q:a 6 -g:v 64 output.ogv
FFmpeg:调整视频大小并转换
以下命令会在保持现有长宽比的前提下将视频调整到 720 像素高(720p)。
如果原始文件分辨率是大于 720p 的,就能够显著降低文件大小:
ffmpeg -i input.mp4 -vf “scale=-1:720” -q:v 6 -q:a 6 -g:v 64 output.ogv
色键视频
色键(Chroma Key)也就是“绿幕”效果,能够移除图像或视频中的特定颜色,替换为其他背景。
我们将通过在 GDScript 中编写自定义着色器,并使用 VideoStreamPlayer 节点来显示视频内容来实现色键效果。
场景设置
确保场景包含用于播放视频的 VideoStreamPlayer 节点,和用于保存用于控制色键效果的 UI 元素的 Control 节点。
编写自定义着色器
要实现色键效果,请按照下列步骤操作:
- 选择场景中的 VideoStreamPlayer 节点。
- 在 CanvasItem > Material 下,创建一个名为“ChromaKeyShader.gdshader”的新着色器。
- 在“ChromaKeyShader.gdshader”文件中,编写自定义着色器代码,如下所示:
1 | shader_type canvas_item; |
着色器使用距离计算来识别接近色键颜色的像素并将其丢弃,从而有效地删除所选颜色。
距离色键颜色稍远的像素将根据 fade_factor 进行淡入淡出,从而使它们与周围的颜色平滑地混合。
此过程会创建所需的色键效果,使其看起来像是背景已被其他图像或视频替换。
上面的代码是色键着色器的简单演示,用户可以根据自己的具体要求进行自定义。
UI 控件
为了允许用户实时操纵色键效果,我们在 Control 节点中创建了滑动条。
Control 节点的脚本包含以下功能:
1 | extends Control |
还要确保滑动条的范围合适,此处我们的设置是:
信号处理
将适当的信号从 UI 元素连接到你创建的 Control 节点的脚本上,来控制色键效果。这些信号处理函数会更新着色器的 uniform 变量,响应用户输入。
保存并运行场景来查看色键效果的实际表现!
通过 godot 提供的 UI 控件,现在你可以实时调整色键颜色、拾取范围(pickup range)和淡入度量(fade amount),从而为你的视频内容实现所需的色键功能。
创建电影
Godot 可以为任何 2D 或 3D 项目录制非实时音视频(离线渲染)适合很多不同的场景:
- 录制游戏预告片以供宣传使用。
- 录制过场动画。
- 记录程序生成的动画或动作设计。在视频录制过程中,仍可以进行用户交互,录制的视频中也可以包含音频(尽管在录制视频时你将无法听到它)。
- 比较动画场景中图形设置、着色器或渲染技术的视觉输出。
借助 Godot 的动画功能,例如 AnimationPlayer 节点、Tweeners、粒子和着色器,它可以有效地用于创建任何类型的 2D 和 3D 动画(以及静态图像)。
使用 Godot 进行视频渲染比 Blender 更加高效。
也就是说,非实时渲染器(例如 Cycles 和 Eevee)可以带来更好的视觉效果(代价是更长的渲染时间)。
与实时视频录像相比,非实时录像的一些优点包括:
- 无论你的硬件性能如何,都可以使用任何图形设置(包括要求极高的设置)。输出视频 始终 具有完美的帧节奏;它永远不会出现丢帧或卡顿的情况。更快的硬件将允许你在更短的时间内渲染给定的动画,而视觉输出保持不变。
- 以比屏幕分辨率更高的分辨率进行渲染,而无需依赖特定于驱动程序的工具,例如 NVIDIA 的动态超级分辨率(Dynamic Super Resolution)或 AMD 的虚拟超级分辨率(Virtual Super Resolution)。
- 以高于视频目标帧率的帧率进行渲染,然后进行后处理以生成高质量的运动模糊。这也使得在多个帧上聚合的效果(例如时间抗锯齿、SDFGI 和体积雾)看起来更好。
警告
此功能并非专为在游戏中捕捉实时镜头而设计。
玩家应该使用OBS Studio或SimpleScreenRecorder <https://www.maartenbaert.be/simplescreenrecorder/> 之类的工具来录制游戏视频,因为它们可以更好地截取合成器的输出,比 Godot 使用 Vulkan 或 OpenGL 所能完成的工作要多。
也就是说,如果你的游戏在录制时以接近实时的速度运行,你仍可以使用此功能(但它将缺少可被听见的音频播放,因为音频会直接保存到视频文件中)。
启用 Movie Maker 模式
在运行项目之前单击编辑器右上角的“电影胶片卷”(movie reel)按钮:
将显示一个菜单,其中包含启用 Movie Maker 模式和转到设置的选项。
启用 Movie Maker 模式时,图标将获得与强调色匹配的背景:
当编辑器退出时,Movie Maker 状态 不会 保留,因此如果需要,你必须在重新启动编辑器后再次重新启用 Movie Maker 模式。
在项目重新启动之前,运行项目时切换 Movie Maker 模式不会产生任何效果。
在通过运行项目录制视频之前,仍然需要配置输出文件路径。可以在项目设置中为所有场景设置该路径:
或者,你可以通过将名为 movie_file 的 String 元数据添加到场景的 根节点 ,来设置每个场景的输出文件路径。仅当主场景设置为相关场景时,或者通过按 F6(在 macOS 上 :kbd:Cmd + R)直接运行场景时,该功能才会被使用。
项目设置或元数据中指定的路径可以是绝对路径,也可以是相对于项目根目录的路径。
在配置并启用 Movie Maker 模式后,从编辑器运行项目时将自动使用该模式。
命令行用法
Movie Maker 也可以通过命令行启用:
godot –path /path/to/your_project –write-movie output.avi
如果输出路径是相对路径,那么它是 相对于项目文件夹 ,而不是当前工作目录。
在上面的示例中,文件将被写入 /path/to/your_project/output.avi。
此行为类似于 –export 命令行参数。
由于 Movie Maker 的输出分辨率是由视口大小设置的,因此如果项目使用 disabled 或 canvas_items 拉伸模式,你可以在启动时调整窗口大小以覆盖它:
godot –path /path/to/your_project –write-movie output.avi –resolution 1280x720
请注意,窗口大小受显示器分辨率的限制。如果你需要以比屏幕分辨率更高的分辨率录制视频,请参阅 以比屏幕分辨率更高的分辨率进行渲染。
录制的 FPS 也可以在命令行上覆盖,而无需编辑项目设置:
godot –path /path/to/your_project –write-movie output.avi –fixed-fps 30
备注
–write-movie 和 –fixed-fps 命令行参数在导出的项目中都可用。
项目运行时无法切换 Movie Maker 模式,但可以使用 OS.execute() 方法来运行导出项目的第二个实例以录制视频文件。
选择输出格式
输出格式由 MovieWriter 类提供。Godot 有 3 个内置的 MovieWriters,更多可以通过扩展实现:
OGV(推荐)
OGV 容器,其中 Theora 用于视频,Vorbis 用于音频。具有有损视频和音频压缩功能,在文件大小和编码速度之间取得了良好的平衡,图像质量比 MJPEG 更好。它有 4 个速度级别,可以通过更改编辑器 > Movie Writer > 编码速度来调整,最快的速度与 AVI 一样快,压缩率更好。在较慢的速度水平下,它可以更好地压缩,同时保持相同的图像质量。可以通过更改视频的编辑器 > Movie Writer > 视频质量和音频的编辑器 > Movie Writer > 音频质量来调整有损压缩质量。
关键帧间隔可以通过更改编辑器 > Movie Writer > 关键帧间隔来调整。在某些情况下,增加此设置可以提高压缩效率,而不会造成任何缺点。
生成的文件可以在 Godot 中使用 VideoStreamPlayer 和大多数视频播放器查看,但不能在网络浏览器中查看。OGV 不支持透明度。
要使用 OGV,请在编辑器 > Movie Writer > 影片文件项目设置中指定要创建的 .ogv 文件的路径。
OGV 只能在编辑器版本中录制。另一方面,OGV 播放 在编辑器和导出模板构建中都是可能的。
AVI
带有 MJPEG 的 AVI 容器,用于视频和未压缩音频。具有有损视频功能 压缩,从而产生中等文件大小和快速编码。
有损压缩质量可以通过更改来调整 编辑 > 电影编剧 > 视频质量 。
生成的文件可以在大多数视频播放器中查看,但必须将其转换为另一种格式才能在 Web 上查看,或在 Godot 使用 VideoStreamPlayer 节点查看。 MJPEG 不支持透明度。 AVI 输出的文件大小目前限制为最大 4 GB。
要使用 AVI,请 .avi 在 编辑器 > Movie Writer > Movie File 项目设置。
PNG
用于视频的 PNG 图像序列和用于音频的 WAV。具有无损视频压缩功能,但代价是文件较大且编码速度较慢。这被设计为 录制后使用外部工具编码为视频文件。
支持透明度,但根视口 必须 将其 transparent_bg 属性设置为 true ,以使透明度在输出图像上可见。这可以通过启用 渲染 > 视口 > 透明背景 高级项目设置来实现。 显示 > 窗口 > 大小 > 透明 和 显示 > 窗口 > 像素级透明度 > 启用 可以选择启用,以允许在录制视频时预览透明度,但不必在录制视频时启用它们。输出图像包含透明度。
要使用 PNG,请指定要在 编辑器 > Movie Writer > 电影文件 项目设置中创建的 .png 文件。生成的 .wav 文件将与 .png 文件同名(去掉扩展名的话)。
自定义
如果你需要直接编码为不同的格式或通过第三方软件传输数据流,可以扩展 MovieWriter 类来创建你自己的电影编写器(movie writers)。出于性能原因,这通常应该使用 GDExtension 来完成。
配置
在项目设置的 编辑器 > Movie Writer 部分中可配置多个选项。其中一些仅在启用“项目设置”对话框右上角的 高级选项 后才可见。
- 混音率: 编写电影时在录制的音频中使用的音频混合率。这可能与项目的混合速率不同,但该值必须能被录制的 FPS 整除,以防止音频随着时间的推移而失去同步。
- 扬声器模式: 编写电影时录制的音频中使用的扬声器模式(stereo 立体声、5.1 环绕声或 7.1 环绕声)。
- 视频质量: 将视频写入 OGV 或 AVI 文件时使用的图像质量,介于 0.01 到 1.0(含)之间。更高的质量值会导致输出更好看,但代价是文件大小更大。建议的质量值介于 0.75 和 0.9 之间。即使在质量 1.0 下,压缩仍然是有损的。此设置不会影响音频质量,并且在写入 PNG 图像序列时将被忽略。
- 电影文件: 电影的输出路径。这可以是绝对路径,或相对于项目根目录的相对路径。
- 禁用垂直同步: 如果启用,则在写入电影时请求禁用垂直同步。如果硬件足够快,能够以高于显示器刷新率的帧速率渲染、编码和保存视频,这可以加快视频写入速度。如果操作系统或图形驱动程序强制垂直同步且应用程序无法禁用它,则此设置无效。
- FPS: 输出影片中每秒渲染的帧数。值越高,动画越平滑,但代价是渲染时间更长和输出文件大小更大。大多数视频托管平台不支持高于 60 的 FPS 值,但你可以使用更高的值并用它来生成运动模糊。
- 音频质量: 将视频写入 OGV 文件时使用的音频质量,介于 -0.1 和 1.0(含)之间。更高的质量值会导致更好的音频质量,但代价是文件大小稍大。建议的质量值介于 0.3 到 0.5 之间。即使在质量上 1.0,压缩保持有损。
- 编码速度: 将视频写入 OGV 文件时要使用的速度级别。速度水平越快,压缩效率越低。图像质量几乎保持不变。
- 关键帧间隔: 也称为 GOP(图片组),写入 OGV 文件时使用的最大帧间数。较高的值可以提高压缩效率而不会造成质量损失,但代价是视频搜索速度较慢。
备注
当使用 disabled 或 2d 拉伸模式 时,输出文件的分辨率由窗口大小设置。确保在启动画面结束 之前 调整窗口大小。为此,建议调整高级设置中的 显示 > 窗口 > 大小 > 窗口宽度覆盖 和 窗口高度覆盖 。
另见 以比屏幕分辨率更高的分辨率进行渲染。
退出 Movie Maker 模式
为了安全退出使用 Movie Maker 模式的项目,请使用窗口顶部的 X 按钮,或在脚本中调用 get_tree().quit()。你也可以使用 –quit-after N 命令行参数,其中 N 是退出前要渲染的帧数。
不建议按 F8 ( Cmd + . 在 macOS 上)或按 运行 Godot 的终端 Ctrl + C ,因为这会导致格式不正确的 AVI 文件,没有持续时间信息。对于 PNG 图像序列,PNG 图像不会受到负面更改,但关联的 WAV 文件仍将缺少持续时间信息。OGV 文件最终的持续时间视频和音轨可能略有不同,但仍然有效。
某些视频播放器可能仍然能够播放包含有效视频和音频的 AVI 或 WAV 文件。但是,使用 AVI 或 WAV 文件的软件(例如视频编辑器)可能无法打开该文件。在这些情况下,使用视频转换器程序 可以提供一些帮助。
如果你使用 AnimationPlayer 来控制场景中的“主要动作”(例如摄像机移动),则可以在相关的 AnimationPlayer 节点上启用 Movie Quit On Finish 属性。启用后,当动画播放完毕 并且 引擎在 Movie Maker 模式下运行时,此属性将使 Godot 自行退出。请注意, 此属性对循环动画没有影响 。因此,你需要确保动画设置为非循环。
使用高质量的图形设置
movie 功能标签 可用于覆盖特定的项目设置。这对于启用高质量图形设置来说非常有用,但这些设置的速度不足以在硬件上以实时速度运行。
请记住,将每个设置设为最大值仍然会降低影片保存速度,尤其是在以更高分辨率录制时。
因此,建议仅在图形设置对输出图像产生可以有价值的影响时,再增加图形设置。
还可以在脚本中查询此功能标签,以提高环境资源中设置的质量设置。例如,为了进一步改善 SDFGI 细节并减少漏光:
1 | extends Node3D |
以比屏幕分辨率更高的分辨率进行渲染
通过4K或8K等高分辨率渲染可以显着提高整体渲染质量。
备注
对于 3D 渲染,Godot 在高级项目设置中提供了 渲染 > 缩放 3D > 缩放 ,可以将其设置为高于 1.0 以获得 超采样抗锯齿 。当 3D 渲染在视口上绘制时,它会被 降采样 。这提供了一种性能代价高昂但高质量的抗锯齿形式,并且不会增加最终的输出分辨率。
首先考虑使用此项目设置,因为与实际增加输出分辨率相比,它可以避免减慢影片写入速度和增加输出文件大小。
如果你希望以更高分辨率渲染 2D,或者实际上你需要更高的原始像素输出来进行 3D 渲染,则可以将分辨率提高到屏幕允许的分辨率之上。
默认情况下,Godot 在项目中使用 disabled 拉伸模式 。如果使用 disabled 或 canvas_items 拉伸模式,窗口大小决定输出视频分辨率。
另一方面,如果项目配置中使用 viewport 拉伸模式,则视口分辨率会决定输出视频分辨率。视口分辨率使用 显示 > 窗口 > 大小 > 视口宽度 和 视口高度 项目设置进行设置。这可用于以比屏幕分辨率更高的分辨率渲染视频。
要在录制过程中缩小窗口而不影响输出视频分辨率,可以将高级项目设置中 显示 > 窗口 > 大小 > 窗口宽度覆盖 和 窗口高度覆盖 设置为大于 0 的值。
要仅在录制电影时应用分辨率覆盖,可以使用 movie 功能标签 来覆盖这些设置。
后期处理步骤
以下列出一些常见的后期处理步骤。
当使用多步后处理时,请尝试在单一 FFmpeg命令中执行所有这些步骤。这将避免掉多个有损的编码步骤从而节省编码时间并提高品质。
将 OGV/AVI 视频转换为 MP4
尽管 YouTube 等一些平台支持直接上传 AVI 文件,但许多其他平台则需要事先进行格式转换。HandBrake(GUI)和 FFmpeg(CLI)都是这方面非常流行的开源工具。FFmpeg 的学习曲线相对陡峭,但功能也更强大。
以下命令将 OGV/AVI 视频转换为恒定速率因子 (CRF) 为 15 的 MP4 (H.264) 视频。这会导致文件相对较大,但非常适合重新编码视频以减小其大小的平台(例如大多数视频共享网站):
ffmpeg -i input.avi -crf 15 output.mp4
要以牺牲质量为代价获得较小的文件,请 增加 上述命令中的 CRF 值。
要获得具有更好的大小/质量比的文件(以较慢的编码时间为代价),请在上述命令中的 -crf 15 之前添加 -preset veryslow。相反地,-preset veryfast 可用于实现更快的编码,但代价是尺寸/质量比更差。
将 PNG 图像序列 + WAV 音频转换为视频
如果你选择录制 PNG 图像序列和 WAV 文件,则需要先将其转换为视频,然后才能在其他地方使用。
Godot 生成的 PNG 图像序列的文件名始终包含 8 位数字,从 0 开始,数字以零填充。如果指定输出路径 folder/example.png ,Godot 将在该文件夹中写入 folder/example00000000.png 、 folder/example00000001.png 等。音频将保存在 folder/example.wav 中。
FPS 使用 -r 参数指定。它应该与录制期间指定的 FPS 相匹配。否则视频会显得速度减慢或加快,并且音频与视频不同步。
ffmpeg -r 60 -i input%08d.png -i input.wav -crf 15 output.mp4
如果你在启用透明度的情况下录制了 PNG 图像序列,则需要使用支持存储透明度的视频格式。 MP4/H.264 不支持存储透明度,因此可以使用 WebM/VP9 作为替代方案:
ffmpeg -r 60 -i input%08d.png -i input.wav -c:v libvpx-vp9 -crf 15 -pix_fmt yuva420p output.webm
视频剪辑
录制视频后你可以剪辑掉不想保留的视频部分。例如,要丢弃 12.1 秒之前的所有内容,并仅保留该点之后 5.2 秒的视频:
ffmpeg -i input.avi -ss 00:00:12.10 -t 00:00:05.20 -crf 15 output.mp4
也可以使用 GUI 工具 LosslessCut 来剪辑视频。
视频缩放
以下命令将视频大小调整为 1080 像素高 (1080p),同时保留其现有的宽高比:
ffmpeg -i input.avi -vf “scale=-1:1080” -crf 15 output.mp4
降低帧率
下面的命令会将视频的帧率更改为 30 FPS,如果输入视频中帧率更高,则会丢弃一些原始帧:
ffmpeg -i input.avi -r 30 -crf 15 output.mp4
使用 FFmpeg 生成累积运动模糊
Godot 没有内置对运动模糊的支持,但仍然可以在录制的视频中创建运动模糊。
如果你以原始帧率的几倍来录制视频,则可以将帧混合在一起,然后再减少帧率以生成具有 累积运动模糊 的视频。这种运动模糊看起来很棒,但因为必须每秒渲染更多帧,生成可能需要很长时间(除了后期处理所花费的时间之外)。
以 240 FPS 的源视频为例,生成 4 倍运动模糊并将其输出帧率降低至 60 FPS:
ffmpeg -i input.avi -vf “tmix=frames=4, fps=60” -crf 15 output.mp4
因为这个操作将能够在给定时间内处理更多数据,所以也会使得在多个帧上的收敛效果(例如时间抗锯齿、SDFGI 和体积雾)变得更快,因此看起来表现更好。如果你想在不添加运动模糊的情况下获得这个表现提升,请参阅 降低帧率。
资产管线
导入流程
Godot 内部会自动导入外部导入文件,存放在隐藏的 res://.godot/imported/ 文件夹中。
在代码中尝试访问导入后的资产需要使用资源加载器(ResourceLoader),因为会自动考虑内部文件的存储位置。
如果用 FileAccess 尝试访问导入后的资产,虽然在编辑器中是可行的,但是在导出后的项目中会失败。
然而资源加载器(ResourceLoader)无法访问未经导入的文件,只有 FileAccess 类可以。
更改导入参数
要在 Godot 中更改资产的导入参数,请在“文件系统”面板中选中相关的资源:
参数调整完毕后,单击重新导入。
还可以同时更改多个资产的导入参数。
在文件系统停靠栏中一起选择所有这些参数,重新导入时,公开的参数将应用于所有这些参数。
当源资产的 MD5 校验发生变化时,Godot 将执行自动重新导入, 应用为该特定资产配置的预设.
重新导入多个资产
在进行项目时有数个资产都需要修改同一个参数的情况,例如启用 mipmap,只改动特定的参数。
为此,请选择要重新导入的多个资产。现在导入选项卡的每个导入参数的左边都会出现一个复选框。
生成的文件
导入时会在源文件的旁边生成额外的 <资产>.import 文件,包含的是导入配置。
请务必将这些文件提交进版本控制系统,包含重要元数据。
此外,额外的资产会被放在隐藏的 res://.godot/imported/ 文件夹中:
如果此文件夹中的任何文件(或整个文件夹)被删除,则会自动重新导入资产。
因此,不建议将 .godot/ 文件夹提交给版本控制系统。提交这个文件夹虽然能够缩短在另一台计算机上检出后重新导入的时间,但是需要相当多的空间和带宽。
在项目创建时生成的默认版本控制元数据将自动忽略 .godot/ 文件夹。
更改导入资源类型
一些源资产可以作为不同类型的资源被导入。为此,选择所需资源的相关类型,然后点击 重新导入 即可:
- 选择 Keep File (exported as is) 作为资源类型以跳过文件导入,具有此资源类型的文件将在项目导出期间按原样保留。
- 选择 Skip File (not exported) 作为资源类型以跳过文件导入并在项目导出期间忽略文件。
更改默认导入参数
不同类型的游戏可能需要不同的默认值。
通过使用预设按钮菜单可以将导入选项更改为预定义的选项集。
除了某些提供预设的资产类型外,还可以保存和清除默认设置:
可以使用“项目设置”对话框的 默认导入设置 选项卡,在项目范围内更改给定资源类型的默认导入参数:
导入图像
Godot 可以导入以下图像格式:
- BMP(.bmp)——不支持每像素 16 位的图像。只支持每像素 1 位、4 位、8 位、24 位和 32 位的图像。
- DirectDraw Surface(.dds)——如果纹理中存在 mipmap,则直接加载。 可以使用自定义 mipmap 制作特效。
- Khronos 纹理 (.ktx) —— 使用 libktx 解码。仅支持 2D 图像。不支持立方体贴图、纹理数组和去填充(de-padding)。
- OpenEXR(.exr)——支持 HDR(强烈推荐使用在全景天空上)。
- Radiance HDR(.hdr)——支持 HDR(强烈推荐使用在全景天空上)。
- JPEG(.jpg、.jpeg)——由于该格式的限制,不支持透明度。
- PNG(.png)——导入时精度限制为每个通道 8 位(无 HDR 图像)。
- Truevision Targa(.tga)
- SVG (.svg) - 使用 ThorVG 栅格化 SVG 导入它们时。 支持有限 ;复杂向量可能无法正确渲染。 文本必须转换为路径 ; 否则,它不会出现在栅格化图像中。 您可以使用其 ThorVG 的 基于 Web 的查看器 。对于复杂的向量,使用 Inkscape 将它们渲染为 PNG 通常是更好的解决方案。这可以自动化,这要归功于它的 命令行界面 。
- WebP(.webp)——WebP 文件支持透明,也支持有损和无损压缩。精度限制是每通道 8 位。
导入纹理
Godot 中的默认操作是将图像导入为纹理。纹理存储在显存中。
纹理的像素数据无法直接从 CPU 访问,除非在脚本中将它们转换回 Image。这就是使绘制纹理变得高效的原因。
在文件系统面板中选择图像后,可以调整十多个导入选项:
可以在“导入”面板中选择其他导入资源的类型:
- BitMap: 1 位单色纹理(旨在用作 TextureButton 和 TouchScreenButton 中的点击遮罩)。
此资源类型无法直接显示在 2D 或 3D 节点上,但可以使用 get_bit 从脚本中查询像素值。 - Cubemap: 将纹理导入为 6 面的立方体贴图,并在立方体贴图的侧面(无缝立方体贴图)之间进行插值,可以在自定义着色器中进行采样。
- CubemapArray: 将纹理导入为 6 面立方体贴图的集合,可以在自定义着色器中进行采样。
此资源类型只能在使用 Forward+ 或 Mobile 渲染器时显示,而不能显示 Compatibility 渲染器。 - Font Data (Monospace Image Font):(字体数据,等宽图像字体)将图像导入为位图字体,其中所有字符都具有相同的宽度。请参阅 使用字体。
- Image: 按原样导入图像。此资源类型无法直接显示在 2D 或 3D 节点上,但可以使用 get_pixel 从脚本中查询像素值。
- Texture2D: 将图像导入为2维纹理,适合在 2D 和 3D 表面上显示。这是默认的导入模式。
- Texture2DArray: 将图像导入为二维纹理的集合。 Texture2DArray 类似于 3 维纹理,但层之间没有插值。
内置 2D 和 3D 着色器无法显示纹理数组,因此必须在 2D 或 3D 中创建自定义着色器,以显示纹理数组中的纹理。 - Texture3D: 将图像导入为3维纹理。这不是应用到3D表面上的2D纹理。 Texture3D 类似于纹理数组,但在层之间进行插值。
通常用于体积雾的 FogMaterial 密度图、particle attractor 向量场、 Environment 3D LUT 颜色校正和自定义着色器。 - TextureAtlas: 将图像导入为不同纹理的 图集 。可用于减少动画 2D 精灵的内存使用量。由于缺少内置 3D 着色器的支持,仅支持 2D。
对于立方体贴图 ,预期的图像顺序是 X+、X-、Y+、Y-、Z+、Z-(在 Godot 的坐标系中,因此 Y+ 是“向上”,Z- 是“向前”)。
以下是可用于立方体贴图图像的模板(右键单击 > 链接另存为…):
检测 3D
默认的导入选项(不带 mipmap 并且使用 Lossless 压缩)适合 2D,对于大多数 3D 项目而言并不理想。
检测 3D能够让 Godot 关注纹理在 3D 场景中的使用(例如用作 BaseMaterial3D 的纹理)。
一旦使用,就会将部分导入选项进行修改,这样纹理标志就对 3D 更友好。
除非修改了 检查 3D > 压缩至,否则此时就会启用 mipmap 并将压缩模式修改为 VRAM Compressed。纹理会自动进行重新导入。
检测到纹理在 3D 中使用时会在“输出”面板中打印一个消息。
如果纹理检测到在 3D 中使用后遇到了质量问题(例如像素风纹理),请在用于 3D 之前修改 检查 3D > 压缩至 选项,或者在用于 3D 之后将 压缩 > 模式 修改为 Lossless。这样比禁用检测 3D 更好,因为仍然会启用 mipmap 生成,从远处观察纹理就不会感觉模糊。
压缩 > 模式
- Lossless:无损压缩。2D 资产的默认压缩模式。显示资产时不会有任何失真,磁盘压缩也比较合适。
但如果与 VRAM 压缩相比,使用的显存就明显要更多。这也是像素画的推荐设置。 - Lossy:有损压缩。适合较大的 2D 资产。会有一些失真,但是比 VRAM 压缩要少,文件大小比无损压缩以及 VRAM 未压缩要小好几倍。
这个模式不会降低显存占用;与无损压缩和 VRAM 未压缩相同。 - VRAM Compressed:VRAM 压缩。 3D 资产的默认压缩模式。
会降低磁盘上的大小,显存占用也会显著降低(通常是 4 到 6 倍)。应该避免在 2D 中使用这个模式,因为会有明显的失真,在低分辨率纹理上尤为明显。 - VRAM Uncompressed:VRAM 未压缩。仅适用于不能压缩的格式,例如原始浮点数图像。
- Basis Universal:这也是一种 VRAM 压缩模式,编码后的纹理格式为能够在加载时转码为大多数 GPU 压缩格式。
这样得到的文件就很小,能够利用 VRAM 压缩,但相对于 VRAM 压缩而言质量较差,压缩耗时也更长。
显存占用通常和 VRAM 压缩相同。Basis Universal 不支持浮点数图像格式(引擎会在内部回退至 VRAM 压缩)。
请注意,在分辨率较高的情况下,VRAM 压缩的影响要大得多。
当 VRAM 压缩采用 4:1 的压缩比(对于 S3TC 的不透明纹理为 6:1)时,可以在 GPU 上使用相同数量显存的同时,有效地使纹理在每个轴上增大两倍。
VRAM 压缩还减少了采样纹理所需的内存带宽,这可以加快在内存带宽受限场景中(集成显卡和移动设备上常见)的渲染速度。
综合这些因素,对于具有高分辨率纹理的 3D 游戏来说,VRAM 压缩技术是必备条件之一。
可以在文件系统停靠栏中双击纹理,然后查看检查器,来预览纹理占用的内存量
压缩 > 高质量
压缩 > HDR 压缩
压缩 > 法线贴图
压缩 > 通道打包
Mipmap > 生成
Mipmap > 限制
粗糙度 > 模式
粗糙度 > 原法线
处理 > 修复 Alpha 边框
处理 > 预乘 Alpha
处理 > 法线贴图翻转 Y
处理 > HDR 作为 sRGB
处理 > HDR 限制曝光
处理 > 大小限制
检查 3D > 压缩至
SVG > 缩放
编辑器 > 使用编辑器缩放
编辑器>为浅色编辑器主题转换颜色
导入包含文本的 SVG 图像
导入音频采样
Godot 提供了三个选项来导入音频数据:WAV、Ogg Vorbis 和 MP3。
不同的格式各有优点:
- WAV 文件使用原始数据或轻度压缩(IMA ADPCM 或 Quite OK Audio)。
目前它们只能以原始格式导入,但 Godot 允许在导入后进行压缩。
它们很轻,可以在 CPU 上播放(这种格式的数百个同时语音就可以了)。缺点是它们占用大量磁盘空间。 - Ogg Vorbis 文件使用更强的压缩,因此文件更小,但需要更多的处理能力才能播放。
- MP3 文件使用比 IMA ADPCM 或 Quite OK Audio 的 WAV 更好的压缩,但比 Ogg Vorbis 差。
这意味着与 Ogg Vorbis 质量大致相同的 MP3 文件将明显更大。从好的方面来说,与 Ogg Vorbis 相比,MP3 需要更少的 CPU 使用率来播放。
小技巧
请考虑使用 WAV 来实现短且重复的音效
使用 Ogg Vorbis 来实现音乐、语音和长音效
MP3 对于 CPU 资源有限的移动端项目和 Web 项目来说很有用,特别是在同时播放多个压缩声音(例如长环境声)时
导入翻译
常规国际化文本通常位于资源文件中(GNU 内容则是 .po 文件)。然
而,游戏可以使用比应用程序多几个数量级的文本,因此它们必须支持能高效处理多语言文本加载的方法。
有两种方法来生成多语言的游戏和应用程序(都基于键值对系统)。
- 使用其中一种语言作为“键”(通常是英语)
- 使用特定的标识符。(同时使用多种语言)
一般来说, 游戏使用第二种方法, 并为每个字符串使用唯一的ID.
这允许你在翻译为其他语言时修改文本. 唯一ID可以是数字, 字符串, 或带有数字的字符串
为了完成图片并允许对翻译的有效支持, Godot 有一个特殊的导入器,可以读取 CSV 文件。
大多数电子表格 编辑器可以导出为这种格式,因此唯一的要求是 文件有特殊的安排。
看 使用电子表格进行本地化, 以获取有关格式化和导入 CSV 的详细信息。
导入 3D 场景
可用的 3D 文件格式
glTF 2.0 (推荐使用)。Godot 完全支持文本(.gltf)和二进制( .glb )格式。
.blend (Blender)。这是通过调用 Blender 以透明方式导出到 glTF 来实现的(需要安装 Blender)。
DAE(COLLADA),一个受支持的比较老的格式。
OBJ(Wavefront)格式 + 它们的 MTL 材质文件。这也是完全支持的,但由于格式的限制支持相当有限(不支持轴心、骨架、动画、UV2、PBR 材质……)。
FBX,通过 ufbx 库支持。之前的导入工作流程使用 FBX2glTF 集成。
这需要安装一个外部程序,该程序链接到 专有的 FBX SDK,因此我们建议使用默认的 ufbx 方法或其他格式 上面列出(如果适合您的工作流程)。从 Blender 导出 glTF 2.0 文件(推荐)
- 作为 glTF 二进制文件(.glb)是较小的选项。包括在 Blender 中设置的网格和纹理。当引入 Godot 时,纹理是对象材质文件的一部分。
- 作为 glTF 文本文件,二进制数据和纹理独立(.gltf 文件 + .bin 文件 + 纹理)
将 glTF 与纹理分开使用的原因有两个:
一是将场景以基于文本的格式和二进制数据,描述在单独的二进制文件中。这对于版本控制很有用,如果要基于文本格式评审更改。
二是你需要将纹理文件与材质文件分开。如果你不需要其中任何一个,glTF 二进制文件(.glb)就可以了。
glTF 导入过程中,首先将 glTF 文件的数据加载到内存中的 GLTFState 类中,然后使用该数据生成 Godot 场景。
在运行时导入文件时,可以直接将该场景添加到树中。
导出过程则与此相反,Godot 场景被转换为 GLTFState 类,然后从中生成 glTF 文件。
在编辑器中导入 glTF 文件时,还有两个步骤:
生成 Godot 场景后,ResourceImporterScene 类用于应用其他导入设置,包括你通过导入面板和高级导入设置对话框配置的设置。
然后将其保存为 Godot 场景文件,这才是你运行/导出游戏时使用的文件。
警告
如果你的模型包含混合形状(也称为“形状关键帧”和“变形目标”)
则需要将 glTF 导出设置 数据 > 骨架 > 仅导出变形骨骼(Data > Armature > Export Deformation Bones Only) 配置为 启用(Enabled)。
无论如何导出不变形的骨骼都会导致不正确的着色。
- 直接在 Godot 中导入 .blend 文件
编辑器可以通过透明的方式调用 Blender 的 glTF 导出功能来直接导入 .blend 文件。
这样可以使你更快地迭代 3D 场景。
可以将场景保存在 Blender 中,按 alt-tab 返回 Godot,然后立即查看更改。
使用版本控制时也更有效,你不再需要将导出的 glTF 文件的副本提交到版本控制。
要使用 .blend 导入,您必须在打开 Godot 编辑器之前安装 Blender(如果打开已包含 .blend 文件的项目)。
如果您将 Blender 安装在其默认位置,Godot 应该能够自动检测其路径。
如果不是这种情况,请在编辑器设置中配置 Blender 可执行文件的路径(文件系统 > 导入)。
.blend 导入过程会首先转换为 glTF,因此仍然使用 Godot 的 glTF 导入代码。因此,.blend 导入过程与 glTF 导入过程相同,但在开始时会有一个额外的步骤。 - 从 Blender 导出的 DAE 文件
Blender 也有内置的 COLLADA 支持,但它无法正常工作以满足游戏引擎的需求,因此不应按原样使用。
但是,使用内置 Collada 支持导出的场景可能仍然适用于没有动画的简单场景。
对于复杂场景或包含动画的场景,强烈建议改用 glTF。 - 在 Godot 中导入 OBJ 文件
OBJ 是最简单的 3D 格式,Godot 应该能够成功导入大多数 OBJ 文件。
不过 OBJ 格式的限制也很多:不支持蒙皮、动画、UV2、PBR 材质。
- 在 Godot 中使用 OBJ 网格的方法有两种:
- 直接将它们加载到 MeshInstance3D 节点,或任何其他需要网格的属性(例如 GPUParticles3D)中。这是默认模式。
- 在导入面板中将其导入模式更改为 场景 ,然后重新启动编辑器。
这会允许使用与 glTF 或 Collada 场景相同的导入选项,例如在导入时展开 UV2(对于 使用光照贴图全局照明)。Blender 3.4 及更高版本可以在 OBJ 文件中导出 RGB 顶点颜色(这是 OBJ 格式的非标准扩展)。Godot 能够导入这些顶点颜色,但它们不会显示在材质上,除非您在材质上启用 顶点颜色 > 用作反照率 。
OBJ 网格的顶点颜色在导入后会保留其原始颜色空间(sRGB/线性),但其亮度被限制为 1.0(这些颜色不能过亮)。
- 在 Godot 中导入 FBX 文件
默认情况下,在 Godot 4.3 或更高版本中,添加到 Godot 项目的任何 FBX 文件都将使用 ufbx 导入方法。
任何在如 4.2 等之前的版本中添加到项目的文件将继续通过 FBX2glTF 方法导入,除非你进入该文件的导入设置,并将导入器更改为 ufbx。
如果你将 .fbx 文件保留在项目文件夹中,但不希望 Godot 导入它们,请在高级项目设置中禁用 文件系统 > 导入 > FBX > 启用 。
如果你想设置 FBX2glTF 的工作流程(一般情况下不推荐),除非你有具体理由——你需要下载 FBX2glTF 可执行文件
然后在编辑器设定 Filesystem > Import > FBX > FBX2glTFPath 中 指定该文件路径
FBX 导入过程中会首先转换为 glTF,因此过程仍然使用 Godot 的 glTF 导入代码。FBX 导入过程与 glTF 导入过程相同,但在开始时多了一个额外的步骤。
导出模型的注意事项
- 单独导出纹理
虽然纹理可以和模型一起以某些文件格式导出,如 glTF 2.0,也可以单独导出它们。
Godot的材质使用 PBR(基于物理的渲染),所以如果一个纹理程序可以导出 PBR 纹理,它们就可以在 Godot 中工作。
这包括 Substance 套件,ArmorPaint (开源) ,Material Maker (开源) 。 - 导出注意事项
由于 GPU 只能渲染三角形,所以包含四边形或 N 边形的网格必须在渲染前进行三角剖分。
Godot 可以在导入时对网格进行三角剖分,但结果可能无法预测或不正确,特别是对于 N 边形。
无论目标应用是什么,在导出场景之前进行三角剖分会得到更一致的结果,因此应该尽可能地进行三角剖分。
为了避免在 Godot 中导入后出现三角剖分不正确的问题,建议让 3D 建模软件自行对对象进行三角剖分。
在Blender中,可以通过向对象添加三角剖分修改器,并确保在导出对话框中勾选 应用修改器 来实现。
另外,根据导出工具的不同,你可以在导出对话框中找到并启用 Triangulate Faces 选项。
为了避免在编辑器中出现 3D 选择的问题,建议在导出场景前在 3D 建模软件中应用对象变换。 - 光照注意事项
虽然可以使用 glTF、 .blend 或 Collada 格式从 3D 场景中导入灯光,但通常建议在导入场景后在 Godot 编辑器中设计场景的照明。
这样可以让你更准确地感受到最终结果,因为不同的引擎会以不同的方式渲染灯光。这也避免了由于导入过程中灯光显示过强或过弱的问题。
使用名称后缀自定义节点类型
下面描述的所有后缀都可以与 -,$ 和 _ 搭配使用,并且是大小写不敏感。
- 选择退出
不希望 Godot 执行下面描述的任何操作
可以将 nodes/use_node_type_suffixes import 选项设置为 false。
这将禁用所有节点类型后缀,从而使节点与指示的原始文件保持相同的类型。
但是,仍然会尊重 -noimp 后缀,以及 -vcol 或 -loop 等非节点后缀。
或者,您可以通过将 nodes/use_name_suffixes import 选项设置为 false 来完全选择退出所有名称后缀。
这将完全阻止常规场景导入代码查看名称后缀。但是,特定于格式的导入代码可能仍会查看名称后缀,例如 glTF 导入器检查 -loop 后缀。
禁用这些选项会使编辑器导入的文件与原始文件更相似,并且更类似于在运行时导入文件。
对于在运行时工作、提供更可预测的结果且仅具有显式定义行为的导入工作流,请考虑将这些选项设置为 false 并改用 GLTFDocumentExtension。 - 删除节点和动画 (-noimp)
具有 -noimp 后缀的节点和动画将在导入时被删除,无论其类型如何。它们不会出现在导入的场景中。
这相当于在高级导入设置对话框中为节点启用 跳过导入 功能。 - 创建碰撞体(-col、-convcol、-colonly、-convcolonly)
-col 选项只作用于网格物体. 如果该选项被检测到, 将会添加一个静态碰撞体的子节点, 用的是跟网格一样的几何体.
这会创建一个三角形网格碰撞体, 这个选项对碰撞检测来说很慢但是精确.
这个选项通常是关卡几何体需要的(但是也看看下面的 -colonly ).
选项 -convcol 将创建一个 ConvexPolygonShape3D 而不是 ConcavePolygonShape3D。
与可以凹陷的三角形网格不同,凸形只能准确表示没有任何凹角的形状(金字塔是凸的,但空心盒子是凹的)。
因此,凸碰撞形状通常不适合关卡几何体。当表示足够简单的网格时,凸碰撞形状可以比三角形碰撞形状产生更好的性能。
该选项非常适合需要最精确碰撞检测的简单或动态对象。
然而,在这两个例子中,视觉几何体处理过于复杂或对于碰撞而言不够光滑。
物理引擎会出现小故障从而不必要地降低了引擎的速度。
为了解决这个问题,存在 -colony 修饰符。
该修饰符将在导入时删除网格,并创建一个 StaticBody3D 静态碰撞体。
这有助于将可视网格和实际碰撞体分开。
-convcolonly 选项的工作方式类似,但会创建一个 ConvexPolygonShape3D,而不是使用凸分解。
对于 Collada 文件,选项 -colonly 也可以与 Blender 的空对象一起使用。
导入时,它将创建一个 StaticBody3D,并将碰撞节点作为子节点。
碰撞节点将具有许多预定义的形状之一,具体取决于 Blender 的空对象绘制类型: - 在 Blender 中为 Empty 选择创建时的绘制类型
- 单箭头将创建 SeparationRayShape3D。
- 方块将创建 BoxShape3D。
- 图像将创建 WorldBoundaryShape3D。
- 球体(和其他未列出的类型)将创建 SphereShape3D。
可能的话,请试着使用少量图元碰撞形状,而不是三角形网格或凸型体。图元形状的性能和可靠性通常更好。为了在 Blender 编辑器上获得更好的可见性,可以在碰撞空物体上设置“透视”选项,并通过更改 编辑 > 偏好设置 > 主题 > 3D 视图 > 空物体 选项为其设置不同的颜色。
- 创建遮挡器(-occ、-occonly)
如果导入带有 -occ 后缀的网格,则会根据网格的几何形状创建一个 Occluder3D 节点,它不会替换网格。带有 -occonly 后缀的网格节点在导入时将转换为 Occluder3D。 - 创建导航(-navmesh)
具有 -navmesh 后缀的网格节点, 将被转换为导航网格. 原始网格节点将在导入时被删除. - 创建 VehicleBody(-vehicle)
具有 -vehicle 后缀的网格节点, 将作为一个 VehicleBody3D 节点的子节点被导入。 - 创建 VehicleWheel(-wheel)
具有 -wheel 后缀的网格节点,将作为一个 VehicleWheel3D 节点的子节点被导入。 - 刚体(-rigid)
具有 -rigid 后缀的网格节点,将作为一个 RigidBody3D 节点的子节点被导入。 - 动画循环(-loop、-cycle)
源 3D 文件中以标记 loop 或 cycle 开始或结束的动画剪辑,将作为设置了循环标志的 Godot Animation 导入。 这与上述其他后缀不同,不需要连字符。
在 Blender 中,这需要使用 NLA 编辑器,并用 loop 或 cycle 前缀或后缀命名该动作。 - 材质 alpha (-alpha)
带有 -alpha 后缀的材质将使用 TRANSPARENCY_ALPHA 透明模式。 - 材质顶点颜色 (-vcol)
带有 -vcol 后缀的材料将与 FLAG_ALBEDO_FROM_VERTEX_COLOR 和 FLAG_SRGB_VERTEX_COLOR 标志设置。
导入配置
Godot提供了多种自定义导入数据的方法,比如导入停靠面板、高级导入设置对话框和继承场景。
这可以用来对导入的场景进行进一步的更改,比如调整网格、添加物理信息和添加新节点。
你还可以编写一个脚本,在导入过程结束时运行代码,以执行任意的自定义操作。
请尽可能在导入前修改初始数据,而不是导入后再配置场景。这有助于最大程度降低场景在 3D 建模软件中与实际导入场景之间的差异。
可参考 导出模型的注意事项 及 使用名称后缀自定义节点类型 来获取更多信息。
导入工作流程
由于 Godot 只能保存自己的场景格式(.tscn/.scn),Godot 无法保存原始 3D 场景文件(使用不同的格式)。
为了允许自定义场景及其材质,Godot 的场景导入器允许不同的工作流程,视数据的导入方式而定。
这个导入过程可以通过3个不同的界面进行自定义,具体取决于你的需求:
- 在“文件系统”面板中单击一次 3D 场景后,可以在导入面板进行操作。
- 高级导入设置对话框,可以通过双击文件系统停靠栏中的 3D 场景或单击导入停靠栏中的高级 … 按钮来访问该对话框。
这允许您在 Godot 中自定义每个对象的选项,并预览模型和动画。请参阅高级导入设置 页面以获取更多信息。 - 导入提示是添加到 3D 建模软件中的对象名称的特殊后缀,它可以允许你在 3D 建模软件中自定义每个对象的选项。
对于基本自定义,使用导入面板的配置就足够了。
但是对于更复杂的操作(例如基于每种材质定义材质覆盖),你需要使用“高级导入设置”对话框或导入提示,亦或者同时使用两者。
使用导入面板
在文件系统面板选中一个 3D 场景之后,可以在导入面板中调整以下选项:
- 根类型: 被用作为根节点的节点类型。
建议使用一个继承自 Node3D 的节点。否则你可能会无法直接在 3D 编辑器里面设置节点的位置。 - 根名称: (Root Name)导入场景中根节点的名称。
在编辑器中实例化场景(或从文件系统面板拖放)时,这通常不太明显,因为在这种情况下,根节点会被重命名以匹配文件名。 - 应用根缩放: (Apply Root Scale)如果启用, 根缩放 将直接 应用 于网格和动画,同时保持根节点的缩放为默认值 (1, 1, 1) 。
这意味着如果你稍后在导入的场景中添加一个子节点,它不会被缩放。如果禁用, 根缩放 将乘以根节点的缩放。
网格
- 确保切线: (Ensure Tangents)如果勾选,在导入的网格没有提供切线数据时,将会使用 Mikktspace 生成顶点的切线。
但更推荐使用 3D 建模软件在导出的时候生成切线图像,而不是依赖这个选项。
正确显示法线和高度贴图以及需要切线的任何材质/着色器功能都需要切线。
如果你不使用需要切线的材质特性,关闭这个选项可以减少导出文件的大小,并且能更快地导入没有存储切线的 3D 文件。 - 生成 LOD: 如果勾选,则会生成网格的低细节的变体,这些变体将显示在远处以提高渲染性能。
并非所有网格体都会受益于 LOD,特别是如果它们从未从远处渲染。
禁用此功能可以减少输出文件大小并加快导入速度。请参阅 网格的细节级别(LOD) 了解更多信息。 - 创建阴影网格: 如果勾选,则可以在导入时生成阴影网格。
这可以通过在可能的情况下将顶点接在一起来优化阴影渲染,而不会降低质量。
这反过来又减少了渲染阴影所需的内存带宽。
阴影网格生成当前不支持使用比源网格更低的细节级别(但阴影渲染将在相关时使用 LOD)。 - Light Baking:光照烘焙,在 3D 场景中配置网格的全局光照模式。
如果设置为 Static Lightmaps(静态光照贴图),则将网格的 GI 模式设置为 Static(静态)并在导入时生成 UV2,用于烘焙光照贴图。 - 光照贴图纹素大小: (Lightmap Texel Size)仅当 光照烘焙 设置为 Static Lightmaps 时可见。
用来控制烘焙光照贴图上每个纹素的大小。
较小的值会产生更精确的光照贴图,但代价是更大的光照贴图大小和更长的烘焙时间。
蒙皮
- 使用具名蒙皮: 如果勾选,则为动画使用命名的 Skins。
MeshInstance3D 节点包含 3 个相关属性:指向 Skeleton3D 节点的骨架 NodePath(通常为 .. )、一个网格和一个蒙皮:- Skeleton3D 节点包含一个骨骼列表,其中包括骨骼的名称、它们的姿势和休息状态、一个名称和一个父骨骼。
- 网格是显示网格所需的所有原始顶点数据。就网格而言,它知道如何对顶点进行加权绘制,并使用一些通常从 3D 建模软件导入的内部编号。
- 蒙皮包含将此网格物体绑定到此 Skeleton3D 上所需的信息。
对于 3D 建模软件选择的每一个内部骨骼 ID,它都包含两个内容。
首先,一个矩阵,称为绑定姿势矩阵(Bind Pose Matrix)、逆绑定矩阵( Inverse Bind Matrix),或者简称 IBM。
其次,蒙皮包含每个骨骼的名称(如果启用 使用具名蒙皮 选项),或者骨骼在 Skeleton3D 列表中的索引(如果禁用了 使用具名蒙皮 选项)。
这些信息加在一起,足以告诉 Godot 如何使用骨骼 3D 节点中的骨骼姿势来渲染每个 MeshInstance3D 中的网格。请注意,每个 MeshInstance3D 都可以共享绑定,这在从 Blender 导出的模型中很常见;或者每个 MeshInstance3D 都可以使用单独的蒙皮对象,这在从其他工具(如 Maya)导出的模型中很常见。
动画
- 导入: 如果选中,则从 3D 场景导入动画。
- FPS: 用线性插值将动画曲线烘焙为一系列点时使用的每秒帧数。
建议将此值设置为与 3D 建模软件中的基准值相匹配。
数值越大,动画越精确,动作变化越快,但文件大小和内存使用量也越大。
由于采用了插值技术,超过 30 FPS 通常不会有太大的好处(因为动画在更高的渲染帧频下仍会显得流畅)。 - 修剪: 如果没有关键帧变化,则修剪动画的开头和结尾。这可以减少某些 3D 场景的输出文件大小和内存使用量,具体取决于其动画轨道的内容。
- 移除不可修改的轨道: 移除只包含默认值的动画轨道。这可以减少某些 3D 场景的输出文件大小和内存使用量,具体取决于其动画轨道的内容。
导入脚本
- Path:路径,导入脚本的路径,该脚本可在导入过程完成后运行代码,以进行自定义处理。
更多信息请参阅 使用导入脚本实现自动化。
glTF
- 嵌入图像处理: 控制如何处理嵌入 glTF 场景中的纹理。
Discard All Textures (忽略所有纹理)不会导入任何纹理,如果你想在 Godot 中手动设置材质,该选项将非常有用。
Extract Textures (提取纹理)将纹理提取到外部图像中,从而减小文件大小,并对导入选项进行更多控制。
Embed as Basis Universal (嵌入为基础通用)和 Embed as Uncompressed (嵌入为未压缩)分别将纹理嵌入已导入的场景中,并对 VRAM 进行压缩和不压缩。
FBX
- 进口商使用哪种导入方法。UBFX 将 FBX 文件作为 FBX 文件进行处理。
FBX2glTF 在导入时将 FBX 文件转换为 glTF,需要额外的设置。
不建议使用 FBX2glTF,除非您有特定的 rason 在 ufbx 上使用它或处理不同的文件格式。 - 启用或禁用几何辅助节点
- 嵌入图像处理: 控制如何处理嵌入 glTF 场景中的纹理。
Discard All Textures (忽略所有纹理)不会导入任何纹理,如果你想在 Godot 中手动设置材质,该选项将非常有用。
Extract Textures (提取纹理)将纹理提取到外部图像中,从而减小文件大小,并对导入选项进行更多控制。
Embed as Basis Universal (嵌入为基础通用)和 Embed as Uncompressed (嵌入为未压缩)分别将纹理嵌入已导入的场景中,并对 VRAM 进行压缩和不压缩。
Blender-specific 选项
仅对 .blend 文件可见。
Nodes 节点
- Visible:All 导入所有内容,甚至是不可见的对象。
仅可见 仅导入可见对象。Renderable 仅导入在 Blender 中标记为可渲染的对象,无论它们是否实际可见。
在 Blender 中,渲染性可以通过单击 Outliner 中每个对象旁边的相机图标来切换,而可见性则通过眼睛图标来切换。 - 仅限活动收集: 如果选中,则仅导入 Blender 中活动集合中的节点。
- 准时灯光: 如果选中,则从 Blender 导入灯光(定向、全向和聚光灯)。“准时”不要与“位置”混淆,这就是为什么还包括定向灯的原因。
- 相机: 如果选中,则从 Blender 导入摄像机。
- 自定义属性: 如果选中,则从 Blender 导入自定义属性作为 glTF extras。
然后,可以从编辑器插件中使用此数据,该插件使用 GLTF 文档。
register_gltf_document_extension(),它可以在导入时设置节点元数据(以及其他用例)。 - 修饰 符: 如果设置为 “无修改器”,则在导入时将忽略对象修改器。如果设置为 “所有修改器”,则在导入时将修改器应用于对象。
网格
- 颜色: 如果选中,则从 Blender 导入顶点颜色。
- 乌布苏: 如果选中,则从 Blender 导入顶点 UV1 和 UV2。
- 法线: 如果选中,则从 Blender 导入顶点法线。
- 导出几何体节点实例: 如果选中,则导入 几何体节点 实例。
- 切线: 如果选中,则从 Blender 导入顶点切线。
- Skins:None 跳过从 Blender 导入骨架皮肤数据。
4 影响(兼容) 导入皮肤数据以与所有渲染器兼容,但代价是精度较低 对于某些钻机。
All Influences 导入具有所有影响的皮肤数据(在 Godot 中最多 8 个),这更精确,但可能与所有渲染器不兼容。 - 仅导出骨骼变形网格体(Export Bones Deforming Mesh): 如果选中,则仅从 Blender 导入使网格变形的骨骼。
Materials材料
- 已启用解压: 如果选中,则将原始图像解压缩到 Godot 文件系统并使用它们。
这允许更改图像导入设置,例如 VRAM 压缩。如果未选中,则允许 Blender 转换原始图像,例如将粗糙度和金属重新打包为一个粗糙度 + 金属纹理。
在大多数情况下,应选中此选项,但如果 .blend 文件的图像格式不正确,则必须禁用此选项才能获得正确的行为。 - 出口材料: 如果设置为 “占位符”,则不导入材质,但保留曲面槽,以便可以将单独的材质分配给不同的曲面。
如果设置为 导出(Export), 则按原样导入材质(请注意,程序化 Blender 材质可能无法正常工作)。
如果设置为 “命名占位符”,则导入材质,但不导入打包到 .blend 文件中的图像。纹理必须在导入的材质中手动重新分配。
动画
- 限制播放: 如果选中,则将动画导入限制在 Blender 中定义的播放范围(Blender 中动画时间轴右侧的 开始 和 结束 选项)。
这可以避免包含未使用的动画数据,使导入的场景更小、加载速度更快。
但是,如果在 Blender 中未正确设置播放范围,这也可能导致动画数据丢失。 - 始终采样: 如果选中,则在导入时强制对动画进行采样,以确保 Blender 和 glTF 执行动画插值的方式之间的一致性,但代价是文件大小较大。
如果未选中,由于两者之间的插值语义不同,您在 Blender 中看到的内容和在 Godot 中导入的场景之间的动画插值方式可能会有所不同。 - 团体轨道: 如果选中,则将动画(活动动画和 NLA 轨道上的动画)导入为单独的轨道。
如果未选中,则所有当前分配的作都将变为一个 glTF 动画。
使用导入脚本实现自动化
可以提供一个特殊脚本来处理导入后的整个场景。
这非常适合后期处理、更换材质和用几何图形做有趣的事情等等。
通过右键单击文件系统面板并选择 新建脚本… ,创建一个不附加到任何节点的脚本。
在脚本编辑器中,编写以下内容:
1 | @tool # 必须添加此注解,才能在编辑器中运行(包括导入时) |
使用动画库
从 Godot 4.0 开始,可以选择从 glTF 文件导入 仅 动画,而不导入其他信息。
这在某些资产管线中用于将动画与模型分开分发。比如,给多个角色使用一套动画,而每个角色不必有重复的动画数据。
那么,请在文件系统栏目中选择 glTF 文件,然后在导入栏中更改导入模式为动画库:
glTF 文件将作为 AnimationLibrary 导入,而不是 PackedScene 。然后,可以使用 AnimationPlayer 节点引用此动画库。
更改导入模式为动画库后,可见的导入选项与使用场景导入模式时相同。有关更多信息,请参阅 使用导入面板 。
过滤脚本
可以使用特殊语法指定过滤器脚本, 以决定应保留哪些动画的哪些轨道.
过滤脚本对每个导入的动画执行。语法由两种类型的语句组成,第一种用于选择要过滤的动画,第二种用于过滤匹配动画中的单个轨道。所有名称模式都使用不区分大小写的表达式匹配,并支持 ? 和 * 通配符【底层使用 String.matchn() 】
脚本必须以动画筛选器语句开头(如以 @ 开头的行表示)。
例如,如果我们想将过滤器应用于名称以 “_Loop” 结尾的所有导入动画:@+*_Loop
同样,可以将其他模式添加到同一行,用逗号分隔。
下面是一个修改后的示例,以额外包括名称以 “Arm_Left” 开头的所有动画,但也排除了名称以 “Attack” 结尾的所有动画:@+*_Loop, +Arm_Left*, -*Attack
在动画选择过滤器语句之后, 我们添加轨道过滤模式来指示保留或丢弃哪些动画轨道. 如果未指定轨道过滤器模式, 则匹配动画中的所有轨道都会被丢弃!
需要注意的是, 轨道过滤器表达式是按顺序作用于动画中的每条轨道, 这意味着, 一行表达式可能包含某个轨道, 但后续的规则仍然可以忽略它. 同样, 一个被之前规则排除的轨道, 可能被过滤器脚本后续的规则重新包含进来.
例如:在名称以 “_Loop” 结尾的动画中包括所有轨道,但丢弃任何影响以 “Control” 结尾的“ 骨架” 的轨道,除非它们的名称中带有 “Arm”:
1 | @+*_Loop |
在上面的示例中,像 “Skeleton:Leg_Control” 这样的轨道会被丢弃,而像 “Skeleton:Head” 或 “Skeleton:Arm_Left_Control” 这样的轨道会被保留。
任何不是以 + 或 - 开头的轨道过滤器行将会被忽略.
场景继承
在许多情况下,可能需要对导入的场景进行手动修改。
默认情况下,这是不可能的,因为如果源 3D 资产发生变化,Godot 将重新导入整个场景。
然而,可以使用场景继承来创建本地修改。如果你尝试使用场景 > 打开场景… 或场景 > 快速打开场景… 来打开已导入的场景,以下对话框将会出现:
在继承场景中,修改的唯一限制是:
- 无法删除基础场景中的节点,但可以在任何地方添加其他节点。
- 子资源无法被编辑(如上所述它们将保存在外部)。
除此之外,一切都是允许的。
高级导入设置
常规导入面板为导入的 3D 模型提供了许多基本选项,而高级导入设置则提供每个对象的选项、模型预览和动画预览。
要打开它,请选择导入停靠栏底部的高级按钮。
这适用于作为场景导入的 3D 模型以及动画库。
使用高级导入设置对话框
你看到的第一个选项卡是场景选项卡。
右侧面板中的选项与“导入”面板相同,但是你还可以看到 3D 预览。
按住鼠标左键并拖动鼠标就能够旋转 3D 预览。缩放可以通过鼠标滚轮调整。
配置节点导入选项
在场景选项卡中,可以在左侧的树状视图中单独选中构成场景的节点:
这样就会出现针对节点的导入选项:
- 跳过导入:勾选后,该节点不会出现在最终导入的场景中。启用这个选项会禁用其他所有选项。
- 生成 > 物理:勾选后,会生成一个 PhysicsBody3D 父节点,碰撞形状会作为该 MeshInstance3D 节点的同级节点。
- 生成 > 导航网格:勾选后,会生成一个 NavigationRegion3D 子节点用于进行导航。
Mesh + NavMesh 会保持原有网格可见,而 NavMesh Only 则只会导入导航网格(不带可视化表示)。
NavMesh Only 应该在手动制作了用于导航的简化网格时使用。 - 生成 > 遮挡器:勾选后,会生成一个 OccluderInstance3D 同级节点用于进行遮挡剔除,会使用网格的几何体作为遮挡器形状的基础。
Mesh + Occluder 会保持原有网格可见,而 Occluder Only 则只会导入遮挡器(不带可视化表示)。
Occluder Only 应该在手动制作了用于遮挡剔除的简化网格时使用。
仅当启用上述某些选项时,这些选项才可见:
- 物理 > 实体类型:仅在启用 生成 > 物理 后可见。
控制创建的 PhysicsBody3D:Static 即创建 StaticBody3D,Dynamic 创建 RigidBody3D,Area 创建 Area3D。 - 物理 > 形状类型: 仅在 生成 > 物理 启用时可见。
Trimesh (三角网格)可实现精确的三角形碰撞,但是它只能与 Static 主体类别一起使用。
其他类型精度较低,可能需要手动配置,但可以用于任何实体类型。对于静态几何体,请使用 Trimesh 。
对于动态几何体, 尽可能使用图元,以获得更好的性能, 如果形状较大且复杂,则可以使用其中一种凸分解模式。 - 拆分 > 高级: 仅当 物理 > 形状类型 为 Decompose Convex (凸面分解)时可见。
如果选中,则可以调整高级拆分选项。如果禁用,则只能调整预设的 精度 (通常就足够了)。 - 拆分 > 精度: 仅当 物理 > 形状类型 为 Decompose Convex 时可见。控制用于凸面分解的精度。
数值越高,碰撞的细节越多,但生成速度会变慢,物理模拟时的 CPU 占用率也会增加,为提高性能,建议在使用时尽可能降低该值。 - 遮挡器 > 化简距离: 仅当 生成 > 遮挡器 设置为 Mesh + Occluder 或 Occluder Only 时可见。
数值越大,遮挡网格的顶点越少(从而降低 CPU 利用率),但代价是会出现更多遮挡剔除问题(如误报或漏报)。
如果你发现当摄像机靠近某个网格时,物体在不该消失的时候消失了,请尝试减小该值。
配置网格体和材质导入选项
在 “高级导入设置 “对话框中,有两种方法可以选择单个网格或材质:
切换到对话框左上角的 网格 或 材质 选项卡。
保留在 场景 选项卡中,但展开左侧树状视图中的选项。选择网格或材质后,会显示与 网格 和 材质 选项卡相同的信息,但显示的是树状视图而不是列表。
如果你选择了一个网格,右侧面板上将会出现不同的选项:
最常见的用例如下:
- 保存到文件: 将 Mesh 资源 保存到一个外部文件中(这不是一个场景文件)。
通常,你不需要使用这个功能来将网格放置在一个3D场景中——相反地,你应该直接实例化 3D 场景。
然而,直接访问 Mesh 资源对于特定节点很有用,例如 MeshInstance3D、 MultiMeshInstance3D 、 GPUParticles3D 或 CPUParticles3D。
启用 保存到文件 后,你还需要使用出现的选项来指定一个输出文件路径。
建议使用 .res 输出文件扩展名,因为它具有较小的文件大小和更快的加载速度,而 .tres 用于写入大量数据时效率不高。 - 生成 > 阴影网格: 针对 使用导入面板 中描述的场景范围导入选项 网格 > 创建阴影网格 的每个网格进行覆盖。
Default (默认)将使用场景范围的导入选项,而 Enable (启用)或 Disable (禁用)可以在特定网格上强制启用或禁用此行为。 - 生成 > 光照贴图 UV: 针对 使用导入面板 中描述的场景范围导入选项 网格> 光照烘焙 的每个网格覆盖。
Default (默认)将使用场景范围的导入选项,而 Enable (启用)或 Disable (禁用)可以在特定网格上强制启用或禁用此行为。
在具有 Static 光烘焙模式的场景中将此设置为 Enable 相当于配置该网格使用 Static Lightmaps (静态光照贴图)。
在具有 Static Lightmaps 光烘焙模式的场景中将此设置为 Disable 相当于配置该网格使用 Disable 。 - 生成 > LOD: 针对 使用导入面板 中描述的场景范围导入选项 网格> 生成 LOD 的每个网格进行覆盖。
Default 将使用场景范围的导入选项,而 Enable 或 Disable 可以在特定网格上强制启用或禁用此行为。 - LOD > 法线合并角度: 在生成网格 LOD 中保留几何边缘所需要的两个顶点之间的最小角度差异。
如果在 LOD 生成中遇到视觉问题,减小此值可能会有所帮助(但会以更低效的 LOD 生成为代价)。
如果你选择一种材质,在右侧面板中只会出现一个选项:
当选中 使用外部 并指定输出路径时,可以使用外部材质而不是原始 3D 场景文件中所包含的材质。请参阅下面的部分。
将材质提取到单独文件中
虽然 Godot 可以导入在 3D 建模软件中编辑过的材质,但默认配置可能不适合你的需求。例如:
- 你想要配置你所使用的 3D 应用程序不支持的材质特性。
- 你想要使用不同的纹理过滤模式,因为从 Godot 4.0 开始此选项是在材质中配置的(而不是在图像中)。
- 你想要将其中一种材质替换为完全不同的材质,例如一个自定义着色器。
为了能够在 Godot 编辑器中修改 3D 场景的材质,需要使用 外部 材质资源。
在“高级导入设置”对话框的左上角,选择 动作 > 提取材质 :
启用 使用外部 时,请注意,“高级导入设置”对话框将继续显示网格的原始材质(在 3D 建模软件中设计的材质)。
这意味着你对材质进行的自定义在此对话框中不可见。
为了预览修改后的材质,需要使用编辑器将导入的 3D 场景放置在另一个场景中。
重新导入源 3D 场景时,Godot 不会覆盖对提取材质所做的更改。
然而,如果源 3D 文件中的材质名称发生更改,则原始材质和提取的材质之间的链接将会丢失。
为此,你需要使用“高级导入设置”对话框,将重命名后的材质与现有提取的材质相关联。
上述操作可以在对话框的 材质 选项卡中完成。
操作方法是,首先选择材质,启用 保存为文件 ,然后使用启用 保存为文件 后出现的 路径 选项指定保存路径。
动画选项
生成的 AnimationPlayer 有几个额外的选项可用节点,以及它们在场景选项卡 。
优化
导入动画时, 会运行优化程序, 从而大大减少动画的大小.
一般情况下, 除非你怀疑动画可能因启用而被破坏, 否则应始终启用此功能.
保存到文件
默认情况下, 动画保存为内置.
可以将它们保存到一个文件中.
这允许向动画添加自定义轨道并在重新导入后保留它们.
切片
可以将单个时间线上的切片指定为多个动画。
这样做的前提是模型上仅存在一个名叫 default 的动画。
要创建切片,请先将切片数量改为大于零的值。
之后就可以对切片进行命名、指定起讫帧、选择动画是否循环。
3D 骨架重定向
在多个骨架之间共享动画
Godot 具有 3D 位置、旋转、缩放轨道(本文称这些轨道为“变换”轨道),其节点路径指向骨骼,用于骨架骨骼动画。
这意味着仅仅通过使用相同的骨骼名称是无法在多个骨架之间共享动画的。
Godot 允许骨骼与骨骼之间存在父子关系,每个骨骼都可以具有旋转、缩放、位置等属性,这意味着即使名称相同的骨骼仍然可以具有不同的变换值。
骨架(Skeleton)会将默认姿势所必须的变换值存储为放松姿势(Bone Rest)。如果骨骼姿势等于放松姿势,那么这个骨架就处于默认姿势。
骨骼模型具有不同的放松姿势,具体取决于导出的环境。例如,Blender 输出的 glTF 模型的骨骼会将“编辑骨骼方向”作为放松姿势的旋转。然而有些骨骼模型是没有任何放松姿势旋转的,比如 Maya 输出的 glTF 模型。
要在 Godot 中共享动画,放松姿势和骨骼名称都需要相匹配,从而在某些情况下删除不需要的轨道。在 Godot 4.0+ 中,可以使用场景导入器来实现。
重定向选项
骨骼映射
在高级场景导入菜单中选中 Skeleton3D 节点时,右侧将出现一个菜单,其中包含“重定向”部分。重定向部分只中有一个属性 bone_map(骨骼映射)。
选中骨架节点后,请先设置一个新的 BoneMap 和 SkeletonProfile。Godot 有一个用于人形模型的预设,名为 SkeletonProfileHumanoid。
本教程假设你使用的就是 SkeletonProfileHumanoid。
备注
如果需要不同于 SkeletonProfileHumanoid 的配置文件,你可以通过选择 Skeleton3D 并使用 3D 视口工具栏中的 Skeleton3D 菜单从编辑器中导出一个 SkeletonProfile。
使用 SkeletonProfileHumanoid 时,将在设置 SkeletonProfile 时执行自动映射。
如果自动映射的效果不佳,你也可以手动映射骨骼。
缺失、重复以及不正确的父子关系映射都会显示为洋红色/红色按钮(取决于编辑器设置)。
这些问题不会阻止导入过程,但会警告动画可能无法正确共享。
备注
自动映射会对骨骼名称进行模式匹配。所以建议骨骼都使用常见的英文名称。
设置 bone_map 后,以下部分中提供了多个选项。
移除轨道
如果是要将资源导入为 AnimationLibrary,那么我们建议启用这些选项。而如果是要将资源导入为场景,那么某些情况下就应该禁用这些选项。例如导入带有动画配件的角色时,这些选项可能会导致配件没有动画。
排除骨骼变换
删除动画中除骨骼变换轨道之外的所有轨道。
非重要位置
删除动画中除了在 SkeletonProfile 中定义过的 root_bone 和 scale_base_bone 以外的位置轨道。在 SkeletonProfileHumanoid 中,这意味着要删除除 Root 和 Hips 之外的位置轨道。自 Godot 4.0+ 起,动画在变换值中包含放松姿势。如果禁用此选项,动画可能会意外地改变身体形状。
未映射骨骼
删除动画中未映射的骨骼变换轨道。
骨骼命名器
重命名骨骼
重命名映射的骨骼。
唯一节点
使骨架成为唯一节点,名称在 skeleton_name 中指定。
这会使得动画轨道路径能够统一,独立于场景层次结构。
放松修复器
SkeletonProfileHumanoid 中定义参考姿势有以下规则:
- 人形物体呈 T 形姿势
- 人形物体在 Y 朝上的右手坐标系中面向 +Z
- 人形物体不应该有变换节点
- 将 +Y 轴从父关节指向子关节
- +X 旋转使关节像肌肉收缩一样弯曲
这些规则是混合动画和反向动力学(IK)的便捷定义。如果你的模型与此定义不符,你需要用这些选项来进行修正。
应用节点变换
如果没有正确导出资产进行共享,导入的骨骼可能会将Transform作为节点。例如,从 Blender 导出的但没有执行 “应用变换”的glTF就是这样的情况。看起来模型与定义相符,但内部Tranforms与定义不同。此选项可通过在导入时应用变换来修复此类模型。
备注
如果导入的场景包含骷髅以外的其他对象,该选项可能会产生负面影响。
归一化位置轨道
位置轨道主要用于模型的移动,但在不同高度的模型之间共享移动动画可能会由于步长的差异而导致滑倒现象。该选项会根据 scale_base_bone 高度标准化位置轨道。scale_base_bone 高度作为 motion_scale 存储在骨骼中,并且标准化的位置轨道值在播放时将乘以该值。如果禁用此选项,则位置轨道不会被标准化,骨架的 motion_scale 始终以 1.0 的形式导入。
对于 SkeletonProfileHumanoid,scale_base_bone 是“臀部”,因此臀部的高度用作 motion_scale。
覆盖轴
通过覆盖模型的 Bone Rest 来统一模型的 Bone Rest,以匹配 SkeletonProfile 中定义的参考姿势。
备注
该选项是 Godot 4.0+ 中共享动画的最重要的选项,但请注意, 如果外部设置的原始 Bone Rest 很重要 ,则此选项可能会产生可怕的结果。如果你想在保留原始 Bone Rest 的情况下共享动画,请考虑使用 实时重定向模块 。
修复剪影
尝试让模型的剪影与 SkeletonProfile 中定义的参考姿势相匹配,例如 T-Pose。
该功能无法修复差异太大的剪影,并且可能不适用于修复骨骼滚动。
使用 SkeletonProfileHumanoid 时,不需要为 T-Pose 模型启用此选项,但应为 A-Pose 模型启用。然而,在这种情况下,根据模型的脚跟高度,固定脚的结果有可能会很糟糕,因此,可能需要添加你并不希望在 filter 数组中固定的 SkeletonProfile 骨骼名称,如下例所示。
此外,对于膝盖或脚弯曲的模型,可能需要调整 scale_base_bone 高度。为此可以使用 base_height_ adjustment 选项。
导出 3D 场景
在 Godot 中可以将 3D 场景导出为 glTF 2.0 文件。
你可以将其导出为 glTF 二进制文件(.glb 文件)或者内嵌 glTF 及纹理(gltf + .bin + 纹理)。
这样就可以在 Godot 中创建场景,比如使用 CSG 网格进行关卡的搭建,然后导出到类似 Blender 的程序里进行整理,然后再弄回 Godot。
要在编辑器中导出场景,请打开场景 > 导出为… > glTF 2.0 场景…
限制
- 不支持导出粒子,因为不同的引擎对粒子的实现是不同的。
- 无法导出 ShaderMaterial。
- 不支持导出 2D 场景。
音频
音频总线
Godot的音频引擎允许创建任意数量的音频总线, 并且可以向每个总线添加任意数量的效果处理器.
运行游戏的设备的硬件会限制总线的数量, 以及在性能开始下降之前可以使用的效果.
分贝标度
分贝(dB)标度是一个相对标度. 它等于声功率比的常用对数的20倍(20 × log10(P/P/0)).
每增/减6分贝, 声幅就会加倍/减半.12dB代表系数4,18dB代表系数8,20dB代表系数10,40dB代表系数100, 以此类推.
由于比例是对数的, 因此无法表示真零(无音频).
0 dB 是数字音频系统中可能的最大振幅. 这个限制不是人为的限制, 而是声音硬件的限制. 因振幅太高而无法完全反映在0dB以下的音频, 会产生一种被称为 削波 的失真.
为了避免削波,你应该调整混音,使master 总线(后面会有更多的介绍)永远不超过 0 dB。
低于0dB限制的每6dB, 声能就会 减半 . 这意味着-6dB的音量是0dB的一半. -12dB是-6dB的一半, 依此类推.
使用分贝时, -60dB和-80dB范围内的声音被认为是听不见的. 也就是说你的工作范围一般在-60dB和0dB之间.
音频总线(audio bus)音频通道 (audio channel)
音频总线可以在Godot 编辑器的底部面板中找到:
音频从扬声器播放出来之前通过的地方.
它可以 修改 和 重路由 音频数据.
音频总线有一个 VU表(播放声音时亮起的条形), 表示通过的信号的幅度.
最左边的总线是 主总线 .
此总线将混音输出到你的扬声器, 因此, 正如之前 分贝标度 部分所述, 请确保主总线中的混音水平永远低于 0 dB.
其余的音频总线可以灵活地进行路由.
在修改声音后, 它们会将其发送到左边的另一条总线上. 非主总线的目标总线可以被单独设置.
而右侧总线的音频会被路由至左侧总线, 这避免了无限循环.
通过总线播放音频
要测试将音频传递到总线, 请创建AudioStreamPlayer节点, 加载AudioStream并选择要播放的目标总线:
自动总线禁用
你不需要手动禁用闲置总线,Godot在检测到总线已经静音数秒之后, 就会禁用它(以及所有效果).
总线重排
流播放器使用总线名称来识别总线, 允许在保留对总线的引用时添加, 删除和移动总线.
然而, 重命名总线会导致引用丢失, 流播放器将输出到主总线.
之所以选择这个系统, 是因为重新排列总线相比重命名总线更为常用.
默认总线布局
默认的总线布局会自动保存到 res:// default_bus_layout.tres 文件中.
自定义总线布局可以从磁盘中保存和加载.
音频特效
Amplify(增幅)
增幅改变信号的音量。不过使用时要小心:把电平调得太高的话,声音就会出现数字削波,从而产生令人不快的噼啪声和爆音。
BandLimit 和 BandPass(带限和带通)
这些是谐振滤波器,可阻止截断(Cutoff)点附近的频率。带通滤波器可用于模拟通过旧电话线或扩音器的声音。调制带通频率可以模拟哇音(wah-wah)吉他踏板的声音
捕获
Capture(捕获)效果器会将其所在音频总线的音频帧复制到内部缓冲区中。可用于从麦克风捕获数据或通过网络实时传输音频。
Chorus(和声)
正如该效果的名称所暗示的那样,和声效果将使单个音频样本听起来像整个合唱。它通过复制信号并稍微改变每个副本的时间和音高,并通过 LFO(低频振荡器)随时间变化来实现此目的。然后,复制信号与原始信号混合在一起,产生丰富、宽广、宏大的声音。尽管和声传统上用于声音,但几乎任何类型的声音都可以使用和声。
Compressor(压缩器)
当输入信号的幅度超过某一阈值时,动态范围压缩器会自动衰减(避开)该信号的电平。所施加的衰减水平与传入音频超过阈值的程度成正比。压缩器的“比例”参数控制衰减的程度。压缩器的主要用途之一是,当一个信号具有非常大声和小声的部分时,压缩器可以用于降低其动态范围。降低信号的动态范围可以使其更方便混音。
压缩器有很多用途。例如:
它可用于主总线中,在受到限制器影响之前压缩整个输出,从而使限制器的效果更加微妙。
它可用于语音通道,以确保它们听起来尽可能均匀。
它可以通过另一个声源来侧链(Sidechain)。也就是说, 它可以利用另一个音频总线的电平进行阈值检测,来降低一个信号的电平。这种技术在电子游戏混音中非常常见。当游戏中或多人游戏的声音需要被玩家清楚听到时,可以“降低”(Duck)音乐或音效的电平。
它可以通过较慢的启动(attack)来突出瞬态,让音效听起来更有力。
Delay(延迟)
数字延迟本质上是复制一个信号,并以指定的速度重复播放它,且每次重复时音量都会衰减。延迟非常适合模拟如峡谷或大房间这样的声学空间,其中声音的反弹在每次重复之间都有很多延迟。这点与混响相反,混响的声音更自然、更模糊。将其与混响结合使用可以创建非常自然的声音环境!
Distortion(失真)
使声音失真。Godot 提供了几种类型的失真:
Overdrive 过载,听起来像吉他失真踏板或扩音器。这种声音失真,听起来像是通过低质量的扬声器或设备发出的。
Tan 正弦,听起来像是另一种有趣的过载风格。
Bit crushing 位破碎,会限制信号的幅度,使其听起来平坦且嘎吱作响。
所有这三种类型的失真都可以为原始声音添加更高频率的声音,使其在混音中更加突出。
EQ(均衡器)
所有其他均衡器都继承自 EQ。可以使用自定义脚本对其进行扩展,以创建一个具有自定义频段数的均衡器。
EQ6、EQ10、EQ21
Godot 提供了三种具有不同频段数量的均衡器,其频段数如标题所示(分别为 6、10 和 21 个频段)。主总线上的均衡器可用于截断设备扬声器无法很好地重现的低频和高频声音。例如,手机或平板电脑扬声器通常不能很好地重现低频声音,并且可能使限幅器或压缩器衰减用户根本听不到的声音。
注意:插入耳机时可以禁用均衡器效果,为用户提供两全其美的效果。
过滤器
所有其他滤波器都继承自 Filter,不应直接使用。
HardLimiter(硬限幅器)
一种与压缩器类似的限幅器,但灵活性较差,旨在防止信号振幅超过给定的 dB 阈值。在主总线的终点添加一个限幅器是一种很好的做法,因为它提供了一种防止削波的简单保护措施。
HighPassFilter(高通滤波器)
截断特定截断频率以下的频率。高通滤波器用于减少信号的低音内容。
HighShelfFilter(高架滤波器)
减少所有高于特定截断频率的频率。
Limiter(限幅器)
这是旧的限制器效果,建议改用新的硬限幅器(HardLimiter)效果。
以下是该效果的一个例子,如果将上限设置为 -12 dB,阈值为 0 dB,则所有通过的音频样本都会减少 12dB。这会改变音频的波形并引入失真。
该效果为了兼容性而保留,但应将其视为已弃用。
LowPassFilter(低通滤波器)
截断特定截断频率以上的频率,也可以产生谐振(增强接近截断频率的频率)。低通滤波器可用于模拟“低沉”的声音。例如,水下的声音、被墙壁阻挡的声音或远处的声音。
LowShelfFilter(低架滤波器)
降低低于特定截断频率的所有频率。
NotchFilter(陷波滤波器)
与带通滤波器相反,它从给定截断频率的频谱中移除一个声音频段。
Panner(声像)
声像效果允许在左右通道之间调整信号的立体声平衡。配置该效果时建议使用耳机。
Phaser(移相器)
这种效果是通过对同一声音的两个副本进行移相而形成的,因此,它们会以一种有趣的方式相互抵消。 移相器会产生令人愉悦的嘶嘶声,在音频频谱中来回移动,如果你要创建科幻风格效果或达斯·维德(Darth Vader)那般的声音,移相器是个好选择。
PitchShift(移调)
这种效果可以独立于速度调整信号的音高。所有频率均可增减,对瞬态的影响极小。PitchShift 可用于创建极其高亢/低沉的声音。需要注意的是,当推到一个狭窄窗口之外时,改变音调可能会听起来不自然。
Record(录制)
录音效果允许用户从麦克风录制声音。
Reverb(混响)
混响模拟不同大小的房间。它具有可调节的参数,可以调整这些参数以获得特定房间的声音。混响通常从 Area3D 输出(参见《混响总线》),或将“室内”感觉应用于所有声音。
SpectrumAnalyzer(频谱分析仪)
这个效果并不会改变音频,相反,你可以把这个效果添加到你想要进行频谱分析的总线上。这通常被用于音频可视化。将声音可视化是一种仅吸引注意力而不增加音量的好方法。使用了这个效果的演示项目可在这里找到。
StereoEnhance(立体增强)
该效果使用一些算法来增强信号的立体声宽度。
音频流
AudioStream(音频流)
音频流是一种产生声音的抽象对象。
声音可以来自许多地方,但最常见的是从文件系统加载。
音频文件可以作为 AudioStream 加载并放置在 AudioStreamPlayer 中。
你可以在《导入音频采样》中找到支持的格式和格式差异等信息。
还有其他类型的 AudioStream,例如 AudioStreamRandomizer。
这种音频流每次都会从音频流列表中挑选不同的音频流进行播放,并应用随机音高和音量调整,这有助于为经常播放的声音添加变化。
AudioStreamPlayer(音频流播放器)
这是一种标准的非位置型的流播放器。它可以播放到任何总线。在 5.1 声音设置中,它可以将音频发送到立体声混音或前置扬声器。
播放类型是一种实验性设置,可能会在 Godot 的未来版本中发生变化。它的存在使得 Web 导出使用基于 Web Audio-API 的样本,而不是像大多数平台那样将所有声音流式传输到浏览器。这可以防止音频在单线程 Web 导出中出现乱码。默认情况下,只有 Web 平台会使用样本。除非有明确的理由,否则不建议更改此设置。你可以在项目设置的音频 > 常规(必须打开高级设置才能看到)下更改 Web 和其他平台的默认播放类型。
AudioStreamPlayer2D
AudioStreamPlayer 的一种变体,在 2D 位置环境中发出声音。
当靠近屏幕左侧时,声像将向左移动。当靠近右侧时,声像将向右移动。
备注
Area2D 可用于将声音从其包含的任何 AudioStreamPlayer2D 转移到特定总线。
这样就可以创造具有不同混响或音质的总线,从而来处理游戏世界特定部分发生的动作。
AudioStreamPlayer3D
AudioStreamPlayer 的一种变体,在 3D 位置环境中发出声音。
根据播放器相对于屏幕的位置,它可以将声音定位为立体声、5.1 或 7.1,具体取决于所选的音频设置。
与 AudioStreamPlayer2D 类似,Area3D 可以将声音转移到特定的音频总线上。
与 2D 版本不同的是,3D 版本的 AudioStreamPlayer 还有一些更高级的选项:
Reverb buses(混响总线)
Godot 允许进入特定 Area3D 节点的 3D 音频流,将干音频和湿音频发送到单独的总线上。
这有益于为不同类型的房间配置多种混响配置。
这可以通过在 Area3D 属性的 Reverb Bus(混响总线)部分中启用这种类型的混响来实现:
同时,还创建了一个特殊的总线布局,其中每个 Area3D 都从每个 Area3D 接收混响信息。
需要在每个混响总线中创建和配置混响效果,才能完成所需效果的设置:Doppler(多普勒)
Area3D 的 Reverb Bus(混响总线)部分还有一个名为 Uniformity(统一性)的参数。
某些类型的房间比其他类型的房间反射的声音更多(例如仓库),因此即使声源可能很远,整个房间几乎都可以均匀地听到混响。
尝试使用该参数可以模拟这种效果。
当发射源和侦听者之间的相对速度发生变化时,这被视为发出的声音的音高增加或减少。
Godot 可以跟踪 AudioStreamPlayer3D 和 Camera 节点的速度变化。
这两个节点都具有该属性,必须手动启用:
根据对象的移动方式通过设置启用它:对于使用 _process 移动的对象使用 Idle,对于使用 _physics_process 移动的对象使用 Physics。跟踪将自动进行。
将游戏玩法与音频和音乐同步
- 使用系统时钟同步
调用 AudioStreamPlayer.play(),声音不会立即开始播放,而是在音频线程处理下一个块时开始。
这种延迟是无法避免的,但可以通过调用 AudioServer.get_time_to_next_mix() 来估算。
输出延迟(混音后发生的延迟),可以通过调用 AudioServer.get_output_latency() 来估算。
将这两者相加,就可以几乎准确地猜测 _process() 期间,声音或音乐将何时在扬声器中开始播放:
1 | var time_begin |
但在长期运行中,由于声音硬件时钟永远不会与系统时钟完全同步,因此时间信息将慢慢漂移。
对于一首歌开始和结束时间各为几分钟的节奏游戏来说,这种方法是可行的(也是推荐的方法)。
对于播放时间可能更长的游戏来说,游戏最终会不同步,因此需要采用一种不同的方法。
- 使用声音硬件时钟同步
使用 AudioStreamPlayer.get_playback_position() 来获取歌曲的当前位置听起来很理想,但这样做并没有那么有用。
该值将分块(每次音频回调混合一个声音块时)递增,因此多次调用可能返回相同的值。
除此之外,由于前面提到的原因,该值也会与扬声器不同步。
为了补偿“分块”(chunked)输出,有个函数可能会有所帮助:AudioServer.get_time_since_last_mix()。
将这个函数的返回值与 get_playback_position() 相加可以提高精度:var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()
为了提高精度,减去延迟信息(音频从混合后到被听见花费的时间):var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix() - AudioServer.get_output_latency()
由于多线程的工作方式,结果可能会有点抖动。只需检查该值是否不小于前一帧的值(如果小于,则丢弃它)。
这种方法也不如之前的方法精确,但它适用于任何长度的歌曲,或将任何东西(例如音效)与音乐同步。
下面是使用这种方法之前相同的代码:
1 | func _ready(): |
使用麦克风录音
在项目设置 Audio > Driver > Enable Input 中启用音频输入,否则你只会获取到空白的音频文件。
演示项目的结构
https://github.com/godotengine/godot-demo-projects/tree/master/audio/mic_record
该演示由单个场景组成。
该场景包含两个主要部分:GUI 和音频。
在该演示中,创建了一个名为 Record 的总线,并附有效果 Record 来处理音频录制。
用一个名为 AudioStreamRecord 的 AudioStreamPlayer 进行录制。
1 | var effect |
音频录制由 AudioEffectRecord 资源处理,该资源具有三种方法:get_recording()、is_recording_active() 和 set_recording_active()。
1 | func _on_record_button_pressed(): |
在演示开始时,录制效果未激活。当用户按下 RecordButton 时,使用 set_recording_active(true) 启用该效果。
在下次按下按钮时,由于 effect.is_recording_active() 为 true,可以通过调用 effect.get_recording() 将录制的流存储到 recording 变量中。
1 | func _on_play_button_pressed(): |
要播放录音,请将录音赋值为 AudioStreamPlayer 的流并调用 play()。
1 | func _on_save_button_pressed(): |
要保存录音,可以带文件路径调用 save_to_wav()。在该演示中,路径由用户通过一个 LineEdit 输入框定义。
文本转语音
基本用法
在能够使用文本转语音进行基本操作前,需要执行一次以下步骤:
- 在 Godot 编辑器中为项目启用 TTS
- 向系统查询可用语音列表
- 存储你想要使用的语音 ID
默认情况下,为了避免不必要的开销,Godot 项目级别的文本转语音设置处于禁用状态。启用方法是: - 前往 项目 > 项目设置
- 确保打开了高级设置开关
- 单击 音频 > 常规
- 确保选中 文本转语音 选项
- 如果出现提示,请重新启动 Godot。
文本转语音会使用特定的语音。用户的系统中可能安装了多种语音。获得语音 ID 后,你就可以用它来读出文本:
1 | # One-time steps. |
功能要求
Godot 包含了文本转语音功能,可以在 DisplayServer 类中找到。
Godot 依赖于系统库来实现文本转语音功能。这些库默认安装在 Windows、macOS、Web、Android 和 iOS 上,但并非安装在所有 Linux 发行版上。
如果它们不存在,文本转语音功能将不起作用。具体来说,tts_get_voices() 方法将返回一个空列表,表示没有可用的语音。
导出
导出项目
单击 导出 按钮,添加导出预设.
一般使用默认选项导出就足够了,通常无需对其进行调整。
但是,许多平台都需要安装其他工具(SDK)才能导出。
此外,Godot 需要安装导出模板来创建软件包。
缺少某些内容时,导出菜单将发出提示,并且在解决该问题之前,用户将无法为该平台进行导出:
菜单底部的按钮允许你用几种不同的方式导出项目:
- 全部导出:将项目导出为所有定义的预设的可播放版本(Godot 可执行文件和项目数据)。所有预设都必须定义导出路径才能正常工作。
- 导出项目:将项目导出为所选预设的可播放版本(Godot 可执行文件和项目数据)。
- 导出 PCK/ZIP:将项目资源导出为 PCK 或 ZIP 包。这不是一个可玩的版本,它仅导出项目数据,没有 Godot 可执行文件。
资源选项
导出时,Godot 会先创建一个所有要导出的文件的列表,然后再创建包。有 3 种不同的导出模式:
- 导出项目中的所有资源
- 导出选中的场景(包括依赖项)
- 导出选中的资源(包括依赖项)
导出为专用服务器将从项目中删除所有视觉对象,并将其替换为占位符。
这包括立方体贴图、立方体贴图阵列、材质、网格体、纹理 2D、纹理 2DArray、纹理 3D。
您还可以进入文件列表并指定您希望保留的特定视觉资源。
资源列表下方是两个可以设置的过滤器。
第一种允许将非资源文件(如.txt、.json 和 .csv)与项目一起导出。
第二个过滤器可用于排除特定类型的每个文件,而无需手动取消选择每个文件。例如,.png 文件。
配置文件
导出配置存储在两个文件中,这两个文件都可以在项目目录中找到:
- export_presets.cfg :此文件中包含绝大多数导出配置,可以安全地提交到版本控制。这里的内容通常没有什么是你需要保密的。
- .godot/export_credentials.cfg :此文件包含被视为机密的导出选项,例如密码和加密密钥。通常不用版本控制或与其他人共享。
由于凭证文件通常不包含在版本控制系统中,因此如果将项目克隆到新计算机,某些导出选项将会丢失。
处理此问题的最简单方法是手动将文件从旧位置复制到新位置。
从命令行导出
在生产中, 自动化构建很有用,Godot使用 –export 和 –export-debug 命令行参数来支持它.
从命令行导出仍需要导出预设来定义导出参数. 该命令的基本调用将是:godot --export-release "Windows Desktop" some_name.exe
假设有一个名为“Windows Desktop”的预设,并且可以找到模板,它将导出为 some_name.exe(如果导出预设的名字中存在空格或特殊字符,就必须放在引号里)。
输出路径是相对于项目的路径或者绝对路径;它使用的不是命令被调用时的目录。
输出的文件扩展名应该与 Godot 导出过程所使用的相匹配:
- Windows:.exe
- macOS:.app 或 .zip( 或从 macOS 导出时 .dmg)
- Linux:任意扩展名(没有也行)。64 位 x86 二进制文件通常使用 .x86_64。
- HTML5:.zip
- Android:.apk
- iOS:.zip
你还可以将其配置为仅导出 PCK 或 ZIP 文件,能够让多个 Godot 可执行文件执行同一个导出的主包文件。
这样做时,仍然必须在命令行中指明导出预设的名称:godot --export-pack "Windows Desktop" some_name.pck
将 –export 标志与 –path 标志组合起来通常很有用,这样你就不必在运行命令之前 cd 到项目文件夹中了:godot --path /path/to/project --export-release "Windows Desktop" some_name.exe
PCK 与 ZIP 打包文件格式对比
- PCK 格式:
未压缩的格式。文件尺寸较大,但读写较快。
尽管有 第三方工具 来提取和创建PCK文件, 但使用用户操作系统上通常存在的工具是不可读和不可写的. - ZIP 格式:
压缩格式。文件尺寸较小,但读写较慢。
可以使用用户操作系统中的常见工具读取或写入. 这对简化制作mod很有用.(另请参阅 导出包、补丁、Mod)
由于已知的错误 ,当将 ZIP 文件用作包文件时,导出的二进制文件将不会尝试自动使用它。
因此,您必须创建一个启动器脚本 ,播放器可以双击或从终端运行该脚本来启动项目::: launch.bat (Windows)
@echo off
my_project.exe –main-pack my_project.zip
# launch.sh (Linux)
./my_project.x86_64 –main-pack my_project.zip
保存启动脚本, 并将它与导出的二进制文件放在同一文件夹中. 在Linux上, 请确保使用 chmod +x launch.sh 命令给予启动脚本可执行权限.
导出包、补丁、Mod
PCK/ZIP 文件概述
Godot 通过资源包实现此功能(扩展名为 .pck 的 PCK 文件或 ZIP 文件)。
如果您想在运行时加载松散的文件(不是 Godot 打包在 PCK 或 ZIP 中),请考虑使用 运行时文件加载和保存 代替。
这对于加载不是用 Godot 制作的用户生成内容非常有用,而无需用户将他们的模组打包成特定的文件格式。
这种方法的缺点是它对游戏逻辑不太透明,因为它不会从与 PCK/ZIP 文件相同的资源管理中受益。
生成 PCK 文件
- 单击导出 PCK/ZIP
- 另一种方法是从命令行导出 使用 –export-pack。
输出文件必须带有 .pck 或 .zip 文件扩展名。导出过程将为 选择的平台。
备注
游戏支持mod将需要其用户创建类似的导出文件。
假设原始游戏需要 PCK 资源的某种结构和/或其脚本具有特定的接口,那么有两种选择……
- 开发人员必须公开这些预期结构/接口的文档,期望模组制作者安装 Godot 引擎
然后,在为游戏构建 Mod 内容时,这些修改者也将遵守文档中定义的 API(这样它将起作用)。
用户然后将如上所述,使用 Godot 的内置导出工具来创建 PCK 文件 - 开发者使用 Godot 来构建 GUI 工具,用这个工具向项目中添加特定的 API 内容。
这个 Godot 工具要么是在启用了工具构建的引擎上执行,要么就必须能够访问到这种版本的邀请(一同分发,或者加入到原版游戏的文件之中)。
这样这个工具就可以使用 OS.execute() 通过命令行使用 Godot 可执行文件来导出 PCK 文件。
游戏本体不应该使用工具构建的引擎(出于安全考虑),所以最好将 mod 工具和游戏分开。
在运行时打开 PCK 或 ZIP 文件
PCK 和 ZIP 文件的加载需要用到 ProjectSettings 单例。
下面的例子需要在游戏可执行文件所在目录中存在 mod.pck 文件。
该 PCK 或 ZIP 文件的根目录中包含一个 mod_scene.tscn 测试场景。
1 | func _your_function(): |
默认情况下,如果您导入的文件路径/名称与项目中已有的文件路径/名称相同,则导入的文件将替换它。
这是创建 DLC 或模组时需要注意的事情。您可以通过使用将 mod 隔离到特定 mods 子文件夹的工具来解决此问题。
然而,这也是为自己的游戏创建补丁的一种方式。此类 PCK/ZIP 文件可以修复先前加载的 PCK/ZIP 的内容(因此,加载包的顺序很重要)。
为了退出这个行为, 把 false 作为第二个参数传递给 ProjectSettings.load_resource_pack().
故障排除
如果您正在加载资源包并且没有注意到任何更改,则可能是由于包装装载太晚。
菜单尤其如此 可能使用 preload() 的 Load () 中。这意味着在菜单中加载包不会影响已预加载的其他场景。
为避免这种情况,您需要尽早加载包。
为此,请创建一个新的自动加载脚本并调用 ProjectSettings.load_resource_pack() 在自动加载脚本的 _init() 函数中,而不是 _enter_tree() 或 _ready()。
功能标签
?
为 Windows 导出
在 PC 上分发游戏的最简单方法是复制可执行文件(godot.exe),将文件夹压缩,然后发送给别人。然而,这样一般并不理想。
Godot 在使用导出系统时,为 PC 分发提供了一种更为优雅的方法。
为 Windows 导出时,导出器将提取所有项目文件并创建一个 data.pck 文件。
该文件与特别优化的二进制文件捆绑在一起,更小、更快,并且不包含编辑器和调试器。
更改可执行文件图标
Godot 会自动使用在项目设置中指定为项目图标的图像,将其转换为 ICO 文件以供导出后的项目使用。
如果想要手动创建 ICO 文件,针对不同分辨率设置不同的图标外观,请查看 手动更改 Windows 的应用程序图标 页面。
代码签名
Godot 能够在导出时自动进行代码签名。
为此,您必须拥有 Windows SDK(在 Windows 上)或 osslsigncode (在任何其他作系统上)已安装。
您还需要一个包签名证书, 有关创建的信息可以在这里找到 。
警告
如果你用内嵌的PCK文件导出到Windows, 你将无法签名程序, 因为它会崩溃.
在Windows上,PCK嵌入也会在杀毒软件中造成误报. 因此, 建议避免使用它, 除非你的项目通过Steam发布, 因为这样就绕过了代码签名和防病毒检查.
场景布置
需要在两个地方更改设置。
- 首先是在编辑器设置的导出 > Windows 下。
单击 Sign Tool 设置旁边的文件夹,如果你使用的是 Windows,请找到并选择 SignTool.exe,如果你使用的是其他操作系统,请选择 osslsigncode。 - Windows 导出预设,可以在项目 > 导出…中找到。
如果尚未添加,则添加一个 Windows 桌面预设。在选项下有一个代码签章类别。
必须将 Enabled 设置为 true,将 Identity 设置为签名证书。其他设置可以根据需要进行调整。完成后,Godot 就会在导出时为项目进行签名。
环境变量
使用以下环境变量在编辑器外部设置导出选项。
在导出过程中,这些值会覆盖你在导出菜单中设置的值。
| 导出选项 | 环境变量 |
|---|---|
| 加密 / 密钥 | GODOT_SCRIPT_ENCRYPTION_KEY |
| 选项 / 代码签名 / 身份类型 | GODOT_WINDOWS_CODESIGN_IDENTITY_TYPE |
| 选项 / 代码签名 / 身份 | GODOT_WINDOWS_CODESIGN_IDENTITY |
| 选项 / 代码签名 / 密码 | GODOT_WINDOWS_CODESIGN_PASSWORD |
导出选项
在 EditorExportPlatformWindows 类引用。
为 Android 导出
安装 OpenJDK 17
下载 Android SDK
在 Godot 中进行设置
设置两个路径:
Java SDK Path 应当为 OpenJDK 17 的安装位置。
Android Sdk Path 设置为Android SDK的安装位置. 例如,Windows上的 %LOCALAPPDATA%\Android\Sdk\ , 或macOS上的 /Users/$USER/Library/Android/sdk/ .
提供启动器图标
启动器图标是 Android 启动器应用把你的应用展示给用户时所用到的。
Godot 只需要高分辨率图标(适用于 xxxhdpi 屏幕),会自动生成低分辨率的变体。
这里有三种类型的图标:
- 主图标:“经典”图标。这会在所有 Android 版本不高于 Android 8(Oreo)中使用。必须至少为 192×192 px。
- 自适应图标:从 Android 8 开始(含)引入了自适应图标(Adaptive Icons)。
应用为了有原生的样式需要包含分离的背景与前景图标。用户的启动程序会控制图标的动画和遮罩。必须至少为 432×432 px。 - 主题图标 (可选):从 Android 13 开始(含)引入了主题图标。
应用程序需要包含一个单色图标来启用此特性。用户的启动程序会控制图标的主题。必须至少为 432×432 px。
若未提供要求的图标(单色图标除外),Godot将按回退链自动替换:当前图标加载失败时,依次尝试链中的下一个候选图标。
- 主图标:提供的主图标 -> 项目图标 -> 默认 Godot 主图标。
- 自适应图标前景:提供的前景图标 -> 提供的主图标 -> 项目图标 -> 默认 Godot 前景图标。
- 自适应图标背景:提供的背景图标 -> 默认 Godot 背景图标。
强烈建议提供所有要求的图标的指定分辨率。这样一来,你的应用程序在所有的 Android 设备和版本上都会显得非常漂亮。
环境变量
| 导出选项 | 环境变量 |
|---|---|
| 加密 / 密钥 | GODOT_SCRIPT_ENCRYPTION_KEY |
| 选项 / 密钥库 / 调试 | GODOT_ANDROID_KEYSTORE_DEBUG_PATH |
| 选项 / 密钥库 / 调试用户 | GODOT_ANDROID_KEYSTORE_DEBUG_USER |
| 选项 / 密钥库 / 调试密码 | GODOT_ANDROID_KEYSTORE_DEBUG_PASSWORD |
| 选项 / 密钥库 / 发布 | GODOT_ANDROID_KEYSTORE_RELEASE_PATH |
| 选项 / 密钥库 / 发布用户 | GODOT_ANDROID_KEYSTORE_RELEASE_USER |
| 选项 / 密钥库 / 发布密码 | GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD |
为 macOS 导出
为 iOS 导出
为 Web 导出
一键部署
添加 Android 导出预设项并且标记为可执行之后
Godot 就可以检测到有 USB 设备插到了电脑上
并且允许用户在该设备上自动导出、安装、然后运行(调试模式的)项目。
使用一键部署
Android:
请在移动设备上打开开发者模式,然后在设备的设置中启用 USB 调试。
启用 USB 调试后,请将设备通过 USB 线缆连接到 PC。
如果你是高级用户,也可以使用无线 ADB。
为专用服务器导出
需要在运行 Godot 的时候使用 headless 显示服务器和 Dummy 音频驱动。
编辑器与导出模板
可以在无头模式下使用编辑器或导出模板(调试或发布)二进位。你应该使用哪一种取决于你的用例:
- Export template(导出模板):不包含编辑器的功能,体积更小、优化更高,是在服务器平台下的最佳选择。
- Editor(编辑器):包含编辑器功能的可执行文件,目的是用来导出项目。
该可执行文件可以用来运行专用服务,但是因为其体积较大、优化程度较低,所以不建议将其作为专用服务器使用。
导出方法
针对服务器导出项目的方法有两种:
- 为服务器托管平台创建单独的导出预设,然后像往常一样导出项目。
- 仅导出PCK档案,最好是与将托管服务器的平台相配对的平台。将此PCK档案放在与导出模板二进位档案相同的文件夹中,将二进位档案重新命名为与PCK相同的名称(减去文件扩展名),然后执行该二进位档案。
两种方法的输出结果应该相同。本页其余部分将重点介绍第一种方法。
启动专用服务器
如果你的客户端和服务器都是同一个 Godot 项目的一部分,则必须添加一种使用命令行参数直接启动服务器的方法。
如果你使用导出为专用服务器导出模式 导出了该项目(或已添加 dedicated_server 作为自定义功能标记),则可以使用 dedicated_server 功能标签来检测是否正在使用专用服务器 PCK:
1 | # Note: Feature tags are case-sensitive. |
如果你还希望在使用内置的 –headless 命令行参数时托管服务器,可以在主场景(或自动加载)的 _ready() 方法中添加以下代码段来实现:
1 | if DisplayServer.get_name() == "headless": |
如果希望使用自定义命令行参数,可在主场景(或自动加载)的 _ready() 方法中添加以下代码段:
1 | if "--server" in OS.get_cmdline_user_args(): |
最好新增至少一个上述命令列参数来启动服务器,因为它可用于从命令列测试服务器功能,而无需导出项目。
如果你的客户端和服务器是独立的Godot项目, 服务器通常应该配置成运行主场景时自启服务的方式.
在 Linux 上,要在崩溃或系统重新启动后重新启动您的专用服务器, 您可以 创建 systemd 服务 。
这还可以让您以更方便的方式查看服务器日志,并由 systemd 提供自动日志轮换。
当您的项目可托管为 systemd 服务时,您还应该启用 application/run/flush_stdout_on_print 项目设置。
这样,journald(systemd 日志记录服务)可以收集 进程运行时的日志。
如果你有容器的经验, 可以考虑将专用服务器包装在一个 Docker 容器中. 这样, 在弹性配置中可以更容易地使用它(这不在本教程的范围内).
文件与数据 I/O
后台加载
后台加载
标准加载方法(ResourceLoader.load 或 GDScript 中更简单的 load)会阻塞线程,让你的游戏在加载资源时显得无响应。
解决这个问题的一种方法是使用 ResourceLoader 在后台线程中异步加载资源。
使用 ResourceLoader
- 使用
ResourceLoader.load_threaded_request将资源加载请求加入队列,其他线程会在后台进行加载。 - 使用
ResourceLoader.load_threaded_get_status检查状态。
给 progress 传一个数组变量就可以获取进度,返回时该数组中包含一个元素,表示百分比。 - 最后调用
ResourceLoader.load_threaded_get即可获取加载到的资源。
调用 load_threaded_get() 有两种结果: - 要么资源已经完成了后台加载,此时就会立即返回;
- 要么加载尚未完成,此时就会和 load() 一样发生阻塞。
如果你希望保证调用时不发生阻塞,就需要确保请求加载和获取资源之间留够时间,或者也可以先手动进行状态检查。
示例
下面这个例子演示的是如何进行场景的后台加载。
按下按钮后就会生成一个敌人。
敌人使用的是 _onready 时加载的 Enemy.tscn,按下按钮时进行实例化。
该场景的路径为 “Enemy.tscn”,位于 res://Enemy.tscn。
首先,我们将启动一个请求来加载资源并连接按钮:
1 | const ENEMY_SCENE_PATH : String = "Enemy.tscn" |
现在按下按钮就会调用 _on_button_pressed。该方法的作用是生成敌人。
1 | func _on_button_pressed(): # Button was pressed. |
Godot 项目中的文件路径
如何在项目中使用 res:// 和 user:// 标记来访问路径,
及 Godot 会在你的以及用户系统上的哪些位置存储项目和编辑器文件。
路径分隔符
Godot 使用 UNIX 风格的路径分隔符(正斜杠 /)。
在 Godot 里写的不是类似 C:\Projects\Game 的路径,而应该写 C:/Projects/Game。
有些路径相关的方法也支持 Windows 风格的路径分隔符(反斜杠\),不过需要写两个(\),因为 \ 一般是用来进行字符转义的,有特殊含义。
这样就能够处理其他 Windows 程序所返回的路径了。仍建议在代码里只使用正斜杠,这样才能确保一切都能正常工作。
访问项目文件夹中的文件(res://)
只要文件夹中存在名叫 project.godot 的文本文件,即便是空文件,Godot 也会认为这个文件夹中包含了一个项目。
包含这个文件的文件夹是项目根文件夹。
相对于这个文件夹的任何文件,都可以通过以 res:// 开头的路径访问,这个前缀代表“资源”(resource)。
访问持久化用户数据(user://)
要存储持久化数据文件,比如玩家的存档、设置等,你会想要使用 user:// 作为路径前缀,而不是 res://。
这是因为游戏运行时,项目的文件系统很可能是只读的。
user:// 前缀指向的是用户设备上的其他目录。
与 res:// 不同,即便在导出后的项目中,user:// 指向的这个目录也会自动创建并且保证可写。
user:// 文件夹的位置由“项目设置”中的配置决定:
- 默认情况下,user:// 文件夹是在编辑器数据路径中创建的 app_userdata/[项目名称] 文件夹。
使用这一默认值的目的是让原型和测试项目能够在 Godot 的数据文件夹中达到自包含。 - 如果在项目设置中启用了 application/config/use_custom_user_dir,则会在 Godot 编辑器数据路径的旁边创建 user:// 文件夹,即在应用程序数据的标准位置。
默认情况下,文件夹名称是从项目名称推导出来的,但可以使用 application/config/custom_user_dir_name 进行进一步的自定义。这个路径可以包含路径分隔符,那么比如你就可以把给定工作室的项目都分组到 工作室名称/游戏名称 这样的目录结构之下。
在桌面平台上,user:// 的实际目录路径为:类型 位置 默认 Windows: %APPDATA%\Godot\app_userdata\[项目名称]
macOS:~/Library/Application Support/Godot/app_userdata/[项目名称]
Linux:~/.local/share/godot/app_userdata/[项目名称]自定义目录 Windows: %APPDATA%\[项目名称]
macOS:~/Library/Application Support/Godot/[项目名称]
Linux:~/.local/share/godot/[项目名称]自定义目录及名称 Windows: %APPDATA%\[自定义目录名称]
macOS:~/Library/Application Support/[自定义目录名称]
Linux:~/.local/share/[自定义目录名称]在移动平台上,这个路径是与项目相关的,每个项目都不一样,并且出于安全原因无法被其他应用程序访问。 在 HTML5 导出中,user:// 会指向保存在设备的虚拟文件系统,这个文件系统使用 IndexedDB 实现。(仍然可以通过 JavaScriptBridge 与主文件系统交互。)
将路径转换为绝对路径或“本地”路径
你可以使用 ProjectSettings.globalize_path() 将类似 res://path/to/file.txt 的本地路径转换为操作系统的绝对路径。
例如,可以使用 ProjectSettings.globalize_path() 在操作系统的文件管理器中通过 OS.shell_open() 打开“本地”路径,因为这个函数只接受原生操作系统路径。
要将操作系统绝对路径转换为以 res:// 或 user:// 开头的“本地”路径,请使用 ProjectSettings.localize_path()。
只对指向项目根目录或者 user:// 文件夹中的文件或文件夹有效。
编辑器数据路径
根据平台的不同,编辑器会使用不同的路径来存储编辑器数据、编辑器设置、缓存。默认情况下,这些路径是:
| 类型 | 位置 |
|---|---|
| 编辑器数据 | Windows:%APPDATA%\Godot/macOS: ~/Library/Application Support/Godot/Linux: ~/.local/share/godot/ |
| 编辑器设置 | Windows:%APPDATA%\Godot/macOS: ~/Library/Application Support/Godot/Linux: ~/.config/godot/ |
| 缓存 | Windows:%TEMP%\Godot/macOS: ~/Library/Caches/Godot/Linux: ~/.cache/godot/ |
| Godot 符合 XDG 基本目录规范 在 Linux/*BSD 上。您可以覆盖 XDG_DATA_HOME、XDG_CONFIG_HOME 和 XDG_CACHE_HOME 环境变量以更改编辑器和项目数据路径。 |
如果你使用的是 Flatpak 打包的 Godot,编辑器数据路径将位于 ~/.var/app/org.godotengine.godot/ 的子文件夹中。
自包含模式
如果你在编辑器二进制文件所在的目录下创建了名为 .sc 或 sc 的文件(macOS 编辑器 .app 捆绑包则是在 MacOS/Contents/ 下),Godot 就会开启自包含模式。
这种模式下,Godot 会将所有编辑器数据、设置、缓存都写入一个与编辑器二进制文件位于同一目录中的名为 editor_data/ 的目录。你可以用它来创建便携安装的编辑器。
Steam 版本的 Godot 默认使用自包含模式。
备注
导出后的项目目前不支持自包含模式。要对相对于可执行文件路径的文件进行读写,请使用 OS.get_executable_path()。
注意,只有可执行文件位于可写的位置时,才能够对可执行文件路径上的文件进行写操作(即不在 Program Files 或者其他普通用户只读的目录中)。
保存游戏
如果你想保存玩家的设置,可以用
ConfigFile来实现这个目的。
识别持久化对象
要保存哪些对象,要保存对象中的哪些信息。
本教程中,我们将使用“分组”来标记和处理要保存的对象,但当然也有其他可行的方法。
将想要保存的对象添加到“Persist”组。
完成这个操作后,我们需要保存游戏时,就可以获取所有需要保存的对象,然后通过这个脚本让这些对象去保存数据:1
2
3var save_nodes = get_tree().get_nodes_in_group("Persist")
for node in save_nodes:
# 现在,我们可以在每个节点上调用我们的保存函数。序列化
下一步是序列化数据。使得读取和存储到磁盘变得更加容易。
在本例中,我们假设组 Persist 的每个成员都是一个实例化节点,因此有一个路径。
GDScript 具有用于在字典和字符串之间转换的辅助类 JSON。
我们的节点需要包含一个返回此数据的保存函数。保存函数将如下所示:
1 | func save(): |
- 保存和读取数据
需要打开一个文件,以便对其进行写入或读取。
现在我们有了调用组并获取其相关数据的方法,让我们使用 JSON 类将其转换为易于存储的字符串,并将它们存储在文件中。
这样做可以确保每一行都是自己的对象,因此我们也可以轻松地从文件中提取数据。
1 | # 注意:此函数可以从场景树的任意位置调用,与节点路径无关。 |
游戏已保存!现在,为了加载,我们将读取每一行。
使用 parse 方法将 JSON 字符串读回到字典中,然后遍历字典以读取我们的值。
但我们首先需要创建对象,然后我们可以使用文件名和父级值来实现这一点。这是我们的加载函数:
1 | # 注意:此函数可以从场景树的任意位置调用,与节点路径无关。 |
现在我们可以保存和加载几乎任何位于场景树中的任意数量的对象了! 每个对象可以根据需要保存的内容存储不同的数据.
一些注释
我们可能忽略了 “将游戏状态设置到适合以加载数据” 这一步.
最终, 这一步怎么做的决定权在项目创建者手里. 这通常很复杂, 需要根据单个项目的需求对此步骤进行大量定制.
另外, 此实现假定没有Persist对象是其他Persist对象的子对象.
否则会产生无效路径. 如果这是项目的需求之一, 可以考虑分阶段保存对象(父对象优先), 以便在加载子对象时可用它们将确保它们可用于 add_child() 调用.
由于 NodePath 可能无效, 因此可能还需要某种方式将子项链接到父项.
JSON 与二进制序列化
简单的游戏状态可能可以使用 JSON,生成的是人类可读的文件,便于调试。
但是 JSON 也存在限制。如果你需要存储比较复杂的游戏状态,或者量比较大,使用二进制序列化可能更合适。
JSON 的限制
以下是一些使用 JSON 时会遇到的大坑。
- 文件大小:JSON 使用文本格式存储数据,比二进制格式要大很多。
- 数据类型:JSON 只提供了有限的数据类型。如果你用到了 JSON 没有的数据类型,就需要自己在这个类型和 JSON 能够处理的类型之间来回转换。
例如 JSON 无法解析以下重要的类型:Vector2、Vector3、Color、Rect2、Quaternion。 - 编解码需要自定义逻辑:如果你想要用 JSON 存储自定义的类,就需要自己编写这些类的编解码逻辑。
二进制序列化
也可以使用二进制序列化来存储游戏状态,可以使用 FileAccess 的 get_var 和 store_var 来实现。
二进制序列化生成的文件比 JSON 小。
二进制序列化能够处理大多数常见数据类型。
二进制序列化在编解码自定义类时需要更少的自定义逻辑。
请注意,并非所有属性都包括在内。只有使用 PROPERTY_USAGE_STORAGE 标志集配置的属性才会被序列化。
你可以通过在类中重写 _get_property_list 方法,来向属性添加新的使用标志。
你还可以通过调用 Object._get_property_list 来检查属性使用是如何配置的。有关可能的使用标志,请参阅 PropertyUsageFlags。
运行时文件加载和保存
纯文本文件和二进制文件
Godot 的 FileAccess 类提供了读写文件系统中文件的方法:
1 | func save_file(content): |
为了处理自定义二进制格式(例如加载 Godot 不支持的文件格式),FileAccess 提供了几种方法来读取/写入整数、浮点数、字符串等。
这些 FileAccess 方法的名称以 get_ 和 store_。
如果你需要对读取二进制文件进行更多控制,或者需要读取不属于文件的二进制流,PackedByteArray 提供了几种辅助方法,可以将一系列字节解码/编码为整数、浮点、字符串等数据类型。这些 PackedByteArray 方法的名称以 decode_ 和 encode_ 开头。另请参阅《二进制序列化 API》。
图像
Image 的 Image.load_from_file 静态方法处理各种事物,包括从基于文件扩展名的格式检测到从磁盘读取文件。
如果你需要错误处理或者更多的控制(例如更改加载 SVG 时的缩放),请根据文件格式使用以下方法:
Image.load_jpg_from_buffer
Image.load_ktx_from_buffer
Image.load_png_from_buffer
Image.load_svg_from_buffer 或 Image.load_svg_from_string
Image.load_tga_from_buffer
Image.load_webp_from_buffer
Godot 还可以在运行时使用以下方法保存几种图像格式:
Image.save_png 或 Image.save_png_to_buffer
Image.save_webp 或 Image.save_webp_to_buffer
Image.save_jpg 或 Image.save_jpg_to_buffer
Image.save_exr 或 Image.save_exr_to_buffer (仅在编辑器版本中可用,无法在导出后的项目中使用)
带有 to_buffer 后缀的方法会将图像保存到 PackedByteArray 而不是文件系统。
这有利于通过网络发送图像或将图像发生到 ZIP 存档,无需将其写入到文件系统。这可以通过降低 I/O 利用率来提高性能。
备注
如果在 3D 曲面上显示加载的图像,请确保调用 Image.generate_mipmaps,以便在远处观察时纹理看起来不会有颗粒感。
在 2D 中,当遵循关于《减少降采样时的锯齿》的说明时也很有用。
加载图像并将其显示在 TextureRect 节点(需要转换为 ImageTexture)中的示例:
1 | # 从文件系统加载任意 Godot 支持格式的图像 |
音视频文件
Godot 支持在运行时加载 Ogg Vorbis、MP3 和 WAV 音频。
请注意,并非所有 扩展名为 .ogg 的文件是 Ogg Vorbis 文件。
有些可能是 Ogg Theora 视频,或者在 Ogg 容器中包含 Opus 音频。这些文件不会 在 Godot 中正确加载为音频文件。
通过 AudioStreamPlayer 节点加载 Ogg Vorbis 音频文件的示例:
1 | $AudioStreamPlayer.stream = AudioStreamOggVorbis.load_from_file(path) |
通过 VideoStreamPlayer 节点加载 Ogg Theora 视频文件的示例:
1 | # 创建一个新的VideoStreamTheora对象实例 |
3D 场景
Godot 在编辑器和导出项目中都对 glTF 2.0 提供了一流的支持。
结合使用 GLTFDocument 和 GLTFState,Godot 可以在导出的项目中加载和保存 glTF 文件,包括文本格式(.gltf)和二进制格式(.glb)。
二进制格式应该优先考虑,因为它写入速度更快且体积更小,但文本格式更易于调试。
从 Godot 4.3 开始,FBX 场景也可以在运行时使用 FBXDocument 和 FBXState 类。
执行此作的代码 与 glTF 相同,但您需要替换 GLTFDocument 和 GLTFState 以及 FBXDocument 和 FBXState 在下面的代码示例中。
存在已知问题 运行时 FBX 加载,因此目前首选使用 glTF。
加载 glTF 场景并将其根节点附加到场景的示例:
1 | # 加载一个现有的 glTF 场景。 |
注
加载 glTF 场景时,必须设置基础路径,以便可以正确加载纹理等外部资源。
从文件加载时,基础路径会自动设置为包含该文件的文件夹。从缓冲区加载时,必须手动设置该基础路径,因为 Godot 无法推断该路径。
要设置基础路径,请在调用 GLTFDocument.append_from_buffer 或 GLTFDocument.append_from_file 之前,在 GLTFState 实例上设置 GLTFState.base_path。
字体
FontFile.load_dynamic_font 支持以下字体文件格式:TTF、OTF、WOFF、WOFF2、PFB、PFM
另一方面,FontFile.load_bitmap_font支持 BMFont 格式(.fnt 或 .font)。
此外,可以使用 Godot 对《系统字体》的支持来加载系统上安装的任何字体。
根据文件扩展名自动加载字体文件,然后将其作为主题覆盖添加到 Label 节点的示例:
1 | var path = "/path/to/font.ttf" |
ZIP 压缩包
Godot 支持使用 ZIPReader 和 ZIPPacker 类读取和写入 ZIP 档案。
这支持任何 ZIP 文件,包括由 Godot 的“导出 PCK/ZIP”功能生成的文件(尽管这些文件将包含导入的 Godot 资源而不是原始项目文件)。
备注
使用 ProjectSettings.load_resource_pack 将 Godot 导出的 PCK 或 ZIP 文件加载为 附加数据包。
这种方法是 DLC 的首选,因为它可以无缝地与附加数据包(虚拟文件系统)进行交互。
这种 ZIP 存档支持可与运行时图像、3D 场景和音频加载相结合,提供无缝的模组化体验,而无需用户通过 Godot 编辑器生成 PCK/ZIP 文件。
示例在 ItemList 节点中列出 ZIP 存档中的文件,然后将从中读取的内容写入新的 ZIP 存档(本质上是复制该存档):
1 | # 加载一个已存在的 ZIP 压缩包 |
二进制序列化 API
Godot 有一个基于 Variant 的序列化 API,用于高效地将数据类型转换为字节数组。
该 API 通过全局 bytes_to_var() 和 var_to_bytes() 函数公开,但它也用在 FileAccess 的 get_var 和 store_var 方法中以及 PacketPeer 的数据包 API。
该格式并不用于二进制场景和资源。
完整对象 vs 对象实例 ID
如果序列化变量时使用了 full_objects = true,则该变量中所包含的 Object 都会进行序列化、包含在结果中。这个过程是递归的。
如果 full_objects = false,则只会对该变量中所包含的 Object 的实例 ID 进行序列化。
数据包规格
根据设计,数据包总是会被填充到 4 个字节。
所有的值都是小端编码的。
所有数据包都有一个 4 字节的头,代表一个整数,指定数据的类型。
最小值 two 字节用于确定类型,而最高值 two 字节包含标志:
1 | base_type = val & 0xFFFF; |
| 类型 | 值 |
|---|---|
| 0 | null 零 |
| 1 | bool 布尔语 |
| 2 | integer 整数 |
| 3 | float 浮 |
| 4 | 字符串 |
| 5 | vector2 矢量2 |
| 6 | rect2 矩形2 |
| 7 | vector3 矢量3 |
| 8 | transform2d 变换 2D |
| 9 | plane 飞机 |
| 10 | 四元数 |
| 11 | aabb 亚伯 |
| 12 | basis 基础 |
| 13 | transform3d 变形 3D |
| 14 | 颜色 |
| 15 | 节点路径 |
| 16 | rid 摆脱 |
| 17 | 对象 |
| 18 | 字典 |
| 19 | 数组 |
| 20 | 原始数组 |
| 21 | int32 数组 |
| 22 | int64 数组 |
| 23 | float32 数组 |
| 24 | float64 数组 |
| 25 | 字符串数组 |
| 26 | vector2 数组 |
| 27 | vector3 数组 |
| 28 | 颜色数组 |
| 29 | max 麦克斯 |
| 在此之后是实际的数据包内容,每种类型的数据包内容都不同。 | |
| 请注意,这里假设 Godot 是用单精度浮点数编译的,这也是默认的。 | |
| 如果 Godot 是用双精度浮点数编译的,那么数据结构中“浮点数”字段的长度应该是 8,偏移量应该是 (offset - 4) * 2 + 4。 | |
| 浮点数“float”类型本身总是使用双精度。 |
国际化
游戏的国际化
资源的本地化
还可以指示 Godot 根据当前语言使用资产(资源)的替代版本。
这可用于本地化诸如游戏内广告牌之类的图像或者语音。
重定向选项卡便可用于此:
自动设置语言
建议默认使用用户的首选语言,可以通过 OS.get_locale_language() 获取。
如果你的游戏不支持该语言,则会回退到 项目设置 > 国际化 > 区域 中的 回退语言,如果为空则回退到 en。
不过,出于各种原因(例如翻译质量或玩家偏好),建议让玩家能够在游戏中更改语言。
1 | var language = "automatic" |
区域设置 vs 语言
示例:
en:英语语言
en_GB:英国的英语 / 英式英语
en_US:美国的英语 / 美式英语
en_DE:德国的英语
甚至可以变得更加复杂。可以想象一下在欧洲和中国提供不同的内容(例如在 MMO 中)。
你需要把每种内容变体都翻译成多种语言,进行相应的存储和加载。
将键转换为文本
对于 Button、Label 等部分控件,如果它们的文本与某个翻译键名相匹配,则将自动获取翻译内容。
例如,如果标签的文本为“MAIN_SCREEN_GREETING1”,并且该键存在于当前翻译中,则该文本将被自动翻译。
这种自动翻译行为在某些情况下可能是不可取的。
例如,当使用 Label 来显示玩家的名字时,如果玩家的名字与翻译键相匹配,你很可能不希望进行翻译。
要禁用某个节点的自动翻译,请在检查器中禁用Localization > Auto Translate(本地化 > 自动翻译)。
在代码中,可以使用 Object.tr() 函数。这将只在翻译中查找文本,并在找到后进行转换:
1 | level.text = tr("LEVEL_5_NAME") |
备注
如果更改语言后不显示任何文字,请尝试换一个字体。
默认项目字体仅支持 Latin-1 字符集的子集,无法用于显示俄语、汉语等文字。
Noto Fonts 是一系列不错的多语言字体资源。如果你使用的是不太常见的语言,请确保下载正确的变体。
下载字体后,将 TTF 文件加载到 DynamicFont 资源中,并将其用作 Control 节点的自定义字体。
为了获得更好的可重用性,请将新的主题资源关联到根 Control 节点,并将 DynamicFont 定义为主题中的默认字体。
占位符
若要在翻译的字符串中使用占位符,请使用 GDScript 格式字符串 或 C# 中的等效功能。
这使得翻译者可以自由移动字符串中占位符的位置,使得翻译听起来更自然。
为了允许翻译人员决定占位符出现的顺序,应尽可能使用搭配使用带命名的占位符和 String.format() 的函数:
1 | # 占位符的位置可以更改,但它们的顺序不能改变。 |
翻译上下文
如果你使用普通的英文作为源字符串(而不是类似于 LIKE_THIS 的消息代码),那么就有可能会遇到歧义的情况,同一个英文字符串可能需要在某些目标语言中翻译为不同的字符串。你可以通过指定可选的翻译上下文来消除歧义,即便源字符串是相同的,也能够让目标语言能够使用不同的字符串:
1 | # "Close", as in an action (to close something). |
复数
很多语言会根据对象的单复数使用不同的字符串。但是把“是否为复数”的条件硬编码为“对象数量是否大于 1 ”并不是对所有语言都有效。
有些语言有两种以上的复数形式,不同的复数需要的对象数量也各不相同。Godot 提供了对复数的支持,目标地区可以自动进行处理。
复数应该只用于正整数(或零)的情况。负数和浮点数所代表的物理实体数量是单数还是复数一般无法明确区分。
使控件的大小可调
Container 可能很有用,Label 的文本换行选项应该也能帮上忙。
要检查您的 UI 是否可以容纳字符串比原始字符串更长的翻译,您可以启用伪本地化 在高级项目设置中。
这将替换所有可本地化的字符串 使用更长的版本,同时还替换了 带有重音版本的原始字符串(同时仍然可读)。
占位符保持原样,以便在伪本地化时继续工作 已启用。
翻译服务器
Godot 中负责底层翻译管理的服务器叫作 TranslationServer。可以在运行时添加或删除翻译;当前语言也可以在运行时更改。
测试翻译
在发布前测试项目的翻译。Godot 为此提供了三种方法。
首先,在项目设置中的国际化 > 区域设置(启用高级设置)下有一个测试属性。
将这个属性设置为你想测试的语言的区域设置代码。
Godot 将在项目运行时使用该区域设置运行该项目(无论是从编辑器运行还是导出后运行)。
请记住,因为这是一个项目设置,设为非空时它会在版本控制中显示。因此,将修改提交到版本控制之前,应该将其设回空值。
其次,在编辑器中点击顶部工具栏的视图,然后在预览翻译中选择要预览的语言。
编辑器场景中的所有文本现在都会以所选语言显示。
还可以在从命令行运行 Godot 时测试翻译。例如,要使用法语测试游戏,可以提供以下参数:godot --language fr
翻译项目名称
项目名称将在导出到不同的操作系统和平台时成为应用名称。
要以多种语言指定项目名称,请转到项目 > 项目设置 > 应用 > 配置。
从这里点击可本地化字符串(大小 0)按钮。
现在下面应该有一个标有添加翻译按钮。
点击该按钮,它将带你到一个页面,可以在其中为项目名称翻译选择语言(如果需要,还可以选择地区)。
完成后,你现在可以输入本地化的名称。
使用电子表格进行本地化
Godot 使用 CSV 格式来支持电子表格。
CSV 文件必须使用 UTF-8 编码保存,不带字节序标记。
CSV 文件必须使用以下格式:
| keys | |||
|---|---|---|---|
| KEY1 | 字符串 | 字符串 | 字符串 |
| KEY2 | 字符串 | 字符串 | 字符串 |
| … | … | … | … |
| KEYN | 字符串 | 字符串 | 字符串 |
CSV 导入器
Godot 默认会将 CSV 文件作为翻译导入,在同一文件夹中生成一个或多个压缩后的翻译资源文件。
导入还会将翻译添加到要在游戏运行时加载的翻译列表中,在 project.godot(或项目设置)中指定。Godot 还允许在运行时加载和删除翻译。
选择 .csv 文件并访问导入停靠面板,以定义导入选项。你可以切换是否压缩导入的翻译,并选择在解析 CSV 文件时使用的定界符。
使用 gettext(PO 文件)进行本地化
Godot 还支持加载使用 GNU gettext 格式的翻译文件(基于文本的 .po 文件,Godot 4.0 开始还支持编译后的 .mo 文件)。
暂不考虑区域设置代码
区域设置代码的格式为 language_Script_COUNTRY_VARIANT,其中:
language - 2 到 3 个字母的语言代码,小写。
Script - 可选,4 个字母的文字代码,首字母大写。
COUNTRY - 可选,2 个字母的地区代码,大写。
VARIANT - 可选,语言变体、区域、排序等。变体中可以包含任意数量使用下划线的关键词。
伪本地化
启用伪本地化及其相关配置非常简单,只需在项目设置中勾选勾选框即可。
在项目设置对话框中启用高级设置切换后,可以在项目 → 项目设置 → 常规 → 国际化 → 伪本地化中找到这些设置:
伪本地化配置
Godot 中的伪本地化可以根据项目的具体用例进行设置。以下是可以通过项目设置配置的伪本地化属性:
- replace_with_accents:将字符串中的所有字符替换为对应的重音变体。
启用该设置后,”The quick brown fox jumped over the lazy dog” 会被转换为 “Ŧh̀é q́üíćḱ ḅŕôŵή f́ôx́ ǰüm̀ṕéd́ ôṽéŕ ŧh̀é łáźý d́ôǵ”。
可以用来发现没有重音的未翻译字符串,也适用于检查项目使用的字体是否缺失字形。 - double_vowels:将字符串中的所有元音加倍。这是在本地化过程中模拟文本扩充的一个很好的近似方法。这可用于检查会溢出其容器的文本(例如按钮)。
- fake_bidi:假双向文字(模拟从右到左的文字)。这对于模拟从右到左的书写系统非常有用,以检查使用从右到左脚本的语言中可能出现的潜在布局问题。
- override:用星号(*)替换字符串中的所有字符。这对于快速查找未本地化的文本很有用。
- expansion_ratio:可用于将元音加倍不足以近似的情况。该设置用下划线(_)填充字符串,并按给定比率扩展它。
对于大多数实际情况来说,扩展比率为 0.3 就足够了;它将使字符串的长度增加 30%。 - prefix 和 suffix:这些属性可用于指定包装文本的前缀和后缀。
- skip_placeholders:跳过字符串格式化的占位符,如 %s 和 %f。这对于识别需要更多参数才能正确显示格式化字符串的位置很有用。
所有这些属性都可以根据项目的用例按需进行切换。
伪本地化可以在运行时使用 pseudolocalization_enabled 属性在 TranslationServer 中。
但是,如果需要伪本地化属性的运行时配置, 它们可以直接使用 ProjectSettings.set_setting(属性,值)
然后调用 TranslationServer.reload_pseudolocalization() 它会重新解析伪本地化属性并重新加载伪本地化。
以下代码片段应打开 replace_with_accents 和 double_vowels 属性,然后调用 reload_pseudolocalization() 以反映更改:
1 | ProjectSettings.set_setting("internationalization/pseudolocalization/replace_with_accents", true) |
输入处理
使用 InputEvent
按下 ESC 键时关闭你的游戏:
1 | func _unhandled_input(event): |
使用 InputMap 功能将更简洁灵活。
你可以在项目 > 项目设置 > 按键映射下设置你的输入映射,这些动作的使用方法如下:
1 | func _process(delta): |
InputEvent 只是一个基本的内置类型,它不代表任何东西,只包含一些基本信息,例如事件 ID(每个事件都会增加)、设备索引等。
| 事件 | 描述 |
|---|---|
| InputEvent | 空输入事件。 |
| InputEventKey | 包含键码和 Unicode 值以及修饰键。 |
| InputEventMouseButton | 包含点击信息,例如按钮、修饰键等。 |
| InputEventMouseMotion | 包含运动信息,例如相对位置、绝对位置和速度。 |
| InputEventJoypadMotion | 包含操纵杆/操纵手柄模拟轴信息。 |
| InputEventJoypadButton | 包含操纵杆/操纵手柄按钮信息。 |
| InputEventScreenTouch | 包含多点触控按下/释放信息。(仅适用于移动设备) |
| InputEventScreenDrag | 包含多点触控拖动信息。(仅适用于移动设备) |
| InputEventMagnifyGesture | 包含位置、系数以及修饰键。 |
| InputEventPanGesture | 包含位置、增量以及修饰键。 |
| InputEventMIDI | 包含 MIDI 相关的信息。 |
| InputEventShortcut | 包含快捷键。 |
| InputEventAction | 包含通用动作。这些事件通常由程序员生成作为反馈。(更多信息见下文) |
输入动作
输入动作是对若干 InputEvent 的分组,为每一组事件赋予能够普遍理解标题(例如默认的“ui_left”动作将手柄向左的输入和键盘上的左方向键分到了一组)。
使用输入动作来代表 InputEvent 不是必须的,但之所以有用,是因为输入动作对游戏逻辑编程时的各种输入进行了抽象。
这样就可以:
- 用相同的代码在不同的设备上处理不同的输入(例如,PC 上的键盘、主机上的游戏手柄)。
- 在运行时重新配置输入。
- 在运行时以编程的方式触发动作。
- 动作可以从“项目设置”菜单中的输入映射选项卡创建并分配输入事件。
任何事件都有InputEvent.is_action()的方法,InputEvent.is_pressed()和InputEvent.is_echo()。
或者,可能需要从游戏代码中向游戏提供一个动作(一个很好的例子是检测手势)。Input 单例有一个方法Input.parse_input_event()来用于此。
通常会像这样使用它:
1 | var ev = InputEventAction.new() |
InputMap
通常需要从代码中自定义输入和重新映射输入。
如果你的整个工作流程都依赖于动作,则 InputMap 单例非常适合在运行时重新分配或创建不同的动作。
该单例不会被保存(必须手动修改),其状态从项目设置(project.godot)运行。
因此,任何该类型的动态系统都需要以程序员认为最合适的方式来存储设置。
输入示例
输入事件
输入事件是从 InputEvent 继承的对象。
根据事件类型,对象将包含与该事件相关的特定属性。
要查看事件的实际样子,请添加一个节点并附加以下脚本:
1 | extends Node |
按键事件会打印为其按键符号。以 InputEventMouseButton 为例,它继承自以下类:
- InputEvent——所有输入事件的基类
- InputEventWithModifiers——增加了检查是否按下 Shift 或 Alt 等修饰键的功能。
- InputEventMouse——增加了如 position 等鼠标事件属性
- InputEventMouseButton——包含按下的按钮的索引、是否是双击等。
如果你尝试访问不包含该属性的输入类型上的属性,则可能会遇到错误——例如在 InputEventKey 上调用 position。
为避免这种情况,请务必先测试事件类型:
1 | func _input(event): |
捕捉动作
定义好动作后,你可以通过传递你要查找的动作的名称,使用 is_action_pressed() 和 is_action_released() 在脚本中处理它们:
1 | func _input(event): |
键盘事件
键盘事件在 InputEventKey 中被捕获。虽然建议改用输入动作,但在某些情况下,你可能会想专门查看按键事件。对于该示例,让我们检查 T:
1 | func _input(event): |
键盘修饰键
修饰键属性继承自 InputEventWithModifiers ,可使用布尔属性检查修饰键的组合。
试想,如果需要在按下 T 时发生一件事,而按下 Shift + T 时发生不同的事:
1 | func _input(event): |
请参阅 @GlobalScope_Key 以获取键码常量列表。
鼠标事件
鼠标事件继承自 InputEventMouse 并被分成 InputEventMouseButton 和 InputEventMouseMotion 两种类型。
注意,这意味着所有鼠标事件都包含 position 属性。
鼠标按钮
捕获鼠标按钮与处理按键事件非常相似。
@GlobalScope_MouseButton 包含每个可能按钮的 MOUSE_BUTTON_* 常量列表,这些常量将在事件的 button_index 属性中报告。
请注意,滚轮也算作一个按钮——准确地说是两个按钮,MOUSE_BUTTON_WHEEL_UP 和 MOUSE_BUTTON_WHEEL_DOWN 都是单独的事件。
1 | func _input(event): |
鼠标运动InputEventMouseMotion 事件在鼠标移动时发生。可以使用 relative 属性找到移动的距离。
下面是一个使用鼠标事件拖放 Sprite2D 节点的示例:
1 | extends Node |
触摸事件
如果你使用的是触摸屏设备,就可以生成触摸事件。
InputEventScreenTouch 相当于鼠标点击事件,而 InputEventScreenDrag 的工作原理与鼠标移动一致。
鼠标和输入坐标
视口显示坐标、
Godot 使用视口(Viewport)显示内容,并且视口可以通过若干选项进行缩放(参见《多分辨率》教程)。
然后,使用节点中的函数来获得鼠标坐标和视口大小,例如:
1 | func _input(event): |
另外,也可以从视口查询鼠标的位置:get_viewport().get_mouse_position()
备注
鼠标模式为 Input.MOUSE_MODE_CAPTURED 时,InputEventMouseMotion 中的 event.position 值为屏幕中心。
请使用 event.relative 代替 event.position 和 event.velocity 来处理鼠标移动和位置变化。
自定义鼠标光标
使用项目设置。这种方式更简单,但功能也更有限。
打开项目设置并进入显示 > 鼠标光标。你将看到以下设置:自定义图像、自定义热点图像 和工具提示位置偏移。
自定义图像热区是图像中要用作光标检测点的点。
自定义图像最多必须为 256×256 像素。为避免呈现问题,建议使用 128×128 或更小的大小。
在 web 平台上,允许的最大光标图像大小为 128×128。使用脚本。这种方式更加可定制化,但需要编写脚本。
1 | extends Node |
检查 Input.set_custom_mouse_cursor() 的文档,以获取有关使用和平台特定注意事项的更多信息。
光标列表
你可定义多种鼠标光标,详见 Input.CursorShape 枚举类型。具体选择取决于你的使用场景。
控制器、手柄和摇杆
Godot 支持数百种开箱即用的控制器模型。
Windows、macOS、Linux、Android、iOS 和 Web 支持控制器。
从 Godot 4.5 开始,引擎依赖于 SDL 3 在 Windows、macOS 和 Linux 上支持控制器。
这意味着 支持的控制器及其行为应与可用的控制器紧密匹配 在使用 SDL 3 的其他游戏和引擎中。请注意,SDL 仅用于输入,不是为了开窗或声音。
当你有两个轴(例如摇杆或 WASD 运动)并且希望两个轴都作为单个输入时,请使用 Input.get_vector():
1 | # `velocity` 将是一个介于 `Vector2(-1.0, -1.0)` 和 `Vector2(1.0, 1.0)` 之间的 Vector2。 |
当你有一个轴可以双向移动时(比如飞行摇杆上的油门),或者你想单独处理不同的轴时,使用 Input.get_axis() :
1 | # `walk` 将是一个介于 -1.0 和 1.0 之间的浮点数,表示左右移动的轴向输入。 |
对于其他类型的模拟输入,例如处理一个触发器或一次处理一个方向,使用 Input.get_action_strength():
1 | # `strength` will be a floating-point number between `0.0` and `1.0`. |
对于非模拟数字/布尔输入(只有 “按下 “ 或 “未按下 “ 的值),如控制器按钮、鼠标按钮或键盘按键,使用 Input.is_action_pressed():
1 | # `jumping` will be a boolean with a value of `true` or `false`. |
备注
如果你想要知道上一帧是否刚刚按下了某个输入,请使用 Input.is_action_just_pressed(),不要使用 Input.is_action_pressed()。
Input.is_action_pressed() 是只要输入处于按下的状态就会返回 true,而 Input.is_action_just_pressed() 只会在按下按钮后的一帧内返回 true。
振动
使用 Input 单例的 start_joy_vibration 方法开启游戏手柄的振动。
要提前结束振动,请使用 stop_joy_vibration(尤其适用于启动时未指定时长的情况)。
在移动设备上,你还可以使用 vibrate_handheld 来振动设备本身(与游戏手柄的振动是分开的),这个功能需要在导出项目前启用 Android 导出预设中的 VIBRATE 权限。
键盘/鼠标和控制器输入之间的差异
- 死区
由于控制器的物理结构,模拟轴的强度永远不会等于 0.0 会徘徊在一个低值。
这种现象被称为漂移,在旧的或有问题的控制器上会更加明显。
一个理想的死区值是足够高的,可以忽略操纵杆漂移引起的输入,但又足够低,不会忽略玩家的实际输入。
Godot 提供了内置的死区系统来解决这个问题。默认值是 0.5,但你可以在“项目设置”的“输入映射”选项卡中针对具体的动作进行调整。Input.get_vector()可以在第五个参数中指定死区。如果没有指定,则会计算向量中的所有动作死区的平均值。 - “回显”事件
与键盘输入不同,按住一个控制器按钮,如十字方向键,不会产生固定间隔的重复输入事件(也被称为“回显”事件)。
这是因为操作系统首先不会为控制器输入发送“回显”事件。
如果你想让控制器按钮发送回显事件,你将不得不通过代码生成 InputEvent 对象,并使用 Input.parse_input_event() 定期解析它们。
这可以在 Timer 节点的帮助下完成。 - 窗口焦点
与键盘输入不同,控制器的输入可以被操作系统中的所有窗口看到,包括未持有焦点的窗口。
虽然这对于第三方分屏功能很有用,但也可能产生不利影响。玩家在与另一个窗口互动时可能会意外地将控制器输入传送到正在执行的项目。
如果你希望在项目窗口未聚焦时忽略事件,则需要使用以下脚本创建一个名为 Focus 的自动加载,并使用它来检查所有输入:
1 | # Focus.gd |
然后,不要使用 Input.is_action_pressed(action),而是使用 Focus.input_is_action_pressed(action),其中 action 是输入动作的名称。
另外,不要使用 event.is_action_pressed(action),而是使用 Focus.event_is_action_pressed(event, action),其中 event 是 InputEvent 引用,action 是输入动作的名称。
4. 防止省电模式
与键盘和鼠标输入不同,控制器输入不会抑制睡眠和省电措施(例如在经过一定时间后关闭屏幕)。
为了解决这个问题,Godot 在项目运行时默认启用预防省电。
如果你注意到在使用游戏手柄玩游戏时系统正在关闭其显示屏,请检查项目设置中的显示 > 窗口 > 节能 > 保持屏幕开启的值。
在 Linux 上,要防止省电,引擎必须能够使用 D-Bus。如果在 Flatpak 中运行项目,请检查 D-Bus 是否已安装且可以访问,因为沙盒限制默认可能会导致无法实现这一点。
处理退出请求
处理通知
在桌面及 Web 平台上, Node 会在窗口管理器发出退出请求时接受到 MainLoop.NOTIFICATION_WM_QUIT_REQUEST 通知.
处理通知的方法如下(在任何节点上):
1 | func _notification(what): |
值得注意的是,默认情况下,当窗口管理器请求退出时,Godot 应用程序具有退出的内置行为。这可以更改,以便用户可以处理完整的退出过程:
1 | get_tree().set_auto_accept_quit(false) |
移动设备
移动平台上并没有与 NOTIFICATION_WM_CLOSE_REQUEST 等价的东西。
由于移动操作系统的特性,退出前运行代码的时机只能是在应用被挂起到后台时。
Android 和 iOS 平台上,处于挂起状态的应用随时都可能被用户或操作系统杀死。
为了应对这种可能性,一种提前计划的方法是利用 NOTIFICATION_APPLICATION_PAUSED 在应用程序暂停时执行需要的操作。
在 Android 上,如果出现以下情况,按“返回”按钮将退出应用程序 应用程序 > 配置 > 退出时返回(退出时返回) 在项目设置(默认设置)中被选中。
这将触发 NOTIFICATION_WM_GO_BACK_REQUEST 。
发送你自己的退出通知
强制关闭应用程序可以通过调用 SceneTree.quit,这样做不会将 NOTIFICATION_WM_CLOSE_REQUEST 发送到场景树中的节点。
通过调用 SceneTree.quit 退出将不允许完成自定义作(例如保存、确认退出或调试),即使您尝试延迟强制退出的行也是如此。
作为替代,如果你想通知节点以及场景树应用程序即将终止,你应当自己发送对应的通知:get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
如此做将会通知所有的节点应用程序即将终止,但是不会关闭应用程序本身 (与3.X版本不同) 。
如果需要达到之前的效果,则应当在发送通知后调用 SceneTree.quit 。
数学
向量数学
实际应用
- 移动
pos2 = pos1 + vel - 指向目标
要找到从 A 指向 B 的向量,请使用 B-A
归一化
对向量进行归一化就是将其长度缩减到 1 方向不变。
提供了专门的 normalized() 方法:a = a.normalized()
反射
有一个 bounce() 方法来处理这个问题
1 | var collision: KinematicCollision2D = move_and_collide(velocity * delta) |
点积
内置的 dot() 方法
1 | var c = a.dot(b) |
叉积
使用内置方法 Vector3.cross() 完成叉积计算:
var c = a.cross(b)
法线计算
计算三角形法线的函数:
1 | func get_triangle_normal(a, b, c): |
指向目标
在上面的点积部分,我们看到如何用它来查找两个向量之间的角度。
然而在 3D 中,这些信息还不够。我们还需要知道在围绕什么轴旋转。
我们可以通过计算当前面对的方向和目标方向的叉积来查找。由此得到的垂直向量就是旋转轴。
高等向量数学
平面
到平面的距离
现在平面是什么就很清楚了,让我们再回到点积上。
单位向量和任何空间点之间的点积(是的,这次我们在向量和位置之间进行点乘),将返回从该点到平面的距离:var distance = normal.dot(point)
但返回的不止是距离的绝对值,如果点位于负半空间,那么这个距离也是负的:这样我们就能够知道点位于平面的哪一侧。
脱离原点
将完整的平面描述为法线 N 和与原点的距离标量 D。这样用 N 和 D 就可以表示我们的平面了。例如:
对于 3D 空间中的平面,Godot 提供了 Plane 内置类型来处理这些计算。
基本上,N 和 D 可以表示空间中的任何平面,无论是 2D 还是 3D(取决于 N 的维数),两者的数学运算相同。它与之前相同,但 D 是从原点到平面的距离,沿 N 方向行进。例如,假设你想要到达平面上的某个点,只需执行以下操作:var point_in_plane = N*D
这将拉伸(调整大小)法线向量并使其接触平面。
这个数学运算可能看起来很混乱,但实际上比看起来要简单得多。
如果我们想再次知道从点到平面的距离,可以以相同方法,但要调整距离:var distance = N.dot(point) - D
也可以用内置函数执行同样的计算:var distance = plane.distance_to(point)
这同样会返回一个正或负的距离。
还可以通过同时对 N 和 D 取负来反转平面的极性。这样,平面的位置不变,但正负半空间倒置:
1 | N = -N |
Godot 还在 Plane 中实现了该运算。因此,使用以下格式将按预期工作:var inverted_plane = -plane
平面的主要实际用途是我们可以计算到平面的距离。
在 2D 中构造平面
平面不会凭空出现,必须先进行构造。
在 2D 空间中构造平面很简单:只需要法线(单位向量)和某一个点,或者空间中任意两点都可以完成。
在法线和点的情况下,由于法线已经被计算出来,大部分计算工作都已完成。因此,只需根据法线和点的点积计算 D 即可。
1 | var N = normal |
对于空间中的两个点,实际上有两个平面穿过它们,共享相同的空间,但正常指向相反的方向。
要从两点计算法线,必须先获得方向矢量,然后需要将其向两侧旋转 90 度:
1 | # 计算从点 a 指向点 b 的方向向量 |
剩余步骤与前例相同。point_a 和 point_b 都可以用于计算,毕竟两者位于同一个平面内:
1 | var N = normal |
平面的一些示例
以下是平面的用途示例。假设有一个凸多边形。比如矩形、梯形、三角形或任何没有面向内弯曲的多边形。
对于多边形的每段,我们计算经过该段的平面。一旦我们有了平面列表,我们就可以做一些有趣的事情,例如检查某个点是否在多边形内。
我们遍历所有平面,如果我们能找到一个到该点的距离为正的平面,那么该点就在多边形外部。如果我们找不到,那么该点就在多边形内部。
1 | var inside = true |
很酷吧?但还会更好!再多花点功夫,类似的逻辑也会让我们知道两个凸多边形何时重叠。这被称为分离轴定理(或 SAT),大多数物理引擎都使用它来检测碰撞。
对于一个点,只要检查是否有一个平面返回正距离,就足以判断该点是否在外部。
对于另一个多边形,我们必须找到一个平面,使另一个多边形的所有点到它的距离都返回为正。
先使用 A 的平面对 B 的点进行检查,然后使用 B 的平面对 A 的点进行检查:
1 | var overlapping = true |
3D 碰撞检测
这是另一个奖励,是对耐心坚持看完这篇长篇教程的奖励。这是另一条智慧。这可能不是直接使用案例(Godot 已经很好地完成了碰撞检测),但几乎所有物理引擎和碰撞检测库都在使用它 :)
还记得将 2D 凸形转换为 2D 平面数组对于碰撞检测很有用吗?你可以检测某个点是否位于任何凸形内,或者两个 2D 凸形是否重叠。
嗯,这在 3D 中也适用,如果两个 3D 多面体发生碰撞,你将无法找到分离平面。如果找到了分离平面,则这两个形状肯定没有发生碰撞。
稍微回顾一下,分离平面意味着多边形 A 的所有顶点都在平面的一侧,而多边形 B 的所有顶点都在另一侧。该平面总是多边形 A 或多边形 B 的面平面之一。
不过,在 3D 中,这种方法存在问题,因为在某些情况下可能找不到分离平面。以下是这种情况的一个示例:
1 | var overlapping = true |
矩阵与变换
变换在大多数情况下应用于平移、旋转、缩放,我们将会重点讲述如何使用矩阵来表示平移、旋转和缩放。
我们指定 t.x.y 这样的值时,表示这是 X 列向量的 Y 分量,换言之,就是这个矩阵的左下角。
类似地,t.x.x 就是左上角、t.y.x 就是右上角、t.y.y 就是右下角。此处的 t 是一个 Transform2D。
缩放变换矩阵
1 | var t = Transform2D() |
要从已存在的变换矩阵中计算对象的缩放尺度,可以对该矩阵的每个列向量使用 length() 方法。
在实际项目中,你可以使用 scaled() 方法去执行缩放变换操作。
旋转变换矩阵
1 | var rot = 0.5 # The rotation to apply. |
要从现有的变换矩阵中计算对象的旋转,可以使用 atan2(t.x.y, t.x.x),其中 t 是 Transform2D。
在实际项目中,可以使用 rotated() 方法进行旋转。
变换矩阵的基
到目前为止,我们只使用 x 和 y 向量,它们负责表示旋转、缩放和/或剪切(高级,会在文末提及)。
X 和 Y 向量合称变换矩阵的基(Basis)。“基”和“基向量”都是非常重要的术语。
你可能已经注意到 Transform2D 实际上有三个 Vector2 值:x、y、origin。
其中 origin 值不是基的一部分,而是变换的一部分,我们需要用它来表示位置。
从现在开始,我们将在所有示例中记录原点向量。你可以将原点看作另一列,但把它认为是完全独立的通常更好。
请注意在 3D 中,Godot 有一个单独的 Basis 结构,里面包含矩阵基的三个 Vector3 的值。
因为代码可能变得复杂,因此将它们从 Transform3D(由一个 Basis 和一个额外的原点 Vector3 组成)中拆分出来是值得的。
变换矩阵的平移
更改 origin 向量被称为对变换矩阵的平移。平移其实上是“移动”对象的一个技术术语,但它不会涉及任何旋转。
让我们通过一个例子来帮助理解这一点。我们将像上次一样从恒等变换开始,但这次我们将记录原点向量。
如果希望对象移动到 (1, 2) 的位置,只需将其 origin 向量设置为 (1, 2):
还有一个 translated_local() 方法,它执行的是与直接增加或更改 origin 不同的操作。这个 translated_local() 方法将让该对象相对于它自己的旋转进行平移。例如,当使用 Vector2.UP 进行 translated_local() 时,顺时针旋转 90 度的对象将向右移动。要相对于全局/父帧平移,请使用 translated()。
融会贯通
1 | var t = Transform2D() |
高级部分暂不考虑
插值
插值是图形编程中的一种常规操作,可以在两个值之间进行混合或过渡。
插值还可以让移动、旋转等运动变得平滑。熟悉插值对拓展游戏开发者的视野大有裨益。
基本思想是从 A 转换到 B。t 值是介于两者之间的状态。
举个例子,如果 t 是 0,那么他的状态是 A。如果 t 是 1,那么它的状态是 B。任何介于两者之间的状态都是插值。
两个实数(浮点数)之间的插值可以描述为:interpolation = A * (1 - t) + B * t
通常简化为:interpolation = A + (B - A) * t
这种以恒定速度将一个值转换为另一个值的插值被称为“线性”。因此,当你听到线性插值时,你就知道他们指的是这个公式。
向量插值
向量类型(Vector2 和 Vector3)也可以插值,向量自带了相关的便捷函数 Vector2.lerp() 和 Vector3.lerp()。
对于三次插值,还有 Vector2.cubic_interpolate() 和 Vector3.cubic_interpolate() ,它们执行 Bezier 式插值。
下面是从 A 点插值到 B 点的示例伪代码:
1 | var t = 0.0 |
变换插值
也可以对整个变换进行插值(确保它们具有均一缩放,或者至少有相同的非均一缩放)。
为此,可以使用函数 Transform3D.interpolate_with()。
下面是将猴子从位置1转换为位置2的例子:
1 | var t = 0.0 |
平滑运动
插值可用于平滑地跟随移动的目标值,例如位置或旋转。
每一帧,lerp() 将当前值移动到目标值,其比例为值之间剩余差值的固定百分比。
当前值将平稳地向目标移动,随着目标的靠近而减慢速度。下面是使用插值平滑跟随鼠标的圆的示例:
1 | const FOLLOW_SPEED = 4.0 |
这对于平滑镜头移动、跟随玩家的盟友(确保他们保持在一定范围内)以及许多其他常见的游戏模式非常有用。
尽管使用了 delta,但上面使用的公式与帧速率有关,因为 lerp() 的权重参数表示剩余值差异的百分比 ,而不是要更改的绝对量 。
在 _physics_process() 中,这通常没问题,因为物理场预计会保持恒定的帧速率,因此增量预计会保持恒定。
对于也可以在 process() 中使用的与帧率无关的插值平滑版本,请改用以下公式:
1 | const FOLLOW_SPEED = 4.0 |
推导此公式超出了本页的范围。有关说明,请参阅改进的插值平滑 或者观看插值平滑被破坏 。
贝塞尔、曲线和路径
贝塞尔曲线是一种自然几何形状的数学近似.
我们用它们来代表一个曲线, 含有尽可能少的信息, 保持高水平的灵活性.
不像抽象的数学概念, 贝塞尔曲线是为工业设计. 它们是图形软件行业中的流行工具.
它们依赖于 插值, 我们在上一篇文章中看到, 如何结合多个步骤来创建平滑的曲线.
为了更好地理解贝塞尔曲线的工作原理, 我们从最简单的形式开始: 二次贝塞尔曲线.
二次贝塞尔曲线
取三个点, 这是建立二次贝塞尔曲线所需的最小值:
要在它们之间画一条曲线,我们首先使用 0 到 1 之间的值,在由这三个点构成的两个线段的每个顶点上逐步插值。
当我们把 t 值从 0 变成 1 时,就得到了两个沿着线段移动的点。
1 | func _quadratic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, t: float): |
然后,我们插值 q0 和 q1,以获得沿着曲线移动的单点 r。
1 | var r = q0.lerp(q1, t) |
这种类型的曲线就被称为二次贝塞尔曲线。
三次贝塞尔曲线
基于前面的例子, 我们可以通过在四个点之间插值得到更多的控制.
首先我们使用一个带有四个参数的函数,以 p0、p1、p2、p3 四个点作为输入:
我们对每两个点进行线性插值, 将它们减少到三个:
然后我们把这三个点缩减为两个点:
然后到一个:
1 | func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float): |
三次贝塞尔插值在三维中也是一样的,只需使用 Vector3 代替 Vector2。
添加控制点
在三次贝塞尔的基础上,我们可以通过改变两个点的工作方式来自由地控制曲线的形状。我们不使用 p0、p1、p2、p3,而是将它们存储为:
point0 = p0:是第一个点,即源
control0 = p1 - p0:是相对于第一个控制点的向量
control1 = p3 - p2:是相对于第二个控制点的向量
point1 = p3:是第二个点,即终点
使用这种方式, 有两个点和两个控制点, 它们是各自点的相对向量. 如果你以前用过图形或动画软件, 这可能看起来很熟悉:
这就是图形软件如何向用户呈现贝塞尔曲线, 以及它们在Godot引擎内的工作原理.
Curve2D、Curve3D、Path 以及 Path2D
有两个包含曲线的对象:Curve3D 和 Curve2D(分别用于 3D 和 2D)。
它们可以包含几个点,允许更长的路径。也可以将它们设置为节点:Path3D 和 Path2D(分别用于 3D 和 2D):
然而它们的使用方法可能不是很直观,下面是对贝塞尔曲线最常见用例的描述。
估值
直接估值也是一种选择,不过在大多数情况下都不是很有用。贝塞尔曲线最大的缺点是,如果你以恒定的速度沿着它走,从 t = 0 到 t = 1,实际的插值不会以恒定的速度移动。速度也是根据点 p0、p1、p2、p3 之间距离插值出来的,无法使用简单的数学方法以恒定的速度通过曲线。
让我们用下面的伪代码举个例子:
1 | var t = 0.0 |
如你所见,即便 t 在匀速递增,圆点的速度还是在不断变化的(以像素每秒为单位)。这也使贝塞尔难以做到任何实际的开箱即用。
绘制
绘制贝塞尔(或基于曲线的对象)是很常见的用例,但这也不容易。几乎在任何情况下,贝塞尔曲线需要被转换成某种线段。然而,这通常很困难,除非创建大量线段。
原因是曲线的某些部分(具体来说是拐角)可能需要大量的点,而其他部分可能不需要:
另外,如果两个控制点都是 0,0(请记住它们是相对向量),贝塞尔曲线就是一条直线(因此绘制大量的点会很浪费)。
在绘制贝塞尔曲线之前,需要进行细分。这通常使用递归或分治函数来完成,该函数将曲线分割,直到曲率量小于某个阈值。
Curve 类通过 Curve2D.tessellate() 函数(接收可选的递归 stages 和角度 tolerance 参数)提供该功能。这样,基于曲线绘制某些东西就更容易了。
遍历
曲线的最后一个常见用途是遍历它们。由于之前提到的恒定速度,这也很困难。
为了使这更容易,需要将曲线烘焙成等距点。这样,它们可以用常规插值来近似(可以使用立方选项进一步优化)。为此,只需将 Curve3D.sample_baked() 方法与 Curve2D.get_baked_length() 一起使用。对其中任何一个的第一次调用都会在内部烘焙曲线。
那么,可以使用以下伪代码来完成恒定速度的遍历:
1 | var t = 0.0 |
然后输出将以恒定速度移动:
随机数生成
计算机不能产生“真正的”随机数。依赖伪随机数生成器(PRNG)。
全局作用域 vs RandomNumberGenerator 类
Godot 提供了两种生成随机数的方式:
- 全局作用域方法更容易设置,但不能提供太多控制。
- RandomNumberGenerator需要使用更多代码,但允许建立多个实例,每个实例都有自己的种子和状态。
本教程使用全局作用域方法, 只存在于RandomNumberGenerator类中的方法除外.
randomize() 方法
备注
自Godot 4.0以来,当项目启动时,随机种子会自动设置为随机值。
这意味着你不再需要在 _ready() 中调用 randomize() 来确保项目运行的结果是随机的。
但是,如果你想使用特定的种子编号,或者使用不同的方法生成它,你仍然可以使用 randomize()。
在全局作用域中,你可以找到一个 randomize() 方法。
该方法应该在你的项目开始初始化随机种子时只调用一次。多次调用它是不必要的,且可能会对性能产生负面影响。
把它放在你的主场景脚本的 _ready() 方法中是个不错的选择:
1 | func _ready(): |
你也可以使用 seed() 设置固定的随机种子。这样做会在运行过程中为你提供确定性的结果:
1 | func _ready(): |
当使用RandomNumberGenerator类时,应该在实例上调用 randomize() ,因为它有自己的种子:
1 | var random = RandomNumberGenerator.new() |
获得一个随机数
- 函数 randi() 返回一个介于 0 和 2^32 - 1 之间的随机数。
由于最大值很大,您很可能希望使用模运算符 (%) 将结果绑定在 0 和分母之间:
1 | # Prints a random integer between 0 and 49. |
- randf() 返回一个介于 0 和 1 之间的随机浮点数。在实现 加权随机概率 系统等时很有用。
randfn()遵循 正态分布 的随机浮点数。这意味着返回值更有可能在平均值附近(默认为 0.0),随偏差变化(默认为 1.0):
1 | print(randfn(0.0, 1.0)) |
- randf_range() 接受两个参数 from 和 to,并返回一个介于 from 和 to 之间的随机浮点数:
1 | print(randf_range(-4, 6.5)) |
- randi_range() 接受两个参数 from 和 to,并返回一个介于 from 和 to 之间的随机整数:
1 | print(randi_range(-10, 10)) |
获取一个随机数组元素
我们可以使用随机整数生成来从数组中获得一个随机元素,或者使用方法 Array.pick_random 来为我们做这件事:
1 | var _fruits = ["apple", "orange", "pear", "banana"] |
为了防止连续多次采摘相同的水果,我们可以给上述方法添加更多的逻辑。
此时无法使用 Array.pick_random ,因其缺少防重复机制:
1 | var _fruits = ["apple", "orange", "pear", "banana"] |
这种方法可以让随机数生成的感觉不那么重复. 不过, 它仍然不能防止结果在有限的一组值之间 “乒乓反复”. 为了防止这种情况, 请使用 shuffle bag 模式来代替.
获取一个随机字典值
我们也可以将数组的类似逻辑应用于字典:
1 | var _metals = { |
加权随机概率
randf() 方法返回一个介于 0.0 和 1.0 之间的浮点数。
我们可以使用它来创建“加权”概率,其中不同的结果具有不同的可能性:
1 | func _ready(): |
您还可以使用 rand_weighted() 方法。这将返回一个介于 0 和作为参数传递的数组大小之间的随机整数。
数组中的每个值都是一个浮点数,表示它作为索引返回的相对可能性。较高的值意味着该值更有可能作为索引返回,而值为 0 表示它永远不会作为索引返回。
例如,如果 [0.5, 1, 1, 2] 作为参数传递,则该方法返回 3(值 2 的索引)的可能性是原来的两倍,返回的可能性是原来的两倍 0(值 0.5 的指数)与指数 1 和 2 相比。由于返回值与数组的大小匹配,因此可以将其用作索引以从另一个数组获取值,如下所示:
1 | # Prints a random element using the weighted index that is returned by `rand_weighted()`. |
使用 shuffle bag 达到“更好”随机性
以上面同样的例子为例, 我们希望随机挑选水果.
然而, 每次选择水果时依靠随机数生成会导致分布不那么 均匀 .
如果玩家足够幸运(或不幸), 他们可能会连续三次或更多次得到相同的水果.
您可以使用洗牌袋模式来完成此作。它的工作原理是在选择元素后从数组中删除元素。
多次选择后,数组最终为空。发生这种情况时,将其重新初始化为默认值:
1 | var _fruits = ["apple", "orange", "pear", "banana"] |
在运行上面的代码时, 仍有可能连续两次得到同一个水果.
我们摘下一个水果时, 它将不再是一个可能的返回值, 但除非数组现在是空的.
当数组为空时, 此时我们将其重置回默认值, 这样就导致了能再次获得相同的水果, 但只有这一次.
随机噪音
当你需要一个 缓慢 根据输入而变化的值时, 上面显示的随机数生成方式就显示出了它们的局限性.
这里的输入可以是位置, 时间或其他任何东西.
为了实现这一点,你可以使用随机噪声函数。噪声函数在程序式生成中特别受欢迎,可用于生成逼真的地形。
Godot 为此提供了 FastNoiseLite,它支持 1D、2D 和 3D 噪声。以下是 1D 噪声的示例:
1 | var _noise = FastNoiseLite.new() |
密码安全的伪随机数生成器
目前为止提到的方法都无法实现密码安全的伪随机数生成(CSPRNG)。这对于游戏而言没有问题,但是对于涉及加密、认证、签名的场景就显得捉襟见肘。
Godot 为此提供了 Crypto 类。这个类可以执行非对称密钥加密、解密、签名和验证,也可以生成密码安全的随机字节块、RSA 密钥、HMAC 摘要、自签名的 X509Certificate。
CSPRNG 的缺点是它比标准伪随机数的生成慢得多。其 API 的使用也不太方便。因此,游戏机制应避免使用 CSPRNG。
使用 Crypto 类生成 0 到 2^32-1(含)之间的 2 个随机整数的示例:
1 | var crypto := Crypto.new() |
导航
2D 导航概述
Godot 提供了多种对象、类和服务器帮助 2D 和 3D 游戏实现基于栅格(Grid)或网格(Mesh)的导航和寻路。
Godot 为 2D 导航提供了如下对象和类:
Astar2D
能够在由具有权重的点构成的图中查找最短路径。
最适合基于单元格的 2D 游戏,角色不需要到达区域中的任意位置,只需要能够到达一些预先指定的独立位置。AstarGrid2D
AStar2D 的变体,专门用于部分 2D 网格。
能够使用 AstarGrid2D 用起来更简单,因为不需要手动创建点,也不需要手动进行连接。NavigationServer2D
提供了强大的服务器 API,能够在区域中查找两个位置之间的最短路径,区域使用导航网格定义。
NavigationServer 最适合的是要求角色能够到达区域中任意位置的 2D 实时游戏,区域由导航网格定义。
基于网格的导航能够轻松扩展到大型游戏世界,因为大型区域通常能够使用单一多边形定义,如果换成栅格则会需要定义许许多多的单元格。
NavigationServer 中存放了不同的导航地图,每一张地图都由若干区块组成,区块中存放的是导航网格数据。
在地图上放置代理就能够进行避障计算。与服务器通信时,使用 RID 来引用内部的地图、区块和代理。NavigationServer 中可用的 RID 类型如下。
- 导航地图 RID
引用指定的导航地图,地图中存放的是区块和代理。地图会尝试将区块中的导航网格根据距离进行合并。
每一个物理帧,地图都会同步区块和代理。 - 导航区块 RID
引用指定的导航区块,区块中存放的是导航网格数据。
使用导航层位掩码可以对区块进行启用/禁用,限制其使用。 - 导航链接 RID
引用指定的导航链接,能够将两个导航网格上的位置进行连接,无视距离。 - 导航代理 RID
引用指定的避障代理。避障使用半径值指定。 - 导航障碍物 RID
引用指定的避障障碍物,会对代理的避障速度产生影响和约束。
- 导航地图 RID
下列场景树节点可以辅助对 NavigationServer2D API 的使用。
NavigationRegion2D 节点
存放 NavigationPolygon 资源的节点,该资源定义的是 NavigationServer2D 中的导航网格。
区块可以启用/禁用。
通过 navigation_layers 掩码,可以对其在寻路中的使用做进一步的限制。
NavigationServer2D 会根据距离将不同区块中的导航网格合并成一个导航网格。NavigationLink2D 节点
将两个导航网格上的位置进行连接的节点,无视距离,可用于寻路。
链接可以启用/禁用。
链接可以设为单向或双向。
通过 navigation_layers 掩码,可以对其在寻路中的使用做进一步的限制。
链接会告诉寻路存在这样的连接、相关的消耗如何。实际的代理处理以及移动需要在自定义脚本中实现。NavigationAgent2D 节点
可选的辅助节点,用于为继承自 Node2D 的父节点提供寻路和避障所需的常规 NavigationServer2D API 调用。请将这个节点放在继承自 Node2D 的父节点下。NavigationObstacle2D 节点
可用于影响和约束启用躲避的代理的躲避速度的节点。此节点不影响代理的寻路。你需要为此更改导航网格。
2D 导航网格由以下资源定义:
- NavigationPolygon 资源
存放 2D 导航网格数据的资源,提供了多边形绘制工具,既能够在编辑器中定义导航区域,也能够在运行时定义。
NavigationRegion2D 节点使用该资源定义其导航区域。
NavigationServer2D 使用该资源更新各个区块的导航网格。
TileSet 编辑器会定义图块的导航区域时在内部创建并使用该资源。
参见
可以使用 2D 导航演示项目和使用 AStarGrid2D 进行基于栅格的导航 演示项目了解 2D 导航如何运作。
2D 场景的设置
下列步骤演示的是最小可行的 2D 导航的基础设置,使用 NavigationServer2D 和 NavigationAgent2D 进行路径移动。
- 在场景中添加一个 NavigationRegion2D 节点。
- 单击该区块节点,向该节点添加一个新的 NavigationPolygon 资源。
- 使用 NavigationPolygon 绘制工具定义可移动导航区域。然后点击工具栏上的 烘焙 NavigationPolygon 按钮。
- 在场景中添加一个 CharacterBody2D 节点,设置基础的碰撞形状,添加一个精灵或网格方便观察。
- 在该角色节点下添加一个 NavigationAgent2D 节点。
- 为 CharacterBody3D 节点添加下面的脚本。场景完全加载后,确保设置移动目标,NavigationServer 有时间进行同步。
1 | extends CharacterBody2D |
备注
第一帧的时候,NavigationServer 上的地图还没有同步区块数据,请求路径时都会返回空。
在脚本中等待一帧就可以让 NavigationServer 进行同步。
3D 导航概述
Godot 为 3D 导航提供了如下对象和类:
Astar3D
能够在由具有权重的点构成的图中查找最短路径。
最适合的是基于单元格的 3D 游戏,角色不需要到达区域中的任意位置,只需要到达预先指定的一些独立位置。NavigationServer3D
提供了强大的服务器 API,能够在区域中查找两个位置之间的最短路径,区域使用导航网格定义。
NavigationServer 最适合的是要求角色能够到达区域中任意位置的 3D 实时游戏,区域由导航网格定义。
基于网格的导航能够轻松扩展到大型游戏世界,因为大型区域通常能够使用单一多边形定义,如果换成栅格则会需要定义许许多多的单元格。
NavigationServer 中存放了不同的导航地图,每一张地图都由若干区块组成,区块中存放的是导航网格数据。
在地图上放置代理就能够进行避障计算。与服务器通信时,使用 RID 来引用内部的地图、区块和代理。NavigationServer 中可用的 RID 类型如下。
- 导航地图 RID
引用指定的导航地图,地图中存放的是区块和代理。地图会尝试将区块中的导航网格根据距离进行合并。每一个物理帧,地图都会同步区块和代理。 - 导航区块 RID
引用指定的导航区块,区块中存放的是导航网格数据。使用导航层位掩码可以对区块进行启用/禁用,限制其使用。 - 导航链接 RID
引用指定的导航链接,能够将两个导航网格上的位置进行连接,无视距离。 - 导航代理 RID
引用指定的避障代理,避障时使用的是半径值。 - 导航障碍物 RID
引用指定的避障障碍物,会对代理的避障速度产生影响和约束。
- 导航地图 RID
下列场景树节点可以辅助对 NavigationServer3D API 的使用。
- NavigationRegion3D 节点
存放 Navigation Mesh 资源的节点,该资源定义的是 NavigationServer3D 中的导航网格。
区块可以启用/禁用。
通过 navigation_layers 掩码,可以对其在寻路中的使用做进一步的限制。
NavigationServer3D 会根据距离将不同的导航网格合并成一个导航网格。 - NavigationLink3D 节点
将两个导航网格上的位置进行连接的节点,无视距离,可用于寻路。
链接可以启用/禁用。
链接可以设为单向或双向。
通过 navigation_layers 掩码,可以对其在寻路中的使用做进一步的限制。
链接会告诉寻路存在这样的连接、相关的消耗如何。实际的代理处理以及移动需要在自定义脚本中实现。 - NavigationAgent3D 节点
方便调用寻路和避障所需的常规 NavigationServer3D API 的辅助节点。该节点的父节点应该继承自 Node3D。 - NavigationObstacle3D 节点
可用于影响和约束启用躲避的代理的躲避速度的节点。此节点不影响代理的寻路。你需要为此更改导航网格。
3D 导航网格由以下资源定义:
- NavigationMesh 资源
存放 3D 导航网格数据的资源,提供了 3D 几何体的烘焙选项,既能够在编辑器中定义导航区域,也能够在运行时定义。- NavigationRegion3D 节点使用该资源定义其导航区域。
- NavigationServer3D 使用该资源更新各个区块的导航网格。
- GridMap 编辑器会在栅格单元格中存在对导航网格的定义时使用该资源。
参见
可以使用 3D 导航演示项目了解 3D 导航如何运作。
3D 场景的设置
下列步骤演示的是最小可行的 3D 导航的基础设置,使用 NavigationServer3D 和 NavigationAgent3D 进行路径移动。
- 在场景中添加一个 NavigationRegion3D 节点。
- 单击该区块节点,向该节点添加一个新的 NavigationMesh 资源。
- 将一个新的 MeshInstance3D 节点添加为该区块节点的子节点。
- 选中该 MeshInstance3D 节点,添加一个新的 PlaneMesh 并将其 XY 大小设为 10。
- 再次选中该区块节点,点击顶栏中的“烘焙导航网格”按钮。
- 现在就会显示出透明的导航网格,悬浮在 PlaneMesh 上方。
- 在场景中添加一个 CharacterBody3D 节点,设置基础的碰撞形状,添加一些网格方便观察。
- 在该角色节点下添加一个 NavigationAgent3D 节点。
- 为 CharacterBody3D 节点添加一个脚本,内容如下。场景完全加载后,我们确保设置移动目标,NavigationServer 有时间进行同步。另外,添加一个 Camera3D、一些灯光以及环境,这样才能够看到东西。
1 | extends CharacterBody3D |
备注
第一帧的时候,NavigationServer 上的地图还没有同步区块数据,请求路径时都会返回空。
在脚本中等待一帧就可以让 NavigationServer 进行同步。
使用 NavigationServer
导航服务器,分别为 NavigationServer2D 和 NavigationServer3D。
与 NavigationServer 通信
要使用 NavigationServer 首先就需要为请求准备参数,这个请求会被发送给 NavigationServer,用来进行更新和数据的请求。
地图、区块、代理等 NavigationServer 内部的对象使用 RID 来进行引用,这是一种用作标识符的数字。
场景树中每个导航相关的节点都提供了返回该节点 RID 的函数。
线程与同步
NavigationServer 不会立即为所有更改执行更新,而是会等到物理帧的结尾再同步所有更改。
对地图、区块、代理进行更改都需要等待同步。
之所以要进行同步,是因为部分更新的开销非常大,并且需要所有其他对象更新后的数据,例如重新计算整个导航地图。
另外 NavigationServer 的部分功能默认会使用线程池,例如代理之间的避障计算。
大多数 get() 函数都不需要等待,因为这些函数只会从 NavigationServer 请求数据,不会进行修改。
请注意,并不是所有数据都会考虑到同一帧里做出的更改。
比如避障代理在当前帧更改了导航地图的话,那么 agent_get_map() 函数在同步之前就仍然会返回旧的地图。
部分节点会在向 NavigationServer 发送更新前在内部存储对应的值,所以这些节点属于例外。
在这种节点上使用某个值的 getter 时,如果同一帧存在更新,那么返回的就是存储在该节点上的更新后的值。
NavigationServer 是线程安全的,因为它会把所有需要执行更改的 API 调用放在队列中,在同步阶段再执行。
NavigationServer 的同步发生在物理帧,在脚本以及节点的场景输入之后。
备注
划重点:大多数对 NavigationServer 的更改都会在下一个物理帧之后生效,不会立即生效。
包括场景树中导航相关节点做出的更改,以及脚本做出的更改。
Setter 和删除函数都需要同步。
2D 和 3D NavigationServer 的区别
NavigationServer2D 和 NavigationServer3D 在各自维度上的功能是等效的。
从技术上讲,可以使用工具在一个维度上为另一个维度创建导航网格,例如,当使用平面三维源几何体时,使用3D NavigationMesh烘焙二维导航网格,或者使用NavigationRegion2D和NavigationPolygons的多边形轮廓绘制工具创建三维平面导航网格。
等待同步
在游戏开始时,新场景或程序导览发生变化,对导览服务器的任何路径查询都将传回空或错误。
此时导航地图仍然为空或未更新。
场景树中的所有节点都需要首先将其导航相关数据上传到NavigationServer。
每个添加或更改的地图、区域或代理都需要在NavigationServer中注册。
之后,NavigationServer需要物理帧进行同步,以更新地图、区域和代理。
一种解决方法是延迟调用自定义设置函数(这样所有节点都准备好了)。
设置功能进行所有导航更改,例如添加程序性内容。之后,函数在继续路径查询之前等待下一个物理帧。
1 | extends Node3D |
服务器避障回调
如果 RVO 避障代理注册了避障回调,NavigationServer 会在 PhysicsServer 同步前发送对应的 velocity_computed 信号。
更多 NavigationAgent 相关的信息见 使用 NavigationAgent。
使用避障的 NavigationAgent 的简化执行顺序如下:
- 物理帧开始。
- _physics_process(delta)
- 设置 NavigationAgent 节点的 velocity 属性。
- 代理向 NavigationServer 发送速度和位置。
- NavigationServer 等待同步。
- NavigationServer 同步并为所有注册的避障代理计算避障速度。
- NavigationServer 通过信号为每个注册的避障代理发送安全速度向量。
- 代理收到信号并移动父节点,例如通过 move_and_slide 或 linear_velocity 移动。
- PhysicsServer 同步。
- 物理帧结束。
因此,在回调函数中使用安全速度来移动角色物理体无论从线程还是物理的角度看都是安全的,因为相关的操作都在同一个物理帧中进行,之后 PhysicsServer 才会提交更改并进行计算。
使用导航地图
NavigationMap 即导航地图,位于 NavigationServer 中,使用 NavigationServer 的 RID 标识。
地图中可以存放并连接几乎无限数量的导航区块,区块中的导航网格可以用来构建游戏世界中的可达区域,用于寻路操作。
地图中可以包含避障代理。碰撞躲避是根据地图中存在的代理来计算的。
备注
不同 NavigationMap 之间是相互独立的,但是导航区块和避障代理可以在不同地图之间进行切换。
切换会在 NavigationServer 同步后生效。
默认导航地图
Godot 默认会为根视口的每个 World2D 和 World3D 创建一个导航地图。
默认的 2D 导航地图 RID 可以从继承自 Node2D 的任何 Node 的 get_world_2d().get_navigation_map() 获取。
默认的 3D 导航地图 RID 可以从继承自 Node3D 的任何 Node 的 get_world_3d().get_navigation_map() 获取。
1 | extends Node2D |
新建导航地图
导航服务器可根据特定游戏的需要创建和支持尽可能多的导航地图。
其他导航地图可通过直接使用导航服务器应用程序接口来创建和处理,例如支持不同的避障代理或演员运动类型。
有关使用不同导航地图的示例,请参阅 支持不同角色类型 和 支持不同角色运动。
每个导航地图都会单独同步其导航区块和避障代理的更改队列。
未收到更改通知的导航地图几乎不会消耗任何处理时间。
导航区块和避障代理只能是单个导航地图的一部分,但它们可以随时切换所属地图。
导航地图切换只有在下一次同步导航服务器后才会生效。
1 | extends Node2D |
使用 NavigationServer2D API 和 NavigationServer3D API 创建的导航地图没有任何区别。
使用导航区块
NavigationRegion 即导航区块,是对 NavigationServer 的导航地图中某个区块的可视化 Node 表示。
每个 NavigationRegion 节点中都存放着导航网格数据资源。
2D 和 3D 版本分别为 NavigationRegion2D 和 NavigationRegion3D。
各个导航区域将其二维导航多边形或三维导航网格资源数据上传到导航服务器。
导航服务器地图会将这些信息转化为用于寻路的组合导航地图。
要使用场景树创建导航区块,请在场景中添加一个 NavigationRegion2D 或 NavigationRegion3D 节点。
所有区块都需要导航网格资源才能正常工作。导航网格的创建和应用见 使用导航网格。
NavigationRegion 会自动将 global_transform 的变化推送到导航服务器上的对应区块,因此可用于移动的平台。
当各个区块的导航网格足够接近时,NavigationServer 就会尝试将它们连接起来,详见 连接导航网格。
NavigationLink 可以连接任意距离的 NavigationRegion,如何创建和使用见 使用 NavigationLink。
警告
虽然修改 NavigationRegion 节点的变换能够更新 NavigationServer 中对应区块的位置,但是修改缩放却不会。
导航网格资源不具备缩放,所以来源几何体的缩放发生改变时需要进行完整的更新。
区块可以启用/禁用,如果禁用,则不会参与寻路查询。
启用/禁用区块时,现有路径不会自动更新。
新建导航区块
新的导航区块节点会在2D/3D 尺寸的默认世界导航地图上自动注册。
然后就可以通过 NavigationRegion 节点的 get_rid() 获取区块 RID。
1 | extends NavigationRegion2D |
还可以使用导航服务器接口创建新区块,并将其添加到任何现有地图中。
如果直接使用导航服务器接口创建区块,则需要手动为其分配导航地图。
1 | extends Node2D |
导航区块只能分配给一个导航地图。如果将现有区块分配到新的导航地图,它就不属于旧地图了。
使用导航网格
2D 和 3D 版本的导航网格分别为 NavigationPolygon 和 NavigationMesh。
导航网格描述的只是代理中心位置的可达区域,会忽略代理可能存在的半径值。如果你想要让寻路考虑代理的(碰撞)尺寸,就需要让导航网格收缩对应的量。
导航系统的工作独立于渲染或物理等其他引擎部分。导航系统在寻路时只考虑导航网格,视觉效果和碰撞形状等会被导航系统完全忽略。
如果在寻路时需要考虑其他数据(例如视觉效果),则需要对导航网格进行相应调整。在导航网格中考虑导航限制的过程通常称为导航网格烘焙。
如果你在遵循导航路径时遇到剪切或碰撞问题,请务必记住,你需要通过合适的导航网格告诉导航系统你的意图。
导航系统本身永远不会知道 “这是树木/岩石/墙壁碰撞形状或可视化网格”,因为它只知道 “我被告知在这里可以安全通过,因为它在导航网格上”。
导航网格的烘焙可以使用 NavigationRegion2D 或 NavigationRegion3D 实现,也可以直接使用 NavigationServer2D 和 NavigationServer3D 的 API。
使用导航区块 NavigationRegion 烘焙导航网格
可以更方便地进行导航网格烘焙。
进行烘焙时,所有解析、烘焙和区块更新步骤都会合并到一个函数中。
2D 和 3D 版本分别为 NavigationRegion2D 和 NavigationRegion3D。
小技巧
导航网格的 source_geometry_mode 可以切换为解析特定的节点组名称,这样需要烘焙的节点就可以放置在场景中的任何位置。
使用 NavigationServer 烘焙导航网格
NavigationServer2D 和 NavigationServer3D 都提供了烘焙导航网格相关的 API 函数,可以在烘焙过程中的不同阶段单独调用。
parse_source_geometry_data() 可用于将源几何体解析为可重用和可序列化的资源。
bake_from_source_geometry_data() 可用于根据已解析的数据烘焙导航网格,例如避免(冗余)解析的运行时性能问题。
bake_from_source_geometry_data_async() 是相同的,但烘焙用线程延迟的导航网格,而不是阻塞主线程。
与NavigationRegion相比,NavigationServer对导航网格烘焙过程提供了更精细的控制。反过来,它的使用更加复杂,但也提供了更高级的选项。
NavigationServer相对于NavigationRegion的其他一些优点是:
服务器可以在不烘焙的情况下解析源几何体,例如缓存以供以后使用。
服务器允许选择根节点,以便手动启动源几何体解析。
服务器可以接受程序生成的源几何图形数据并从中烘焙。
服务器可以按顺序烘焙多个导航网格,同时(重新)使用相同的源几何体数据。
若要使用NavigationServer烘焙导航网格,则需要源几何体。
源几何体是导航网格烘焙过程中应考虑的几何体数据。二维和三维导航网格都是通过从源几何体烘焙来创建的。
2D 和 3D 版本的源几何体资源分别为 NavigationMeshSourceGeometryData2D 和 NavigationMeshSourceGeometryData3D。
源几何体可以是从视觉网格、物理碰撞或程序创建的数据阵列(如轮廓(2D)和三角面(3D))解析的几何体。
为方便起见,通常直接从场景树中的节点设置解析源几何体。对于运行时导航网格(重新)烘焙,请注意几何体解析始终发生在主线程上。
SceneTree不是线程安全的。从SceneTree解析源几何体只能在主线程上完成。
警告
需要从GPU接收来自视觉网格和多边形的数据,从而在此过程中停止RenderingServer。
对于运行时(重新)烘焙,更喜欢使用物理形状作为解析的源几何体。
源几何图形存储在资源中,因此创建的几何图形可以重复用于多次烘焙。
例如,为来自同一源几何体的不同代理大小烘焙多个导航网格。
这也允许将源几何体保存到磁盘,以便稍后加载,例如避免在运行时再次解析的开销。
几何数据通常应保持非常简单。需要尽可能多的边缘,但尽可能少。
尤其是在2D中,应避免重复和嵌套几何体,因为它会强制计算多边形孔,从而导致翻转多边形。
嵌套几何体的示例是完全放置在另一个StaticBody2D形状的边界内的较小StaticBody2D形状。
针对大世界烘焙导航网格块
为了避免不同区块之间的边缘不对齐,导航网格提供了两个重要的属性,用于控制烘焙过程:烘焙边界和边框大小。
两者配合使用就可以确保不同区块的边缘能够完美对齐。
烘焙边界在 2D 中是轴对齐的 Rect2、在 3D 中是 AABB,能够限制所使用的来源几何体,所有位于边界外的几何体都会被丢弃。
NavigationPolygon 的 baking_rect 和 baking_rect_offset 属性可以用来创建并放置 2D 的烘焙边界。
NavigationMesh 的 filter_baking_aabb 和 filter_baking_aabb_offset 属性可以用来创建并放置 3D 的烘焙边界。
只设置烘焙边界的话仍然会存在另一个问题:最终得到的导航网格会不可避免地受到 agent_radius 等必要的偏移量的影响,导致边缘无法正确对齐。
这个时候就需要用到导航网格的 border_size 属性了。边框大小是烘焙边界的内边距。边框大小的重要特征就是它不受 agent_radius 等大多数偏移和后期处理的影响。
边框大小并不会丢弃来源几何体,而是会丢弃烘焙后导航网格最终表面上的部分区域。如果烘焙边界足够大,边框大小就能够去除表面上有问题的部分区域,只保留预期的区块大小。
备注
烘焙边界需要设置得足够大,才能够包含来自所有相邻块的合理数量的来源几何体。
警告
在 3D 中,边框大小的功能仅限于 XZ 轴。
烘焙导航网格时的常见问题
- 运行时烘焙导航网格会造成帧率问题
默认情况下,导航网格烘焙是在后台线程上完成的,因此只要平台支持线程,实际烘焙就很少是任何性能问题的根源(假设运行时重新烘焙的几何体大小合理且复杂)。
运行时性能问题的常见来源是涉及节点和场景树的源几何体的解析步骤。SceneTree不是线程安全的,因此所有节点都需要在主线程上解析。一些具有大量数据的节点在运行时可能非常重且解析缓慢,例如,TileMap为每个使用的单元和TileMapLayer都有一个或多个多边形要解析。持有网格的节点需要从RenderingServer请求数据,从而在此过程中停止渲染。
为了提高性能,请使用更优化的形状,例如在详细的视觉网格上使用碰撞形状,并提前合并和简化尽可能多的几何体。如果没有任何帮助,请不要解析SceneTree并使用脚本添加源几何体过程。如果仅使用纯数据数组作为源几何体,则可以在后台线程上完成整个烘焙过程。 - Navigation mesh 在 2D 创建 unintended holes。
2D 中的导航网格烘焙是通过基于轮廓路径执行多边形剪裁操作来完成的。创建更加复杂的 2D 多边形时,多边形带有“孔洞”是必要之恶,不过对需要用到大量复杂形状的用户而言,可能会变得无法预测。
为避免多边形孔计算出现任何意外问题,请避免将任何轮廓嵌套在同一类型的其他轮廓内(可遍历/障碍物)。这包括来自节点的已解析形状。例如,将较小的StaticBody2D形状放置在较大的StaticBody2D形状内可能会导致所生成的多边形翻转。 - 导航网格显示在三维几何体内部。
3D中的导航网格烘焙没有“内部”的概念。用于光栅化几何体的体素单元被占用或未被占用。删除其他几何图形中位于地面上的几何图形。如果无法做到这一点,请在内部添加较小的“虚拟”几何体,并尽可能少地添加三角形,这样单元就会被一些东西占据。
烘焙导航网格时也可以用 NavigationObstacle3D 形状来丢弃几何体。
导航网格脚本模板
以下脚本使用NavigationServer解析场景树中的源几何体,烘焙导航网格,并使用更新的导航网格更新导航区域。
1 | extends Node2D # Node3D |
以下脚本使用 NavigationServer 来更新导航区块,导航区块中使用程序式生成的导航网格数据。
1 | extends Node2D |
使用 NavigationPath
导航路径可以直接从NavigationServer查询,并且不需要任何其他节点或对象,只要导航地图具有可使用的导航网格即可。
要获取 2D 路径,请使用 NavigationServer2D.map_get_path(地图, 起点, 终点, 优化, 导航层)。
要获取 3D 路径,请使用 NavigationServer3D.map_get_path(地图, 起点, 终点, 优化, 导航层)。
查询所需的参数之一是导航地图的RID。每个游戏世界都有一个自动创建的默认导航地图。默认导航地图可以使用 get_world_2d().get_navigation_map() 从任何Node2D继承节点检索,也可以使用 get_world_3d().get_navigation_map() 从任意Node3D继承节点检索。第二和第三参数是起始位置和目标位置,作为2D的Vector2或3D的Vector3。
如果 optimized 参数为 true,则会额外做一遍漏斗算法,将路径点沿着多边形的角落缩短。这种处理适用于在多边形大小不一致的导航网格上自由移动的情况,因为路径会沿着 A* 算法找到的多边形走廊绕过拐角。如果单元格较小,A* 算法就会创建出非常狭窄的漏斗形走廊,使用栅格时,路径的拐角处就会很难看。
如果 optimized 参数为 false ,则路径位置将放置在每个多边形边缘的中心。这适用于具有相同大小多边形的导航网格上的纯网格移动,因为路径将穿过网格单元的中心。在网格之外,由于多边形通常用一条长边覆盖大的开放区域,这可能会产生不必要的长迂回路径。
1 | extends Node2D |
NavigationServer返回的 path 将是2D的 PackedVector2Array 或3D的 PackedVector3Array。这些只是一个经过内存优化的矢量位置 Array。阵列内的所有位置矢量都保证位于NavigationPolygon或NavigationMesh内。如果路径数组不是空的,则其导航网格位置最靠近第一个索引 path[0] 位置处的起始位置。离目标位置最近的可用导航网格位置是最后一个索引 path[path.size()-1] 位置。之间的所有索引都是参与者在不离开导航网格的情况下到达目标所应遵循的路径点。
如果目标位置位于未合并或连接的不同导览网格上,则导览路径将通往起始位置导览网格上最接近的可能位置。
以下脚本透过使用 set_movement_target() 设定目标位置,使用预设导览地图沿着导览路径移动Node3D继承节点。
1 | @onready var default_3d_map_rid: RID = get_world_3d().get_navigation_map() |
使用 NavigationPathQueryObject
使用 NavigationAgent
使用 NavigationObstacle
使用 NavigationLink
使用 NavigationLayer
导航调试工具
连接导航网格
支持不同角色类型
支持不同角色运动
支持不同角色区域权限
网络
高级多人游戏
高层API vs 底层 API
Godot 始终支持通过 UDP、 TCP 和一些更高级别的协议(如 SSL 和 HTTP )进行标准的低级网络连接。
使用 Godot 的高级网络 API,牺牲了对低级网络的一些细度控制,有更强的易用性。中层抽象
Godot 使用了一个中间层级的 MultiplayerPeer 对象。
不应直接创建这种对象,它被设计为由多个 C++ 实现所提供。
这个对象扩展自 PacketPeer 类,继承了所有用于序列化、发送和接收数据的方法。
此外,该对象还添加了设置对等体、传输模式等方法。它还包括让你知道对等体何时连接或断开的信号。
这个类接口可以抽象出大多数类型的网络层、拓扑结构和库。
默认情况下,Godot 会提供一个基于 ENet 的实现(ENetMultiplayerPeer)、一个基于 WebRTC 的实现(WebRTCMultiplayerPeer)以及一个基于WebSocket的实现(WebSocketPeer),而该类接口可以用来实现移动 API(用于特设的 WiFi、蓝牙等)或自定义设备/控制台中特定的网络 API。
但大多数常见情况下,不鼓励直接使用这个对象,因为 Godot 提供了更高级别的网络使用方法。
只有当游戏对较低级别的API有特殊需求的情况下,才使用该对象。
- 服务器托管的注意事项
托管服务器时,LAN 上的客户端可以使用内网 IP 地址进行连接,该地址的格式通常是 192.168..。 非 LAN/Internet 客户端无法访问此内部 IP 地址。
在 Windows 中, 你可以在命令提示符中输入 ipconfig 命令, 在 macOS 中,你可以在终端中输入 ifconfig 命令,在 Linux 中,你可以在终端中输入 ip addr 命令,来找到你的内网 IP 地址。
如果你在自己的机器上托管了服务器,并且想让非内网客户端连接,那么你可能需要将服务器端口 转发 到你的路由器,由于大多数家用网络都使用 NAT 技术,因此转发服务器端口是让你的服务器能通过互联网访问的必经步骤。Godot 的高级多人 API 只使用 UDP 协议,所以你的端口转发也必须是 UDP 协议的端口,不能只转发 TCP 协议的端口。
在转发了 UDP 端口之后,你需要确保你的服务器使用这个端口。可以前往这个网站 https://icanhazip.com/去查询你的公网 IP 地址,然后把这个公网 IP 地址发送给想联机到你服务器的互联网客户端即可。
Godot 的高级多人联机 API 使用的是一个修改过的 ENet,包含全 IPv6 支持。
网络初始化
Godot 中的高级网络由 SceneTree 管理。
每个节点都有一个 multiplayer 属性,它是对场景树为其配置的 MultiplayerAPI 实例的引用。
每个节点在初始化时都会配有相同预设的 MultiplayerAPI 物件。
可以创建一个新的 MultiplayerAPI 对象,并将其分配给场景树中的 NodePath,这将覆盖该路径上的节点及其所有后代的多人游戏 。这允许将兄弟节点配置为不同的对等节点,从而可以在 Godot 的一个实例中同时运行服务器和客户端。
1 | # By default, these expressions are interchangeable. |
要想初始化网络, 你必须先创建一个 MultiplayerPeer 对象,将其初始化为服务器或客户端,然后将其传给 MultiplayerAPI。
1 | # Create client. |
可以通过下述方法来停止联网功能:
1 | multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() |
警告
导出到 Android 时,确保在 Android 导出预设中启用 INTERNET 权限。否则,Android 系统会阻止该程序任何形式的网络通信。
管理连接
系统会给每个对等体都分配一个唯一 ID(UID),服务器的 ID 永远为 1,客户端的 ID 则会被分配给一个随机的正整数。
可以通过连接到 MultiplayerAPI 的信号来响应连接或断开连接:
- peer_connected(id: int) 此信号在每个其他对等体上与新连接的对等体 ID 一起发出,并在新对等点上多次发出,其中一次与每个其他对等点ID一起发出。
- peer_disconnected(id:int) 当一个对等体断开连接时,剩余的每个对等体都会发出此信号。
以下信号仅在客户端上发送: - connected_to_server()
- connection_failed()
- server_disconnected()
通过下述方法来取得关联到对等体的UID:multiplayer.get_unique_id()
通过下述方法来对等体是服务器还是客户端:multiplayer.is_server()
远程过程调用
远程过程调用(RPC)是可以在其他对等方上调用的函数。
要创建一个 RPC,请在函数定义之前使用 @rpc 注解。
若要调用 RPC,请在每个对等体中通过 Callable 的 rpc() 方法调用之,或使用 rpc_id() 在特定对等方中调用之。
1 | func _ready(): |
警告
RPC 既不会序列化对象,也不会序列化可调用体。
要使远程调用成功,发送方节点和接收方节点需要具有相同的 NodePath ,也就是说,这些节点必须具有相同的节点名称。
对预期使用 RPC 的节点调用 add_child() 时,请将参数 force_readable_name 设置为 true。
如果在客户端脚本(或服务器脚本)上用 @rpc 注释函数,则该函数也必须在服务器脚本(或客户端脚本)上声明。两个 RPC 必须具有相同的签名,该签名使用所有 RPC 的校验和进行评估。脚本中的所有 RPC 都会被一次检查,并且必须在客户端脚本和服务器脚本上声明所有 RPC, 即使是当前未使用的函数。
RPC 的签名包括 @rpc() 声明、函数、返回类型、 和 NodePath。如果 RPC 驻留在附加到 /root/Main/Node1 的脚本中,则它必须驻留在客户端脚本和服务器脚本上完全相同的路径和节点中。不检查函数参数在服务器和客户端代码之间的匹配(例如:func sendstuff(): 和 func sendstuff(arg1, arg2): 将传递签名匹配)。
如果不满足这些条件(即如果所有RPC都没有通过签名匹配),脚本则可能会打印错误,错误消息可能与你当前正在构建和测试的 RPC 函数无关;也可能会导致非预期行为的发生。
请参阅本帖的进一步解释和故障排除: 点我前往.
@rpc 注解可以采用多个参数,这些参数具有预设值,相当于:@rpc("authority", "call_remote", "unreliable", 0)
其参数及作用如下:
mode: 模式 :
- “authority”:只有多人游戏权限才能远程调用。 默认情况下,权限是服务器,但可以使用 Node.set_multiplayer_authority。
- “any_peer” :也允许客户端进行远程调用该函数,用于传输用户输入。
sync : 同步 : - “call_remote”: 让该函数不会在本地对等体上调用。
- “call_local”:让该函数也可以在本地对等体上调用,在服务器也是玩家时非常有用。
transfer_mode : - “unreliable” 数据包不被确认,可能丢失,并且可以按任意顺序到达接收方。
- “unreliable_ordered” 数据包按照发送的顺序接收,透过忽略迟达的数据包(如果已经收到在这些数据包之后发送的另一个数据包)来实现的。使用不当可能会导致丢包。
- “reliable” 发送重新传送尝试,直到数据包被确认为止,且这些数据包的顺序会被保留。具有明显的性能损失。
transfer_channel 是信道索引。
前3个参数在注解中的顺序任意,但 transfer_channel 参数必须始终位于注解中的最后。
在 RPC 所调用的函数中,可用函数 multiplayer.get_remote_sender_id() 来获取 RPC 发送方对等体的 UID。
1 | func _on_some_input(): # Connected to some input. |
信道
现代网络协定支持信道系统。信道是网络连接内的单独连接,允许多个数据包流互不干扰。
像是游戏聊天相关信息和一些核心游戏信息等都应该可靠地发送,但游戏信息不应等待聊天信息被确认后在发送,这一点可以通过使用不同的信道来实现。
当与不可靠的有序传输模式一起使用时,信道也十分有用。
使用此传输模式发送可变大小的数据包可能会导致丢包,因为迟达的数据包将会被接收方忽略。
通过使用信道,将它们拆分成多个同质数据包流,可以实现有序传输,且丢包很少,不会因可靠模式而导致延迟损失。
索引为 0 的默认信道实际上是三个不同的信道——每个传输模式一个。
大厅实现示例
下面为一个示例大厅,可以处理对等体的加入和离开,通过信号来通知UI场景,并在所有客户端加载游戏场景后启动游戏。
1 | extends Node |
游戏场景的根节点应命名为 Game,在其所附加的脚本中:
1 | extends Node3D # Or Node2D. |
为专用服务器导出
一旦你制作好了一款多人游戏,你可能会想将其导出到一个没有 GPU 的专用服务器上运行,对此可参见 为专用服务器导出 来获取更多信息。
该页面上的范例代码并不是为了在专用服务器上运行而设计的,你必须修改这些代码来让避免系统将服务器误认为玩家,此外,你还必须修改游戏的启动机制,让第一个加入的玩家可以自行启动游戏。
进行 HTTP 请求
HTTP 可以用在游戏的登录系统、大厅浏览器,可以从 Web 获取信息,也可以下载游戏资产。
在Godot中, 用 HTTPRequest 节点发出HTTP请求是最简单的方法. 它继承自更低级别的 HTTPClient .
导出到 Android 时,在导出项目或使用一键部署之前,请确保在 Android 导出预设中启用 Internet 权限。否则,任何类型的网络通信都将被 Android 操作系统阻止。
示例(向GitHub发出HTTP请求以检索最新Godot版本的名称)
- 添加一个根节点 Node 并向其添加一个脚本。然后添加一个 HTTPRequest 节点作为子节点。
1 | extends Node |
你必须等待一个请求完成后才能发送另一个请求。一次发出多个请求需要每个请求都有一个节点。
一种常见的策略是在运行时根据需要创建和移除 HTTPRequest 节点。
向服务器发送数据
1 | var json = JSON.stringify(data_to_send) |
设置自定义 HTTP 报头
要设置自定义用户代理(HTTP User-Agent 标头),你可以使用以下代码:
1 | $HTTPRequest.request("https://api.github.com/repos/godotengine/godot/releases/latest", ["User-Agent: YourCustomUserAgent"]) |
危险
请注意,有人可能会分析和反编译你发布的应用程序,从而可能获得任何嵌入的授权信息,如令牌、用户名或密码。
这意味着在你的游戏中,嵌入诸如数据库访问凭据之类的信息,通常不是一个好主意。尽可能避免提供对攻击者有用的信息。
HTTP 客户端类
HTTPClient 提供了对HTTP通信的低层次访问.
对于一个更高级的接口, 你可能需要看看 HTTPRequest.
这是使用 HTTPClient 类的示例. 它只是一个脚本, 因此它可以通过执行以下命令来运行:c:\godot> godot -s http_test.gd
它将连接并获取一个网站.
1 | extends SceneTree |
TLS/SSL 证书
通常希望使用 TLS 连接(也称为 SSL 连接)进行通信 以避免“中间人”攻击。
Godot 有一个连接包装器 StreamPeerTLS 可以采用常规连接并增加其安全性。
HTTPClient 和 HTTPRequest 类也支持使用相同包装器的 HTTPS。
Godot 会尝试使用操作系统提供的 TLS 证书捆绑包,但同时也包含了 Mozilla 的 TLS 证书捆绑包以备不时之需。
你也可以在“项目设置”中强制使用自己的证书包:
设置后,默认情况下,此文件将覆盖作系统提供的捆绑包。 此文件应包含任意数量的公共证书 PEM 格式。
获取证书有两种方法:
- 从证书颁发机构获取证书
- 生成自签名证书
创建自签名证书的方法是:生成一对私钥和公钥,然后将(PEM 格式的)公钥添加到“项目设置”中指定的 CRT 文件中。
出于开发目的,Godot 可以通过以下方式生成自签名证书 Crypto.generate_self_signed_certificate。
或者,OpenSSL 有一些关于生成密钥的文档 和证书 。
使用 WebSockets
WebSocket 是通过 WebSocketPeer 在 Godot 中实现的。
WebSocket 实现与高级多人游戏兼容。有关更多详细信息,请参阅有关高级多人游戏的部分。
最小客户端示例
这个示例演示的是如何建立与远程服务器的 WebSocket 连接,以及如何收发数据。
1 | extends Node |
最小服务器示例
这个例子将告诉你如何创建一个监听远程连接的WebSocket服务器,以及如何发送和接收数据。
1 | extends Node |
WebRTC
Godot的一大特点是它能够导出到HTML5/WebAssembly平台, 当用户访问你的网页时, 你的游戏可以直接在浏览器中运行.
这对于演示和完整的游戏来说都是一个很好的机会, 但过去有一些限制, 在网络领域, 浏览器过去只支持HTTPRequests, 直到最近, 首先是WebSocket, 然后是WebRTC被提出为标准.
WebSocket
WebSocket 协议在 2011 年 12 月被标准化,它允许浏览器与 WebSocket 服务器建立稳定的双向连接。该协议是向浏览器发送推送通知的强大工具,已用于实现聊天、回合制游戏等。
不过,WebSockets仍然使用TCP连接, 这对可靠性有好处, 但对减少延迟没有好处, 所以不适合实时应用, 比如VoIP和快节奏的游戏.
WebRTC
为此, 从2010年开始, 谷歌开始研究一项名为WebRTC的新技术, 后来在2017年, 这项技术成为W3C候选推荐.
WebRTC是一套复杂的集合规范, 并且在后台依靠许多其他技术(ICE, DTLS, SDP)来提供两个对等体之间快速, 实时, 安全的通信.
其想法是找到两个对等体之间最快的路线, 并尽可能建立直接通信(尽量避开中继服务器).
然而, 这是有代价的, 那就是在通信开始之前, 两个对等体之间必须交换一些媒介信息(以会话描述协议–SDP字符串的形式). 这通常采取所谓的WebRTC信号服务器的形式.
对等体连接到信号服务器(例如 WebSocket 服务器)并发送其媒介信息.
然后, 服务器将此信息转发到其他对等体, 允许它们建立所需的直接通信.
这一步完成后, 对等体可以断开与信号服务器的连接, 并保持直接的点对点(P2P)连接打开状态.
在 Godot 中,WebRTC 的实现是通过两个主要类 WebRTCPeerConnection 和 WebRTCDataChannel,加上多人游戏 API 实现 WebRTCMultiplayerPeer。
有关更多详细信息,请参阅《高级多人游戏》章节。
备注
这些类在 HTML5 中自动可用,但在原生(非 HTML5)平台上需要外部 GDExtension 插件。查看 webrtc-native 插件存储库,以获取说明并获取最新发布。
最小连接示例
这个例子将向你展示如何在同一应用程序中的两个对等体之间创建WebRTC连接. 这在现实场景中并不是很有用, 但会让你对如何设置WebRTC连接有一个很好的概览.
1 | extends Node |
本地信号示例
这个例子在上一个例子的基础上进行了扩展, 将对等体分离在两个不同的场景中, 并使用 singleton 作为信号服务器.
1 | extends Node |
现在是本地信号服务器:
这个本地信号服务器应该是作为一个 singleton 来连接同一场景中的两个对等体.
1 | # A local signaling server. Add this to autoloads with name "Signaling" (/root/Signaling) |
然后, 你可以这样使用它:
1 | # Main scene (main.gd) |
性能
常规
要达到最佳效果, 我们有两种方法:
- 工作更快
- 工作更智能
我们最好将两者混合使用
测量性能
对于优化来说, 最重要的工具可能是衡量性能的能力–找出瓶颈所在, 并衡量我们突破瓶颈的尝试是否成功.
有几种衡量性能的方法, 包括:
- 在感兴趣的代码周围放置一个开启/停止的计时器.
- 使用 Godot 分析器。
- 使用 外部 CPU 分析器。
- 使用外部 GPU 分析器/调试器,例如 NVIDIA Nsight 显卡 , Radeon GPU 分析器 , PIX(仅限 Direct3D 12)、 Xcode(仅限 Metal),或 Arm 性能工作室 。
- 检查帧速率(禁用垂直同步)。第三方实用程序,例如 RivaTuner 统计服务器 (Windows), Special K(Windows)或 MangoHud (Linux) 在这里也很有用。
- 使用一个非官方的
调试菜单附加组件 <https://github.com/godot-extended-libraries/godot-debug-menu>.
在一个以上的设备上测量计时通常是个好主意. 如果你的目标是移动设备, 情况尤其如此.
限制
CPU分析器通常是测量性能的常用方法. 然而, 它们并不总是能反映全部情况.
瓶颈往往在GPU上,”由于”CPU给出的指令.
由于在Godot中使用的指令(例如, 动态内存分配)”导致” 操作系统进程(在Godot之外)可能出现巅峰.
由于需要进行初始设置, 你可能并不总是能够对特定设备进行配置, 例如手机.
你可能需要解决你无法访问的硬件上出现的性能问题.
由于这些限制, 你经常需要使用侦测工作来找出瓶颈所在.
侦查工作
侦测工作对于开发人员来说是一项至关重要的技能(无论是在性能方面, 还是在错误修复方面).
这可以包括假设测试和二进制搜索.
- 假设检验
- 精灵
- 二分查找
- 帧
分析器
分析器允许你在运行程序时对其进行计时.
分析器提供结果, 告诉你在不同的功能和区域所花费的时间百分比, 以及功能被调用的频率
这对于确定瓶颈和衡量改进的结果都非常有用.
有时, 改善性能的尝试可能会适得其反, 导致性能变慢. 始终使用分析器和时长来指导你的工作
原则
开发者时间有限. 与其盲目地试图加快一个程序的所有方面, 应该集中精力在真正重要的方面.
虽然过早的优化是不可取的, 但高性能的软件是高性能设计的结果.
高性能的设计
鼓励人们在必要时再考虑优化的危险在于,它很容易忽略了考虑性能的最重要时间是在设计阶段,甚至在按下键盘按键之前。如果一个程序的设计或算法是低效的,那么以后再多的细节修饰也不会使它运行得很快。它可能运行得更快,但永远不会像一个以性能为设计目标的程序那样快。
这在游戏或图形编程中往往比在一般编程中更为重要. 一个高性能的设计, 即使没有低水平的优化, 通常也会比一个低水平优化的平庸设计快很多倍.
渐进式设计
当然, 在实践中, 除非你事先有知识, 否则你不可能在第一次就拿出最好的设计. 相反, 你往往会对某一特定区域的代码做出一系列版本, 每一个版本都采取不同的方法来解决这个问题, 直到你得出一个满意的解决方案. 重要的是, 在你最终确定整体设计之前, 在这个阶段不要在细节上花费太多时间. 否则, 你的很多工作都会被淘汰.
很难给出高性能设计的一般规范,因为这与问题本身有很大关系。不过有一点值得一提,在 CPU 方面,现代 CPU 几乎总是受到内存带宽的限制。这导致了面向数据的设计的重新兴起,涉及到围绕数据的缓存本地性(cache locality)和线性访问进行数据结构和算法的设计,避免在内存中进行跳转。
优化过程
假设我们有一个合理的设计, 优化的第一步应该是找出最大的瓶颈–最慢的功能, 可轻松实现的目标.
一旦我们成功地提高了最慢区域的速度, 它可能就不再是瓶颈了. 因此, 我们应该再次进行测试/分析, 找到下一个需要关注的瓶颈.
因此, 该过程是:
分析和确定瓶颈.
优化瓶颈.
返回步骤1.
优化瓶颈
有些分析器甚至会告诉你一个函数的哪个部分在减慢速度(哪些数据访问, 计算).
与设计一样,您应该首先集中精力确保算法和数据结构达到最佳状态。数据访问应该是本地的(以充分利用 CPU 缓存),并且使用紧凑的数据存储通常会更好(同样,始终分析以测试结果)。通常,您会提前预先计算繁重的计算。这可以通过在加载关卡时执行计算、加载包含预计算数据的文件或将复杂计算的结果存储到脚本常量中并读取其值来完成。
如果确认算法和数据没有问题,你通常可以在例程中做一些小的改变来提高性能。例如,可以将一些计算移到循环之外,或者将嵌套的 for 循环转化为非嵌套的循环。(如果你事先知道 2D 数组的宽和高,应该就是可行的。)
每次更改后, 一定要重新测试你的时长和瓶颈. 有些改变会提高速度, 有些则可能会产生负面效果. 有时, 一个小的积极效果会被更复杂的代码的负面效果所抵消, 可以选择不做这种优化.
利用服务器进行优化
服务器
Godot 有许多非常有趣的设计决定,其中之一就是整个场景系统都是可选的。
虽然目前还不能在编译时去除,但你完全可以绕过它。
Godot 在核心中使用了“服务器”的概念。
它们是非常底层的 API,用来控制渲染、物理、声音等。
场景系统建立在它们之上,直接使用它们。最常见的服务器有:
- RenderingServer: 处理所有与图形相关的内容。
- PhysicsServer3D: 处理所有与 3D 物理相关的内容。
- PhysicsServer2D: 处理所有与 2D 物理相关的内容。
- AudioServer: 处理与音频相关的一切.
你只需研究它们的 API 就会意识到,它们所提供的函数全部都是 Godot 允许你所进行的操作的底层实现。
RID
使用服务器的关键是理解资源 ID(Resource ID,即 RID)对象。
这些对象是服务器实现的非公开的句柄。它们是手动分配和释放的。
几乎服务器中的每个功能都需要 RID 来访问实际的资源。
大多数 Godot 节点和资源都包含这些来自服务内部的 RID,它们可以通过不同的函数获得。
事实上,任何继承 Resource 的东西都可以直接转换成 RID。
不过并不是所有资源都包含 RID:在这种情况下,RID 为空。可以用 RID 的形式将资源传递给服务器 API。
警告
资源是引用计数的(见 RefCounted),对资源 RID 的引用在确定资源仍在使用时不进行计数。请确保在服务器外部保持对资源的引用,否则将删除资源及其 RID。
对于节点来说, 有很多函数功能可以使用:
- 对于 CanvasItem,CanvasItem.get_canvas_item() 方法将在服务器中返回该画布项目的 RID。
- 对于CanvasLayer来说, CanvasLayer.get_canvas() 方法将返回服务器中的canvas RID.
- 对于视口, Viewport.get_viewport_rid() 方法将返回服务器中的视口RID.
- 对于 3D,World3D 资源(可在 Viewport 和 Node3D 节点中获取)包含获取 RenderingServer 场景和 PhysicsServer 空间的函数。这允许直接使用服务器 API 创建 3D 对象并使用它们。
- 对于 2D,World2D 资源(可在 Viewport 和 CanvasItem 节点中获取)包含获取 RenderingServer 画布和 Physics2DServer 空间的函数。这允许直接使用服务器 API 创建 2D 对象并使用它们。
- VisualInstance3D 类允许分别通过 VisualInstance3D.get_instance() 和 VisualInstance3D.get_base() 获取场景 instance 和 instance base。
请尝试探索你所熟悉的节点和资源,找到获得服务器 RID 的功能。
不建议控制已经有节点关联的对象的 RID。服务器函数应始终用于创建和控制新的对象、与现有对象进行交互。
创建精灵
这是一个如何从代码创建精灵并使用低级 CanvasItem API 移动它的示例。
1 | extends Node2D |
服务器中的 Canvas Item API 允许你向其添加绘制图元。一旦添加,它们就不能被修改。需要清除 Item,并重新添加图元(设置变换时则不然,变换可根据需要多次进行)。
图元的清除方式为:
1 | RenderingServer.canvas_item_clear(ci_rid) |
将网格实例化到 3D 空间
3D API 与 2D API 不同,所以必须使用实例化 API。
1 | extends Node3D |
创建 2D 刚体并使用它移动精灵
这段代码使用 PhysicsServer2D API 创建了一个 RigidBody2D,并在该物体移动时移动 CanvasItem。
1 | # Physics2DServer expects references to be kept around. |
3D 版本应该非常相似,因为 2D 和 3D 物理服务器是相同的(分别使用 RigidBody3D 和 PhysicsServer3D)。
从服务器获取数据
除非你知道自己在做什么,否则请尽量永远不要通过调用函数从 RenderingServer、PhysicsServer2D 或 PhysicsServer3D 请求任何信息。这些服务器通常会异步运行以提高性能,调用任何返回值的函数都会使它们停滞,并迫使它们处理任何待处理的内容,直到实际调用该函数。如果你每帧都调用它们,这将严重降低性能(而且原因并不明显)。
正因为如此, 这类服务器中的大部分API都被设计成连信息都无法请求回来, 直到这是可以保存的实际数据.
CPU
对于 CPU 来说,找出瓶颈的最简单方法就是使用性能剖析器。
CPU 分析器
剖析器与你的程序一起运行, 并进行时间测量, 以计算出每个功能所花费的时间比例.
Godot集成开发环境有一个方便的内置剖析器. 它不会在每次启动项目时运行: 必须手动启动和停止. 这是因为, 与大多数剖析器一样, 记录这些时序测量会大大减慢你的项目速度.
剖析后, 你可以回看一帧的结果.
备注
我们可以看到物理, 音频等内置流程的消耗, 也可以在底部看到自己脚本功能的消耗.
等待各种内置服务器的时间可能不会被计算在剖析器中. 这是一个已知的错误.
外部分析器
虽然Godot IDE剖析器非常方便有用, 但有时你需要更强大的功能, 以及对Godot引擎源代码本身进行剖析的能力.
你可以 使用若干个第三方 C++ 分析器 来实现。
手动计时函数
另一个方便的技术, 特别是当你使用分析器确定了瓶颈后, 就是手动为功能或被测区域计时. 具体细节因语言而异, 但在GDScript中, 你可以做如下操作:
1 | var time_start = Time.get_ticks_usec() |
当手动为函数计时时, 通常最好是多次(1000次或更多次)运行该函数, 而不是只运行一次(除非是非常慢的函数). 这样做的原因是, 定时器的精度往往有限. 此外,CPU会以一种无序的方式调度进程. 因此, 一系列运行的平均值比单次测量更准确.
当你尝试优化功能时, 一定要反复对它们进行剖析或计时. 这将为你提供关键的反馈, 说明优化是否有效(或无效).
缓存
CPU缓存是另外一个需要特别注意的东西, 特别是在比较一个函数的两个不同版本的时序结果时. 其结果可能高度依赖于数据是否在CPU缓存中.CPU不会直接从系统RAM中加载数据, 尽管它与CPU缓存相比非常巨大(几千兆字节而不是几兆字节). 这是因为系统RAM的访问速度非常慢. 相反,CPU从一个较小, 较快的内存库中加载数据, 称为cache. 从缓存中加载数据的速度非常快, 但每次你试图加载一个没有存储在缓存中的内存地址时, 缓存必须前往主内存并缓慢地加载一些数据. 这种延迟会导致CPU长时间闲置, 被称为 “cache miss”.
这意味着, 第一次运行一个函数时, 由于数据不在CPU缓存中, 它可能运行得很慢. 第二次和以后的时间, 可能运行得更快, 因为数据在缓存中. 由于这个原因, 在计时时一定要使用平均数, 并且要注意缓存的影响.
了解缓存对于CPU优化也是至关重要的. 如果你有一个算法(例程), 从主内存随机分布的区域加载小数据位, 这可能会导致大量的缓存失误, 很多时候,CPU会在附近等待数据, 而不是做别的工作. 相反, 如果你能使你的数据访问本地化, 或者更好的是以线性方式访问内存(像一个连续的列表), 那么缓存将以最佳方式工作,CPU将能够尽可能快地工作.
Godot 通常会为你处理这些底层细节。例如,服务器 API 会确保数据已经针对渲染和物理等方面的缓存进行了优化。不过在编写 GDExtensions 时,你还是需要格外留意缓存问题。
线程
在进行大量的计算时, 考虑使用线程, 这些计算可以相互并行运行. 现代CPU有多个核心, 每个核心能做的工作量有限. 通过将工作分散在多个线程上, 你可以进一步向CPU的峰值效率迈进.
线程的缺点是,你必须非常小心。由于每个 CPU 核心都是独立运行的,它们最终可能会在同一时间试图访问相同的内存。一个线程可以在另一个线程在写入变量的时候读取该变量:这被称为竞态条件。在你使用线程之前,请确保你了解这些危险以及如何尝试和防止这些竞态条件。线程会使调试变得更加困难。
SceneTree
在 Godot 渲染器中,每个节点都是单独处理的。因此,减少节点的数量、让每个节点多做一些工作,可以获得更好的性能。
SceneTree 比较奇怪的一点是:你有时可以通过从 SceneTree 中删除节点,而非暂停或隐藏节点这种方式来获得更好的性能,不一定要删除一个从场景树中分离出来的的节点。例如,你可以保留一个节点的引用,使用 Node.remove_child(node) 将该节点从场景树中分离出来,然后使用 Node.add_child(node) 将其重新加回场景树。对于在游戏中添加和删除区域,这一点十分有用。
你可以通过使用服务器API来完全避免使用 SceneTree。
物理
以下是一些加速物理的技巧:
- 尝试使用渲染简单的几何图形来处理碰撞形状,虽然在通常情况下对终端用户来说这一点并不明显,但可以大大提高性能。
- 尝试禁用不在视野中/在当前区域之外的物理物体的物理效果,在视野中/在当前区域之内时则给这些物理对象启用物理效果(例如,你允许每个区域有8个怪物,并允许重新启用这些怪物的物理效果)。
物理的另一个关键方面是物理刻速率。在一些游戏中,你可以大大降低物理刻率,比如说,你可以不用每秒更新物理 60 次,而只需每秒更新 30 次甚至 20 次。这样可以大大降低 CPU 的负载。
改变物理刻速率的缺点是,当物理更新速率与每秒渲染的帧数不匹配时,可能会出现抖动。另外,降低物理刻速率会增加输入延迟。建议在大多数以玩家实时移动为特色的游戏中,坚持使用默认的物理刻速率(60 Hz)。
抖动的解决方案是使用固定时间步长插值, 其中涉及 平滑多个帧的渲染位置和旋转,以匹配 物理。Godot 具有内置的物理插值,您可以阅读有关 这里 。在性能方面,与运行物理滴答相比,插值是一个非常便宜的作。它的速度快了几个数量级,因此这可以显着提高性能,同时还可以减少抖动。
GPU
了解和调查GPU瓶颈与CPU上的情况略有不同. 这是因为, 通常情况下, 你只能通过改变你给GPU的指令来间接改变性能. 另外, 测量起来可能更困难. 在许多情况下, 衡量性能的唯一方法是通过检查每帧渲染时间的变化.
绘制调用、状态更变、API
Godot 通过图形 API(Vulkan、OpenGL、OpenGL ES 或 WebGL)向 GPU 发送指令。
期间涉及到的通信和驱动流程可能存在相当大的开销,使用 OpenGL、OpenGL ES 和 WebGL 时尤为明显。
如果我们能以驱动和 GPU 喜欢的方式提供这些指令,就可以大大提升性能。
OpenGL中几乎每一个API命令都需要一定的验证, 以确保GPU处于正确的状态.
即使是看似简单的命令, 也会导致一连串的幕后工作.
因此, 我们的目标是将这些指令减少到最低限度, 并尽可能地将相似的对象分组, 以便它们可以一起渲染, 或者以最少的数量进行这些昂贵的状态变化.
2D
在 2D 中,单独处理每个项目的成本可能高得令人望而却步——屏幕上很容易有数千个项目。这就是 2D 批处理的原因 被使用。将多个相似项分组 一起并通过单个绘制调用批量渲染,而不是进行 每个项目单独的抽奖调用。此外,这意味着状态变化, 材料和纹理的变化可以保持在最低限度。
3D
在 3D 中,我们的目标仍然是尽量减少绘制调用和状态更改。但是,将多个对象批量合并到单个绘制调用中可能会更加困难。3D 网格往往包含数百或数千个三角形,而实时组合大型网格的成本非常高。随着每个网格的三角形数量增加,加入它们的成本很快就会超过任何收益。一个更好的选择是提前加入网格(彼此相关的静态网格)。这可以由设计师完成,也可以使用插件在 Godot 中以编程方式完成。
在 3D 中将对象批处理在一起也是有成本的。渲染为一个的多个对象不能单独剔除。如果将屏幕外的整个城市连接到屏幕上的一片草叶,则仍将被渲染。因此,在尝试将 3D 对象批处理在一起时,应始终考虑对象的位置和剔除。尽管如此,连接静态对象的好处通常超过其他考虑因素,特别是对于大量远处或低多边形对象。
复用着色器和材质
Godot 渲染器和其它的渲染器不同,是以尽量减少 GPU 状态更改为目标的。StandardMaterial3D 可以在所需着色器相似时很好地复用材质。如果是用自定义着色器,那么请尽量进行复用。Godot 的优先级是:
复用材质:场景中不同的材质越少, 渲染的速度就越快. 如果一个场景有大量的物体(数以百计或数以千计), 可以尝试重复使用这些材质. 在最坏的情况下, 使用图集来减少纹理变化的数量.
复用着色器:如果材质无法复用,至少要尝试复用着色器。注意:即使 StandardMaterial3D 具有不同的参数,共享相同配置(可用勾选框启用或禁用该功能)的 StandardMaterial3D 之间也会自动复用着色器。
例如,如果一个场景中有 20,000 个物体,每个物体有 20,000 种不同的材质,渲染就会很慢。如果同一个场景中有 20,000 个物体,但只使用 100 种材质,渲染就会快得多。
像素成本与顶点成本
你可能听说过, 一个模型中的多边形数量越少, 它的渲染速度就越快. 这其实是 相对的 , 取决于许多因素.
在现代PC和控制台, 顶点成本很低.GPU最初只渲染三角形. 这意味着每一帧:
所有顶点都必须由 CPU 进行转换(包括剪裁)。
所有顶点都必须从主 RAM 发送到 GPU 内存。
如今,所有这些都在 GPU 内部处理,大大提高了性能。3D 设计师通常对多边形计数性能有错误的感觉,因为 3D 建模软件(如 Blender、3ds Max 等)需要将几何图形保存在 CPU 内存中才能进行编辑,从而降低了实际性能。游戏引擎更多地依赖 GPU,因此它们可以更高效地渲染许多三角形。
在移动设备上, 情况则不同. 个人电脑和控制台的GPU是粗暴的怪物, 可以从电网中获取所需的电力. 移动GPU被限制在一个很小的电池里, 所以它们需要更高的功率效率.
为了提高工作效率, 移动GPU试图避免 overdraw . 当屏幕上的同一个像素被渲染了不止一次时, 就会出现Overdraw. 想象一下, 一个有几座建筑的小镇. 在绘制之前,GPU不知道哪些是可见的, 哪些是隐藏的. 例如, 一栋房子可能被画出来, 然后在它前面又画了一栋房子(这意味着同一像素的渲染发生了两次).PC GPU通常不怎么关心这个问题, 只是把更多的像素处理扔给硬件以提高性能(这也会增加功耗).
在移动设备上使用更多的电力是不可能的,所以移动设备使用一种叫做基于图块的渲染的技术,将屏幕划分为一个网格。每个单元格都保存着绘制的三角形列表,并按深度进行排序,以尽量减少过度绘制。这种技术提高了性能,降低了功耗,但对顶点性能造成了影响。因此,可以处理更少的顶点和三角形进行绘制。
此外,当屏幕的一小部分区域内存在包含大量几何体的小物体时,基于分块的渲染会遇到困难。这会迫使移动 GPU 对单个屏幕单元施加很大的压力,从而显著降低性能,因为所有其他单元必须等待该单元完成后才能显示当前帧。
总而言之, 在移动端不要担心顶点数量, 但 避免顶点集中在屏幕的一小部分 . 如果一个角色, NPC, 车辆等离得很远(这意味着它看起来很小), 就使用一个较小的细节级别模型(LOD). 即使在桌面GPU上, 最好也不要让三角形小于屏幕上一个像素的大小.
使用时要注意额外的顶点处理:
蒙皮(骨骼动画)
变形(形态键)
Vertex-lit objects (common on mobile)
顶点光照对象(在移动设备上常见)
像素/片段着色器和填充速率
与顶点处理相比,片段(每像素)着色的成本历年来急剧增加。屏幕分辨率已经提高:4K 屏幕的面积为 8,294,400 像素,而旧式 640×480 VGA 屏幕的面积为 307,200 像素。面积是 27 倍!此外,片段着色器的复杂性也呈爆炸式增长。基于物理的渲染需要对每个片段进行复杂的计算。
你可以很容易地测试一个项目是否受到填充率限制. 关闭V-Sync以防止每秒帧数的上限, 然后比较使用大窗口运行时的每秒帧数和使用非常小的窗口运行时的帧数. 如果使用阴影, 你也可以从同样减少阴影贴图大小中获益. 通常, 你会发现使用小窗口的FPS会增加不少, 这说明你在某种程度上受到了填充率的限制. 另一方面, 如果FPS几乎没有增加, 那么你的瓶颈就在其他地方.
您可以通过减少 GPU 必须执行的工作量来提高填充率受限项目的性能。为此,你可以简化着色器(如果你使用的是 StandardMaterial3D,则可能会关闭昂贵的选项),或者减少所用纹理的数量和大小。此外,当使用非着色粒子时,请考虑在其材质中强制顶点着色,以降低着色成本。
在支持的硬件上,可以使用 可变速率着色 降低着色过程的损耗,并且不影响最终图片边缘的锐度。
在针对移动设备时, 考虑使用你能合理负担得起的最简单的着色器.
读取纹理
片段着色器的另一个因素是读取纹理的成本。读取纹理是一项昂贵的操作,尤其是在一个片段着色器中从多个纹理中读取时。另外,考虑到过滤可能会进一步减慢它的速度(mipmap 之间的三线性过滤,以及平均)。读取纹理在功耗方面也很昂贵,这在手机上是个大问题。
如果你使用第三方着色器或编写自己的着色器, 请尽量使用需要尽可能少的纹理读取的算法.
纹理压缩
默认情况下,Godot在导入3D模型时使用视频RAM(VRAM)压缩来压缩纹理. 视频RAM压缩在存储时不如PNG或JPG有效, 但在绘制足够大的纹理时, 会极大地提高性能.
这是因为纹理压缩的主要目标是在内存和GPU之间减少带宽.
在3D中, 物体的形状更多地取决于几何体而不是纹理, 所以压缩一般不明显. 在2D中, 压缩更多的是取决于纹理内部的形状, 所以2D压缩产生的伪影比较明显.
作为警告, 大多数Android设备不支持具有透明度的纹理的纹理压缩(仅不透明), 因此请记住这一点.
备注
即便在 3D 中,“像素画”纹理也应该禁用 VRAM 压缩,因为压缩会对外观产生负面影响,较低的分辨率也无法得到显著的性能提升。
后期处理和阴影
就片段着色活动而言, 后期处理效果和阴影也可能很昂贵. 始终测试这些对不同硬件的影响.
减少阴影图的大小可以提高性能 , 无论是在写还是读取阴影贴图方面. 除此之外, 提高阴影性能的最好方法是关闭尽可能多的灯光和物体的阴影. 较小或较远的OmniLights/SpotLights通常可以禁用它们的阴影, 而对视觉影响很小.
透明度和混合
透明物体对渲染效率带来了特殊的问题. 不透明的对象(尤其是在3D中)基本上可以以任意顺序渲染,Z-缓冲区将确保只有最前面的对象得到阴影. 透明或混合对象则不同, 在大多数情况下, 它们不能依赖Z-缓冲区, 必须以 “画家顺序”(即从后到前)渲染才能看起来正确.
透明对象的填充率也特别差, 因为每一个项目都要绘制, 即使之面会在上面绘制其他透明对象.
不透明的对象不需要这样做. 它们通常可以利用Z-缓冲区, 只先向Z-缓冲区写入数据, 然后只在 “胜利” 的片段上执行片段着色器, 也就是在某一像素处处于前面的对象.
在多个透明对象重叠的情况下, 透明度特别昂贵. 通常情况下, 使用透明区域越小越好, 以尽量降低这些填充率要求, 尤其是在移动端. 事实上, 在很多情况下, 渲染更复杂的不透明几何体最终可能比使用透明度来 “作弊” 更快.
多平台建议
如果你的目标是在多个平台上发布,请尽早在所有平台上(尤其是移动平台上)进行经常性得测试。在桌面上开发游戏,但在最后一刻试图将其移植到移动设备,这是灾难的根源。
一般来说,你应该针对最低的共性设计游戏,然后为更强大的平台添加可选的增强功能。例如,你可能希望在同时针对桌面和移动平台的情况下,使用兼容性渲染方法。
移动端和图块渲染
如上所述, 移动设备上的GPU与桌面上的GPU工作方式有很大不同. 大多数移动设备都使用图块渲染器. 图块渲染器将屏幕分割成规则大小的图块, 这些图块可以放入超快的缓存中, 从而减少了对主内存的读和写操作次数.
不过也有一些缺点. 图块渲染会让某些技术变得更加复杂, 执行起来也更加昂贵. 依赖于不同图块渲染的结果, 或者依赖于早期操作的结果被保存的图块可能会非常慢. 要非常小心地测试着色器, 视图纹理和后期处理的性能.
使用 MultiMesh 优化
对于需要不断处理(且保留一定控制的)大量实例(成千上万),建议直接使用服务器进行优化。
当对象数量达到数十万或数百万时, 这些方法都不再有效. 尽管如此, 根据要求, 还有另一种可能的优化方法.
3D
剔除
Godot会自动执行视图视锥剔除, 以防止渲染视口外的物体. 这对于发生在小范围内的游戏来说效果很好, 然而在较大的关卡中, 事情很快就会变得很麻烦.
遮挡剔除
比如走在一个小镇上, 你可能只能看到你所在的街道上的几栋建筑, 以及天空和几只飞过头顶的鸟. 然而就一个天真的渲染器而言, 你仍然可以看到整个小镇. 它不会只渲染你前面的建筑, 它会渲染那后面的街道, 与那条街上的人, 那后面的建筑. 你很快就会遇到这样的情况: 你试图渲染比可见的东西多10倍或100倍的东西.
事情并没有看上去那么糟糕,因为 Z 缓冲区通常允许 GPU 仅完全遮蔽位于前方的物体。这被称为深度预处理,且在使用 Forward+ 或 Compatibility 渲染方法时 Godot 会默认启用。但是,不需要的对象仍然会降低性能。
我们可以减少渲染量的一种方法是利用遮挡 。Godot 4.0 及更高版本提供了一种使用遮挡器节点进行遮挡剔除的新方法。请参阅遮挡剔除 ,了解有关在场景中设置遮挡剔除的说明。
备注
在某些情况下,你可能需要调整关卡设计以增加更多遮挡机会。例如,你可能需要添加更多墙壁以防止玩家看得太远,否则会因失去遮挡剔除的机会而降低性能。
透明物体
Godot通过 Material 和 Shader 对对象进行排序以提高性能. 然而, 这对透明物体来说是不可能的. 透明物体从后往前渲染, 以便与后面的物体混合. 因此, 尽量少使用透明对象 . 如果一个物体有一小部分是透明的, 尽量让这部分成为一个独立的表面, 有自己的材质.
更多信息请参阅 GPU 优化 文档。
细节程度(LOD)
在某些情况下, 特别是在远处, 用简单的版本**代替复杂的几何图形可能是个好主意. 最终用户可能看不出什么区别. 考虑看看远处的大量树木. 有几种策略可以替换不同距离的模型. 你可以使用较低的多边形模型, 或者使用透明度来模拟更复杂的几何体.
Godot 4 提供了多种控制细节层次的方法:
使用 网格的细节级别(LOD) 进行网格导入的自动方法。
在3D节点中使用 可见范围(HLOD) 配置的手动方法。
Decals 和 lights 也可以使用它们各自的 Distance Fade 属性从细节级别中获益。
虽然它们可以单独使用,但一起使用时这些方法最有效。例如,你可以设定可见范围来隐藏距离玩家太远而无法注意到的粒子效果。同时,你可以依靠网格LOD来使粒子效果的网格在远处算绘时细节较少。
可见范围也是为远处几何体设定冒充者的好方法(见下文)。
Billboard 和 imposter
使用透明度处理 LOD 的最简单版本是广告牌。例如,你可以使用单个透明四边形来表示远处的一棵树。除非彼此前面有很多棵树,否则这可以非常便宜地渲染。在这种情况下,透明度可能会开始侵蚀填充率(有关填充率的更多信息,请参阅《GPU 优化》)。
另一种方法是不只渲染一棵树, 而是将一些树作为一组来渲染. 如果你能看到一个区域, 但在游戏中不能实际接近它, 这可能是特别有效的.
你可以通过预先渲染对象的不同角度的视图来制作冒牌货. 或者你甚至可以更进一步, 周期性地将一个物体的视图重新渲染到一个纹理上, 作为一个冒牌货使用. 在远处, 你需要将观察者移动相当长的距离, 视角才会发生显著变化. 这可能是复杂的工作, 但可能是值得的, 这取决于你正在制作的项目类型.
使用自动实例化
这仅在 Forward+ 渲染器中实现,而不是在移动或兼容性中实现。
如果场景中有许多相同的对象,则可以使用自动实例化来减少绘制调用的数量。对于使用相同网格和材质的 MeshInstance3D 节点,这会自动发生:无需手动设置。
要使自动实例化有效,材质必须是不透明的或经过 alpha 测试(alpha 剪刀或 alpha 哈希)。Alpha 混合或深度预传递材质永远不会以这种方式实例化。相反,你必须使用 MultiMesh,如下所述。
使用手动实例化(MultiMesh)
如果必须在同一地点或附近绘制多个相同的对象, 请尝试使用 MultiMesh 来代替.MultiMesh允许以很小的性能代价来绘制成千上万的对象, 这使得它非常适合用于绘制羊群, 草地, 粒子以及其他任何有成千上万相同对象的地方.
另请参阅《使用 MultiMesh》文档。
烘焙照明
照明对象是最昂贵的渲染操作之一。实时照明、阴影(尤其是多个光源)和全局光照都特别昂贵。对于低功耗的移动设备来说,它们可能简化得太多而无法处理。
考虑使用烘焙照明,尤其是移动设备。这看起来很棒,但缺点是它不是动态的。有时,这是值得做出的权衡。
有关使用烘焙光照贴图的说明,请参阅 使用光照贴图全局照明。为了获得最佳性能,你应该将灯光的烘焙模式设置为Static(静态),而不是默认的Dynamic(动态),因为这将跳过具有烘焙光照的网格上的实时光照。
使用 Static 烘焙模式的灯光的缺点是,它们无法将阴影投射到具有烘焙照明的网格上。这可以使具有室外环境和动态对象的场景看起来平坦。性能和质量之间的良好平衡是 DirectionalLight3D 节点保持 Dynamic,并对大多数(如果不是全部)泛光灯和聚光灯使用 Static。
动画和皮肤
在某些平台上,动画和顶点动画(例如蒙皮和变形)可能非常昂贵。你可能需要大幅降低动画模型的多边形数量,或限制任意时间在屏幕上的模型数量。你还可以降低远处或遮挡网格的动画速率,或者如果玩家不太可能注意到动画被停止时完全暂停动画。
VisibleOnScreenEnabler3D 和 VisibleOnScreenNotifier3D 节点可用于此目的。
庞大的世界
如果你要制作大型游戏, 则与小型游戏可能会有所不同.
大型的世界可能需要用碎片建立, 可以在你在世界中移动时按需加载, 这可以防止内存使用失控, 也可以将所需的处理限制在局部区域.
由于大型世界中的浮点错误,渲染和物理也可能出现故障。可以使用 大世界坐标 解决该问题。如果无法使用大型世界坐标,你可以使用一些技术,例如围绕玩家定位世界(而不是相反),或定期移动原点以使事物以 Vector3(0, 0, 0) 为中心。
用 MultiMeshInstance3D 动画化数以千计条鱼
本教程探索了游戏 ABZU 中使用的一种技术, 该技术使用顶点动画和静态网格实例, 来渲染和制作成千上万的鱼动画.
在 Godot 中,这可以通过自定义着色器和 MultiMeshInstance3D 来实现。使用以下技术,即使在低端硬件上,也可以渲染数千个动画对象。
我们将从一条鱼的动画开始. 然后, 我们将看到如何将该动画扩展到数千条鱼.
动画化一条鱼
我们将从单条鱼开始。将鱼模型加载到 MeshInstance3D 中,然后添加一个新的 ShaderMaterial。
这是我们用于示例图像的鱼,你可以使用任何你喜欢的鱼模型。
本教程中的鱼模型由 QuaterniusDev 制作,并以知识共享许可共享。CC0 1.0 通用 (CC0 1.0) 公共领域贡献 https://creativecommons.org/publicdomain/zero/1.0/
通常,你会使用骨骼和 Skeleton3D 来为对象制作动画。但是,骨骼是在 CPU 上进行动画处理的,因此您最终必须每帧计算数千个作,并且不可能拥有数千个对象。在顶点着色器中使用顶点动画,可以避免使用骨骼,而是可以在几行代码中完全在 GPU 上计算完整的动画。
动画由四个关键帧动作组成:
- 从一边运动到另一边
- 绕着鱼的中心作旋转运动
- 平移波动运动
- 平移扭转运动
所有的动画代码都在顶点着色器中,并由 uniform 控制运动量。我们使用 uniform 来控制运动的强度,这样你就可以在编辑器中调整动画,并实时看到结果,而不用重新编译着色器。
所有的运动都将使用余弦波应用于模型空间中的 VERTEX . 我们希望顶点在模型空间中, 使运动总是相对于鱼的方向. 例如,side-to-side将始终使鱼在其左至右的方向上来回移动, 而不是在世界方向的 x 轴上.
为了控制动画的速度,我们将通过使用 TIME 定义自己的时间变量开始。float time = TIME * time_scale;
我们将实施的第一项议案是左右运动. 它可以通过 TIME 的 cos 抵消 VERTEX.x 来制作. 每次渲染网格时, 所有顶点都会移动到 “cos(时间)” 的数量.
//side_to_side is a uniform floatVERTEX.x += cos(time) * side_to_side;
生成的动画看起来是这样的:
接下来,我们添加轴心点。因为鱼以 (0, 0) 为中心,我们所要做的只是将 VERTEX 乘以旋转矩阵,使其围绕鱼的中心旋转。
我们构造一个旋转矩阵, 如下所示:
1 | //angle is scaled by 0.1 so that the fish only pivots and doesn't rotate all the way around |
然后我们把它乘以 VERTEX.xz,应用到 x 和 z 轴上。VERTEX.xz = rotation_matrix * VERTEX.xz;
在只应用轴心的情况下,你会看到这个:
接下来的两个动作需要沿着鱼的脊柱平移. 为此, 我们需要一个新的变量, body . body 是一个浮点数,在鱼的尾部是 0 ,在头部是 1 .float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2
下一个运动是沿着鱼的长度向下移动的余弦波. 为了让它沿着鱼的脊柱移动, 我们用脊柱的位置来偏移输入到 cos 的位置, 也就是我们在上面定义的变量 body。
//wave is a uniform floatVERTEX.x += cos(time + body) * wave;
这看起来很像我们上面定义的左右运动, 但在这个例子中, 通过使用 body 来偏移 cos,沿着脊柱的每个顶点在波浪中都有不同的位置, 使它看起来像是沿着鱼移动的波浪.
最后一个动作是扭转,也就是沿着脊柱滚动。类似轴心运动,我们首先构造一个旋转矩阵。
1 | //twist is a uniform float |
我们在 xy 轴上应用旋转, 使鱼看起来绕着它的脊柱滚动. 要做到这一点, 鱼的脊柱需要以 z 轴为中心.VERTEX.xy = twist_matrix * VERTEX.xy;
这是应用扭曲的鱼:
如果我们一个接一个地应用这些运动, 就得到一个类似液体凝胶似的运动.
通常鱼主要使用身体的后半部分游泳,所以我们需要将平移运动限制在鱼的后半部分。为此,我们创建一个新变量 mask (遮罩)。
mask 是个浮点数,从鱼头的 0 过渡到鱼尾的 1 ,我们用 smoothstep 来控制在哪里进行由 0 到 1 的过渡。
//mask_black and mask_white are uniformsfloat mask = smoothstep(mask_black, mask_white, 1.0 - body);
下面是把 COLOR 设置成 mask 后这条鱼的样子:
我们在做波浪运动的地方乘以 mask 就可以把动作限制在后半部分。
//wave motion with maskVERTEX.x += cos(time + body) * mask * wave;
为了将遮罩应用于扭曲, 我们使用 mix . mix 允许在完全旋转的顶点和未旋转的顶点之间混合顶点位置. 需要使用 mix 而不是将 mask 乘以旋转后的 VERTEX , 因为不是将运动加到 VERTEX 上, 而是用旋转后的版本替换 VERTEX . 如果把它乘以 mask , 就会把鱼缩小.
//twist motion with maskVERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);
将四个动作组合在一起, 就得到了最终的动画效果.
继续发挥 uniform 的作用, 以改变鱼的游泳周期. 你会发现, 你可以用这四个动作创造出各种各样的游泳方式.
制作一群鱼
Godot 使用 MultiMeshInstance3D 节点可以轻松渲染数千个相同的对象。
MultiMeshInstance3D 节点的创建和使用与 MeshInstance3D 节点相同。在本教程中,我们将把 MultiMeshInstance3D 节点命名为 School,因为它将包含一群鱼。
拥有 MultiMeshInstance3D 后,添加一个 MultiMesh,然后使用上面的着色器向该 MultiMesh 添加网格 。
MultiMeshes 使用三个额外的实例属性来绘制 Mesh:变换(旋转、平移、缩放)、颜色和自定义。自定义用于使用 Color 传入 4 个多用途变量。
instance_count 指定要绘制的网格的实例数量。现在,将 instance_count 保留为 0,因为当 instance_count 大于 0 时,你不能更改任何其他参数。我们稍后将在 GDScript 中设置 instance_count。
transform_format 指定使用的变换是 3D 还是 2D。对于本教程,请选择 3D。
对于 color_format 和 custom_data_format,你可以在 None、Byte、Float 之间选择。None 意味着你不会将这些数据(无论是每个实例的 COLOR 变量还是 INSTANCE_CUSTOM)传递给着色器。Byte 意味着组成你传入的颜色的每一个数字将被存储为 8 位,而 Float 意味着每一个数字将被存储为浮点数(32 位)。Float 速度较慢但更精确,Byte 占用内存较少、速度较快,但你可能会看到一些视觉上的伪像。
现在,将 instance_count 设置为你想要的鱼的数量。
接下来, 我们需要设置每个实例的变换.
有两种方法可以为 MultiMesh 设置每个实例的变换。第一种方法完全在编辑器中,并在《MultiMeshInstance3D 教程》中进行了描述。
第二种方法是, 遍历所有实例, 并在代码中设置它们的变换. 下面, 我们使用GDScript遍历所有实例, 并将它们的变换设置为随机位置.
1 | for i in range($School.multimesh.instance_count): |
运行该脚本,将会在 MultiMeshInstance3D 位置周围的框中,把鱼放置在随机位置。
你应该已经注意到所有鱼的游泳动作都是同步的了吧?这样看上去非常机械。下一步我们要做的就是让每一条鱼都处于游泳周期的不同位置,这样整个鱼群看起来就会更自然。
动画鱼群
使用 cos 函数给鱼做动画的一个好处是,它们只需要一个 time 参数。为了让每条鱼在游泳周期中处于单独的位置,我们只需要偏移 time。
为此,我们将每个实例的自定义值 INSTANCE_CUSTOM 添加到 time 中。float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
接下来,我们需要向 INSTANCE_CUSTOM 传递一个值。通过在上面的 for 循环中添加一行来实现这一点。在 for 循环中,为每个实例分配一组四个随机浮点数来使用。$School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))
现在这些鱼在游泳周期中都有独特的位置。你可以通过使用 INSTANCE_CUSTOM 乘以 TIME 让它们游泳更快或更慢,从而让它们更个性化。
1 | //将速度设置为正常速度的50至150 |
甚至你还可以像更改每个实例的自定义值一样, 尝试更改每个实例的颜色.
此时你会遇到的一个问题是,鱼虽然有动画,但它们并没有移动。你可以通过每帧更新每条鱼的实例变换来移动它们。虽然这样做比每帧移动数千个 MeshInstance3D 要快,但速度可能仍然很慢。
在下一个教程中,我们将介绍如何使用GPUParticles3D来利用 GPU 并单独移动每条鱼,同时还能获得实例化的好处。
用粒子控制数千条鱼
MeshInstance3D 的问题在于更新其变换数组的成本很高。它非常适合在场景周围放置许多静态对象。但在场景周围移动对象仍然很困难。
为了使每个实例以有趣的方式移动,我们将使用一个 GPUParticles3D 节点。粒子通过在 Shader 中计算和设置每个实例的信息来利用 GPU 加速。
首先创建一个 Particles 节点。然后在“Draw Passes”下将粒子的“Draw Pass 1”设置为你的 Mesh。然后在“Process Material”下创建一个新的 ShaderMaterial。
将 shader_type 设置为 particles。shader_type particles
然后添加以下两个函数:
1 | float rand_from_seed(in uint seed) { |
这些函数来自默认的 ParticleProcessMaterial。它们用于从每个粒子的 RANDOM_SEED 生成一个随机数。
粒子着色器的一个独特之处在于一些内置变量可以跨帧保存。TRANSFORM、COLOR 和 CUSTOM 都可以在网格着色器中访问,也可以在下次运行时在粒子着色器中访问。
接下来,设置你的 start() 函数。粒子着色器包含一个 start() 函数和一个 process() 函数。
start() 函式中的代码仅在粒子系统启动时运作。process() 函式中的代码将始终运作。
我们需要生成 4 个随机数:其中 3 个用于创建一个随机位置,1 个用于游泳周期的随机偏移。
首先,使用上面提供的 hash() 函数在 start() 函数内生成 4 个种子:
1 | uint alt_seed1 = hash(NUMBER + uint(1) + RANDOM_SEED); |
然后,使用这些种子生成随机数,使用 rand_from_seed:
1 | CUSTOM.x = rand_from_seed(alt_seed1); |
最后,将 position 赋值给 TRANSFORM[3].xyz,它是保存位置信息的变换的一部分。TRANSFORM[3].xyz = position * 20.0;
请记住,到目前为止所有这些代码都位于 start() 函数内部。
网格的顶点着色器, 可以完全复用前一教程中的.
现在每一帧你都可以单独移动每条鱼了,可以直接增加 TRANSFORM 也可以设置 VELOCITY。
让我们通过在 start() 函数中设置 VELOCITY 来变换鱼。VELOCITY.z = 10.0;
这是设置 VELOCITY 的最基本方法,每个粒子(或鱼)都有相同的速度。
只要设置 VELOCITY,你就可以让鱼自由游动。例如,尝试下面的代码。VELOCITY.z = cos(TIME + CUSTOM.x * 6.28) * 4.0 + 6.0;
这将为每条鱼在 2 和 10 之间设置不同的速度。
如果你在 process() 函式中设定速度,你也可以让每条鱼随着时间的推移改变其速度。
如果你在上一个教程中使用了 CUSTOM.y,你也可以基于 VELOCITY 来设置游泳动画的速度。直接用 CUSTOM.y 就好了。CUSTOM.y = VELOCITY.z * 0.1;
代码产生的效果如图:
使用 ParticleProcessMaterial,你可以根据需要使鱼的行为变得简单或复杂。在本教程中,我们只设置了速度,但在你自己的着色器中,你还可以设置 COLOR、旋转、缩放(通过 TRANSFORM)。有关粒子着色器的更多信息,请参阅《粒子着色器参考》。
线程
线程允许同时执行代码。它允许从主线程卸载工作。
创建线程请使用如下代码:
1 | var thread: Thread |
然后, 你的函数将在一个单独的线程中运行, 直到它返回. 即使函数已经返回, 线程也必须收集它, 所以调用 Thread.wait_to_finish() , 它将等待线程完成(如果还没有完成), 然后妥善处理它.
警告
创建线程是一项缓慢的作,尤其是在 Windows 上。为避免不必要的性能开销,请确保在需要大量处理之前创建线程,而不是实时创建线程。
例如,如果你在游戏过程中需要多个线程,你可以在关卡加载时创建线程,然后才真正开始处理它们。
此外,互斥锁的锁定和解锁也可能是一项昂贵的作。锁定时应小心;避免过于频繁(或过长时间)锁定。
Mutex
并不总是支持从多个线程访问对象或数据(如果你这样做, 会导致意外行为或崩溃). 请阅读 线程安全的 API 文档, 了解哪些引擎API支持多线程访问.
在处理自己的数据或调用自己的函数时, 通常情况下, 尽量避免从不同的线程直接访问相同的数据. 你可能会遇到同步问题, 因为数据被修改后,CPU核之间并不总是更新. 当从不同线程访问一个数据时, 一定要使用 Mutex .
当调用 Mutex.lock() 时, 一个线程确保所有其他线程如果试图 锁 同一个mutex, 就会被阻塞(进入暂停状态). 当通过调用 Mutex.unlock() 来解锁该mutex时, 其他线程将被允许继续锁定(但每次只能锁定一个).
下面是一个使用 Mutex 的例子:
1 | var counter := 0 |
Semaphore信号量
有时您希望您的线程 “按需” 工作。换句话说,告诉它什么时候工作,当它什么都不做时让它暂停。为此,使用了信号量 。功能 Semaphore.wait() 用于在线程中暂停它,直到一些数据到达。
而主线程则使用 Semaphore.post() 来表示数据已经准备好被处理:
1 | var counter := 0 |
线程安全的 API
线程
线程是用来平衡各CPU和核心的处理能力.Godot支持多线程, 但不是在整个引擎中.
下面是可以在Godot的不同区域使用多线程的方法列表.
全局作用域
全局范围单例都是线程安全的。支持从线程访问服务器(对于 RenderingServer 和 Physics 服务器,请确保在项目设置中启用了线程或线程安全操作!)。
这使它们成为在服务器中创建数十万个实例并从线程控制它们的代码的理想选择. 当然, 还需要更多的代码, 因为这是直接使用的而不是嵌入场景树中使用.
场景树
与活动场景树的交互是线程 不 安全的. 当在线程之间发送数据时, 请确保使用mutexes. 如果你想从一个线程调用函数, 可以使用 call_deferred 函数:
1 | # Unsafe: |
但是, 可以在激活的场景树外创建场景块(以树形式排列的节点). 这样, 可以在线程中构建或实例化部分场景, 然后将其添加到主线程中:
1 | var enemy_scene = load("res://enemy_scene.scn") |
不过, 只有当你有 一个 线程加载数据时, 这才真正有用. 从多个线程加载或创建场景块可能有效, 但你要冒着资源被多线程调整的风险(在Godot中只加载一次), 从而导致意外行为或崩溃.
只有当你 “真正” 知道自己在做什么, 并且确信一个资源没有被多个资源使用或设置时, 才可以使用多个线程来生成场景数据. 否则, 直接使用服务端的API(它是完全线程安全的)而不接触场景或资源会更安全.
渲染
默认情况下,实例化能够渲染 2D 或 3D 内容的节点(比如 Sprite)不是线程安全的。要让渲染做到线程安全,请将项目设置中的Rendering > Driver > Thread Model设为 Multi-Threaded。
请注意,Multi-Thtreaded 线程模型有若干已知的问题,所以无法胜任所有场景。
你应该避免调用涉及与其他线程上的 GPU 直接交互的函数,例如创建新纹理或修改和检索图像数据,这些操作可能会导致性能停滞,因为它们需要与 RenderingServer 同步,因为数据需要传输到 GPU 或在 GPU 上更新。
GDScript 数组、字典
在 GDScript 中,可以从多个线程读取和写入元素,但是任何改变容器大小的操作(调整大小、添加或删除元素)都需要锁定互斥锁。
资源
不支持从多个线程修改唯一资源。但是支持在多个线程上处理引用,因此在单个线程上加载资源(场景、纹理、网格等)也可以在单个线程上加载和操作,然后添加到主线程上的活动场景中。此处的限制如上所述,必须注意不要同时从多个线程加载相同的资源,因此最简单的方法是使用一个线程来加载和修改资源,然后使用主线程来添加它们。
物理
物理介绍
碰撞检测:游戏中的两个对象在何时相交或接触
碰撞响应:检测到碰撞时希望某些事情发生
碰撞物体
Godot 提供了四种碰撞对象,它们都扩展了 CollisionObject2D。
下面列出的最后三个是物理形体,并额外扩展了 PhysicsBody2D。
- Area2D
Area2D 节点提供 检测 和 影响 . 它们可以检测物体何时重叠, 并在物体进入或离开时发出信号. Area2D 也可用于覆盖物理属性, 例如一定区域内的重力或阻尼. - StaticBody2D
静态主体是物理引擎不移动的主体. 它参与碰撞检测, 但不会响应碰撞而移动. 它们通常用于属于环境的对象或不需要任何动态行为的对象. - RigidBody2D
这是实现模拟2D物理的节点. 你不直接控制 RigidBody2D , 而是你对它施加力(重力, 冲动等), 物理引擎计算得到的运动. 阅读更多关于使用刚体的信息. - CharacterBody2D
提供碰撞检测的物体, 但没有物理特性. 所有移动和碰撞响应必须在代码中实现.
物理材质
静态体和刚性体可以配置物理材质。
允许调整物体的摩擦和弹性,并设置是否具有吸收性、粗糙性。
物理体可以包含任意数量的 Shape2D 对象作为子对象.
这些形状用于定义对象的碰撞边界并检测与其他对象的接触.
分配形状的最常用方法是添加 CollisionShape2D 或 CollisionPolygon2D 作为对象的子项.
这些节点允许你直接在编辑器工作区中绘制形状.
重要
注意,不要在编辑器中缩放碰撞形状。“检查器”中的“Scale”属性应保持为 (1, 1)。
改变碰撞形状的大小时,你应该使用尺寸控制柄,而不是 Node2D 缩放控制柄。缩放形状可能会导致意外的碰撞行为。
物理过程回调
物理引擎以固定的速率运行(默认为每秒60次迭代)。这个速率通常与帧率不同,帧率会根据渲染内容和可用资源而波动。
所有与物理相关的代码都必须以这个固定速率运行。
因此,Godot 区分了 空闲处理与物理处理. 每帧运行的代码称为空闲处理,而每个物理周期运行的代码称为物理处理。
Godot 提供了两个不同的回调函数,分别用于这两种处理速率。
物理回调函数 Node._physics_process() 在每个物理步骤之前被调用。
任何需要访问物体属性的代码都应该在这个函数里运行。
该方法将传递一个名为 delta 的参数,它是一个浮点数,表示自上一步以来经过的 秒数 。
当使用默认的 60 Hz 物理更新速率时,它通常等于 0.01666… (但并不总是,详见下文)。
建议在物理计算中使用 delta 参数, 以便当你更改物理更新速率或玩家设备跟不上时, 游戏能够正确运行.
碰撞层与遮罩
碰撞层系统是最强大但经常被误解的碰撞功能之一。
该系统允许你在各种对象之间构建复杂的交互。
关键概念是层(Layer)与遮罩(Mask)。
每个 CollisionObject2D 都有 32 个不同的物理层可以相互作用。
- collision_layer 表示该对象位于哪些层。默认情况下,所有实体都在图层 1 上。
- collision_mask 表示该对象会对哪些层上的实体进行扫描。如果对象不在任何遮罩层中,则该实体将其忽略。默认情况下,所有实体都会扫描图层 1。
可以通过代码配置这些属性,也可以在“检查器”中对其进行编辑。
跟踪每个图层的用途可能很困难,因此您可能会发现为正在使用的图层分配名称很有用。可以在项目设置 > 图层名称 > 2D 物理中分配名称。
代码示例
在函数调用中,层被指定为位掩码。
如果函数默认启用所有图层,则图层蒙版将作为 0xffffffff 给出。
您的代码可以对图层蒙版使用二进制、十六进制或十进制表示法,具体取决于您的偏好。
1 | # Example: Setting mask value for enabling layers 1, 3 and 4 |
您也可以通过调用 set_collision_layer_value(layer_number, value) 或 set_collision_mask_value(layer_number, value) 在任何给定的 CollisionObject2D 上,如下所示:
1 | # Example: Setting mask value to enable layers 1, 3, and 4. |
导出注释可用于在编辑器中通过用户友好的 GUI 导出位掩码:@export_flags_2d_physics var layers_2d_physics
Area2D
Area 节点的作用是检测和影响。它们可以检测物体何时重叠,并在物体进入或离开时发出信号。Area 也可用于覆盖物理属性,例如一定区域内的重力或阻尼。
Area2D 的主要用途有三种:
- 覆盖给定区域中的物理参数(例如重力)。
- 检测其他实体何时进入或退出某个区域或当前哪个实体位于某个区域。
- 检查是否与其他区域重叠。
默认情况下,area还会接收鼠标和触摸屏输入.
StaticBody2
静态主体是物理引擎不移动的主体.
它参与碰撞检测, 但不会响应碰撞而移动.
然而, 它可以使用它的 constant_linear_velocity 和 constant_angular_velocity 属性将运动或旋转传递给碰撞体, 好像 它正在移动一样.
StaticBody2D 节点最常用于属于环境的对象或不需要任何动态行为的对象.
StaticBody2D 的示例用法:
平台(包括可移动的平台)
输送带
墙壁和其他障碍
RigidBody2D
这是实现模拟2D物理的节点. 你不能直接控制一个 RigidBody2D.
取而代之的是, 对它施加力, 物理引擎会计算由此产生的运动, 包括与其他物体的碰撞, 以及碰撞响应, 如弹跳, 旋转等.
你可以通过“Mass”(质量)“Friction”(摩擦)“Bounce”(反弹)等属性修改刚体的行为,这些都可以在检查器中设置。
身体的行为也受到世界属性的影响,如 项目设置 > 物理, 或通过输入 Area2D 这覆盖了全局物理属性。
当一个刚体处于静止状态, 有一段时间没有移动, 它就会进入睡眠状态.
睡眠的物体就像一个静态的物体, 它的力不会被物理引擎计算. 当力被施加时, 无论是通过碰撞还是通过代码, 该物体都会被唤醒.
使用 RigidBody2D
使用刚体的一个好处是,可以“免费”获得许多行为而无需编写任何代码。
例如,如果你正在制作一个带有下降块的《愤怒的小鸟》式游戏,你只需要创建 RigidBody2D 并调整它们的属性。堆叠、下降、弹跳将由物理引擎自动计算。
但是,如果您确实希望对主体进行一些控制,则应注意 - 更改刚体的位置、linear_velocity 或其他物理属性可能会导致意外行为。
如果你需要改变任何与物理相关的属性,你应该使用 _integrate_forces() callback 而不是 _physics_process()。
在此回调中,您可以访问主体的 PhysicsDirectBodyState2D,它允许安全地更改属性并将它们与物理引擎同步。
例如,以下是《爆破彗星》式宇宙飞船的代码:
1 | extends RigidBody2D |
请注意, 我们不是直接设置 linear_velocity 或 angular_velocity 属性, 而是将力( thrust 和 torque )施加到物体上并让物理引擎计算出最终的运动.
备注
当一个刚体进入睡眠状态时, _integrate_forces() 函数将不会被调用.
要重写这一行为, 你需要通过创建碰撞, 对其施加力或禁用 can_sleep 属性来保持物体的激活. 请注意, 这可能会对性能产生负面影响.
接触报告
默认情况下,刚体不会跟踪接触点,因为如果场景中存在着许多物体,这可能需要大量的内存。
若要启用接触报告,请将 max_contacts_reported 属性设置为非零值。
然后可以通过 PhysicsDirectBodyState2D.get_contact_count() 和相关函数获得接触。
通过信号的接触监控, 启用 contact_monitor 属性. 请参阅 RigidBody2D 的可用信号列表.
CharacterBody2D
CharacterBody2D 物体能够检测到与其他物体的碰撞,但不会受到重力、摩擦力等物理属性的影响。必须由用户通过代码来控制。物理引擎不会移动角色体。
移动角色体时,你不应该直接设置 position,而应该使用 move_and_collide() 或 move_and_slide() 方法。
这些方法会让物体沿着给定的向量移动,与其他物体发生碰撞就会立即停止移动。发生碰撞后,必须手动编写对碰撞的响应逻辑。
响应角色碰撞
发生碰撞后,你可能会希望该物体发生反弹、沿着墙体滑动、或者修改被碰撞对象的属性。处理碰撞响应的方法取决于移动 CharacterBody2D 的方法。
move_and_collide
当使用 move_and_collide() 时, 该函数返回一个 KinematicCollision2D 对象, 其中包含有关碰撞和碰撞体的信息. 你可以使用此信息来确定响应.
例如, 如果要查找发生碰撞的空间点:
1 | extends PhysicsBody2D |
或者从碰撞物体反弹:
1 | extends PhysicsBody2D |
move_and_slide
滑动是一种常见的碰撞响应;
想象一个游戏角色在上帝视角的游戏中沿着墙壁移动, 或者在平台游戏中上下坡.
虽然可在使用 move_and_collide() 之后自己编写这个响应, 但 move_and_slide() 提供了一种快捷方法来实现滑动且无需编写太多代码.
警告
move_and_slide() 在计算中自动包含时间步长,因此你 不应该 将速度向量乘以 delta。
特别的 ,对于 gravity ,它是一个加速度量,与时间有关,因此需要乘以 delta 进行缩放。
例如, 使用以下代码制作一个可以沿着地面(包括斜坡)行走的角色, 并在站在地面时跳跃:
1 | GDScriptC# |
有关使用 move_and_slide() 的更多详细信息, 请参阅 运动学角色(2D) , 包括带有详细代码的演示项目.
使用 Jolt Physics
要将 3D 物理引擎更改为 Jolt Physics,请将 项目设置 > 物理 > 3D > 物理引擎 到 Jolt Physics。
完成此作后,单击“ 保存并重新启动 ”按钮。当编辑器再次打开时,3D 场景现在应该使用 Jolt 进行物理处理。
与 Godot Physics 的主要区别
3D 关节节点的当前接口与 Jolt 自己的关节的接口不太一致。
因此,有许多关节属性不受支持,主要是与配置关节的软限制相关的属性。
不支持的属性有:
- PinJoint3D: bias, damping, impulse_clamp
- HingeJoint3D: bias, softness, relaxation
- SliderJoint3D: angular_*, *_limit/softness, *_limit/restitution, *_limit/damping
- ConeTwistJoint3D: bias, relaxation, softness
- Generic6DOFJoint3D: limit/softness, limit/restitution, limit/damping, limit/erp
目前,如果将这些属性设置为默认值以外的任何值,则会发出警告。
单物体关节
你可以省略一个关节体,换取一个双体关节,而实际上让“世界”成为另一个体。
但是,将您分配正文的节点路径(node_a 与 node_b)将被忽略。
Godot Physics 的行为总是像你把它分配给 node_a 一样,而且由于 node_a 也是定义关节极限的参考系,所以你最终会得到倒置的极限和潜在的奇怪极限形状,特别是如果你的极限允许线性和角度自由度。
Jolt 的行为就像您将身体分配给 node_b 一样,而是使用 node_a 代表“世界”。
有一个名为 物理 > 震动物理 3D > 关节 > 世界节点的项目设置 如果您需要与现有项目兼容,则可以切换此行为。
碰撞边距
Jolt(和其他类似的物理引擎)使用 Jolt 称之为“凸半径”的东西来帮助改进 Jolt 所依赖的凸形状碰撞检测类型的性能和行为。
其他物理引擎(包括 Godot)可能会将它们称为“碰撞裕度”。
Godot 将这些作为每个 Shape3D 派生类的边距属性公开,但 Godot Physics 本身并没有将它们用于任何事情。
这些碰撞边距有时在其他引擎中的作用(如 Godot 的文档中所述)是在形状周围有效地添加一个“壳”,稍微增加其大小,同时将任何边缘/角弄圆。
然而,在 Jolt 中,这些边距首先用于缩小形状,然后应用“壳”,导致边缘/角类似地变圆,但不会增加形状的大小。
为了避免必须手动调整此边距属性,因为其默认值对于较小的形状可能会出现问题,因此 Jolt 模块公开了一个名为 物理 > Jolt Physics 3D > Collisions > Collision Margin Fraction 的项目设置 将其乘以形状 AABB 的最小轴以计算 实际保证金。然后,形状的 margin 属性被用作 upper 绑定。
在大多数情况下,这些碰撞边距的影响微乎其微,但在进行形状查询时偶尔会导致异常的碰撞法线。
你可以通过降低上述项目设置值(甚至设为0.0)来缓解此问题,但边距过小同样可能引发异常碰撞结果,因此通常不建议这样做。
Baumgarte 稳定法
一种解决穿透体并将其推到刚接触状态的方法。
在 Godot 物理学中,这就像弹簧一样工作。
这意味着物体可以加速,并可能导致物体超调并完全分离。
使用 Jolt,稳定仅应用于位置,而不应用于身体的速度。
这意味着它不能超调,但可能需要更长的时间才能解决穿透问题。
可以使用项目设置调整此稳定的强度 物理 > 震动物理 3D > 模拟 > 鲍姆加特稳定系数 。
将此项目设置设置为 0.0 将关闭 Baumgarte 稳定。将其设置为 1.0 将在 1 个模拟步骤中解决穿透问题。这很快,但通常也不稳定。
幽灵碰撞
Jolt 采用两种技术来缓解重影碰撞,即与形状/主体的内部边缘发生碰撞,从而导致与运动方向相反的碰撞法线。
第一种技术称为“主动边缘检测”,在 ConcavePolygonShape3D或 HeightMapShape3D 设置为“活动”或“非活动”,具体取决于与相邻三角形的角度。
当与非活动边发生碰撞时,碰撞法线将被三角形的法线替换,以减轻重影碰撞的影响。
此活动边检测的角度阈值可通过项目设置 物理 >Jolt 物理 3D > 碰撞 > 活动边阈值进行配置。
第二种技术称为“增强的内部边缘去除”,而是增加了运行时间 根据接触点检查边是活动还是非活动 两个机构。
这样做的好处是不仅适用于与 ConcavePolygonShape3D和 HeightMapShape3D,但也包括同一主体内任何形状之间的边缘。
增强型内部边缘移除(Enhanced Internal Edge Removal) 可以使用物理 >Jolt 物理 3D > 模拟 > 使用增强型内部边缘移除(Enhanced Internal Edge Removal) 、项目设置和类似设置进行查询 和运动查询 。
请注意,在处理两个不同形体之间的重影碰撞时,主动边缘检测和增强的内部边缘移除都不适用。
内存占用
Jolt 在其模拟步骤中使用堆栈分配器进行临时分配。
此堆栈分配器需要预先分配一定数量的内存,可以使用物理 > 震动物理 3D > 限制 > 临时内存缓冲区大小进行配置 项目设置。
射线投射的面索引
intersect_ray() 结果中返回的 face_index 属性 默认情况下,RayCast3D 将始终为 -1 和 Jolt。
项目设置 物理 > 震动物理 3D > 查询 > 启用光线投射面索引(Enable Ray Cast Face Index) 将启用它们。
请注意,启用此设置将增加ConcavePolygonShape3D 约占 25%。
运动学 RigidBody3D 接触
使用 Jolt 时,RigidBody3D 会冻结 FREEZE_MODE_KINEMATIC 默认情况下,将不报告与其他静态/运动学碰撞的接触 身体,出于性能原因,即使在设置非零 max_contacts_reported 时也是如此。如果你有许多/大型运动体与复杂的静态几何体重叠,例如 ConcavePolygonShape3D 或 HeightMapShape3D,你最终可能会在不知不觉中浪费大量的 CPU 性能和内存。
因此,此行为是通过项目设置选择加入的 物理 > 震动物理 3D > 模拟 > 生成所有运动学接触 。
接触冲量
由于 Jolt 内部的限制,.get_contact_impulse() 提供的PhysicsDirectBodyState3D接触脉冲 根据接触流形和速度等内容提前估计 碰撞体的。
这意味着报告的脉冲只是准确的 在两个相关物体没有与任何其他物体碰撞的情况下。
Area3D 与 SoftBody3D
Jolt 目前不支持 SoftBody3D 之间的任何交互 和 Area3D,例如重叠事件,或 区域 3D。
WorldBoundaryShape3D
WorldBoundaryShape3D,旨在表示无限平面,与 Godot Physics 相比,Jolt 中的实现方式略有不同。
两种发动机对这架飞机的有效尺寸都有上限,但在使用 Jolt 时,这个尺寸要小得多,以避免精度问题。
你可以使用 物理 > 震动物理 3D > 限制 > 世界边界形状大小(> World Boundary Shape Size) 配置此大小 项目设置。
与 Godot Jolt 扩展的主要区别
虽然内置的 Jolt 模块在很大程度上是 Godot Jolt 扩展的直接端口,但也有一些不同之处。
项目设置
所有项目设置已从 physics/jolt_3d 分类移至 physics/jolt_physics_3d。
此外,部分项目设置也进行了重命名和重构,包括:
sleep/enabled 现已更名为 simulation/allow_sleep
sleep/velocity_threshold 现已更名为 simulation/sleep_velocity_threshold
sleep/time_threshold 现已更名为 simulation/sleep_time_threshold
collisions/use_shape_margins 现已更名为 collisions/collision_margin_fraction 为 ,其中值 0 等同于禁用它。
collisions/use_enhanced_internal_edge_removal 现已更名为 simulation/use_enhanced_internal_edge_removal
collisions/areas_detect_static_bodies 现已更名为 simulation/areas_detect_static_bodies
collisions/report_all_kinematic_contacts 现已更名为 simulation/generate_all_kinematic_contacts
collisions/soft_body_point_margin 现已更名为 simulation/soft_body_point_radius
collisions/body_pair_cache_enabled 现已更名为 simulation/body_pair_contact_cache_enabled
collisions/body_pair_cache_distance_threshold 现已更名为 simulation/body_pair_contact_cache_distance_threshold
collisions/body_pair_cache_angle_threshold 现已更名为 simulation/body_pair_contact_cache_angle_threshold
continuous_cd/movement_threshold 现已更名为 simulation/continuous_cd_movement_threshold ,但以分数而不是百分比表示。
continuous_cd/max_penetration 现已更名为 simulation/continuous_cd_max_penetration 是 ,但以分数而不是百分比表示。
kinematics/use_enhanced_internal_edge_removal 现已更名为 motion_queries/use_enhanced_internal_edge_removal
kinematics/recovery_iterations 现已更名为 motion_queries/recovery_iterations ,但以分数而不是百分比表示。
kinematics/recovery_amount 现已更名为 motion_queries/recovery_amount
queries/use_legacy_ray_casting 已被删除。
solver/velocity_iterations 现已更名为 simulation/velocity_steps
solver/position_correction 现已更名为 simulation/baumgarte_stabilization_factor 是 ,但表示为分数而不是百分比。
solver/active_edge_threshold 现已更名为 collisions/active_edge_threshold
solver/bounce_velocity_threshold 现已更名为 simulation/bounce_velocity_threshold
solver/contact_speculative_distance 现已更名为 simulation/speculative_contact_distance
solver/contact_allowed_penetration 现已更名为 simulation/penetration_slop
limits/max_angular_velocity 现在存储为弧度。
limits/max_temporary_memory 现已更名为 limits/temporary_memory_buffer_size
关节节点
Godot Jolt 扩展中提供的关节节点(JoltPinJoint3D、JoltHingeJoint3D、JoltSliderJoint3D、JoltConeTwistJoint3D 和 JoltGeneric6DOFJoint3D)未包含在 Jolt 模块中。
线程安全
与 Godot Jolt 扩展不同,Jolt 模块确实具有线程安全,包括对物理 > 3D > 在单独线程上运行的支持 项目设置。
然而,这还没有经过非常彻底的测试,所以它应该是 被认为是实验性的。
使用 RigidBody
刚体是由物理引擎直接控制的物体,用于模拟物体的的物理行为。
为了定义刚体的形状,必须为其指定一个或多个 Shape3D 对象。
注意,设置这些形状的位置将影响物体的质心。
如果只需要放置一次刚体,例如设置它的初始位置,可以使用 Node3D 节点提供的方法,例如 set_global_transform() 或 look_at()。
但是,这些方法不能每一帧都被调用,否则物理引擎将无法正确地仿真物体的状态。
举个例子,考虑一个刚体,你想旋转它,使它指向另一个对象。在实现这种行为时,一个常见的错误是每一帧都使用 look_at() ,这样会破坏物理仿真。
下面,我们将演示如何正确地实现这一点。
你不能使用 set_global_transform() 或 look_at() 方法并不意味着你不能完全控制一个刚体.
相反, 你可以通过使用 _integrate_forces() 回调来控制它. 在这个方法中, 你可以添加 力 , 应用 冲量 , 或者设置 速度 , 以实现你想要的任何运动.
“look at”方法
如上所述,使用 Node3D 节点的 look_at() 方法不能每一帧都用来跟踪一个目标。
这里有一个自定义的 look_at() 方法叫做 look_follow() ,可以适用于刚体:
1 | extends RigidBody3D |
此方法使用刚体的 angular_velocity 属性来旋转主体。
旋转轴由当前正向方向与想要查看的方向之间的叉积给出。
钳位是一种简单的方法,用于防止旋转量超过要查看的方向,因为所需的旋转总量由点积的反余弦给出。
此方法也可以与 axis_lock_angular_* 一起使用。如果需要更精确的控制,则可能需要依赖四元数的解决方案,如使用 3D 变换中所述。
使用 Area2D
Area2D 定义了二维空间的区域。
在这个空间中,你可以检测到其他 CollisionObject2D 节点的重叠,进入和退出。
区域 (Area) 还允许覆盖本地物理属性. 我们将在下面讨论这些功能中的每一个。
Area 的属性
Gravity ,Linear Damp 和 Angular Damp 用于配置区域的物理覆盖行为。
Monitoring 和 Monitorable 用于启用和禁用该区域。
Audio Bus 部分 ,允许覆盖该区域内的音频,例如玩家移动时的音频效果。。
重叠检测
用于触碰和重叠检测. 当需要知道两个物体已经触碰, 但不需要物理碰撞时, 可以使用一个区域来通知.
例如, 要做一个硬币让玩家去捡. 硬币并不是一个实心的物体, 玩家不能站在上面, 也不能推它, 只是想让它在玩家触碰它的时候消失.
为了检测重叠,我们将在 Area2D 上连接相应的信号,使用哪个信号取决于玩家的节点类型。如果玩家是另一个区域(Area2D), 就使用 area_entered。
然而当假设玩家是一个 CharacterBody2D (也是一个 CollisionObject2D 类型)时,我们将连接 body_entered 信号.
1 | extends Area2D |
区域影响
第二个主要用途是改变物理效果。
默认情况下区域不会启用这个功能,但你可以用 Space Override(空间覆盖)属性来启用。
当区域重叠时,它们会按照 Priority(优先级)的顺序进行处理(优先处理优先级高的区域)。覆盖功能有四个选项:
- Combine(合并)——区域会将其数值加到目前计算得到的数值上。
- Replace(替换)——区域会替换物理属性,忽略优先级更低的区域。
- Combine-Replace(合并后替换)——区域会将重力/阻尼数值加到目前计算得到的数值上(按优先级顺序),忽略优先级更低的区域。
- Replace-Combine(替换后合并)——区域会替换目前计算得到的重力/阻尼数值,但会继续计算其他区域。
你可以利用这些属性为相互重叠的区域创建非常复杂的行为。
可以覆盖的物理属性有:
- Gravity(重力)——区域内的重力强度。
- Gravity Direction(重力方向)——该向量不需要归一化。
- Linear Damp(线性阻尼)——物体停止移动的快慢——每秒损失的线速度。
- Angular Damp(角度阻尼)——物体停止旋转的快慢——每秒损失的角速度。
重力点
重力点 (Gravity Point) 属性允许你创建一个“吸引器”。
区域中的重力将朝向一个由 点中心 (Point Center) 属性给出的点进行计算。
这些值相对于 Area2D,因此例如使用 (0, 0) 将吸引物体到区域的中心。
使用 CharacterBody2D/3D
CharacterBody2D 用于实现通过代码控制的物体。
Character bodies 在移动时可以检测到与其他物体的碰撞,但不受引擎物理属性(如重力、摩擦力等)的影响。
虽然这意味着你必须编写一些代码来创建它们的行为,但这也意味着你可以更精确地控制它们如何移动和反应。
运动与碰撞
当移动一个 CharacterBody2D 时,你不应该直接设置它的 position 属性,而应该使用 move_and_collide() 或 move_and_slide() 方法。
应该在
_physics_process()回调中处理物理体的运动。
move_and_collide()
这个方法需要一个 Vector2 参数以表示物体的相对运动。
通常,这是速度向量乘以帧时间步长( delta )。
如果引擎在沿着此向量方向的任何位置检测到碰撞,则物体将立即停止移动。
如果发生这种情况,该方法将返回一个 KinematicCollision2D 对象。
KinematicCollision2D 是一个包含碰撞和碰撞对象数据的对象. 使用这些数据, 你可以计算出你的碰撞响应.
当你只想移动物体并检测碰撞,并且不需要任何自动碰撞响应时,move_and_collide 最有用。
例如,如果你需要一颗从墙上弹开的子弹,你可以在检测到碰撞时直接更改速度角度。move_and_slide()
方法旨在简化常见情况下的碰撞响应, 即你希望一个物体沿着另一个物体滑动. 例如, 在平台游戏或自上而下的游戏中, 它特别有用.
当调用 move_and_slide() 时,该函数使用许多节点属性来计算其滑动行为。这些属性可以在检查器中找到,或在代码中设置。
- velocity - 默认值: Vector2( 0, 0 )
此属性表示身体的速度向量(以每秒像素为单位)。move_and_slide() 会在碰撞时自动修改此值。 - motion_mode - 默认值: MOTION_MODE_GROUNDED
这个属性通常用于区分 横向滚动视角 和 俯视角 。
默认情况下,你可以使用 is_on_floor() ,is_on_wall() 和 is_on_ceiling() 方法来检测物体与哪种表面接触,以及物体会与这些斜坡互动。
当使用 MOTION_MODE_FLOATING 时,所有碰撞都会被认为是“墙”。 - up_direction - 默认值: Vector2( 0, -1 )
这个参数允许你定义哪些表面应该被引擎视为地板。
设置这个参数然后使用 is_on_floor() ,is_on_wall() 和 is_on_ceiling() 方法来检测物体接触的表面类型。
默认值意味着所有水平表面的顶部都被认为是“地面”。 - floor_stop_on_slope - 默认值: true
该参数可以防止物体站立不动时从斜坡上滑落. - wall_min_slide_angle - 默认值: 0.261799 (以弧度表示,相当于 15 度)
这是当身体在遇到斜坡时允许滑动的最小角度。 - floor_max_angle - 默认值: 0.785398 (以弧度表示,相当于 45 度)
这是表面不再被视为 “地板” 之前的最大角度
在特定情况下,还有许多其他属性可用于修改身体的行为。详情请参见 CharacterBody2D 文档。
检测碰撞
当使用 move_and_collide() 时, 函数直接返回一个 KinematicCollision2D , 你可以在代码中使用这个.
当使用 move_and_slide() 时,有可能发生多次碰撞,因为滑动响应也被计算在内。
要处理这些碰撞,使用 get_slide_collision_count() 和 get_slide_collision():
1 | # 使用 move_and_collide. |
get_slide_collision_count() 只计算物体碰撞和改变方向的次数。
下面两个代码片段的结果是相同的碰撞响应:
1 | # 使用 move_and_collide |
用 move_and_slide() 做的任何事情都可以用 move_and_collide() 来完成, 但需要更多的代码.
发射射线
空间
在物理世界中所有低级碰撞和物理信息存储在一个空间中。
当前 2D 空间(用于 2D 物理) 可以通过访问获得 CanvasItem.get_world_2d().space
对于 3D 是 Node3D.get_world_3d().space
对于 3D 和 2D,得到的空间 RID 可分别在 PhysicsServer3D 和 PhysicsServer2D 中使用。
获取空间
Godot 物理默认与游戏逻辑在同一个线程中运行,但可以设置为在单独的线程中运行以提高效率。
因此,唯一安全访问空间的时间是在 Node._physics_process() 回调期间。从该函数之外访问空间可能会产生一个错误,因为空间会被锁定。
要对物理空间执行查询,请 PhysicsDirectSpaceState2D 和 PhysicsDirectSpaceState3D 必须使用。
1 | # 在 2D 中使用以下代码: |
Raycast 查询
要执行 2D 射线查询,可以使用 PhysicsDirectSpaceState2D.intersect_ray() 方法。例如:
1 | func _physics_process(delta): |
3D 空间中的数据也是类似的,只不过使用的是 Vector3 坐标。
请注意,要启用与 Area3D 的碰撞,必须将布尔值参数 collide_with_areas 设置为 true。
1 | const RAY_LENGTH = 1000 |
为了避免自相交,intersect_ray() 参数对象可以通过其 exclude 属性获取一个排除数组。
这是一个如何从 CharacterBody2D 或任何其他碰撞对象节点使用它的示例:
1 | extends CharacterBody2D |
例外数组可以包含对象或 RID。
碰撞遮罩
虽然例外方法适用于排除父体, 但如果需要大型和/或动态的例外列表, 则会变得非常不方便. 在这种情况下, 使用碰撞层/遮罩系统要高效得多.intersect_ray() 参数对象也可以提供一个碰撞掩码。
例如,要使用与父物体相同的掩码,请使用 collision_mask 成员变量。排除数组也可以作为最后一个参数提供:
1 | extends CharacterBody2D |
来自屏幕的 3D 光线投射
将射线从屏幕投射到 3D 物理空间对于拾取对象非常有用。
没有必要这样做,因为 CollisionObject3D 有一个“input_event”信号,可以让你知道何时点击它,但如果你希望手动执行该操作,可这样。
要从屏幕投射光线,你需要一个 Camera3D 节点。
Camera3D 可以有两种投影模式:透视和正交。
因此,必须获取射线原点和方向。
这是因为 origin 在正交模式下会发生变化,而 normal 在透视模式下会发生变化:
要使用相机获取它, 可以使用以下代码:
1 | const RAY_LENGTH = 1000.0 |
请记住,在 _input() 期间空间可能被锁定,所以实践中应该在 _physics_process() 中运行这个查询。
布娃娃系统
布娃娃依靠物理模拟来创建逼真的程序式动画被用于许多游戏中的死亡动画。
PhysicalBone3D 节点。
为了简化设置,你可以使用骨架节点中的“创建物理骨架”功能生成 PhysicalBone 节点。
选择 Skeleton 节点。顶部栏菜单上显示骨架按钮,创建物理骨架。
Godot 将为骨架中的每个骨骼生成 PhysicalBone 节点和碰撞形状,并用钉关节将它们连接在一起:
一些生成的骨骼不是必需的:例如 MASTER 骨骼。因此,我们将通过删除它们来清理骨架。
清理骨骼
引擎需要模拟的每一个 PhysicalBone 都有性能成本, 所以你要把每一个太小的骨头都去掉, 以便所有的实用骨头在模拟中发挥作用.
例如, 如果我们拿一个人形动物来说, 不希望每个手指都有物理骨骼. 可以用一根骨头代替整个手, 或者用一根骨头代替手掌, 一根骨头代替拇指, 最后一根骨头代替其他四个手指.
删除这些物理骨骼:MASTER、waist、neck、headtracker。这样就有了一个优化的骨架,使其更容易控制布娃娃。
碰撞形状调整
接下来的任务是调整碰撞形状和物理骨骼的大小, 以匹配每个骨骼应该模拟的身体部位.
关节调整
一旦你调整了碰撞形状,布娃娃就差不多准备好了。只需要调整钉关节以获得更好的模拟效果。
PhysicalBone 节点在默认情况下有一个不受限制的钉关节。
要改变钉关节,请选择 PhysicalBone 并在 Joint 部分改变约束类型。在那里,你可以改变约束的方向和限制。
模拟布娃娃
要开始模拟并播放布娃娃动画需调用 physical_bones_start_simulation 方法。
将脚本附加到骨架节点并在 _ready 方法中调用该方法:
1 | func _ready(): |
要停止模拟, 请调用 physical_bones_stop_simulation() 方法.
碰撞层与遮罩
确保正确设置碰撞层和遮罩,这样 CharacterBody3D 的胶囊不会妨碍物理模拟
运动学角色(2D)
动力学角色控制器使用的是一个具有无限惯性张量的刚体。
这是一个不能旋转的刚体. 物理引擎总是让物体移动和碰撞, 然后一并解决它们的碰撞.
这使得动态角色控制器能够与其他物理对象无缝交互, 就像在平台游戏演示中看到的那样.
然而, 这些互动并不总是可预测的.
碰撞可能需要多于一帧的时间来解决, 所以几个碰撞可能看起来会有很小的位移. 这些问题是可以解决的, 但需要一定的技巧.
运动学角色控制器总是假设以非碰撞状态开始,并将总是移动到非碰撞状态。
如果它开始时处于碰撞状态, 将像刚体一样尝试释放自己, 但这是特例, 而不是规则.
这使得它们的控制和运动更可预测, 更容易编程. 然而, 有一个缺点, 它们不能直接与其他物理对象交互, 除非在代码中手动完成.
管理运动物体或角色的逻辑建议使用物理过程处理.CharacterBody2D.move_and_collide() 该函数以 Vector2 作为参数,并尝试将该运动应用于运动学物体。如果发生碰撞,它会在碰撞发生时立即停止。
1 | extends CharacterBody2D |
使用 SoftBody3D
SoftBody3D 节点用于柔性物体模拟。
创建一个弹性立方体:
- 创建以 Node3D 为根的新场景。
- 创建 SoftBody3D 节点。在网格属性中添加 CubeMesh。
- 增加网格的细分(subdivide_XXX)以进行模拟。
- 设置参数以获得你想要的柔体类型。尽量保持 Simulation Precision(模拟精度)的数值高于 5,否则该柔体结构可能会瓦解。

碰撞形状(2D)
Godot提供了以下基本碰撞形状类型:
- RectangleShape2D 矩形形状 2D
- CircleShape2D 圆形 2D
- CapsuleShape2D 胶囊体形状 2D
- SegmentShape2D 线段形状 2D
- SeparationRayShape2D (专为角色设计)
- WorldBoundaryShape2D (无限平面)
建议动态对象使用原始图形(如 RigidBodies 和 KinematicBodies),因为它们的行为是可靠的,通常也能提供更好的性能。
凸型碰撞形状
凸碰撞形状 是图元碰撞形状和凹碰撞形状之间的折衷。
它们可以表示任何复杂程度的形状,但有一个重要的注意事项。
顾名思义,单个形状只能表示凸形状。例如,金字塔是凸的,而空心盒子是凹的。要用单个碰撞形状定义凹物体,你需要使用凹碰撞形状。
根据对象的复杂程度, 可能要通过使用多个凸形而不是一个凹形碰撞形状来获得更好的性能.Godot可以使用 凸分解 来生成与空心物体大致匹配的凸形.
请注意, 在一定数量的凸形之后, 就没有了这种性能优势, 对于大而复杂的对象, 如整个关卡, 建议使用凹形代替.
凹面或三面体碰撞形状
凹面碰撞形状 ,也称为三网格碰撞形状,可以采用任何形式,从几个三角形到数千个三角形。
凹形是 Godot 中最慢的选择,但也是最准确的。你只能在静态形体中使用凹形。
它们不适用于角色主体或刚体,除非刚体的模式为 静态(Static) 。
即使凹形提供了最准确的 碰撞, 但触碰信息的精度可能不如基础形状.
在不使用 TileMap 进行关卡设计时,凹形是关卡碰撞的最佳方法。
您可以在检查器中配置CollisionPolygon2D节点的构建模式。
如果将其设置为 实体 (默认值),则碰撞将包括多边形及其包含的区域。
如果设置为 线段(Segments),则碰撞将仅包括多边形边。
你可以通过选择 Sprite2D,并使用 2D 视口顶部的 Sprite2D 菜单,从编辑器生成凹碰撞形状。
Sprite2D 菜单下拉菜单显示一个名为 创建 CollisionPolygon2D 同级节点 的选项。
点击它后,它会显示一个包含 3 个设置的菜单:
- 简化:较高的值将导致形状的细节较少,从而以准确性为代价来提高性能。
- 收缩(像素):较高的值将使生成的碰撞多边形相对于精灵的边缘收缩。
- 增长(像素):数值越大,生成的碰撞多边形就越会相对于精灵边缘增长。
需要注意的是,将 “增长 ”和 “收缩 ”设置为相等的值,产生的结果可能会与将它们都设置为 0 的结果不同。
如果你的图片包含许多小细节,建议创建一个简化版本并使用它来生成碰撞多边形。
这样可以带来更好的性能表现和游戏体验,因为玩家不会被小小的、装饰性的细节阻碍。
要使用单独的图像生成碰撞多边形,可创建另一个 Sprite2D,从中生成一个碰撞多边形的同级节点,然后移除 Sprite2D 节点。这样就可以将小细节排除在生成的碰撞之外。
性能方面的注意事项
每个 PhysicsBody(物理体)不限于一个碰撞形状。尽管如此,我们还是建议尽量减少碰撞形状的数量以提高性能。
特别是对于像 RigidBody(刚体)和 CharacterBody(角色体)这样的动态对象。
除此之外,避免平移、旋转或缩放碰撞形状,以从物理引擎的内部优化中受益。
在 StaticBody 中使用单个未变换的碰撞形状时,引擎的宽相位算法可以丢弃不活跃的 PhysicsBody。这个窄相位只需考虑到活跃物体的形状。
如果一个 StaticBody 有许多碰撞形状, 那么宽相位就会失败。较慢的窄相位必须对每个形状执行碰撞检查。
如果遇到性能问题,你可能需要在准确性方面进行权衡。
大多数游戏都没有100%的精确碰撞。他们找到了一些具有创造性的方法来隐藏它,或者在正常的游戏中让它变得不被人注意到。
碰撞形状(3D)
Godot提供了以下基本碰撞形状类型:
- BoxShape3D 盒子形状 3D
- SphereShape3D 球体形状 3D
- CapsuleShape3D 胶囊体形状 3D
- CylinderShape3D 圆柱体形状 3D
凸型碰撞形状
凸碰撞形状 是图元碰撞形状和凹碰撞形状之间的折衷。它们可以表示任何复杂程度的形状,但有一个重要的注意事项。顾名思义,单个形状只能表示凸形状。例如,金字塔是凸的,而空心盒子是凹的。要用单个碰撞形状定义凹物体,你需要使用凹碰撞形状。
根据对象的复杂程度, 可能要通过使用多个凸形而不是一个凹形碰撞形状来获得更好的性能.Godot可以使用 凸分解 来生成与空心物体大致匹配的凸形. 请注意, 在一定数量的凸形之后, 就没有了这种性能优势, 对于大而复杂的对象, 如整个关卡, 建议使用凹形代替.
你可以通过选择 MeshInstance3D 并使用 3D 视口顶部的网格菜单从编辑器生成一个或多个凸碰撞形状。编辑器提供两种生成模式:
- 创建单凸碰撞同级 使用Quickhull算法, 创建一个CollisionShape碰撞形状节点, 并自动生成一个凸碰撞形状, 由于只生成单个形状, 因此提供了良好的性能, 非常适合小对象.
- 创建多个凸形碰撞同级 使用V-HACD算法. 创建多个CollisionShape碰撞形状节点, 每个节点都有一个凸形, 由于它能生成多个形状, 所以对于凹形物体来说, 精度更高, 但性能不佳. 对于中等复杂度的对象, 可能会比使用单个凹形碰撞形状更快.
凹面或三面体碰撞形状
凹面碰撞形状 ,也称为三网格碰撞形状,可以采用任何形式,从几个三角形到数千个三角形。
凹形是 Godot 中最慢的选择,但也是最准确的。 你只能在静态形体中使用凹形。
它们不适用于角色主体或刚体,除非刚体的模式为 静态(Static) 。
即使凹形提供了最准确的 碰撞, 但触碰信息的精度可能不如基础形状.
当不使用网络地图进行关卡设计时, 凹形是关卡碰撞的最佳方法, 也就是说, 如果关卡有一些小细节, 可能希望将这些细节排除碰撞之外, 以保证性能和游戏体验, 要做到这一点, 可以在3D建模中建立一个简化的碰撞网格, 并让Godot为其自动生成一个碰撞形状. 下面会有更多的介绍
请注意, 与基础形状和凸形状不同, 凹形碰撞形状没有实际的 “体积”, 既可以将对象放置在形状的 外侧, 也可以放置在 内侧.
选中 MeshInstance3D,然后使用 3D 视口顶部的网格菜单就可以在编辑器中生成一个凹形碰撞形状。编辑器提供了两个选项:
创建三角网格网格静态体是个方便的选择. 它创建一个包含与网格几何学匹配的凹形的静态体.
Create Trimesh Collision Sibling creates a CollisionShape node with a concave shape matching the mesh’s geometry.
创建三网格碰撞同级(Create Trimesh Collision Sibling) 创建一个 CollisionShape 节点,其凹形形状与网格体的几何体匹配。
参见
关于如何为 Godot 导出模型,以及如何在导入时自动生成碰撞形状,见 导入 3D 场景。
性能方面的注意事项
每个 PhysicsBody(物理体)不限于一个碰撞形状。尽管如此,我们还是建议尽量减少碰撞形状的数量以提高性能。特别是对于像 RigidBody(刚体)和 CharacterBody(角色体)这样的动态对象。除此之外,避免平移、旋转或缩放碰撞形状,以从物理引擎的内部优化中受益。
在 StaticBody 中使用单个未变换的碰撞形状时,引擎的宽相位算法可以丢弃不活跃的 PhysicsBody。
这个窄相位只需考虑到活跃物体的形状。如果一个 StaticBody 有许多碰撞形状, 那么宽相位就会失败。较慢的窄相位必须对每个形状执行碰撞检查。
如果遇到性能问题,你可能需要在准确性方面进行权衡。大多数游戏都没有100%的精确碰撞。
他们找到了一些具有创造性的方法来隐藏它,或者在正常的游戏中让它变得不被人注意到。
大世界坐标
大世界坐标主要用于 3D 项目;2D 项目很少会用到。此外,启用大世界坐标后,2D 渲染目前无法从精度的增加中获益,但 3D 渲染可以。
在 Godot 中,物理仿真和渲染都依赖于浮点数。
然而,计算机中浮点数的精度和范围是有限的,可能在太空、星球尺度的仿真游戏等拥有庞大世界的游戏中产生问题。
浮点数的精度在 0.0 附近最高。
在实践中,这意味着玩家远离世界原点(2D 游戏的 Vector2(0, 0) 和 3D 游戏的 Vector3(0, 0, 0)),精度就会下降。
精度的丢失可能会导致远离世界原点的对象看上去在“抖动”,因为模型的位置会吸附到最接近的浮点数能够表示的值。
这种情况下,如果玩家远离世界原点,还可能导致物理方面的问题。
范围决定的是所能够存储的最小和最大值。如果玩家尝试移出这个范围就会直接失败。但是实际情况下,在能够受到范围影响之前几乎都会遇到浮点数精度问题。
范围和精度(两个指数间隔的最小步长)取决于浮点数的类型。
单精度浮点数的理论范围支持存储极高的值,单精度很低。
实践中,无法表示所有整数值的浮点数类型并不是很有用。极值附近的精度会变得非常低,低到连两个整数值也无法区分。
以下是浮点数能够表示整数值的范围:
单精度浮点数范围(表示所有整数):-16,777,216 和 16,777,216 之间
双精度浮点数范围(表示所有整数):-9 千万亿和 9 千万亿之间
大世界坐标的工作原理
大世界坐标(也叫双精度物理)能够增加引擎中所有浮点数计算的精度级别。
在 GDScript 中,float 默认为 64 位,但 Vector2, Vector3 和 Vector4 为 32 位。
这意味着向量类型的精度受到很大限制。为了解决这个问题,我们可以增加向量类型中用于表示浮点数的位数。
这样一来,精度就会呈指数增长,这意味着最终值的精度不仅提高了一倍,而且在数值较高时,精度可能会提高数千倍。
从单精度浮点数到双精度浮点数,可表示的最大值也大大增加。
为了避免远离世界原点时出现模型吸附(model snapping)问题,Godot 的 3D 渲染引擎将在启用大世界坐标时提高渲染的精度。
出于性能原因,着色器不使用双精度浮点数,但会使用 替代解决方案 来模拟双精度,以便使用单精度浮点数进行渲染。
备注
只有确实需要大世界坐标时才启用它,因为启用大世界坐标会对性能和内存占用带来负面影响,这种负面影响在32位CPU上更加明显。
此功能专为中端/高端桌面平台量身定制。
大世界坐标在低端移动设备上可能表现不佳,除非你采取措施通过其他方式来减少 CPU 使用率(例如减少每秒的物理循环 physics tick)。
在低端平台上,可以使用原点移位方法来实现大型世界,而无需使用双精度物理和渲染。
原点移位适用于单精度浮点数,但它会给游戏逻辑带来更多复杂性,尤其是在多人游戏中。因此,本页不会详细介绍原点移位。
物理插值
启用物理插值:项目设置 > 物理 > 通用 > 物理插值
确保在_physics_process()中移动物体、运行游戏逻辑,不要在 _process() 中进行。
包括物体的直接移动和间接移动(例如移动父级、使用其他机制自动移动节点)。
确保在首次定位或传送节点之后调用 Node.reset_physics_interpolation,防止出现“拖影”现象。
暂时将项目设置 > 物理 > 通用 > 每秒物理周期数设置为 10,观察启用和禁用插值时的区别。
在 Godot 中需要理解的一个关键概念是物理刻度(有时称为迭代或物理帧)和渲染帧之间的区别。
物理以固定的滴答率进行(在 项目设置(Project Settings > Physics > Common > Physics Tick per Second) 中设置),默认为每秒 60 刻。
但是,引擎不一定以相同的速率渲染 。尽管许多显示器以 60 Hz(每秒周期数)刷新,但许多显示器以完全不同的频率(例如 75 Hz、144 Hz、240 Hz 或更高)刷新。即使显示器可能能够显示新帧,例如每秒 60 次,也不能保证 CPU 和 GPU 能够以这种速率提供帧。例如,当使用 V-Sync 运行时,计算机对于 60 FPS 来说可能太慢,并且只能达到 30 FPS 的截止日期,在这种情况下,您看到的帧将以 30 FPS 的速度变化(导致卡顿)。
但这里有一个问题。如果物理刻度与帧不重合会发生什么?
如果物理滴答率与帧速率异相,会发生什么情况?
或者更糟糕的是,如果物理滴答率低于渲染的帧率,会发生什么?
如果我们考虑一个极端情况,这个问题就更容易理解了。
如果将物理滴答率设置为每秒 10 刻,则在渲染帧速率为 60 FPS 的简单游戏中。
如果我们根据渲染的帧绘制对象位置的图表,您可以看到位置似乎每 1/10 秒“跳跃”一次,而不是给出平滑的运动。
当物理场为新对象计算新位置时,它不会仅在该位置渲染一帧,而是渲染 6 帧。
这种跳跃可以在滴答/帧速率的其他组合中看到,如故障或抖动,这是由于物理滴答时间和渲染帧时间之间的差异而导致的阶梯效应。
渲染帧和物理周期不同步怎么办?
是否要将物理周期和渲染帧锁定?
最明显的解决方案是通过确保每一帧都有一个与每一帧重合的物理滴答来摆脱这个问题。
这曾经是旧游戏机和固定硬件计算机上的方法。
如果您知道每个玩家都将使用相同的硬件,您可以确保它足够快,可以以例如 50 FPS 的速度计算滴答和帧,并且您将确保它对每个人都适用。
然而,现代游戏通常不再为固定硬件制作。您通常会计划在台式电脑、手机等设备上发布。
所有这些在性能上都有巨大的差异,以及不同的显示器刷新率。我们需要想出一种更好的方法来处理这个问题。
适应物理周期率?
我们可以允许滴答率根据最终用户的硬件进行缩放,而不是以固定的物理滴答率设计游戏。
例如,我们可以使用适用于该硬件的固定滴答率,甚至可以改变每个物理滴答的持续时间以匹配特定的帧持续时间。
这有效,但存在一个问题。物理( 和游戏逻辑 ,通常也在 _physics_process 中运行)在 固定的、预定的滴答率。如果您尝试以 10 TPS 等速度运行设计为 60 TPS(每秒滴答声)的赛车游戏物理,则物理性能将完全不同。控制可能反应较慢,碰撞/轨迹可能完全不同。
您可以在 60 TPS 下彻底测试您的游戏,然后发现当它以不同的滴答率运行时,它在最终用户的机器上会中断。
这可能会使质量保证变得困难,因为难以重现错误,尤其是在 AAA 游戏中,此类问题可能代价高昂。
对于多人游戏来说,为了竞争的完整性,这也可能是一个问题,因为以一定的滴答率运行游戏可能比其他游戏更有利。
锁定物理周期率,但在物理周期之间使用插值让渲染帧平滑
这已成为处理该问题的最流行的方法之一,尽管它是可选的并且默认禁用。
我们已经确定,为了保持一致性和可预测性,最理想的物理/游戏逻辑安排是在设计时固定的物理滴答率。
问题在于记录的物理位置与我们“希望”物理对象显示在帧上以提供平滑运动的位置之间的差异。
答案很简单,但一开始可能有点难以理解。
我们不仅跟踪物理对象在引擎中的当前位置,还跟踪对象的当前位置和上一个物理刻度上的前一个位置。
为什么我们需要前一个位置 (实际上是整个变换,包括旋转和缩放)?
通过使用一点数学魔法,我们可以使用插值 要计算对象在这两点之间的变换,请在 我们理想的平稳连续运动世界。
线性插值
实现此目的的最简单方法是线性插值或 lerping,您以前可能使用过它。
让我们只考虑位置,以及我们知道之前的物理刻度 X 坐标是 10 个单位,而当前的物理刻度 X 坐标是 30 个单位的情况。
备注
虽然这里解释了数学,但您不必担心细节,因为这一步将为您执行。
在幕后,戈多可能会使用更复杂的插值形式,但线性插值在解释方面是最简单的。
物理插值比例
假设(在这个例子中)物理周期是每秒 10 次,那么渲染帧位于 0.12 秒的话会发生什么呢?要实现两个周期之间的平滑运动,物体所处的位置,我们来解一个数学题就知道了。
首先,我们要计算物体在物理周期中经过了多久。如果上一个物理周期在 0.1 秒,那么我们就在物理周期中经过了 (0.12 - 0.1) 即 0.02 秒,因为一个周期总共 0.1 秒(每秒 10 个周期)。所以周期中的比例就是:
fraction = 0.02 / 0.10
fraction = 0.2
这个值叫做物理插值比例 ,Godot 会帮你计算好,可以在任何帧中调用 Engine.get_physics_interpolation_fraction 来获取。
计算插值位置
得到插值比例后,我们就可以把它代入标准线性插值公式。所以 X 坐标就是:x_interpolated = x_prev + ((x_curr - x_prev) * 0.2)
将 x_prev 替换为 10,将 x_curr 替换为 30:
1 | x_interpolated = 10 + ((30 - 10) * 0.2) |
让我们来拆解一下:
我们知道 X 从上一周期的坐标(x_prev)开始,该坐标为 10 个单位。
我们知道在完整的一个周期之后,会加上当前周期与上一周期之间的差值(x_curr - x_prev)(即 20 个单位)。
我们唯一需要改变的是我们添加的这种差异的比例,根据我们在物理刻度中的距离。
备注
尽管此示例对位置进行插值,但对对象的旋转和缩放也可以执行相同的作。没有必要知道细节,因为戈多会为您完成这一切。
物理周期之间的平滑变换?
将所有这些放在一起表明,应该可以对当前和之前的物理刻度之间的对象变换进行很好的平滑估计。
但是等等,你可能已经注意到了。如果我们是在当前周期和上一个周期之间进行插值,那我们并不是在估计物体现在的位置,而是在估计物体过去的位置。
准确地说,我们是在估计物体在过去的 1 到 2 个周期之间的位置。
过去
这是什么意思?这种方案确实可行,但也意味着我们本质上是在屏幕上看到的内容和物体应该在的位置之间引入了一个延迟。
在实践中,大多数人不会注意到这种延迟,或者更确切地说,通常不会 令人反感 。游戏已经存在严重的延迟,我们通常不会注意到它们。最显着的影响是输入可能会有轻微的延迟,这可能是快肌游戏的一个因素。在其中一些快速输入情况下,你可能希望关闭物理插值并使用不同的方案,或者使用高滴答率,以减轻这些延迟。
为什么回顾过去?为什么不预测未来?
这种方案还有另一种选择,那就是:我们不是在前一个和当前的刻度之间进行插值,而是使用数学来推断未来。
我们试图预测物体的位置 ,而不是显示它的位置。这是可以做到的,并且将来可能会作为一种选择提供,但有一些明显的缺点:
预测可能不正确,尤其是当一个对象在物理滴答期间与另一个对象发生碰撞时。
如果预测不正确,物体可能会外推到“不可能”的位置,例如在墙内。
如果移动速度较慢,这些错误的预测可能不会有太大问题。
当预测不正确时,对象可能必须跳转到或弹回校正后的路径上。这在视觉上可能会很不和谐。
固定时间步长插值
在 Godot 中,整个系统被称为物理插值,但您可能也听到它被称为 “固定时间步长插值”,因为它是在以固定时间步长(物理每秒滴答声)移动的物体之间进行插值。
在某些方面,第二项更准确,因为它也可以用来插值不受物理驱动的物体。
小技巧
尽管物理插值通常是一个不错的选择,但也有例外情况,您可以选择不使用 Godot 的内置物理插值(或以有限的方式使用它)。
一个示例类别是互联网多人游戏。多人游戏通常会从其他玩家或服务器接收基于滴答或计时的信息,这些信息可能与本地物理滴答不一致,因此自定义插值技术通常更适合。
使用物理插值
如何在 Godot 游戏中加入物理插值?有什么注意事项吗?
我们试图使系统尽可能易于使用,许多现有游戏只需进行很少的更改即可运行。也就是说,有些情况需要特殊处理,我们将对此进行描述。
启用物理插值设置
第一步是在 项目设置 > 物理 > 通用 > 物理插值 您现在可以运行游戏了。
可能看起来没有什么大不相同,特别是当您以 60 TPS 或其倍数运行物理时。然而,幕后还发生了更多的事情。
小技巧
要将现有游戏转换为使用插值,强烈建议 您临时设置 项目设置 > 物理 > 常用 > 物理每秒滴答 设置为 10 等低值,这将使插值问题更加明显。
将(几乎)所有游戏逻辑从 _process 移到 _physics_process
物理插值的最基本要求(您可能已经在这样做)是您应该在 _physics_process 内(以物理滴答运行)而不是 _process 内对对象移动和执行游戏逻辑 (在渲染帧上运行)。
这意味着您的脚本通常应该执行 _physics_process 内的大部分处理,包括响应输入和人工智能。
仅在物理刻度内设置对象的变换允许自动插值处理物理刻度之间的变换,并确保游戏在运行的任何机器上都运行相同的内容。
作为奖励,如果游戏以高 FPS 渲染,这也会降低 CPU 使用率,因为 AI 逻辑(例如)将不再在每个渲染帧上运行。
备注
如果尝试在物理刻度之外设置插值对象的变换,则插值位置的计算将不正确,并且会出现抖动。
这种抖动在您的机器上可能不可见,但对于某些玩家来说会出现这种情况 。
因此,应避免在物理滴答之外设置插值对象的变换。如果检测到这种情况,Godot 将尝试在编辑器中生成警告。
小技巧
这只是一个软规则 。在某些情况下,你可能希望将对象传送到物理刻度之外(例如,在开始关卡或重生对象时)。
不过,一般来说,你应该应用物理更新中的变换。
确保所有间接移动都在物理周期中进行
考虑到在 Godot 中,节点不仅可以直接在您自己的脚本中移动,还可以通过补间、动画和导航等自动方法移动。如果您使用所有这些方法来移动对象, 则还应将其计时设置为在物理刻度上运行,而不是在每一帧(“空闲”)上运行( 这些方法也可用于控制未插值的属性 )。
备注
此外,还要考虑节点不仅可以通过移动自身来移动节点,还可以通过移动场景树中的父节点来移动节点。因此,父母的运动也应该只发生在物理滴答声期间。
选择物理周期率
使用物理插值时,渲染与物理解耦,你可以根据自己的游戏选择合适的值,不再受限于用户显示器刷新率的倍数(如果达到目标 FPS,可以实现无卡顿的游戏体验)。
大致的参考:
低周期率(10-30)中等周期率(30-60)高周期率(60+)
CPU 性能更佳 复杂场景中物理行为良好 适合快速物理
引入一些输入延迟 适合第一人称游戏 适合赛车游戏
简单物理行为
您可以随时在开发时更改滴答率,就像更改项目设置一样简单。
传送对象时调用 reset_physics_interpolation()
大多数时候,插值是你想要在两个物理刻度之间进行的。
但是,在一种情况下,它可能不是您想要的。
这是您最初放置对象或将它们移动到新位置时。
在这里,您不希望对象所在的位置(例如原点)和初始位置之间有平滑的运动 - 您需要瞬时移动。
解决此问题的方法是调用 Node.reset_physics_interpolation 功能。
此功能在后台的作用是将内部存储的 对象的上一个变换等于当前变换 。
这确保了在这两个相等的变换之间插值时,不会有移动。
即使您忘记调用此电话,在大多数情况下通常也不会成为问题(尤其是在高滴答率下)。
您可以轻松地将此留给游戏的打磨阶段。最糟糕的情况是,当您移动它们时,会看到一帧左右的条纹运动 - 您会知道何时需要它!
实际上有两种使用 reset_physics_interpolation() 的方法:立定起步(例如玩家)
- 设置初始变换
- 调用 reset_physics_interpolation()
上一个变换和当前变换相同,因此不会产生初始移动。
移动起步(例如子弹)
- 设置初始变换
- 调用 reset_physics_interpolation()
- 第一个运动周期后会立即设置变换
上一个变换将是起始位置,当前变换将表现得好像已经发生了模拟。
这将立即开始移动对象,而不是静止不动的刻度延迟。
重要
确保设置转换并调用 reset_physics_interpolation() 按上述正确的顺序,否则您将看到不需要的“条纹”。
测试和调试贴士
即使您打算以 60 TPS 运行物理,为了彻底测试您的插值并获得最流畅的游戏体验,强烈建议暂时将物理滴答率设置为较低的值,例如 10 TPS。
游戏玩法可能并不完美,但它应该使您能够更轻松地看到应该调用 Node.reset_physics_interpolation 的情况, 或者您应该在 相机 3D。
修复这些情况后,您可以将物理滴答率设置回所需的设置。
以低滴答率进行测试的另一大优势是,您经常会注意到其他游戏系统与物理滴答同步,并产生您可能想要解决的故障。
典型的示例包括设置动画混合值,您可以决定在 _process() 中设置并手动插值。
自动物理插值的预期
即使物理插值处于活动状态,也可能存在一些局部情况 禁用自动插值将使 节点 (或 SceneTree 的分支),并可以更精细地控制手动执行插值。
这可以使用 Node.physics_interpolation_mode 属性,该属性存在于所有节点中。
例如,如果关闭插值 对于节点,子项也将递归受到影响(因为它们默认为 继承父设置)。这意味着您可以轻松地禁用 整个子场景。
你可能想要执行自己的插值的最常见情况是摄像机。
相机
在许多情况下,Camera3D 可以像任何其他节点一样使用自动插值。
但是,为了获得最佳结果,尤其是在低物理滴答率下,建议您采用手动方法进行摄像机插值。
这是因为观众对镜头运动非常敏感。例如,每 1/10 秒轻微重新对齐一次(以 10tps 滴答率)的 Camera3D 通常会很明显。
通过 _process 每帧移动摄像机并手动跟随插值目标,可以获得更平滑的结果。
手动相机插值
确保相机使用全局坐标空间
执行手动相机插值时的第一步是确保在全局空间中指定 Camera3D 变换,而不是继承移动父级的变换。
这是因为在 Camera3D 的父节点的移动和相机节点本身的移动之间可能会发生反馈,这可能会扰乱插值。
有两种方法可以做到这一点:
- 移动 Camera3D,使其独立于自己的分支,而不是移动对象的子项。
- 调用 Node3D.top_level 并将此设置为 true,这将使摄像机忽略其父级的变换。
典型案例
自定义方法的一个典型示例是在 _process() 中的每一帧中使用 Camera3D 中的 look_at 函数来查看目标节点(例如播放器)。
但有一个问题。如果我们在 Camera3D“目标”节点上使用传统的 get_global_transform(), 则此变换只会将 Camera3D 聚焦在当前物理刻度的目标上。
这不是我们想要的,因为当目标移动时,摄像机会在每个物理刻度上跳跃。
即使摄像机可能每帧更新一次,但如果目标只改变每个物理刻度,这无助于提供平滑的运动。
get_global_transform_interpolated()
我们真正想要聚焦摄像机的不是目标在物理刻度上的位置,而是插值位置,即目标将被渲染的位置。
我们可以使用 Node3D.get_global_transform_interpolated 功能。
这就像得到 Node3D.global_transform 但它为您提供了插值转换 (在 _process() 调用期间)。
重要
对于相机等特殊情况, get_global_transform_interpolated() 只能使用一次或两次。
它不应该在代码中随处使用(无论是出于性能原因还是为了提供正确的游戏玩法)。
备注
除了相机等例外情况外,在大多数情况下,您的游戏逻辑应该采用 _physics_process() 格式。
在游戏逻辑中,您应该调用 get_global_transform() 或 get_transform() ,这将给出当前的物理变换(分别在全局或局部空间中),这通常是你想要的游戏代码。
手动相机示例脚本
下面是一个简单的固定相机的示例,它跟随插值目标:
1 | extends Camera3D |
鼠标外观
鼠标外观是控制相机的一种非常常见的方式。但有一个问题。
与可以在物理刻度上定期采样的键盘输入不同,鼠标移动事件可以连续出现。
相机应该在下一帧做出反应并跟随这些鼠标移动,而不是等到下一个物理滴答声。
在这种情况下,最好禁用摄像机节点的物理插值(使用 Node.physics_interpolation_mode) 并直接将鼠标输入应用于相机旋转,而不是将其应用于 _physics_process。
有时,尤其是对于相机,您需要使用插值和非插值的组合:
第一人称摄像机可能会将摄像机放置在玩家位置(可能使用 Node3D.get_global_transform_interpolated),但从鼠标外观控制摄像机旋转,而不进行插值。
第三人称摄像机可以使用类似的方式确定摄像机的注视(目标位置) Node3D.get_global_transform_interpolated,但使用鼠标外观(不插值)定位摄像机。
摄像机类型有许多排列和变化,但应该清楚的是,在许多情况下,禁用自动物理插值并自行处理可以获得更好的结果。
禁用其他节点上的插值
尽管摄像机是最常见的示例,但在许多情况下,您可能希望其他节点控制自己的插值,或者不插值。例如,考虑俯视图游戏中的玩家,其旋转由鼠标外观控制。禁用物理旋转允许玩家旋转实时匹配鼠标。
MultiMesh多网格体
尽管大多数可视化节点都遵循单节点单可视化实例范式, 多网格体可以控制来自同一节点的多个实例。
因此,他们有 一些额外的函数,用于控制插值功能 基于每个实例 。
如果你使用的是插值多网格体,你应该探索这些函数。
MultiMesh.reset_instance_physics_interpolation
MultiMesh.set_buffer_interpolated
详情见 MultiMesh 文档。
2D 和 3D 物理插值
在 3D 中,物理插值对每个 3D 实例的全局变换独立执行。
在 2D 中,物理插值是在局部变换上执行的 每个 2D 实例的 2D 实例。
在 3D 中,通过 physics_interpolation_mode 在每个节点的级别轻松打开和关闭插值 属性,可以设置为 On、Off 或 Inherited。
然而,这意味着在 3D 中,场景树中出现的枢轴(由于父子关系)只能在物理刻度上大致插值。在大多数情况下,这并不重要,但在某些情况下,插值可能看起来有点错误。
在 2D 中,插值的局部变换在渲染过程中传递给子项。这意味着,如果父级将 physics_interpolation_mode 设置为 On,但子项设置为 Off,则如果父项移动,子项仍将值。 只有子项的局部转换是未插值的。 因此,控制 2D 节点的开/关行为需要更多 思考和计划。
从积极的一面来看,场景树中的枢轴行为在 2D 插值期间得到了完美的保留,从而提供了超级平滑的行为。
重置物理插值
每当对象移动到一个全新的位置,并且不需要插值(以防止“条纹”伪影)时,用户有责任调用 reset_physics_interpolation()。
好消息是,在 2D 中,当节点首次进入树时,这会自动为您完成。这减少了样板,并减少了使现有项目正常运行所需的工作量。
备注
如果您在添加到场景树后移动对象,您仍然需要像 3D 一样调用 reset_physics_interpolation()。
2D 粒子
目前,只有 CPUParticles2D 支持 2D 物理插值。
建议使用每秒至少 20-30 刻的物理滴答率,以保持粒子看起来流畅。
Particles2D(GPU 粒子)尚未插值,因此目前建议转换为 CPUParticles2D(但保留 Particles2D,以防我们让这些工作)。
其他
get_global_transform_interpolated() 目前仅适用于 3D。
MultiMesh 在 2D 和 3D 中均受支持。
物理问题的故障排除
高速运动的对象会互相穿透
这称为隧道。 在 刚体(RigidBody) 属性中启用 连续 CD(Continuous CD) 有时可以解决此问题。如果这没有帮助,您可以尝试其他解决方案:
使你的静态碰撞形状更厚。例如,如果你有一个玩家无法以某种方式穿过的薄地板,你可以使碰撞器比地板看起来更厚。
根据你的快速移动物体的运动速度调整其碰撞形状。物体移动得越快,碰撞形状应该越向外扩展,以确保它能够更可靠地与薄墙发生碰撞。
在高级项目设置中增加每秒物理周期数。虽然这有其他好处(例如更稳定的模拟和减少输入延迟),但这会增加 CPU 的使用率,可能不适用于移动或网页平台。对于大多数显示器来说,应该优先选择默认值 60 的倍数(如 120、180 或 240),以获得平滑的外观。
堆叠的对象摇摆不定
尽管看起来像一个简单问题,但是在物理引擎中带有堆叠物体的稳定刚体模拟很难实现。这是因为力结合后的相互作用。堆叠的物体越多,相互之间的作用力越强。这最终导致了模拟变得不稳定,使得物体不能相互堆叠在一起而不发生移动。
提升物理模拟频率可以帮助减少这个问题。为了这么做,增加高级项目设置中的 每秒物理周期数 。请注意,这会增加 CPU 的使用率,可能不适用于移动或网页平台。对于大多数显示器来说,应该优先选择默认值 60 的倍数(如 120、180 或 240),以获得平滑的外观。
在 3D 中,将物理引擎从默认的 GodotPhysics 切换到 Jolt 也可以提高稳定性。请参阅使用 Jolt Physics 了解更多信息。
缩放后的物理体或碰撞形状无法正确碰撞
Godot 当前没有支持缩放物理体或碰撞形状。作为替代方案,改变碰撞形状的范围而不是其缩放比例。如果你想让视觉表现的尺寸也发生变化,可以改变底层视觉表现(Sprite2D、MeshInstance3D……)并单独改变它的碰撞形状的范围。在这种情况下,请确保碰撞形状不是视觉表现节点的子节点。
由于资源默认是共享的,如果你不想让更改应用于场景中使用相同碰撞形状资源的所有节点,你需要让这个碰撞形状资源变得独一无二。这可以通过在碰撞形状资源的脚本中调用 duplicate() 方法来实现,然后在改变其尺寸之前进行操作。这样,更改将只影响当前的资源实例,而不会影响其他使用相同资源的节点。
当薄物体放在地板上时,它们会显得不稳定
可能是由以下两个原因之一造成的:
地板的碰撞形状太薄了。
刚体的碰撞形状太薄了。
在第一个场景中,这可以通过加厚地板的碰撞形状来缓解。例如,如果你有一个玩家不能以某种方式穿过的地板,你可以通过使碰撞器比地板看起来还要厚来解决。
在第二个场景中,常常只能通过提高物理仿真比率来解决(因为使形状更厚会导致刚体的视觉表现与其碰撞效果之间出现脱节)。
在这两种情况下,提高物理仿真的速率也可以帮助缓解这个问题。为此,可以在高级项目设置中增加 每秒物理周期数 。请注意,这会增加CPU的使用率,可能不适用于移动或网页平台。对于大多数显示器来说,应该优先选择默认值60的倍数(如120、180或240),以获得平滑的外观。
圆柱碰撞形状不稳定
将物理引擎从默认的 GodotPhysics 切换到 Jolt 应该会使圆柱体碰撞形状更加可靠。请参阅使用 Jolt Physics 了解更多信息。
在从Bullet到Godot 4中的GodotPhysics的过渡期间,圆柱体碰撞形状不得不从头开始重新实现。
然而,圆柱体碰撞形状是最难支持的形状之一,这就是为什么许多其他物理引擎不提供对它们的支持。目前已知圆柱体碰撞形状存在一些问题。
如果您坚持使用 GodotPhysics,我们建议您暂时为角色使用盒子或胶囊体碰撞形状。
盒子通常提供最佳的可靠性,但缺点是使角色在对角线上占用更多空间。胶囊体碰撞形状没有这个缺点,但它们的形状会使精确平台化变得更加困难。
VehicleBody 仿真不稳定,尤其是在速度较大的时候
当一个物理体以高速移动时,它在每一个物理步骤中移动很长的一段距离。
例如,当在 3D 中使用1单位等于1米的的标准时,一个以360 km/h速度移动的物体将会在每秒内移动100个单位长度。
使用默认的物理仿真频率 60 Hz时,该物体每个物理周期移动 ~1.67个单位长度。
这意味着小物体可能被物体完全忽略(由于隧道效应),同时这也意味着在如此高的速度下,仿真本身通常只有很少的数据可供处理。
快速移动的物体可以从被增加的物理仿真频率中得到很多好处。为了这么做,在高级项目设置中调高 每秒物理周期数。
请注意,这会提高CPU的使用率而且可能在移动或web平台上不适用。
对于大多数显示器来说,应该优先选择默认值60的倍数(如120、180或240),以获得平滑的外观。
当一个物体在瓦片上移动时,碰撞可能会导致颠簸
在物理引擎中,这里有一个因为物体在形状的边缘发生碰撞导致的已知问题,即使这条边被其它形状覆盖。这可以在2D和3D中触发。
解决该问题最好的代替方案是创建一个“复合”碰撞体。
这意味着不使用带有自己碰撞体的独立图块,而是创建一个单独的碰撞形状代表一组图块的碰撞体。
通常,你应该按照岛屿基础来分割复合碰撞器(这意味着每组相连的瓦片都有自己的碰撞器)。
在某些情况下,使用一个复合碰撞体也可以提升物理仿真性能。然而,由于复合碰撞体要复杂的多,这也许并不是在所有情况下都是对总体性能的提升。
小技巧
在 Godot 4.5 及更高版本中,使用 TileMapLayer 节点时会自动创建复合碰撞器。
块大小(默认情况下每个轴上有 16 个图块)可以使用 TileMapLayer 检查器中的 Physics Quadrant Size 属性进行设置。
较大的值提供更可靠的碰撞,但代价是更改 TileMap 时更新速度变慢。
当一个对象接触另一个对象时,帧率会下降
这很可能是因为其中一个物体使用的碰撞形状过于复杂。
出于性能原因,凸形碰撞形状应该尽可能使用最少数量的形状。当依赖于Godot的自动生成时,可能会为单个凸形碰撞资源创建数十甚至数百个形状。
在一些场景中,用一组基础的碰撞形状(盒,球体或胶囊)替换一个凸形碰撞器可能带来更好的性能。
这个问题也可能发生在使用非常详细的三角网格(凹形)碰撞的静态刚体(StaticBodies)上。
在这种情况下,使用简化的几何表示作为碰撞器。
这不仅可以显著提高物理模拟的性能,还可以通过让你移除小的固定装置和缝隙,从而不被碰撞考虑,来提高稳定性。
在 3D 中,将物理引擎从默认的 GodotPhysics 切换到 Jolt 也可以提高性能。请参阅使用 Jolt Physics 了解更多信息。
超过某个物理模拟量后帧率突然降至非常低的值
发生这种情况是因为物理引擎无法跟上预期的模拟速率。在这种情况下,帧速率将开始下降,但引擎只允许在每个渲染帧中模拟一定数量的物理步长。
这会像滚雪球一样滚雪球,帧率不断下降,直到达到非常低的帧率(通常为 1-2 FPS),这被称为物理死亡螺旋 。
为避免这种情况,您应该检查项目中是否存在可能导致同时发生过多物理模拟(或碰撞形状过于复杂的情况)。
如果无法避免这些情况,你可以增加 每帧最大物理步数(Max Physics Steps per Frame) 项目设置和/或减少 每秒物理滴答数(Physics Ticks per Second) 来缓解这种情况。
物理仿真在远离世界原点的地方是不可靠的
这是由浮点数的精度误差引起的,当距离世界原点越远时,误差越明显。
这个问题也影响渲染,当远离世界原点时可能会导致相机移动使摇晃。有关更多信息,参见 大世界坐标 。
平台相关
Android
Godot Android 库
Godot Android 插件
Android 应用内购买
与 Android API 集成
iOS 插件
创建 iOS 插件
iOS 的插件
Web
JavaScriptBridge 单例
HTML5 shell 类参考
导出自定义 HTML 页面
Godot 的游戏主机支持
插件
编辑器插件
区分插件
区分编辑器插件和非编辑器插件的方法是在存放插件的仓库中查找 plugin.cfg 文件。
如果仓库的 addons/ 中包含 plugin.cfg 文件,那么它就是编辑器插件。安装插件
解压 ZIP 压缩包,并将其包含的 addons/ 文件夹移动到你的项目文件夹中。启用插件
打开编辑器顶部的项目 > 项目设置,然后转到插件选项卡。
如果插件被正确打包,你应该会在插件列表中看到它。点击启用勾选框以启用该插件。(不需要重启)
制作插件
本教程将指导你创建两个插件:
- 可以添加到项目中的任何场景的自定义节点。
- 添加到编辑器的自定义停靠面板。
创建插件
首先创建两个文件:
一个是 plugin.cfg 用于配置和具有此功能的工具脚本.
插件在项目文件夹里面有一个标准路径, 比如 addons/plugin_name.Godot 提供了一个属性框, 用于生成这些文件并将它们放在需要的位置.
在主工具栏中,点击项目下拉菜单,然后点击项目设置…。然后转到插件选项卡,点击右上角的创建新插件按钮。
你会看到出现了一个对话框,类似这样:
每个字段中文本属性都描述了它会影响到哪些配置文件的值.
如果要继续使用该例子,请使用下列的值:
1 | Plugin Name: My Custom Node |
plugin.cfg 是一个包含插件元数据的 INI 文件。名称和描述可帮助人们了解插件的作用。你的名字有助于你获得适当的工作认可。
版本号可帮助其他人了解他们是否拥有过时的版本;如果你不确定如何得出版本号,请查看语义版本控制。脚本文件将指示 Godot 插件在激活后在编辑器中执行的操作。
脚本文件
创建插件后,对话框将自动为你打开 EditorPlugin 脚本。
该脚本有两个你无法更改的要求:
- 必须是一个 @tool 脚本,否则它将无法在编辑器中正确加载,并且它必须从 EditorPlugin 继承。
警告
除了 EditorPlugin 脚本之外,插件用到的其他 GDScript 也必须是工具脚本。
编辑器使用的任何没有 @tool 的 GDScript 都会像一个空文件一样!
处理资源的初始化和清理非常重要。
一个好的做法是使用虚函数 _enter_tree() 来初始化插件,并使用 _exit_tree() 来清理插件。
幸运的是,对话框会为你生成这些回调。你的脚本应该看起来像这样:
1 | @tool |
自定义节点
有时你希望在许多节点中存在某种行为, 例如可以重复使用的自定义场景或控件.
实例化在很多情况下都很有用, 但有时它会很麻烦, 特别是如果你在许多项目中使用它.
一个很好的解决方案是创建一个插件, 添加一个具有自定义行为的节点.
警告
通过 EditorPlugin 添加的节点是“CustomType”(自定义类型)节点。
虽然它们可以用于任何脚本语言,但功能比 Script 类系统少。如果你正在编写 GDScript 或 NativeScript,建议使用 Script 类代替。
要创建一个新的节点类型,你可以使用 EditorPlugin 类中的函数 add_custom_type()。
该函数可以向编辑器添加新类型(节点或资源)。但是,在创建类型之前,你需要一个脚本作为该类型的逻辑。
虽然该脚本不必使用 @tool 注解,但可以添加它以便脚本在编辑器中运行。
在本教程中,我们将创建一个按钮,当点击时会打印一条消息。为此,我们需要一个从 Button 扩展的脚本。
如果你愿意,它也可以扩展 BaseButton:
1 | @tool |
这就是我们的基本按钮。你可以在插件文件夹中将其保存为 my_button.gd。
你还需要一个 16×16 图标来显示在场景树中。
如果你没有,你可以从引擎中获取默认图标,并将其保存在 addons/my_custom_node 文件夹中作为 icon.png,或者使用默认的 Godot 徽标(preload(“res://icon.svg”))。
小技巧
用作自定义节点图标的 SVG 图像应具有 编辑器 > 缩放(Editor Scale) 和 编辑器 > 转换颜色(Editor > Convert Colors) 启用了编辑器主题导入选项 。
如果图标的设计与 Godot 自己的图标具有相同的调色板,则允许图标遵循编辑器的比例和主题设置。
现在,我们需要把它作为一个自定义类型添加,以便它显示在新建 Node 的对话框中。为此,将 custom_node.gd 脚本改为以下内容:
1 | @tool |
完成后, 插件应该已经在 项目设置 的插件列表中可用, 因此请按照 Checking the results 中的说明激活它.
然后通过添加新节点来尝试:
当你添加节点时, 你可以看到它已经有你创建的脚本附加在上面. 给这个按钮设置一个文本, 保存并运行场景. 当你点击按钮时, 你可以在控制台中看到一些文字:
自定义窗口
有时, 你需要扩展编辑器并添加始终可用的工具. 一种简单的方法是添加一个带插件的新扩展面板.
Docks只是基于Control的场景, 因此它们的创建方式与通常的GUI场景类似.
创建一个自定义栏好的方法和自定义节点一样. 在 addons/my_custom_dock 文件夹中创建一个新的 plugin.cfg 文件, 然后在其中添加以下内容:
1 | [plugin] |
然后在同一文件夹中创建脚本 custom_dock.gd。填写之前见过的模板以获得良好的开端。
由于我们正在尝试添加新的自定义窗口, 因此我们需要创建窗口的内容. 这只不过是一个标准的Godot场景: 只需在编辑器中创建一个新场景然后编辑它.
对于编辑器停靠站, 根节点 必须是 Control 或其子类之一. 在本教程中, 你可以创建一个按钮. 根节点的名称也将是面板对话框中显示的名称, 因此请务必为其指定一个简短的描述性名称. 另外, 不要忘记在按钮上添加一些文字.
把这个场景保存为 my_dock.tscn .
现在, 我们需要抓取我们创建的场景, 然后在编辑器中把它添加为一个栏目.
为此, 你可以依赖 add_control_to_dock() 这个函数, 它来自 EditorPlugin 类.
你需要选择一个停靠位置并定义要添加的控件, 也就是你刚刚创建的场景. 不要忘了在插件停用时 remove the dock . 脚本可以是这样的:
1 | @tool |
请注意, 虽然Dock最初会出现在其指定的位置, 但用户可以自由改变其位置, 并保存所产生的布局.
检查结果
现在是时候检查工作成果了。打开项目设置并点击插件选项卡。你的插件应该是列表中唯一的插件。
你可以看到该插件未启用。点击启用勾选框以激活该插件。在关闭设置窗口之前,该停靠面板应该已经可见。你现在应该有一个自定义的停靠面板了:
在插件中注册自动加载/单例
编辑器插件可以在启用时自动注册自动加载。同样也包含了在插件禁用时反注册该自动加载。
这样用户就可以更快速地设置插件了,因为你的编辑器插件要求使用自动加载时,他们不必再手动去项目设置里添加自动加载了。
在编辑器插件中使用以下代码注册单例:
1 | @tool |
使用子插件
通常一个插件会添加多个功能,例如自定义节点和面板。在这种情况下,为每个功能使用单独的插件脚本可能更简单。此时就可以使用子插件。
首先和普通插件一样创建所有插件和子插件:
然后将子插件移动到主插件文件夹中:
Godot 会在插件列表中隐藏子插件,这样用户就无法将其启用或禁用。而主插件脚本就应该像这样来启用和禁用子插件:
1 | @tool |
制作主屏幕插件
主屏幕插件允许您在编辑器的中央部分创建新的 UI,这些 UI 显示在“2D”、“3D”、“脚本”、“游戏”和“AssetLib”按钮旁边。
此类编辑器插件称为“主屏幕插件”。
初始化插件
首先从Plugins菜单中创建一个新插件.
在本教程中, 我们将把它放在一个名为 main_screen 的文件夹中, 但你可以使用任何你喜欢的名字.
插件脚本会自带 _enter_tree() 和 _exit_tree() 方法, 但对于主场景插件来说, 我们需要添加一些额外的方法. 增加五个额外的方法, 脚本就像这样:
1 | @tool |
该脚本中最重要的部分是 _has_main_screen() 函数,该函数是重载的,因此它返回 true。该函数在插件激活时由编辑器自动调用,以告知它该插件为编辑器添加了一个新的中心视图。现在,我们将原样保留该脚本,稍后再回来查看。
主画面场景
创建一个新的场景,其根节点由 Control 派生而来(在这个示例插件中,我们将使根节点为 CenterContainer)。
选择这个根节点,在视口中,点击 布局 菜单,选择 整个矩形。你还需要在检查器中启用 Expand 垂直尺寸标志。面板现在使用主视口中的所有可用空间。
接下来, 让我们为我们的主屏幕插件示例添加一个按钮. 添加一个 Button 节点, 并将文本设置为 “Print Hello “或类似的内容. 给按钮添加一个脚本, 像这样:
1 | @tool |
然后将 “按下” 信号连接到自身. 如果你需要信号方面的帮助, 请参考 使用信号 一文.
我们完成了主屏幕面板. 将场景保存为 main_panel.tscn.
更新插件脚本
我们需要更新 main_screen_plugin.gd 脚本,让插件实例化我们的主面板场景,并将其放置在需要的位置。这是完整的插件脚本:
1 | @tool |
添加了几行特定的代码。MainPanel 是一个保存对场景的引用的常量,我们将该场景实例化到 main_panel_instance 中。
_enter_tree() 函数在 _ready() 之前被调用。在这里我们实例化主面板场景,并将它们添加为编辑器特定部分的子项。我们使用 EditorInterface.get_editor_main_screen() 来获取主编辑器屏幕,并将我们的主面板实例添加为其子项。我们调用 _make_visible(false) 函数来隐藏主面板,这样它在首次激活插件时就不会争夺空间。
当插件停用时,将调用 _exit_tree() 函数。如果主屏幕仍然存在,我们将调用 queue_free() 来释放实例,并将其从内存中移除。
可重写 _make_visible() 函数以根据需要隐藏或显示主面板。当用户点击编辑器顶部的主视口按钮时,编辑器会自动调用该函数。
_get_plugin_name() 和 _get_plugin_icon() 函数控制插件主视口按钮的显示名称和图标。
另一个你可以添加的函数是 handles() 函数, 它允许你处理一个节点类型, 当选择该类型时自动聚焦主屏幕. 这类似于点击一个3D节点会自动切换到3D视口.
试试这个插件
在项目设置中激活插件. 你会观察到主视口上方的2D, 3D, 脚本旁边有一个新按钮. 点击它将带你进入新的主屏幕插件, 中间的按钮将打印文本.
如果你想试试这个插件的完成版, 请在这里查看插件演示:https://github.com/godotengine/godot-demo-projects/tree/master/plugins
如果你想看一个更完整的例子, 了解主屏幕插件的能力, 请看这里的 2.5D 演示项目:https://github.com/godotengine/godot-demo-projects/tree/master/misc/2.5d
导入插件
导入插件是一种特殊的编辑器工具, 它允许Godot导入自定义资源, 并将其作为一级资源对待.
编辑器本身捆绑了很多导入插件来处理常见的资源, 如PNG图片, Collada和glTF模型, Ogg Vorbis声音等等.
本教程介绍如何创建导入插件以加载自定义文本文件作为材质资源。
该文本文件将包含三个以逗号分隔的数值,以代表颜色的三个通道,生成的颜色将用作导入材质的反照率(主颜色)。
配置
首先, 我们需要一个通用插件来处理导入插件的初始化和销毁. 让我们先添加 plugin.cfg 文件:
1 | [plugin] |
然后我们需要 material_import.gd 文件来在需要时添加和删除导入插件:
1 | # material_import.gd |
当这个插件被激活时, 它将创建一个新的导入插件实例(我们很快就会制作), 并使用 add_import_plugin() 方法将其加入编辑器. 我们在类成员 import_plugin’ 中存储它的引用, 这样我们就可以在以后删除它时引用它. remove_import_plugin() 方法在插件停用时被调用, 以清理内存并让编辑器知道导入插件不再可用.
注意, 导入插件是一个引用类型, 所以它不需要明确地用 free() 函数从内存中释放. 当它超出范围时, 将被引擎自动释放.
EditorImportPlugin 类
这个展示的主角是 EditorImportPlugin 类. 它负责实现Godot需要知道如何处理文件时调用的方法.
让我们开始编写我们的插件, 一个方法:
1 | # import_plugin.gd |
第一种方法是 _get_importer_name()的。
这是插件的唯一名称,Godot 使用它来了解某个文件中使用了哪个导入。
当需要重新导入文件时,编辑器将知道要调用哪个插件。
1 | func _get_visible_name(): |
_get_visible_name() 方法负责返回它导入的类型的名称,并将在导入面板中将其显示给用户。
你选择的名字应该可以接到“导入为”后面,例如“导入为 Silly Material”。
你可以随心所欲地命名,但我们建议为你的插件起一个描述性的名字。
1 | func _get_recognized_extensions(): |
Godot 的导入系统通过扩展名检测文件类型。
在 _get_recognized_extensions() 方法中,你将返回一个字符串数组,表示该插件可以理解的每个扩展名。
如果扩展名被多个插件识别,则用户可以在导入文件时选择使用哪个插件。
小技巧
许多插件可能会使用像 .json 和 .txt 这样的常见扩展.
此外, 项目中可能存在仅作为游戏数据的文件, 不应导入. 导入时必须小心以验证数据. 永远不要指望文件格式正确.
1 | func _get_save_extension(): |
导入的文件被保存在项目根部的 .import 文件夹中.
它们的扩展名应与你要导入的资源类型相匹配, 但由于Godot不能告诉你将使用什么(因为同一资源可能有多个有效的扩展名), 你需要声明将在导入时使用的内容.
由于我们正在导入材质, 因此我们将对此类资源类型使用特殊扩展. 如果要导入场景, 可以使用 scn .
通用资源可以使用 res 扩展名. 但是, 引擎不会以任何方式强制执行此操作.
1 | func _get_resource_type(): |
导入的资源具有特定类型,编辑器可以据此知道它属于哪个属性槽。这样就能够将其从文件系统面板拖放到检查器的属性之中。
在我们的示例中,它是一个 StandardMaterial3D,可以应用于 3D 对象。
备注
如果需要从同一扩展中导入不同类型, 则必须创建多个导入插件. 你可以在另一个文件上抽象导入代码, 以避免在这方面出现重复.
选项和预设
你的插件可以提供不同的选项, 以允许用户控制资源的导入方式.
如果一组选定的选项很常见, 你还可以创建不同的预设以使用户更容易. 下图显示了选项在编辑器中的显示方式:
由于可能有许多预设并且它们用数字标识, 因此使用枚举是一个很好的做法, 因此你可以使用名称来引用它们.
1 | @tool |
既然定义了枚举, 让我们继续看一下导入插件的方法:
1 | func _get_preset_count(): |
_get_preset_count() 方法返回该插件定义的预设数量。
我们现在只有一个预设,但我们可以通过返回 Presets 枚举的大小来使该方法适应未来的需求。
1 | func _get_preset_name(preset_index): |
这里我们有 _get_preset_name() 方法,它给出展示给用户的预设的名称,因此请确保使用简短而清晰的名称。
我们可以在这里使用 match 语句来使代码更加结构化.
这样, 将来很容易添加新的预设. 我们使用catch all模式来返回一些东西. 虽然Godot不会要求超出你定义的预设计数的预设, 但最好是安全起见.
如果你只有一个预设, 则可以直接返回其名称, 但如果你这样做, 则在添加更多预设时必须小心.
1 | func _get_import_options(path, preset_index): |
这是定义可用选项的方法。 _get_import_options() 返回一个字典数组,每个字典包含几个键,这些键被检查以自定义向用户显示的选项。下表显示了可能的键:
键 类型 描述
name 字符串 选项的名称. 显示时, 下划线变为空格, 首字母大写.
default_value 任何类型 此预设的选项的默认值.
property_hint 枚举值 PropertyHint 中的一个值, 作为提示使用.
hint_string 字符串 属性的提示文本. 与你在GDScript中的 export 语句中添加相同.
usage 枚举值 PropertyUsageFlags 中的一个值来定义用途.
name 和 default_value 键是 强制 , 其余是可选的.
请注意,_get_import_options 方法接收预设编号,因此你可以为每个不同的预设配置选项(特别是默认值)。
在该示例中,我们使用 match 语句,但如果你有很多选项,并且预设只更改了值,你可能需要先创建选项数组,然后根据预设进行更改。
警告
即使你没有定义预设(通过使 _get_preset_count 返回零),_get_import_options 方法也会被调用。
你必须返回一个数组,即使它是空的,否则你可能会收到错误。
1 | func _get_option_visibility(path, option_name, options): |
对于 _get_option_visibility() 方法,我们只需返回 true,因为我们的所有选项(即我们定义的单个选项)始终可见。
如果需要仅当另一个选项设置了某个值时才使某个选项可见,则可以在该方法中添加逻辑。
import 方法
_import() 方法负责将文件转换为资源,这是该过程中最重要的部分。我们的示例代码有点长,因此我们将其分为几个部分:
1 | func _import(source_file, save_path, options, r_platform_variants, r_gen_files): |
导入方法的第一部分打开并读取源文件。我们使用 FileAccess 类来执行该操作,并传递编辑器提供的 source_file 参数。
如果打开文件时出错,我们会返回该错误以让编辑器知道导入不成功。
1 | var channels = line.split(",") |
该代码取出之前读取的文件行,并将其拆分成以逗号分隔的片段。如果值多于或少于三个,则认为该文件无效并报告错误。
然后它创建一个新的 Color 变量,并根据输入文件设置其值。如果启用了 use_red_anyway 选项,则它会将颜色设置为纯红色。
1 | var material = StandardMaterial3D.new() |
这部分制作了一个新的StandardMaterial3D资源,即导入的资源。我们创建它的一个新实例,然后将其反照率颜色设置为我们之前得到的值。return ResourceSaver.save(material, "%s.%s" % [save_path, _get_save_extension()])
这是最后一部分,也是相当重要的部分,因为在这里我们将制作的资源保存到磁盘。保存文件的路径由编辑器通过 save_path 参数生成并告知。请注意,这不带扩展名,因此我们使用 string formatting 添加它。为此,我们调用我们之前定义的 _get_save_extension 方法,这样我们就可以确保它们不会不同步。
我们还返回 ResourceSaver.save() 方法的结果, 所以如果这一步有错误, 编辑器会知道.
平台变体和生成的文件
你可能已经注意到我们的插件忽略了 import 方法的两个参数。
那些是返回参数(因此它们的名称以 r 开头),这意味着编辑器会在调用你的 import 方法之后读取它们。它们都是可以填充信息的数组。
r_platform_variants 参数用于需要根据目标平台导入不同的资源.
虽然被称为 平台 变体, 但它是基于 feature tags 的存在, 所以即使是同一个平台也可以有多个变体, 这取决于设置.
要导入平台变体, 需要在扩展名之前使用feature标记保存它, 然后将标记推送到 r_platform_variants 数组, 以便编辑可以知道你做了.
例如, 假设我们为移动平台保存一个不同的材质. 我们将需要做如下的事情:
1 | r_platform_variants.push_back("mobile") |
r_gen_files 参数用于在导入过程中生成并需要保留的额外文件. 编辑器将查看它以了解依赖关系并确保不会无意中删除额外文件.
这也是一个数组, 应该填充你保存的文件的完整路径. 例如, 让我们为下一个传递创建另一个材质并将其保存在不同的文件中:
1 | var next_pass = StandardMaterial3D.new() |
试试这个插件
这是理论上的, 但是现在导入插件已经完成了, 让我们来测试一下.
确保你创建了示例文件(包含介绍部分中描述的内容)并将其另存为 test.mtxt . 然后在 “项目设置” 中激活插件.
如果一切顺利, 导入插件将添加到编辑器中并扫描文件系统, 使自定义资源显示在FileSystem基座上. 如果选择它并聚焦导入面板, 则可以看到选择该选项的唯一选项.
在场景中创建一个 MeshInstance3D 节点,并为其 Mesh 属性设置一个新的 SphereMesh。展开检查器中的 Material 部分,然后将文件从“文件系统”停靠面板拖到 Material 属性。对象将在视口中更新为导入材质的蓝色。
转到导入面板, 启用 “强制使用红色” 选项, 然后单击 “重新导入”. 这将更新导入的材质, 并应该自动更新显示红色的视图.
就是这样! 你的第一个导入插件已经完成! 现在就发挥创造力,为自己心爱的格式制作插件吧。
这对于以自定义格式编写数据然后在 Godot 中使用它就像它们是本机资源一样非常有用。这显示了导入系统如何强大和可扩展。
3D 小工具插件
定义自定义小工具的两种主要方法:
第一种方法适用于简单的小工具,可以减少插件结构的混乱。
第二种方法可以让你存储各个小工具的一些数据。
EditorNode3DGizmoPlugin
无论我们选择哪种方法,我们都需要创建一个新的 EditorNode3DGizmoPlugin。
这将允许我们为新的小工具类型设置名称,并定义其他行为,例如是否可以隐藏小工具。
这是一个基本设置:
1 | # my_custom_gizmo_plugin.gd |
简单的小工具只要继承 EditorNode3DGizmoPlugin 就足够了。
如果你想存储各个小工具的一些数据,或者你要把 Godot 3.0 的小工具移植到 3.1+,你应该选择第二种方法。
简单方法
第一步,在我们的自定义小工具插件中,覆盖 _has_gizmo() 方法,以便当节点参数是目标类型时返回 true。
1 | # ... |
我们可以覆盖譬如 _redraw() 之类的方法,或所有与句柄相关的方法。
1 | # ... |
请注意,我们在 _init 方法中创建了一个材质,并在 _redraw 方法中使用 get_material() 检索了该材质。
该方法根据小工具的状态(选中和/或可编辑)来检索材质的变体之一。
你最后的场景应该是这样的:
1 | extends EditorNode3DGizmoPlugin |
请注意,我们刚刚在 _redraw 方法中添加了一些句柄,但我们仍然需要在 EditorNode3DGizmoPlugin 中实现其余与句柄相关的回调,以获得正常工作的句柄。
替代方法
在某些情况下,我们希望提供自己的 EditorNode3DGizmo 实现,可能是因为希望在各个小工具中存储一些状态,或者因为正在移植旧的小工具插件,而不想经历重写过程。
在这些情况下,我们需要做的就是在新小工具插件中覆盖 _create_gizmo(),这样它就会返回我们想要针对的 Node3D 节点的自定义小工具实现。
1 | # my_custom_gizmo_plugin.gd |
这样,所有的小工具逻辑和绘制方法都可以在扩展 EditorNode3DGizmo 的新类中实现,像这样:
1 | # my_custom_gizmo.gd |
请注意,我们刚刚在 _redraw 方法中添加了一些句柄,但我们仍然需要在 EditorNode3DGizmo 中实现其余与句柄相关的回调,以获得正常工作的句柄。
检查器插件
检查器面板支持以插件的形式来创建自定义小工具编辑属性。
尽管可以用它来修改内置类型的检查器小工具,但它在需要处理自定义数据类型和资源时尤其有用。
你不但可以为特定的属性或者整个对象设计自定义控件,还可以为特定数据类型设计单独的控件。
这份指南会介绍如何使用 EditorInspectorPlugin 和 EditorProperty 类来为整数类型创建自定义的界面,将默认的行为替换为一个按了以后就会生成 0 到 99 之间随机数的按钮。
创建你的插件
从创建新的空插件开始。
让我们假设你的插件文件夹叫做 my_inspector_plugin。
那么此时你新建的 addons/my_inspector_plugin 文件夹中就有两个文件:plugin.cfg 和 plugin.gd。
和之前一样,plugin.gd 是一个扩展了 EditorPlugin 的脚本,你需要在 _enter_tree 和 _exit_tree 方法中加入新的代码。
要创建自己的检查器插件,你必须加载对应的脚本,然后创建并调用 add_inspector_plugin() 来添加实例。禁用插件时,你应该调用 remove_inspector_plugin() 将该实例移除。
备注
这里,你正在加载一个脚本,而不是一个打包的场景。因此,你应该使用 new() 而不是 instantiate()。
1 | GDScriptC# |
与检查器交互
要和检查器面板交互,你的 my_inspector_plugin.gd 脚本必须继承自 EditorInspectorPlugin 类。
这个类提供了不少虚方法,可以用来控制检查器对属性的处理。
要产生任何效果,脚本必须实现 _can_handle() 方法。
此函数为每个已编辑的对象调用,如果此插件应处理对象或其属性,则必须返回 true。
备注
要处理附加在该对象上的 Resource 也同样如此。
你可以实现另外四种方法,在特定位置向检查器添加控件。_parse_begin() 和 _parse_end() 方法在每个对象解析开始时和结束时分别只调用一次。
它们可以通过调用 add_custom_control() 在检查器布局的顶部或底部添加控件。
当编辑器解析对象时,它会调用 _parse_category() 和 _parse_property() 方法。
在那里,除了 add_custom_control() 之外,你还可以调用 add_property_editor() 和 add_property_editor_for_multiple_properties()。
可使用最后两种方法专门添加基于 EditorProperty 的控件。
1 | # my_inspector_plugin.gd |
添加编辑属性的界面
EditorProperty 是一种特殊的 Control,可以与检查器面板所编辑的对象进行交互。
它本身不显示任何内容,但可以放入其他控件节点,甚至是复杂的场景。
扩展 EditorProperty 的脚本有三个必不可少的部分:
必须定义 _init() 方法,设置控件节点的结构。
应该实现 _update_property(),处理外部对数据的更改。
必须在某处使用 emit_changed 触发信号,告知检查器本控件对属性进行了修改。
显示自定义小工具的方法有两种。可以只用默认的 add_child() 方法可以把它显示到属性名称的右边,在 add_child() 之后再调用 set_bottom_editor() 就可以把它显示到名称的下边。
1 | # random_int_editor.gd |
使用上面的示例代码,可以实现用自定义的小工具替代整数默认的 SpinBox 控件,点击 Button 后生成随机值。
可视化着色器插件
Visual Shader插件用于在GDScript中创建自定义的 VisualShader 节点.
创建过程与一般的编辑器插件不同. 你不需要创建一个 plugin.cfg 文件来注册, 而是创建并保存一个脚本文件, 只要用 class_name 注册了自定义节点, 就可以使用了.
本篇短教程将讲解如何制作 Perlin-3D 噪声节点(原代码来自这个 GPU 噪声着色器插件。
创建一个 Sprite2D 并为其材质插槽分配一个 ShaderMaterial:
将 VisualShader 给着色器材质的插槽:
不要忘记将其模式改为“CanvasItem”(如果你使用的是 Sprite2D):
创建一个从 VisualShaderNodeCustom 派生的脚本. 这是你初始化你的插件所需要的全部内容.
1 | # perlin_noise_3d.gd |
保存并打开 Visual Shader。你应该在 Addons 类别下的成员对话框中看到你的新节点类型(如果你看不到新节点,请尝试重启编辑器):
放到一个图并连接所需的端口:
这就是你需要做的所有事情, 正如你所看到的那样, 创建你自己的自定义VisualShader节点很容易!
在编辑器中运行代码
@tool 是什么?
是一行强大的代码,添加到脚本的顶部后,脚本就会在编辑器中执行。
还可以决定脚本的哪些部分在编辑器中执行、哪些部分在游戏中执行、哪部分在两者中均执行。
可以使用它来做很多事情, 它在层次设计中非常有用, 可以直观地呈现难以预测的事物. 以下是一些用例:
如果你有一门发射受物理学(重力)影响的炮弹的大炮, 你可以在编辑器中画出炮弹的轨迹, 使关卡设计容易得多.
如果你有不同跳跃高度的跳线, 你可以绘制游戏角色能跳过的最大跳跃高度, 也可以让关卡设计变得更容易.
如果你的游戏角色不使用精灵, 却使用代码来绘制, 你可以在编辑器中执行该绘图代码以查看你的游戏角色.
@tool 脚本在编辑器中运行,并允许您访问当前编辑场景的场景树。
这是一个强大的功能,但也有警告,因为编辑器不包括对潜在滥用 @tool 脚本的保护。
在作场景树时要格外小心,尤其是通过 Node.queue_free,因为如果你在编辑器运行涉及节点的逻辑时释放节点,可能会导致崩溃。
如何使用 @tool
要把脚本变成工具脚本,请在代码顶部添加 @tool 注解。
要检查你当前是否在编辑器中,请使用:Engine.is_editor_hint()。
1 | if Engine.is_editor_hint(): |
没有上述两个条件之一的代码片段将可在编辑器和游戏中运行。
重要信息
一般规则是 ,您的工具脚本使用的任何其他 GDScript 都必须也是一个工具 。
编辑器无法在没有 @tool 的情况下从 GDScript 文件构造实例,这意味着您无法从中调用方法或引用成员变量。
但是,由于静态方法、常量和枚举可以在不创建实例的情况下使用,因此可以从 @tool 脚本调用它们或引用它们到其他非工具脚本。
静态变量是一个例外。 如果您尝试在没有 @tool,它将始终返回 null,但这样做时不会打印警告或错误。
此限制不适用于静态方法,无论目标脚本是否处于工具模式,都可以调用静态方法。
扩展 @tool 脚本不会自动使扩展脚本成为 @tool。从扩展脚本中省略 @tool 将禁用超类的工具行为。因此,扩展脚本还应指定 @tool 注解。
编辑器中的修改是永久性的,无法撤消/重做。为 例如,在下一节中,当我们删除脚本时,节点将保留其 旋转。
小心避免进行不必要的修改。考虑设置 版本控制以避免在出错时丢失工作。
当前不支持在工具脚本上使用调试器和断点。在脚本编辑器中放置或使用 breakpoint 关键字的断点将被忽略。可以改用 print 语句来显示变量的内容。
试试 @tool
在场景中添加一个 Sprite2D 节点,并将纹理设置为 Godot 图标。附加并打开一个脚本,将其更改为:
1 | @tool |
保存脚本并返回编辑器. 现在你应该看到你的对象在旋转. 如果你运行游戏, 它也会旋转.
警告
你可能需要重启编辑器。这是在所有 Godot 4 版本中发现的已知问题:GH-66381 。
备注
如果你没有看到变化, 请重新加载场景(关闭它并再次打开).
现在让我们选择何时运行代码. 将 _process() 函数修改为:
1 | func _process(delta): |
保存脚本. 现在, 对象将在编辑器中顺时针旋转, 但如果你运行游戏, 它将逆时针旋转.
编辑变量
添加并导出一个变量 speed 到脚本。
要更新 speed 并重置旋转角度,请添加一个设值函数 set(new_speed),该函数使用检查器的输入执行。
修改 _process() 以包含旋转速度。
1 | @tool |
备注
来自其他节点的代码不会在编辑器中运行。您对其他节点的访问受到限制。
您可以访问树和节点及其默认属性,但无法访问用户变量。如果要这样做,其他节点也必须在编辑器中运行。
资源变化时获取通知
有时您希望您的工具使用资源。
但是,当您在编辑器中更改该资源的属性时,将不会调用工具的 set() 方法。
1 | @tool |
要解决这个问题,首先必须将资源变成一个工具脚本,并使其在设置属性时发出 changed 信号:
1 | # Make Your Resource a tool. |
然后,你需要在设置新资源时连接该信号:
1 | @tool |
最后,记住断开信号,因为在其他地方使用和更改旧资源会导致不必要的更新。
1 | @export var resource: MyResource: |
报告节点配置警告
Godot 使用 节点配置警告 系统来警告用户有关配置错误的节点。
当某个节点配置不正确时,场景面板中该节点名称旁边会出现黄色警告标志。
当你悬停在该图标上或点击该图标时,会弹出警告标志。
脚本中可以使用这一特性来帮助你和你的团队避免在设定场景过程中出现错误。
使用节点配置警告时,如果能够影响警告或移除警告的值发生了变化,那么你就需要调用 update_configuration_warnings。默认只会在关闭并重新打开场景时才会更新警告。
1 | # Use setters to update the configuration warning automatically. |
使用 EditorScript 运行一次性脚本
有时,你只需运行一次代码,以自动执行编辑器中未提供的特定任务。一些示例可能是:
无需运行项目即可用作 GDScript 或 C# 脚本的游乐场。print() 输出显示在编辑器输出面板中。
缩放当前编辑的场景内的所有灯光节点,因为你会注意到在将灯光放置在所需的位置后,你的关卡最终看起来太暗或太亮。
用场景实例替换复制粘贴的节点,以便以后更容易修改。
这可以在 Godot 内通过扩展脚本中的 EditorScript 来实现。这提供了一种在编辑器中运行单个脚本而无需创建编辑器插件的方法。
要创建一个 EditorScript,请右键单击文件系统面板中的文件夹或空白处,然后选择新建 > 脚本…。在脚本创建对话框中,点击树图标以选择要扩展的对象(或直接在左侧字段中输入 EditorScript,但请注意区分大小写):
在脚本编辑器创建对话框中创建一个编辑器脚本
这将自动选择适合 EditorScript 的脚本模板,其中已插入 _run() 方法:
1 | @tool |
当你使用文件 > 运行或键盘快捷键 Ctrl + Shift + X 且 EditorScript 是脚本编辑器中当前打开的脚本时,将执行该 _run() 方法。该键盘快捷键仅在当前聚焦于脚本编辑器时有效。
扩展 EditorScript 的脚本必须是 @tool 脚本才能运行。
备注
EditorScripts 只能从 Godot 脚本编辑器运行。如果您使用的是外部编辑器,请在 Godot 脚本编辑器中打开脚本以运行它。
危险
EditorScripts 没有撤消/重做功能,因此如果脚本旨在修改任何数据,请确保在运行场景之前保存场景。
要访问当前编辑场景中的节点,请使用 EditorScript.get_scene 方法,该方法返回当前编辑场景的根节点。下面是一个示例,它递归地获取当前编辑场景中的所有节点,并将所有 OmniLight3D 节点的范围加倍:
1 | @tool |
小技巧
即使在脚本视图打开时,你也可以在编辑器顶部更改当前编辑的场景。这将影响 EditorScript.get_scene 的返回值,因此请确保在运行脚本之前选择了要迭代的场景。
实例化场景
在编辑器中,你可以正常实例化打包场景,并将它们添加到当前打开的场景中。
默认情况下,使用 Node.add_child(node) 添加的节点或场景在“场景”树面板中是不可见的,也不会持久化到磁盘上。
如果你希望节点和场景在场景树面板中可见,并在保存场景时持久化到磁盘上,则需要将这些子节点的 owner 属性设为当前编辑场景的根节点。
如果你使用的是 @tool:
1 | func _ready(): |
如果你使用 EditorScript:
1 | func _run(): |
警告
不适当地使用 @tool 会产生许多错误。
建议先按需要编写代码,然后再将 @tool 注解添加到顶部。
此外,请确保将在编辑器中运行的代码与在游戏中运行的代码分开。这样,你可以更轻松地找到错误。
渲染
渲染器概述
Godot 4 包括 三个渲染器:
- Forward+。最先进的渲染器,仅适合桌面平台,桌面平台默认使用。
该渲染器使用 Vulkan、Direct3D 12 或 Metal 作为渲染驱动,使用 RenderingDevice 后端。 - Mobile(移动)。
功能较少,但渲染简单场景的速度更快。
适用于移动平台和桌面平台。移动平台默认使用该渲染器。
该渲染器使用 Vulkan、Direct3D 12 或 Metal 作为渲染驱动,使用 RenderingDevice 后端。 - Compatibility(兼容),在 OpenGL 3.3、OpenGL ES 3.0 和 WebGL 2.0 以上运行。
最低级的图形后端,适合低端桌面和移动平台。在 Web 平台上默认使用。
使用视口
将视口 Viewport 想象为展示游戏画面的屏幕。
为了看到游戏,我们需要首先在一块表面上绘制游戏。这块表面就是“根视口”。
SubViewport 是子视口节点,这也是一种 Viewport,能够在场景中添加,这样我们就有了多个可以用来绘图的表面。
如果我们在 SubViewport 上绘图,那这个子视口就叫做“渲染目标”。
我们可以通过对应的 texture 来访问渲染目标的内容。
使用 SubViewport 作为渲染目标,我们就可以同时渲染多个场景,也可以将画面渲染到视口纹理 ViewportTexture 上,从而在场景中的物体上显示,实现动态天空盒之类的效果。
SubViewport 的用处有很多,包括:
- 在2D游戏中渲染3D物体
- 在3D游戏中渲染2D元素
- 渲染动态纹理
- 在运行时生成程序式纹理
- 在同一场景中渲染多个摄像机
所有这些用例的共同点是, 你被赋予了在纹理上绘制物体的能力, 就好像它是另一个屏幕一样, 然后可以选择如何处理产生的纹理.
Godot 中的另一种视口是 Window,可以将内容投影到窗口上。
虽然根视口是 Window,但窗口的灵活性较差。
如果想使用 Viewport 的纹理,你大多数时候处理的是 SubViewport。
输入
Viewport 还负责向其子节点传递经过适当调整和缩放的输入事件。
默认情况下,SubViewport 不会自动接收输入,除非它们从其直接 SubViewportContainer 父节点接收输入。
在这种情况下,可以使用 Disable Input 属性来禁用输入。
Listener监听器
Godot 支持 3D 声音(在 2D 和 3D 节点中)。
有关这方面的更多信息,请参阅音频流教程 。
要使这种类型的声音可听见,需要将视口启用为监听器(用于 2D 或 3D)。
如果你使用子视口来显示你的 World3D 或 World2D,别忘了启用这个!
摄像机(2D 和 3D)
当使用 Camera3D 或 Camera2D 时始终显示在最近的父级 Viewport 上(朝向根节点)
一个 Viewport 只能有一个活动相机,因此存在多个相机时,请确保所需的那个已经设置了 current 属性,或通过调用以下命令使其成为当前相机:
1 | camera.make_current() |
默认情况下,相机将渲染其世界中的所有对象。
在 3D 中,相机可以使用其 cull_mask 属性与 VisualInstance3D 的 layer 属性结合来限制哪些对象被渲染。
缩放和拉伸
SubViewports 有一个 size 属性,该属性表示 SubViewport 的像素大小。
对于 SubViewportContainers 的子级 SubViewport,这些值会被覆盖,但对于所有其他 SubViewport,这会设置它们的分辨率。
还可以缩放 2D 内容并使 SubViewport 分辨率与大小指定的分辨率不同,可通过调用:
1 | sub_viewport.set_size_2d_override(Vector2i(width, height)) # Custom size for 2D. |
有关使用根视口进行缩放和拉伸的信息,请访问《多分辨率教程》
世界
对于 3D, 视口将包含一个 World3D。这基本上是将物理和渲染联系在一起的宇宙。
基于 Node3D 的节点将使用最近视口的 World3D 进行注册。
默认情况下,新创建的视口不包含 World3D,但使用与其父视口相同的视口。
根视口始终包含一个 World3D,这是默认情况下渲染到的对象。
可以使用 World 3D 属性在 Viewport 中设置 World3D,这将分离该 Viewport 的所有子节点,并阻止它们与父 Viewport 的 World3D 交互。
这在以下场景中特别有用,例如,你可能希望在游戏中以 3D 形式显示单独的角色(像在星际争霸中一样)。
当你想要创建显示单个对象的 Viewport 而不想创建 World3D 时,作为帮助工具,Viewport 可以选择使用它自己的 World3D。
当你想要在 World2D 中实例化 3D 角色或对象时,这很有用。
对于 2D,每个 Viewport 始终包含它自己的 World2D。这在大多数情况下就足够了,但如果需要共享它们,可以通过代码在 Viewport 上设置 world_2d 来实现。
捕获
可以查询 Viewport 内容的捕获。
对于根 Viewport,这实际上是一个屏幕截图。这可以通过以下代码完成:
1 | # Retrieve the captured Image using get_image(). |
但是如果你在 _ready() 中使用, 或者从 Viewport 的 初始化的第一帧开始使用, 你会得到一个空的纹理, 因为没有什么可以作为纹理获得. 你可以用来处理它, 例如:
1 | # Wait until the frame has finished before getting the texture. |
视口容器
如果 SubViewport 是 SubViewportContainer 的子项,它将变为活动状态并显示其内部的所有内容。布局看起来像这样:
如果在 SubViewportContainer 中将 Stretch 设置为 true,则 SubViewport 将完全覆盖其父级 SubViewportContainer 的区域。
备注
SubViewportContainer 的大小不能小于 SubViewport 的大小。
渲染
由于 Viewport 是进入另一个渲染表面的入口,因此它公开了一些可能与项目设置不同的渲染属性。
你可以选择为每个 Viewport 使用不同级别的 MSAA。该默认行为是 Disabled。
如果你知道视口将仅用于 2D,你可以禁用 3D。然后,Godot 将限制视口的绘制方式。
与启用的 3D 相比,禁用 3D 的速度稍快,使用的内存也更少。如果您的视口没有以 3D 形式渲染任何内容,最好禁用 3D。
备注
如果你需要在视口中渲染 3D 阴影,请确保将视口的 positional_shadow_atlas_size 属性设置为大于 0 的值。否则,阴影将不会被渲染。默认情况下,等效项目设置在桌面平台上设置为 4096,在移动平台上设置为 2048。
Godot 还提供了一种使用 Debug Draw 自定义 Viewport 中所有内容绘制方式的方法。
Debug Draw 允许你指定一种模式,该模式决定了 Viewport 将如何显示其中绘制的内容。
默认情况下,Debug Draw 处于 Disabled 状态。
其他一些选项包括 Unshaded、Overdraw 和 Wireframe。有关完整列表,请参阅《Viewport 文档》。
Debug Draw = Disabled(默认):场景正常绘制。
Debug Draw = Unshaded:无阴影绘制场景时不使用照明信息,因此所有物体都以其反照颜色扁平地显示。
Debug Draw = Overdraw:过度绘制使用加法混合将网格绘制成半透明的,以便你可以看到网格是如何重叠的。
Debug Draw = Wireframe:线框仅使用网格中三角形的边来绘制场景。
备注
使用兼容性渲染方法时,目前不支持调试绘制模式。它们将显示为常规绘制模式。
渲染目标
渲染到子视口时,里面的任何内容在场景编辑器中都不可见。
要显示内容,您必须在某处绘制 SubViewport 的 ViewportTexture。这可以通过代码请求(例如):
1 | # This gives us the ViewportTexture. |
或者可以通过选择”New ViewportTexture”在编辑器中指定它
然后选择你想要使用的 Viewport.
每一帧,Viewport 的纹理都会使用默认清除颜色(如果 Transparent BG 设置为 true,则使用透明颜色)清除。
这可以通过将 Clear Mode 设置为 Never 或 Next Frame 来更改。
顾名思义,Never 表示纹理永远不会被清除,而 next frame 将在下一帧清除纹理并将自身设置为 Never。
默认情况下,SubViewport 的重新渲染发生在其 ViewportTexture 被绘制的那一帧之时。
可见则会渲染,否则不会。可以通过将 Update Mode 设置为 Never、Once、Always 或 When Parent Visible 来更改此行为。
Never 和 Always 分别表示永不或始终重新渲染。Once 将重新渲染下一帧,之后更改为 Never。这可用于手动更新视口。
这种灵活性允许用户渲染一次图像,然后使用该纹理,而无需承担渲染每一帧的成本。
多分辨率
最常见的方法是使用一个单一的基准分辨率,然后将其适用于其他所有情况。
这个分辨率符合大多数玩家预期的游戏体验(基于其硬件配置)。对于移动端,谷歌有在线统计,而对于桌面端 Steam 也有。
举个例子,Steam显示最常见的 主要显示分辨率是 1920×1080, 所以明智的做法是为这个分辨率开发一个游戏, 然后期处理不同尺寸和长宽比的缩放.
Godot 还提供了一系列通用的容器.
基本大小
窗口的基本尺寸可以在项目设置中的 Display → Window 下指定.
然而, 它的作用并不完全明显; 引擎不尝试将显示器切换到此分辨率.
相反, 将此设置视为 “设计大小”, 即你在编辑器中使用的区域的大小. 此设置直接对应于2D编辑器中蓝色矩形的大小.
通常需要支持具有与该基本大小不同的屏幕和窗口大小的设备. Godot提供了许多方法来控制视口的大小调整和拉伸到不同的屏幕大小.
要在运行时从脚本配置拉伸基本大小,请使用 get_tree().root.content_scale_size 属性(请参阅 Window.content_scale_size)。
更改此值可以间接更改 2D 元素的大小。但是,要 提供用户可访问的缩放选项,使用 建议拉伸缩放 ,因为它更容易调整。
备注
在该页面上,窗口指的是系统分配给你的游戏的屏幕区域,而视口指的是游戏控制以填充该屏幕区域的根对象(可从 get_tree().root 访问)。
该视口是一个 Window 实例。回想一下《前言》,所有 Window 对象都是视口。
调整大小
设备有多种类型,其屏幕也有多种类型,而屏幕又具有不同的像素密度和分辨率。
处理所有这些可能是一项艰巨的工作,因此 Godot 试图让开发人员的生活更轻松一些。
Viewport 节点有多个函数来处理调整大小,并且场景树的根节点始终是一个视口(加载的场景实例化为其子节点,并且始终可以通过调用 get_tree().root 或 get_node(“/root”) 来访问它)。
无论如何,虽然更改根视口参数可能是解决问题最灵活的方法,但它可能需要大量的工作、代码和猜测,因此 Godot 在项目设置中提供了一组参数来处理多种分辨率。
以低于 2D 元素的分辨率渲染 3D(无需 单独的视口),你可以使用 Godot 的 分辨率缩放支持。
这是在 GPU 瓶颈场景中显着提高性能的好方法。这适用于任何拉伸模式和拉伸纵横比组合。
拉伸设置
拉伸设置位于项目设置中, 提供了几个选项:
- 拉伸模式
拉伸模式设置定义了如何拉伸基本大小以适应窗口或屏幕的分辨率。
下面的动画使用仅 16×9 像素的“基本大小”来演示不同拉伸模式的效果。
同样为 16×9 像素的单个精灵覆盖整个视口,并在其上添加一条对角线 Line2D:- Stretch Mode = Disabled(默认):不发生拉伸。场景中的一个单位对应屏幕上的一个像素。在这种模式下,拉伸纵横比设置无效。
- Stretch Mode = Canvas Items:在这种模式下,项目设置中以宽度和高度指定的基本大小将被拉伸以覆盖整个屏幕(会考虑拉伸纵横比设置)。
这意味着所有内容都直接以目标分辨率渲染。3D 不受影响,而在 2D 中,精灵像素和屏幕像素之间不再是 1:1 的对应关系,这可能会导致缩放伪影。 - Stretch Mode = Viewport : 视口缩放意味着根 Viewport 的尺寸被精确地设置为在项目设置的 Display 部分指定的基本尺寸.
场景首先被渲染到这个视口. 最后, 这个视口被缩放以适应屏幕(考虑 Stretch Aspect 的设置).
要在运行时从脚本配置拉伸模式,请使用 get_tree().root.content_scale_mode 属性(参见 Window.content_scale_mode 和 ContentScaleMode 枚举)。
- 拉伸比例
只有在 Stretch Mode 被设置为 Disabled 以外的情况下生效.
黑色区域由引擎添加, 无法绘制. 灰色区域是场景的一部分, 可以绘制.- Stretch Aspect = Ignore : 在拉伸屏幕时忽略长宽比.
这意味着原始分辨率将被拉伸以完全填满屏幕, 即使它更宽或更窄. 这可能会导致不均匀的拉伸, 事物看起来比设计的更宽或更高. - Stretch Aspect = Keep : 在拉伸屏幕的时候保持长宽比.
这意味着无论屏幕分辨率如何, 视口都会保留原来的尺寸, 黑条会被添加到屏幕的顶部或底部(“宽屏模式 “)或侧面(“ 竖屏模式”).
如果你事先知道目标设备的宽高比, 或者你不想处理不同的宽高比, 这是一个不错的选择. - Stretch Aspect = Keep Width : 在拉伸屏幕时保持长宽比. 如果屏幕比基本尺寸宽, 则会在左右两边添加黑条(竖屏模式).
但如果屏幕比基本分辨率高, 视口将在垂直方向上增长(更多的内容将在底部可见). 你也可以把它看作是 “垂直扩展” .
这通常是创建可扩展的GUI或HUD的最佳选择, 因此一些控件可以锚定到底部( 大小和锚点). - Stretch Aspect = Keep Height : 在拉伸屏幕时保持长宽比.
如果屏幕比基本尺寸高, 则会在顶部和底部添加黑条(宽屏模式).
但如果屏幕比基本分辨率宽, 视口将在水平方向上增长(更多的内容将在右边可见). 你也可以把它看作是 “水平扩展” .
这通常是水平滚动的2D游戏的最佳选择(如跑步者或平台游戏者). - Stretch Aspect = Expand : 在拉伸屏幕时保持长宽比, 但既不保持基本宽度也不保持高度.
根据屏幕的长宽比, 视口将在水平方向(如果屏幕比基本尺寸宽)或垂直方向上变大(如果屏幕比原始尺寸高).
- Stretch Aspect = Ignore : 在拉伸屏幕时忽略长宽比.
要在运行时从脚本配置拉伸纵横比,请使用 get_tree().root.content_scale_aspect 属性(参见 Window.content_scale_aspect 和 ContentScaleAspect 枚举)。
小技巧
为了以类似的自动确定的比例系数支持纵向和横向模式,请将你的项目的基本分辨率设置为 方形 (1:1长宽比)而不是矩形。
例如,如果你希望以1280×720为基本分辨率进行设计,但又希望同时支持纵向和横向模式,那么在项目设置中使用720×720作为项目的基本窗口尺寸。
要允许用户在运行时选择他们喜欢的屏幕方向,请记住将 显示 > 窗口 > 手持 > 方向设置为传感器 。
拉伸缩放
设置设置允许你在上面拉伸选项已提供的内容之上添加额外的缩放系数。默认值 1.0 意味着不会发生额外的缩放。
例如,如果你将 Scale 设置为 2.0 并将 Stretch Mode 保留为 Disabled,则场景中的每个单元将对应屏幕上的 2×2 像素。这是为非游戏应用程序提供缩放选项的好方式。
如果将 Stretch Mode 设置为 canvas_items,2D 元素将相对于基本窗口大小进行缩放,然后乘以 Scale 设置。
这可以向玩家公开,以允许他们根据自己的喜好调整自动确定的比例,从而获得更好的可访问性。
如果将 Stretch Mode 设置为 viewport,则视口的分辨率将除以 Scale。
这会使像素看起来更大,并降低渲染分辨率(在给定窗口大小的情况下),从而可以提高性能。
要从脚本在运行时配置拉伸比例,请使用 get_tree().root.content_scale_factor 属性(参见 Window.content_scale_factor)。
您还可以使用 GUI > 主题 > 默认主题比例项目设置调整生成默认项目主题的比例。
这可用于在明显高于或低于默认值的基本分辨率下创建逻辑大小更大的 UI。
但是,此项目设置无法在运行时更改,因为其值仅在项目启动时读取一次。
拉伸缩放模式
自 Godot 4.2 起,拉伸比例模式设置允许你将自动确定的比例系数(以及手动指定的拉伸比例设置)限制为整数值。
默认情况下,该设置设置为 fractional,允许应用任何比例系数(包括如 2.5 等小数值)。
设置为 integer 时,该值将向下舍入为最接近的整数。
例如,不使用比例系数 2.5,而是向下舍入为 2.0。这对于防止显示像素艺术时出现失真很有用。
比较使用 viewport 拉伸模式显示的像素艺术,其拉伸比例模式设置为 fractional:
小数缩放示例(像素画显示异常)
棋盘看上去并不“均匀”。徽标和文本中的线宽也存在大幅变化。
该像素艺术也以 viewport 拉伸模式显示,但这次拉伸比例模式设置为 integer:
整数缩放示例(像素画正确显示)
棋盘看上去就非常均匀了。线宽也是一致的。
打个比方,如果视口的基础大小为 640×360 而窗口大小为 1366×768:
使用 fractional 时,视口会以 1366×768 的分辨率显示(缩放系数大约是 2.133×),占用整个屏幕空间。视口中的每个像素都对应 2.133×2.133 像素的显示区域。不过因为显示器只能显示“完整”的像素,就会导致不均匀的像素缩放,造成像素画的显示异常。
使用 integer 时,视口会以 1280×720 的分辨率显示(缩放系数为 2×)。四周的剩余空间使用黑条填充,这样视口中的每个像素都对应 2×2 像素的显示区域。
这个设置在所有拉伸模式下均会生效。不过使用 disabled 拉伸模式时只会影响拉伸缩放设置,将其向下取整。可用于使用像素风 UI 的 3D 游戏,此时 3D 视口的可见区域并不会减少(而在 canvas_items 和 viewport 拉伸模式下启用 integer 缩放模式时就会减少)。
小技巧
游戏应该使用独占全屏窗口模式,而不是全屏,后者旨在防止 Windows 自动将窗口视为独占全屏。
全屏旨在供想要使用每像素透明度而又不存在被操作系统禁用的风险的 GUI 应用程序使用。它通过在屏幕底部留下一条 1 像素的线来实现这一点。
相比之下,独占全屏使用实际屏幕大小,并允许 Windows 减少全屏游戏的抖动和输入延迟。
当使用整数缩放时,这一点尤为重要,因为全屏模式下 1 像素的高度减少可能导致整数缩放使用比预期更小的比例系数。
常见使用场景
如果要适配多种分辨率和纵横比,推荐使用以下设置。
桌面游戏
非像素风:
- 将基础窗口宽度设置1920×1080。如果显示器小于 1920×1080,请将窗口宽度覆盖和窗口高度覆盖设置为较小的值,项目启动时就会将窗口调小。
- 或者,如果您主要面向高端设备,请将基本窗口宽度设置为 3840,将窗口高度设置为 2160。
这允许您提供更高分辨率的 2D 资产,从而以更高的内存使用量和文件大小为代价获得更清晰的视觉效果。
您还需要将 GUI > 主题 > 默认主题比例增加到 2.0 之间的值 和 3.0 以确保 UI 元素保持可读性。
请注意,这将使非 mipmapped 纹理在低分辨率设备上变得颗粒状, 因此,请务必按照 减少缩减取样的混叠 . - 将拉伸模式设置为 canvas_items(画布项)。
- 将拉伸比例设置为 expand (扩展)。这样可以支持多种分辨率,并且能够更好地利用较长的智能手机屏幕(例如 18:9 和 19:9 的长宽比)。
- 使用布局菜单将 Control 节点的锚点吸附到正确的角落。
- 对于 3D 游戏,请考虑在游戏的选项菜单中公开分辨率缩放 ,以允许玩家将 3D 渲染分辨率与 UI 元素分开调整。这对于性能调整很有用,尤其是在低端硬件上。
像素风:
- 将基础窗口大小设置为你想要使用的视口尺寸。
大多数像素风游戏使用的视口尺寸在 256×224 和 640×480 之间。
640×360 是不错的基准,因为使用整数缩放时无论是缩放到 1280×720、1920×1080、2560×1440 还是 3840×2160 都不会出现黑条。
视口尺寸越大,所需资产的分辨率也就越高,除非你想要能够显示更大的游戏世界区域。 - 将拉伸模式设置为 viewport(视口)。
- 将拉伸比例设置为 keep(保持)可以(通过添加黑条的方式)强制使用固定的长宽比。
如果你想支持不同长宽比的话,也可以把拉伸模式设置为 expand(扩展)。 - 如果选用 expand 拉伸比例,使用布局菜单将 Control 节点的锚点吸附到正确的角落。
- 将拉伸缩放模式设置为 integer。这样就能够防止出现非整数倍的像素缩放,让像素画保持原样显示。
备注
viewport 拉伸模式会先以较低分辨率渲染,然后拉伸到最终窗口的大小。
如果你能够接受精灵可以移动或者旋转到“次像素”位置,或者希望有高分辨率的 3D 视图,可以把 viewport 拉伸模式换成 canvas_items 模式。
横屏的手机游戏
Godot 默认使用横屏模式,所以你无需在项目设置中调整显示方向。
- 将基础窗口宽度设置为 1280,窗口高度设置为 720。
- 或者,如果您主要面向高端设备,请将基本窗口宽度设置为 1920,将窗口高度设置为 1080。
这允许您提供更高分辨率的 2D 资产,从而以更高的内存使用量和文件大小为代价获得更清晰的视觉效果。
许多设备具有更高分辨率的显示屏 (1440p),但鉴于智能手机显示屏尺寸较小,与 1080p 的差异几乎看不见。
您还需要将 GUI > 主题 > 默认主题比例增加到 1.5 之间的值 和 2.0 以确保 UI 元素保持可读性。- 请注意,这将使非 mipmapped 纹理在低分辨率设备上变得颗粒状, 因此,请务必按照 减少缩减取样的混叠 .
- 将拉伸模式设置为 canvas_items(画布项)。
- 将拉伸比例设置为 expand (扩展)。这样可以支持多种分辨率,并且能够更好地利用较长的智能手机屏幕(例如 18:9 和 19:9 的长宽比)。
- 使用布局菜单将 Control 节点的锚点吸附到正确的角落。
小技巧
为了更好地支持平板和折叠屏手机(这些设备的显示器纵横比通常接近 4:3),请在按照其他步骤操作时考虑使用纵横比为 4:3 的基础分辨率。
例如可以将基础窗口宽度设置为 1280、基础窗口高度设置为 960。
竖屏的手机游戏
- 将基础窗口宽度设置为 720、窗口高度设置为 1280。
- 或者,如果主要面向高端设备,请将基本窗口宽度设置为 1080,将窗口高度设置为 1920。
这允许您提供更高分辨率的 2D 资产,从而以更高的内存使用量和文件大小为代价获得更清晰的视觉效果。
许多设备具有更高分辨率的显示屏 (1440p),但鉴于智能手机显示屏尺寸较小,与 1080p 的差异几乎看不见。
您还需要将 GUI > 主题 > 默认主题比例增加到 1.5 之间的值 和 2.0 以确保 UI 元素保持可读性。- 请注意,这将使非 mipmapped 纹理在低分辨率设备上变得颗粒状, 因此,请务必按照 减少缩减取样的混叠 .
- 将显示 > 窗口 > 手持 > 朝向设置为 portrait(竖屏)。
- 将拉伸模式设置为 canvas_items(画布项)。
- 将拉伸比例设置为 expand (扩展)。这样可以支持多种分辨率,并且能够更好地利用较长的智能手机屏幕(例如 18:9 和 19:9 的长宽比)。
- 使用布局菜单将 Control 节点的锚点吸附到正确的角落。
小技巧
为了更好地支持平板和折叠屏手机(这些设备的显示器纵横比通常接近 4:3),请在按照其他步骤操作时考虑使用纵横比为 3:4 的基础分辨率。
例如可以将基础窗口宽度设置为 960、基础窗口高度设置为 1280。
非游戏应用
- 将基础窗口宽高设置为你想要支持的最小窗口尺寸。这不是必须的,但是可以保证你在设计 UI 时考虑较小的窗口尺寸。
- 保持拉伸模式为默认值 disabled(禁用)。
- 保持拉伸比例为默认值 ignore(因为拉伸模式是 disabled,所以这里的值不会被用到)。
- 你可以通过在脚本的 _ready() 函数中调用 get_window().set_min_size() 来定义最小窗口大小。这可以防止用户将应用程序调整到低于某个大小,否则可能会破坏 UI 布局。
- 在应用程序的设置中添加设置以更改根视口的 stretch scale,以便可以放大 UI 以考虑 hiDPI 显示。另请参阅下面有关 hiDPI 支持的部分。
支持 hiDPI 高分辨率屏幕
默认情况下,Godot 项目作系统视为 DPI 感知。这由 显示 > 窗口 > DPI > 允许 hiDPI 项目设置控制,应尽可能保持启用状态。
禁用 DPI 感知可能会破坏 Windows 上的全屏行为。
由于 Godot 项目具有 DPI 感知能力,因此在 hiDPI 显示器上启动时,它们可能会以非常小的窗口大小显示(与屏幕分辨率成比例)。对于游戏,解决该问题的最常见方法是默认将它们设置为全屏。或者,你可以根据屏幕大小在自动加载的 _ready() 函数中设置窗口大小。
为了确保 2D 元素在 hiDPI 显示屏上不会显得太小:
对于游戏,使用 canvas_items 或 viewport 拉伸模式,以便 2D 元素根据当前窗口大小自动调整大小。
对于非游戏应用程序,请使用禁用的拉伸模式并将 将比例拉伸到与显示比例因子相对应的值 autoload 的 _ready() 函数。显示比例因子在作系统的设置中设置,可以使用 screen_get_scale 进行查询。此方法目前在 Android、iOS、Linux(仅限 Wayland)、macOS 和 Web 上实现。在其他平台上,您必须实现一种方法来根据屏幕分辨率猜测显示比例因子(使用允许用户在需要时覆盖此设置)。这是 Godot 编辑器目前使用的方法。
允许 hiDPI 设置仅在 Windows 和 macOS 上有效。它在所有其他平台上都被忽略了。
备注
Godot 编辑器本身始终被标记为 DPI 感知。
只有在 项目设置(Project Settings) 中启用 允许 hiDPI(Allow hiDPI) 时,从编辑器运行项目才会感知 DPI。
减少缩减取样的混叠
如果游戏的基本分辨率很高(如 3840×2160),当采样降到相当低的分辨率(如 1280×720)时,可能会出现锯齿。
要解决此问题,您可以在所有 2D 纹理上启用 mipmap。但是,启用 mipmap 会增加内存使用量,这在低端移动设备上可能是一个问题。
处理纵横比
一旦考虑到不同分辨率的缩放, 请确保你的 user interface 也能为不同的长宽比进行缩放.
这可以使用 anchors 和/或 containers 来完成.
视场角缩放
3D相机节点的 Keep Aspect 属性默认为 Keep Height 缩放模式(也称为 Hor+ ).
在横屏模式下, 这通常是桌面游戏和手机游戏的最佳选择, 因为宽屏显示器会自动使用更宽的视野.
然而, 如果你的3D游戏打算使用纵向模式, 那么使用 Keep Width保持宽度 称为( Vert- )可能会更有意义.
这样, 宽高比大于16:9(例如19:9)的智能手机将使用 更高 的视野, 这在这里更符合逻辑.
2D 和 3D 元素使用不同的缩放
要以与 2D 元素(例如 UI)不同的分辨率渲染 3D,请使用 Godot 的 分辨率缩放功能。
这允许你控制用于 3D 的分辨率比例因子,而无需使用单独的视口节点。这既可用于通过以较低分辨率渲染 3D 来提高性能,也可以通过超级采样提高质量。
修复抖动、卡顿和输入延迟
抖动
抖动的原因可能有很多。最典型的发生在游戏 物理频率 (通常为 60 Hz)以与显示器刷新率不同的分辨率运行。检查您的显示器刷新率是否与 60 Hz 不同。
有时,只有某些对象(字符或背景)出现抖动。当它们在不同的时间源中处理时(一个在物理步骤中处理,另一个在空闲步骤中处理)时,就会发生这种情况。
这种抖动的原因可以通过启用 物理插值 在项目设置中。物理插值将通过以下方式平滑物理更新 在物理帧之间插值物理对象的变换。 这样,物理对象的视觉表示将始终看起来 无论帧速率和物理滴答率如何,都能平滑。
启用物理插值有一些你应该注意的注意事项。 例如,传送对象时应小心,以便它们 不要在旧位置和新位置之间明显插值 当它不是故意的时。请参阅 物理插值文档了解详情。
备注
启用物理插值将增加依赖于物理更新的行为(例如玩家移动)的输入延迟。在大多数游戏中,这通常比抖动更可取,但对于以固定帧速率运行的游戏(如格斗或节奏游戏)请仔细考虑这一点。输入延迟的这种增加可以通过增加物理滴答率来补偿,如输入延迟部分所述。
卡顿
由于多种不同的原因,可能会发生口吃。原因之一是游戏 由于 CPU 或 GPU 瓶颈而无法保持全帧率性能。 解决此问题是特定于游戏的,需要 优化 。
卡顿的另一个常见原因是着色器编译卡顿 。当在游戏中首次生成新材质或粒子效果时需要编译着色器时,就会发生这种情况。这种卡顿通常只发生在第一次通关时,或者在着色器缓存失效时图形驱动程序更新后。
从 Godot 4.4 开始,当使用 Forward+ 或 Mobile 渲染器时,引擎会尝试使用 ubershader 方法避免着色器编译卡顿。为了使这种方法最有效,在设计场景和资源时必须小心,以便 Godot 可以在加载场景/资源时收集尽可能多的信息,而不是在第一次绘制时收集信息。有关更多信息,请参阅减少着色器(管线)编译导致的卡顿 。
但是,当使用兼容性渲染器时,无法使用它 由于 OpenGL 中的技术限制,ubershader 方法。因此,要避免 shader compilation casutter 时,您需要生成每个 当关卡加载时,摄像机前的网格和视觉效果。 这将确保在加载关卡时编译着色器,而不是 在游戏过程中发生。这可以在实体 2D UI 后面完成(例如全屏 ColorRect 节点),使其对玩家不可见。
备注
在支持禁用垂直同步的平台上,可以通过在项目设置中禁用垂直同步来减少卡顿的明显程度。然而,这会导致出现撕裂,尤其是在刷新率低的显示器上。如果您的显示器支持它,请考虑启用可变刷新率 (G-Sync/FreeSync),同时启用垂直同步。这允许在不引入撕裂的情况下减轻某些形式的口吃。但是,它对大卡顿没有帮助,例如由着色器编译卡顿引起的卡顿。
强制显卡使用最大性能配置文件也有助于减少卡顿,但代价是增加 GPU 功耗。
此外,底层作系统可能会引起卡顿。以下是有关不同作系统上的卡顿的一些信息:
Windows
众所周知,Windows 会导致窗口游戏出现卡顿。这主要取决于安装的硬件、驱动程序版本和并行运行的进程(例如,打开许多浏览器选项卡可能会导致正在运行的游戏出现卡顿)。为了避免这种情况,Godot 将游戏优先级提高到“高于正常”。这有很大帮助,但可能无法完全消除口吃。
完全消除这种情况需要为您的游戏提供完全权限以成为“时间关键”,这是不建议的。有些游戏可能会这样做,但建议学会忍受这个问题,因为这在 Windows 游戏中很常见,大多数用户不会玩窗口游戏(在窗口中玩的游戏,例如益智游戏,通常不会出现这个问题)。
对于全屏来说,Windows 系统对游戏给予了特殊的优先级,所以卡顿现象不再可见,也非常罕见。大多数游戏都是这样玩的。
使用轮询率为 1,000 Hz 或更高的鼠标时,请考虑使用完全最新的 Windows 11 安装,该安装附带了与高轮询率鼠标的高 CPU 利用率相关的修复程序。这些修补程序在 Windows 10 及更早版本中不可用。
小技巧
游戏应该使用独占全屏窗口模式,而不是全屏,后者旨在防止 Windows 自动将窗口视为独占全屏。
全屏旨在供想要使用每像素透明度而又不存在被操作系统禁用的风险的 GUI 应用程序使用。它通过在屏幕底部留下一条 1 像素的线来实现这一点。相比之下,独占全屏使用实际屏幕大小,并允许 Windows 减少全屏游戏的抖动和输入延迟。
Linux
在桌面 Linux 上可能会出现卡顿现象,但这通常与不同的视频驱动程序和合成器有关。某些合成器也可能引发该问题(例如 KWin),因此建议尝试使用其他合成器以排除其原因。某些窗口管理器(例如 KWin 和 Xfwm)允许你手动禁用合成,这可以提高性能(但代价是撕裂)。
除了向驱动程序或合成器开发人员报告问题外,没有其他方法可以解决驱动程序或合成器卡顿问题。在窗口模式下玩游戏时,卡顿可能比在全屏模式下玩游戏时更严重,即使禁用了合成功能也是如此。
Feral GameMode 可用于在运行特定进程时自动应用优化(例如强制使用 GPU 性能配置文件)。
macOS
通常情况下,macOS 不会出现卡顿现象,尽管最近在全屏运行时报告了一些 bug(这是 macOS 的 bug)。如果你的机器表现出这种行为,请将这问题提交给我们。
Android
通常情况下,Android 平台不会出现卡顿和抖动现象,因为运行活动拥有所有的优先级。也就是说,可能会出现问题的设备(较旧的 Kindle Fire 就是其中之一)。如果你在 Android 平台上看到这些问题,请将问题提交给我们。
iOS
iOS设备通常没有卡顿现象,但运行新版操作系统的旧式设备可能会出现问题。这通常是不可避免的。
输入延迟
项目配置
在支持禁用垂直同步的平台上,可以通过在项目设置中禁用垂直同步来降低输入延迟的明显性。然而,这会导致出现撕裂,尤其是在刷新率低的显示器上。建议将垂直同步作为玩家切换的选项。
使用 Forward+ 或 Mobile 渲染方法时,启用垂直同步时减少视觉延迟的另一种方法是使用双缓冲垂直同步,而不是默认的三缓冲垂直同步。从 Godot 4.3 开始,这可以通过将 Display > Window > V-Sync > Swapchain Image Count 项目设置降低到 2 来实现。 使用双缓冲的缺点是帧速率会 如果由于 CPU 或 GPU 而无法达到显示刷新率,则稳定性较差 瓶颈。例如,在 60 Hz 显示器上,如果帧速率正常 在使用三重缓冲的游戏过程中降至 55 FPS,则必须下降 使用双缓冲暂时达到 30 FPS(然后在以下情况下返回到 60 FPS 可能)。因此,仅在可以的情况下才建议使用双缓冲垂直同步 始终达到目标硬件上的显示刷新率。
增加每秒物理迭代次数也可以减少 物理引起的输入延迟。这在使用物理时尤其明显 插值(提高平滑度,但增加延迟)。为此,请将 物理 > 常见 > 物理每秒滴答数(Physics Ticks Per Second) 设置为高于默认值 60 的值,或在运行时 Engine.physics_ticks_per_second 在 脚本。值是显示器刷新率的倍数(通常 60) 禁用物理插值时效果最佳,因为它们可以避免抖动。这意味着 120、180 和 240 等值是很好的起点。作为奖励,更高的物理 FPS 使隧道和物理不稳定问题不太可能发生。
增加物理 FPS 的缺点是 CPU 使用率会增加,这会导致具有大量物理模拟代码的游戏中出现性能瓶颈。可以通过仅在低延迟至关重要的情况下增加物理 FPS 或让玩家调整物理 FPS 以匹配其硬件来缓解这一问题。但是,不同的物理 FPS 会导致物理模拟的不同结果,即使在游戏逻辑中始终使用 delta。这可以让某些玩家比其他玩家更具优势。因此,对于竞争性多人游戏,应避免允许玩家自己更改物理 FPS。
最后,你可以通过在脚本中调用 Input.set_use_accumulated_input(false) 来禁用每个渲染帧的输入缓冲。这将使脚本中的 _input() 和 _unhandled_input() 函数在每个输入时被调用,而不是累积输入并等待一帧来渲染。禁用输入累积会增加 CPU 使用率,因此应谨慎操作。
小技巧
在任何 Godot 项目上,您可以使用 –disable-vsync 命令行参数强制禁用 V-Sync。从 Godot 4.2 开始,–max-fps
针对硬件/操作系统
如果你的显示器支持,请考虑启用可变刷新率(G-Sync/FreeSync)并保持 V-Sync 启用,然后根据该页面将项目设置中的帧速率限制为略低于显示器最大刷新率的值。例如,在 144 Hz 显示器上,你可以将项目的帧速率上限设置为 141。这乍一看可能违反直觉,但将 FPS 限制在最大刷新率范围以下可确保操作系统永远不必等待垂直消隐完成。这会导致相似的输入延迟,就像在帧速率上限相同的情况下禁用 V-Sync 一样(通常不大于 1 毫秒),但不会出现任何撕裂。
这可以通过更改应用程序 > 运行 > Max FPS 项目设置或在脚本中在运行时分配 Engine.max_fps 来完成。
在某些平台上,你还可以在图形驱动程序选项中选择低延迟模式(例如 Windows 上的 NVIDIA 控制面板)。Ultra 设置将为你提供尽可能低的延迟,但平均帧率会略低。强制 GPU 使用最高性能配置文件也可以进一步减少输入延迟,但代价是更高的功耗(以及由此产生的热量/风扇噪音)。
最后,确保你的显示器在操作系统的显示设置中以尽可能最高的刷新率运行。
此外,请确保你的鼠标被配置为使用其最高轮询率(游戏鼠标通常为 1,000 Hz,有时更高)。但是,高 USB 轮询率可能会导致 CPU 使用率过高,因此对于低端 CPU,500 Hz 可能是更安全的选择。如果你的鼠标提供多个 DPI 设置,请考虑使用尽可能最高的设置并降低游戏内灵敏度以减少鼠标延迟 https://www.youtube.com/watch?v=6AoRfv9W110。
在 Linux 上使用 X11 时,在允许合成的窗口管理器(例如 KWin 或 Xfwm)中禁用合成可以显着减少输入延迟。
报告卡顿、抖动或输入延迟问题
如果你报告的卡顿或抖动问题(提交 Issue)不是由上述原因引起的,请尽可能详细说明关于你的设备配置、操作系统、驱动程序版本等信息。这有助于我们更好地排除故障。
如果你要报告输入延迟问题,请附上使用高速相机(例如手机的慢动作视频模式)拍摄的画面。拍摄画面必须同时显示屏幕和输入设备,以便计算输入和屏幕结果之间的帧数。此外,请务必提及显示器的刷新率和输入设备的轮询率(尤其是鼠标)。
此外,请确保根据所表现出的行为使用正确的术语(抖动、卡顿、输入延迟)。这将有助于更快地了解你的问题。提供一个可用于重现该问题的项目,如果可以,请包含演示该 Bug 的屏幕截图。
合成器
能够用来控制 Viewport 渲染内容时所使用的渲染管线。
可以在 WorldEnvironment 节点上进行配置,并应用于所有视口;
也可以在 Camera3D 上进行配置,并仅应用于使用该相机的视口。
Compositor 资源用于配置 Compositor。首先,在相应的节点上创建一个新的合成器:
目前只有移动渲染器和 Forward+ 渲染器支持合成器功能。
合成器效果
允许你在渲染管线的各个阶段插入额外逻辑。
这是一项高级功能,需要对渲染管线有很高的理解才能充分利用它。
由于合成器效果的核心逻辑是从渲染管线调用的,因此需要注意的是,该逻辑将在渲染发生的线程内运行。
务必小心,以确保我们不会遇到线程问题。
为了说明如何使用合成器效果,我们将创建一个简单的后期处理效果,让你可以编写自己的着色器代码并通过计算着色器应用该全屏。你可以在这里找到完成的演示项目。
首先创建一个名为 post_process_shader.gd 的新脚本。
我们将把它作为一个工具脚本,这样就可以在编辑器中看到合成器效果的工作情况。
我们需要从 CompositorEffect 扩展我们的节点。还必须为脚本指定一个类名。
1 | @tool |
接下来,我们将为着色器模板代码定义一个常量。这是使计算着色器工作的样板代码。
1 | const template_shader: String = """ |
这里重要的一点是,对于屏幕上的每个像素,我们的 main 函数都会被执行,在其中我们加载像素的当前颜色值,执行用户代码,并将修改后的颜色写回到彩色图像中。
#COMPUTE_CODE 应被我们的用户代码替换掉。
为了用户代码,我们需要一个导出变量。我们还将定义一些将使用的脚本变量:
1 | @export_multiline var shader_code: String = "": |
请注意我们代码中 Mutex 的使用。我们的大多数实现都是从渲染引擎调用的,因此需在我们的渲染线程中运行。
我们需要确保设置新的着色器代码,并将着色器代码标记为脏,同时渲染线程不会访问这些数据。
接下来初始化我们的效果。
1 | # Called when this resource is constructed. |
这里最重要的是设置我们的 effect_callback_type,它告诉渲染引擎在渲染管线的哪个阶段调用我们的代码。
备注
目前我们只能访问 3D 渲染管线的各个阶段!
我们还获得了对渲染设备的引用,这将非常方便。
我们还需要自己进行清理,为此我们对 NOTIFICATION_PREDELETE 通知做出反应:
1 | # System notifications, we want to react on the notification that |
请注意,即使我们在渲染线程内创建了着色器,我们也不会在此处使用互斥锁。
我们的渲染服务器上的方法是线程安全的,并且 free_rid 将推迟清理着色器,将其推迟到当前正在渲染的所有帧都完成之后。
还要注意,我们无需释放管线。渲染设备会进行依赖跟踪,由于管线依赖于着色器,因此当着色器被销毁时,管线会被自动释放。
从此刻起,我们的代码将在渲染线程上运行。
我们的下一步是一个辅助函数,它将在用户代码发生更改时重新编译着色器。
1 | # Check if our shader has changed and needs to be recompiled. |
在这个方法的顶部,我们再次使用互斥锁来保护对用户着色器代码和脏标记的访问。如果我们的用户着色器代码脏了,我们会在本地线程中复制用户着色器代码。
如果我们没有新的代码片段,并且我们已经有一个有效的管线,我们就返回 true。
如果我们确实有新的代码片段,我们会将其嵌入到我们的模板代码中,然后进行编译。
警告
此处显示的代码将在运行时中编译我们的新代码。这对于原型设计非常有用,因为我们可以立即看到更改后的着色器的效果。
这可以防止预编译和缓存此着色器,这在类似主机的某些平台上可能是一个问题。请注意,演示项目附带了一个替代示例,其中 glsl 文件包含整个计算着色器,并且使用它。Godot 能够使用此方法预编译和缓存着色器。
最后我们需要实现我们的效果回调,渲染引擎将在渲染的正确阶段调用它。
1 | # Called by the rendering thread every frame. |
在这个方法开始时,我们检查是否有渲染设备,回调类型是否正确,以及检查是否有着色器。
备注
检查效果类型只是一种安全机制。我们在 _init 函数中设置了它,但用户可以在 UI 中更改它。
我们的 p_render_data 参数使我们能够访问一个对象,该对象保存了当前正在渲染的帧的特定数据。我们目前只对我们的渲染场景缓冲区感兴趣,它使我们能够访问渲染引擎使用的所有内部缓冲区。请注意,我们将其转换为 RenderSceneBuffersRD 以公开此数据的完整 API。
接下来,我们获得我们的内部尺寸,即我们的 3D 渲染缓冲区在放大之前的分辨率(如果适用),放大发生在我们的后期处理运行之后。
根据我们的内部大小,我们计算出我们的分组大小,在我们的模板着色器中查看我们的局部大小。
我们还填充了推送常量,以便着色器知道大小。Godot 暂时不支持结构体,因此我们使用 PackedFloat32Array 来存储这些数据。请注意,我们必须用 16 字节对齐填充该数组。换句话说,我们的数组长度需要是 4 的倍数。
现在我们循环遍历视图,以防我们使用适用于立体渲染(XR)的多视图渲染。大多数情况下,我们只有一个视图。
备注
此处使用多视图进行后处理并没有性能优势,像这样单独处理视图仍然可以使 GPU 在有利的情况下使用并行性。
接下来我们获取该视图的颜色缓冲区。这是我们的 3D 场景被渲染到的缓冲区。
然后我们准备一个统一的集合,以便我们可以将颜色缓冲区传递给我们的着色器。
请注意我们使用 UniformSetCacheRD 缓存,以确保我们可以每帧检查 uniform 集。由于我们的颜色缓冲区可以逐帧更改,并且我们的 uniform 缓存会在缓冲区释放时自动清理 uniform 集,因此这是确保我们不泄漏内存或使用过时集的安全方法。
最后,我们通过绑定管线、绑定 uniform 集、推送推送常量数据、以及为我们的组调用调度,来构建我们的计算列表。
合成器效果完成后,我们现在需要将其添加到合成器中。
在合成器上,我们扩展合成器效果属性并按 Add Element。
现在我们可以添加合成器效果:
选择 PostProcessShader 后,我们需要设置你的用户着色器代码:
1 | float gray = color.r * 0.2125 + color.g * 0.7154 + color.b * 0.0721; |
完成这一切后,我们的输出是灰度的。
有关后期效果的更高级示例,请查看由 Bastiaan Olij 创建的基于径向模糊的天空光线示例项目。
编写脚本
GDScript
GDScript 示例:
1 | # 一个脚本文件就是一个类! |
| 关键字 | 描述 |
|---|---|
| match | 相当于 switch 语句,但提供了一些附加功能。将 switch 替换为 match。删除 case。删除 break。将 default 替换为单个下划线。 |
| when | 用于 match 语句中的模式防护。 |
| break | 退出当前 for 或 while 循环的执行。 |
| continue | 立即跳到 for 或 while 循环的下一个迭代。 |
| pass | 语法上要求在不希望执行代码的语句中使用,例如在空函数中使用。 |
| return | 从函数当中返回一个值。 |
| class | 定义内部类。见内部类。 |
| class_name | 将脚本定义为具有指定名称的全局可访问类。见注册具名类。 |
| extends | 定义当前类的父类。 |
| is | 检测变量是否继承自给定的类,或检测该变量是否为给定的内置类型。 |
| in | 通常情况下用来检测字符串、列表、范围、字典、节点中是否存在某个值,而和 for 关键字连用时,则用于遍历字符串、列表、范围、字典、节点中的内容。 |
| as | 尝试将值转换为给定类型的值。 |
| self | 引用当前类实例。见 self。 |
| super | 解析父类作用域内的方法。见继承。 |
| signal | 定义信号。见信号。 |
| func | 定义函数。见函数。 |
| static | 将一个函数声明为静态函数,或将一个成员变量声明为静态成员变量。 |
| const | 定义常量。见常量。 |
| enum | 定义枚举。见枚举。 |
| var | 定义变量。见变量。 |
| breakpoint | 用来设置脚本编辑器辅助调试断点的关键字。与在脚本编辑器每行最左侧点击红点所创建的断点不同,breakpoint 关键字可以储存在脚本内部。在不同设备上使用版本工具进行调试时,由 breakpoint 关键字创建的断点仍旧有效。 |
| preload | 预加载类或变量。见类作为资源。 |
| await | 等待信号或协程完成。见等待信号和协程。 |
| yield | 以前的版本中用于协程,现保留为关键字,以便旧版本迁移至新版本。 |
| assert | 断言条件,若断言失败则记录错误。非调试版本中会忽略断言语法。见 Assert 关键字。 |
| void | 用于代表函数不返回任何值。 |
| PI | PI(π)常数。 |
| TAU | TAU(τ)常数。 |
| INF | 无穷常量,用于比较和计算结果。 |
| NAN | NAN(非数)常量,用作计算后不可能得到的结果。 |
| 运算符 | 描述 |
|---|---|
x[index] |
下标 |
x.attribute |
属性引用 |
foo() |
函数调用 |
await x |
等待信号或协程 |
x is Nodex is not Node |
类型检查 另见 is_instance_of() 函数。 |
x ** y |
幂(乘方) 将 x 与其自身相乘 y 次,类似于调用 pow() 函数。 |
~x |
按位取反 |
+x-x |
取同 / 取负(相反数) |
x * yx / yx % y |
乘法/除法/余数% 运算符也用于字符串的格式化。注意:这些运算符的运算机制与其在 C++ 中的运算机制一致,而对于使用 Python、JavaScript 等语言的用户则可能会存在在其意料之外的运算机制,详情见表后。 |
x + yx - y |
加法(或连接)/减法 |
x << yx >> y |
位移位 |
x & y |
按位与 |
x ^ y |
按位异或 |
x | y |
按位或 |
x == yx != yx < yx > yx <= yx >= y |
比较 详情见表后。 |
x in yx not in y |
检查包含关系in 也在 for 关键字的语法中使用。 |
not x!x |
布尔“非”及其不推荐使用的形式 |
x and yx && y |
布尔“与”及其不推荐使用的形式 |
x or yx || y |
布尔“或”及其不推荐使用的形式 |
真表达式 if 条件 else 假表达式 |
三元(目)运算符 if/else |
x as Node |
类型转换 |
一些运算符的运算机制可能会与你所预期的运算机制有所不同:
- 若运算符 / 两端的数值均为 int,则进行整数除法而非浮点数除法。例如: 5 /2 == 2 中该算式的结果为 2 而非 2.5。
若希望进行浮点数运算,请将该运算符两端的其中一个数值的类型改为 float ,如直接使用浮点数( x / 2.0 )、转换类型( float(x) / y )、乘以 1.0 ( x * 1.0 / y )等。 - 运算符 % 仅适用于整型数值的取余运算,对于小数的取余运算,请使用 fmod() 方法。
- 对于负值,% 运算符和 fmod() 函数会使用 截断算法 进行运算,而非向负无穷大舍入,此时余数会带有符号(即余数可能为负)。
如果你需要数学意义上的余数,请改用 posmod() 和 fposmod() 函数。 - == 和 != 运算符在有些情况下允许比较不同类型的值(例如 1 == 1.0 的结果为真),但在其他情况下可能会发生运行时错误。
若你不能确定操作数的类型,可使用 is_same() 函数来进行安全比较(但请注意,该函数对类型和引用更加严格)。
要比较浮点数,请改用 is_equal_approx() 和 is_zero_approx() 函数。
| 示例 | 描述 |
|---|---|
| $NodePath | get_node(“NodePath”) 的简写 |
| %UniqueNode | get_node(“%UniqueNode”) 的简写 |
整数和浮点数中的数字可用 _ 分隔,方便阅读。
注解
@onready 注解
能够将成员变量的初始化推迟到调用 _ready() 时。
1 | var my_label |
1 | @onready var my_label = get_node("MyLabel") |
在 GDScript 中,一行语句可通过反斜杠(\)接续到下一行。
核心特性
如何阅读 Godot API
继承,派生,简要描述,描述,教程,属性,方法,信号,枚举,常量,属性说明,方法描述
调试
输出面板
调试器面板
调试菜单选项
使用远程调试部署
使用网络文件系统进行小型部署
显示碰撞区域
分组
与标签类似。你可以将节点加入若干个分组之中,然后在代码中通过 SceneTree 来:
- 获取某个分组中的节点列表。
- 对分组中的所有节点调用方法。
- 向分组中的所有节点发送通知。
这个功能可以用来组织大型场景、解耦代码。
着色器
着色器简介
着色器(Shader)是一种在图形处理单元(GPU)上运行的特殊程序。
类似 Godot 的现代渲染引擎都会用着色器来执行所有绘制操作:图形卡可以并行执行成千上万条指令,可以达到惊人的渲染速度。
因为并行,所以着色器处理信息的方式与普通的程序有所不同。着色器代码是单独针对顶点或像素执行的。
无法在帧与帧之间存储数据。因此,使用着色器时,你需要使用与其他编程语言不同的编码和思考方式。
假设您要将纹理中的所有像素更新为给定颜色。在 GDScript 中,您的代码将使用 for 循环:
1 | for x in range(width): |
在着色器中,你的代码已经是循环的一部分了,所以对应的代码应该类似这样。
1 | void fragment() { |
图形卡会为需要绘制的每一个像素调用若干次 fragment() 函数。后面会详细说明。
Godot 所提供的着色语言是基于流行的 OpenGL 着色语言(GLSL)的简化。引擎会为你处理一些底层的初始化工作,让编写复杂着色器更为简单。
在 Godot 中,着色器由若干主函数组成,这些函数被称为”处理器函数“。处理器函数是着色器程序的入口。有七种不同的处理器函数。
- vertex() 函数会为网格中的所有顶点各运行一次,用来设置顶点的位置和其他与顶点相关的变量。在 canvas_item 着色器和空间着色器中使用。
- fragment() 函数会为网格所覆盖的所有像素各运行一次。这个函数会用到 vertex() 函数输出的值,这些值会在顶点之间进行插值。在 canvas_item 着色器和空间着色器中使用。
- light() 函数会为每个像素和每个灯光各运行一次。这个函数会用到 fragment() 函数以及前几次运行中的变量。在 canvas_item 着色器和空间着色器中使用。
- start() 函数会在粒子系统中的每个粒子出生时各运行一次。在粒子着色器中使用。
- process() 函数会为粒子系统中的每个粒子每帧时各运行一次。在粒子着色器中使用。
- sky() 函数会在辐射度立方体贴图需要更新时为辐射度立方体贴图中的每个像素各运行一次,也会为当前屏幕上的每个像素运行一次。在天空着色器中使用。
- fog() 函数会为体积雾片段体素缓冲中与 FogVolume 相交的每个片段体素运行一次。在雾着色器中使用。
警告
如果启用了 vertex_lighting 渲染模式,或者在项目设置中启用了 Rendering > Quality > Shading > Force Vertex Shading(渲染 > 质量 > 着色 > 强制顶点着色),则不会运行 light() 函数。在移动平台上默认启用。Godot 还为用户编写完全自定义的 GLSL 着色器暴露了 API。
着色器类型
编写的着色器必须指定类型(2D、3D、粒子、天空、雾),不存在所有场景都可以使用的通用配置。
不同的类型支持不同的渲染模式、内置变量、处理函数。
在 Godot 中,所有的着色器都需要在第一行指定它们的类型,类似这样:shader_type spatial;
有以下类型可用:
- 用于 3D 渲染的 spatial。
- 用于 2D 渲染的 canvas_item。
- 用于粒子系统的 particles。
- 用于渲染 Skies 的 sky。
- 用于渲染 FogVolumes 的 fog。
渲染模式
可以在着色器的第二行,也就是在着色器类型之后,指定渲染模式,类似这样:shader_type spatial;render_mode unshaded, cull_disabled;
渲染模式会修改 Godot 应用着色器的方式。
例如,unshaded 模式会让引擎跳过内置的光线处理器函数。
每种着色器类型都有不同的渲染模式。
每种着色器类型的完整渲染模式列表请参阅参考手册。
顶点处理器vertex()
处理函数对中的每个顶点调用一次空间和canvas_item着色器。
你的世界中的几何体上,每一个顶点都有位置、颜色等属性。
该函数会修改这些值,并将其传入片段函数。你也可以借助 varying 向片段着色器传递额外的数据。
默认情况下,Godot 会为你对顶点信息进行变换,这是将几何体投影到屏幕上所必须的。
可以使用渲染模式来自行变换数据;示例见 Spatial 着色器文档。
片段处理器fragment() 处理函数用于设置每个像素的 Godot 材质参数。此代码在对象或图元绘制的每个可见像素上运行。它仅在空间着色器和 canvas_item 着色器中可用。
片段函数的标准用途是设置用于计算光照的材质属性。例如,你可以为 ROUGHNESS、RIM、TRNASMISSION 等设置值,告诉光照函数光照应该如何处理对应的片段。这样就可以控制复杂的着色管线,而不必让用户编写过多的代码。如果你不需要这一内置功能,那么你可以忽略它,自行编写光照处理函数,Godot 会将其优化掉。例如,如果你没有向 RIM 写入任何值,那么 Godot 就不会计算边缘光照。编译时,Godot 会检查是否使用了 RIM;如果没有,那么它就会把对应的代码删除。因此,你就不会在没有使用的效果上浪费算力。
光照处理器light() 处理器也会在每一个像素上运行,并且同时还会在每一个影响该对象的灯光上运行。如果没有灯光影响该对象则不会运行。它会被用于 fragment() 处理器,一般会在 fragment() 函数中进行材质属性设置时执行。
2D 和 3D 中的工作方式不同;每种工作方式的详细描述请参阅它们对应的文档 CanvasItem 着色器 和 Spatial 着色器。
着色参考
着色语言
Godot 使用类似于 GLSL ES 3.0 的着色语言。
数据类型
| 类型 | 描述 |
|---|---|
void |
Void 数据类型,只对不返回任何内容的函数有用。 |
bool |
布尔数据类型,只能包含 true 或 false。 |
bvec2 |
布尔的两分量向量。 |
bvec3 |
布尔的三分量向量。 |
bvec4 |
布尔的四分量向量。 |
int |
32 位有符号标量整数。 |
ivec2 |
有符号整数的双分量向量。 |
ivec3 |
有符号整数的三分量向量。 |
ivec4 |
有符号整数的四分量向量。 |
uint |
无符号标量整数;不能包含负数。 |
uvec2 |
无符号整数的两分量向量。 |
uvec3 |
无符号整数的三分量向量。 |
uvec4 |
无符号整数的四分量向量。 |
float |
32 位浮点标量。 |
vec2 |
浮点值的两分量向量。 |
vec3 |
浮点值的三分量向量。 |
vec4 |
浮点值的四分量向量。 |
mat2 |
2x2 矩阵,按列主序存储。 |
mat3 |
3x3 矩阵,按列主序存储。 |
mat4 |
4x4 矩阵,按列主序存储。 |
sampler2D |
用于绑定被读取为浮点数的 2D 纹理的采样器类型。 |
isampler2D |
用于绑定被读取为有符号整数的 2D 纹理的采样器类型。 |
usampler2D |
用于绑定被读取为无符号整数的 2D 纹理的采样器类型。 |
sampler2DArray |
用于绑定被读取为浮点数的 2D 纹理数组的采样器类型。 |
isampler2DArray |
用于绑定被读取为有符号整数的 2D 纹理数组的采样器类型。 |
usampler2DArray |
用于绑定被读取为无符号整数的 2D 纹理数组的采样器类型。 |
sampler3D |
用于绑定被读取为浮点数的 3D 纹理的采样器类型。 |
isampler3D |
用于绑定被读取为有符号整数的 3D 纹理的采样器类型。 |
usampler3D |
用于绑定被读取为无符号整数的 3D 纹理的采样器类型。 |
samplerCube |
用于绑定被读取为浮点数的立方体贴图的采样器类型。 |
samplerCubeArray |
用于绑定立方体贴图数组的采样器类型,读取为浮点数。仅支持 Forward+ 和 Mobile,不支持 Compatibility。 |
samplerExternalOES |
外部采样器类型。仅支持 Compatibility/Android 平台。 |
| 警告 | |
| 局部变量不会初始化为默认值,例如 0.0。如果使用变量而不先赋值,它将包含该内存位置中已经存在的任何值,并且会出现不可预测的视觉故障。但是,uniforms 和 varyings 会初始化为默认值。 |
类型转换
与 GLSL ES 3.0 一样,不允许在大小相同但类型不同的标量和向量之间进行隐式转换。
也不允许强制转换不同大小的类型。转换必须通过构造函数显式完成。
成员
向量类型的单个标量成员可通过“x”、“y”、“z”和“w”成员访问。或者,使用“r”、“g”、“b”和“a”也行且效果相同。
对于矩阵,请使用 m[column][row] 索引语法访问每个标量,或使用 m[column] 按列索引访问向量。例如,要从 mat4 转换矩阵(第 4 列,第 2 行)访问转换的 y 分量,请使用 m[3][1] 或 m[3].y。
构造
向量类型的构造必须始终通过:
1 | // The required amount of scalars |
矩阵类型的构造需要与矩阵具有相同维度的向量,解释为列。您还可以使用 matx(float) 语法构建对角矩阵。因此,mat4(1.0) 是一个恒等矩阵。
1 | mat2 m2 = mat2(vec2(1.0, 0.0), vec2(0.0, 1.0)); |
矩阵也可以由另一维的矩阵构建。有两个规则:
- 如果较大的矩阵是从较小的矩阵构造的,则附加的行和列将设置为它们在恒等矩阵中具有的值。
- 如果较小的矩阵是从较大的矩阵构造的,则使用较大矩阵的顶部左侧子矩阵。
1 | mat3 basis = mat3(MODEL_MATRIX); |
调换
只要结果是另一种向量类型(或标量),就可以按任意顺序获得组件的任意组合。百闻不如一见:
1 | vec4 a = vec4(0.0, 1.0, 2.0, 3.0); |
精度
可以为数据类型添加精度修饰符;将它们用于 uniform、变量、参数、varying:
1 | lowp vec4 a = vec4(0.0, 1.0, 2.0, 3.0); // low precision, usually 8 bits per component mapped to 0-1 |
对某些操作使用较低的精度可以加快所涉及的数学运算速度(但代价是精度较低)。
这在顶点处理函数中鲜有需要(大多数情况下需要全精度),但在片段处理函数中通常很有用。
某些架构(主要是移动架构)可以从中受益匪浅,但也存在一些缺点,例如精度转换的额外开销。
有关更多信息,请参阅目标架构的文档。
在许多情况下,移动驱动程序会导致不一致或意外的行为,除非必要,否则最好避免指定精度。
数组
数组是用于多个相似类型的变量的容器。
局部数组
局部数组在函数中声明。它们可以使用除采样器之外的所有允许的数据类型。
数组声明遵循 C 样式语法:[const] + [precision] + typename + identifier + [array size]。
1 | void fragment() { |
它们可以在开始时被初始化,像这样:
1 | float float_arr[3] = float[3] (1.0, 0.5, 0.0); // first constructor |
你可以在一个表达式中声明多个数组(即使大小不同):
1 | float a[3] = float[3] (1.0, 0.5, 0.0), |
要访问一个数组元素,请使用索引语法:
1 | float arr[3]; |
备注
如果你使用的索引小于 0 或大于数组大小——着色器将崩溃并中断渲染。
为防止这种情况,请使用 length()、if 或 clamp() 函数来确保索引介于 0 和数组长度之间。
务必仔细测试和检查你的代码。如果你传递一个常量表达式或数字,编辑器将检查其边界以防止这种崩溃。
全局数组
你可以在全局作用域中将数组声明为 const 或 uniform:
1 | shader_type spatial; |
备注
全局数组的语法与局部数组相同,只是在声明时需要添加 const 或 uniform。注意,uniform 数组不能有默认值。
常量
在变量声明前使用 const 关键字,可以使该变量不可变, 这意味着它不能被修改。
除采样器外的所有基本类型都可以被声明为常量。访问和使用常量值的速度比使用 uniform 的速度略快。常量必须在其声明时被初始化。
1 | const vec2 a = vec2(0.0, 1.0); |
常量不能被修改,也不能有提示,但可以在单个表达式中声明多个常量(如果它们具有相同的类型),例如
1 | const vec2 V1 = vec2(1, 1), V2 = vec2(2, 2); |
与变量类似,数组也可以用 const 来声明。
1 | const float arr[] = { 1.0, 0.5, 0.0 }; |
常量既可以被全局声明(在任何函数之外),也可以被本地声明(在函数内部)。
当你想要访问整个着色器中不需要修改的值时,全局常量非常有用。与 uniform 一样,全局常量在所有着色器阶段之间共享,但在着色器外部无法访问。
1 | shader_type spatial; |
float 类型常量的初始化必须使用整数部分后的 . 符号或科学计数法。还支持可选的 f 后缀。
1 | float a = 1.0; |
uint(无符号整数)类型的常量必须有后缀 u,以区别于有符号整数。或者,也可以使用 uint(x) 内置转换函数来实现这一点。
1 | uint a = 1u; |
结构体
结构体是一种复合类型,可以对着色器代码进行更好的抽象。你可以在全局范围内声明它们,如下所示:
1 | struct PointLight { |
声明后,你可以像这样实例化和初始化它们:
1 | void fragment() |
或者使用结构体的构造函数达到同样的效果:
1 | PointLight light = PointLight(vec3(0.0), vec3(1.0, 0.0, 0.0), 0.5); |
结构体中可以包含其他结构体或者数组,你还可以把它们作为全局常量实例化:
1 | shader_type spatial; |
也可以把它们传递给函数:
1 | shader_type canvas_item; |
控制流
Godot 着色语言支持最常见的控制流类型:
1 | // `if`, `else if` and `else`. |
请记住,在现代 GPU 中,无限循环可以存在,并可能冻结你的应用程序(包括编辑器)。Godot 无法保护你免受这种影响,因此请小心,不要犯这种错误!
此外,将浮点值与数字进行比较时,请确保将它们与一个范围而不是一个精确数字进行比较。
类似 if (value == 0.3) 的比较可能不会评估为 true。浮点数学通常是近似的,可能会与预期不符。它还可能根据硬件的不同而表现不同。
不要这样做。
1 | float value = 0.1 + 0.2; |
相反,始终使用 epsilon 值进行范围比较。浮点数越大(浮点数越不精确),则 epsilon 值应该越大。
1 | const float EPSILON = 0.0001; |
有关更多信息,请参阅 floating-point-gui.de。
丢弃
片段函数、光照函数以及自定义函数(从片段或光照中调用)可使用 discard 关键字。若使用该关键字,则丢弃当前片段且不写入任何数据。
请注意,使用 discard 会降低性能,因为它会阻止预深度阶段在使用着色器的任何表面上起作用。
此外,被丢弃的像素仍需在顶点着色器中渲染,这意味着,与一开始就不渲染任何对象相比,在所有像素上使用 discard 的着色器的渲染成本仍然更高。
函数
可以在 Godot 着色器中定义函数。它们使用以下语法:
1 | ret_type func_name(args) { |
你只能使用已在调用它们的函数上方(编辑器中较高位置)定义的函数。重新定义已在上方定义的函数(或使用内置函数名称)将导致错误。
函数参数可以有特殊的限定符:
in:表示该参数仅供读取(默认)。
out:表示该参数仅供写入。
inout:表示参数完全通过引用传递。
const:表示参数是常量且不能更改,可以与 in 限定符结合使用。
以下为示例:
1 | void sum2(int a, int b, inout int result) { |
支持函数重载。您可以定义多个具有相同名称但参数不同的函数。请注意,不允许在重载函数调用中进行隐式强制转换 ,例如从 int 到 float(1 到 1.0)。
1 | vec3 get_color(int t) { |
Varying变化
要从顶点处理器函数往片段(或者灯光)处理器函数里发送数据,可以使用 varying。它们在顶点处理器中为每个图元顶点设置,并且该值对片段处理器中的每个像素进行插值。
1 | shader_type spatial; |
Varying 也可以是一个数组:
1 | shader_type spatial; |
也可以使用 varying 关键字将数据从片段处理器发送到灯光处理器。在片段函数中赋值,然后在灯光函数中使用即可。
1 | shader_type spatial; |
请注意,在自定义函数或灯光处理器函数中可能无法为 varying 赋值,例如:
1 | shader_type spatial; |
加入这一限制的目的是为了防止在初始化前进行错误的使用。
插值限定符
某些值在着色管道期间进行插值。你可以使用插值限定符来修改这些插值的方式。
1 | shader_type spatial; |
有两种可能的插值限定符:
限定符 描述
flat平 该值未插值。
smooth光滑 该值以透视校正方式进行插值。这是默认值。
Uniform均匀
可以使用制服将值传递给着色器,制服在 着色器的全局范围,函数之外。
当着色器稍后 分配给材质时,制服将作为可编辑参数显示在 材料检查员。
无法从着色器中编写制服。任何 除 void 之外的数据类型可以是统一的。
1 | shader_type spatial; |
您可以在材质检查器的编辑器中设置制服。或者,您可以从代码中设置它们。
Uniform提示
Godot 提供了可选的 uniform 提示,用于让编译器理解 uniform 的用途,以及编辑器应如何允许用户修改它。
1 | shader_type spatial; |
Uniform 也可以分配默认值:
1 | shader_type spatial; |
请注意,同时添加默认值和提示时,默认值应该写在提示的后面。
| 类型 | 提示 | 描述 |
|---|---|---|
| vec3、vec4 | source_color |
用作颜色。 |
| int 整数 | hint_enum("String1", "String2")hint_enum(“字符串 1”、“字符串 2”) |
在编辑器中将 int 输入显示为下拉控件。 |
| int、float | hint_range(min, max[, step])hint_range(最小值,最大值[,步长]) |
限制取值范围(最小值/最大值/步长)。 |
| sampler2D 采样器 2D | source_color |
用作反照颜色。 |
| sampler2D 采样器 2D | hint_normal |
用作法线贴图。 |
| sampler2D 采样器 2D | hint_default_white |
作为值或反照颜色,默认为不透明白色。 |
| sampler2D 采样器 2D | hint_default_black |
作为值或反照颜色,默认为不透明黑色。 |
| sampler2D 采样器 2D | hint_default_transparent |
作为值或反照颜色,默认为透明黑色。 |
| sampler2D 采样器 2D | hint_anisotropy |
作为 FlowMap,默认为右。 |
| sampler2D 采样器 2D | hint_roughness[_r, _g, _b, _a, _normal, _gray]hint_roughness[_r、_g、_b、_a、_normal、_gray] |
用于导入时的粗糙度限制器(尝试减少镜面锯齿)。_normal是引导粗糙度限制器的法线贴图,在具有高频细节的区域中粗糙度会增加。 |
| sampler2D 采样器 2D | filter[_nearest, _linear][_mipmap][_anisotropic]filter[_nearest, _linear][_mipmap][_anisotropic] |
启用指定的纹理过滤。 |
| sampler2D 采样器 2D | repeat[_enable, _disable]repeat[_enable, _disable] |
启用纹理重复。 |
| sampler2D 采样器 2D | hint_screen_texture |
纹理是屏幕纹理。 |
| sampler2D 采样器 2D | hint_depth_texture |
纹理是深度纹理。 |
| sampler2D 采样器 2D | hint_normal_roughness_texture |
纹理是法线粗糙度纹理(仅在 Forward+ 中受支持)。 |
使用 hint_enum
您可以使用 hint_enum uniform 将 int 值作为可读的下拉小部件访问:uniform int noise_type : hint_enum("OpenSimplex2", "Cellular", "Perlin", "Value") = 0;
您可以使用类似于 GDScript 的冒号语法将显式值分配给 hint_enum 制服:uniform int character_speed: hint_enum("Slow:30", "Average:60", "Very Fast:200") = 60;
该值将存储为整数,对应于所选选项的索引(即 0、1 或 2)或冒号语法分配的值(即 30、60 或 200)。
将值设置为 set_shader_parameter() 时,必须使用整数值,而不是字符串 名字。
使用 source_color
任何包含 sRGB 颜色数据的纹理都需要 source_color 提示才能正确采样。
这是因为 Godot 在线性色彩空间中渲染,但某些纹理包含 sRGB 颜色数据。如果不使用此提示,纹理将显得褪色。
反照率和颜色纹理通常应该有 source_color 提示。法线、粗糙度、金属纹理和高度纹理通常不需要 source_color 提示。
在 Forward+ 和 Mobile 渲染器中需要使用 source_color 提示,当 HDR 2D 时,在 canvas_item 着色器中需要使用 已启用。
source_color 提示对于兼容性渲染器是可选的,如果禁用了 HDR 2D,则对于 canvas_item 着色器。
但是,建议始终使用 source_color 提示,因为即使您更改渲染器或禁用 HDR 2D,它也能正常工作。
Uniform groups统一组
若要在检查器中的某个部分中对多个制服进行分组,您可以使用 group_uniform 关键字如下:
1 | group_uniforms MyGroup; |
结束分组的方法是:group_uniforms;
这一语法还支持子分组(在此之前不需要声明基础分组):group_uniforms MyGroup.MySubgroup;
全局 Uniform
有时你会想要统一修改很多不同着色器中的某个参数。
使用普通的 uniform 就会很麻烦,因为你需要记录这些着色器,并且需要一个个地设 uniform。
使用全局 uniform 就可以创建并更新所有着色器中均可以使用的 uniform,所有类型的着色器都适用(canvas_item、spatial、particles、sky、fog)。
全局 uniform 适用于能够影响场景中大量对象的环境效果,例如玩家在附近时的植被弯曲效果、物体随风移动的效果等。
备注
全局均匀与单个着色器的全局范围不同。虽然常规统一是在着色器函数外部定义的,因此是着色器的全局作用域,但全局统一对于整个项目中的所有着色器都是全局的(但在每个着色器中,也在全局作用域中)。
要创建全局 uniform,请打开项目设置,切换到着色器全局量选项卡。为 uniform 指定名称(区分大小写)和类型,然后点击对话框右上角的添加。点击 uniform 列表中的值即可编辑 uniform 的取值:
在“项目设置”的“着色器全局量”中添加全局 uniform
创建全局 uniform 之后,在着色器中的使用方法如下:
1 | shader_type canvas_item; |
请注意,保存着色器的时候该全局 uniform 必须在“项目设置”中存在,否则编译就会失败。虽然可以在着色器代码中使用 global uniform vec4 my_color = … 赋默认值,但是这个默认值会被忽略,因为全局 uniform 必须在“项目设置”中定义。
要在运行时更改全局制服的值,请使用 RenderingServer.global_shader_parameter_set 脚本中的方法:RenderingServer.global_shader_parameter_set("my_color", Color(0.3, 0.6, 1.0))
全局 uniform 可以重复赋值,不会影响性能,因为设置数据不需要在 CPU 和 GPU 之间进行同步。
您还可以在运行时添加或删除全局制服:
1 | RenderingServer.global_shader_parameter_add("my_color", RenderingServer.GLOBAL_VAR_TYPE_COLOR, Color(0.3, 0.6, 1.0)) |
在运行时添加或删除全局统一会产生性能成本,尽管与从脚本获取全局统一值相比,它没有那么明显(请参阅下面的警告)。
警告
虽然你可以在运行时在脚本中使用 RenderingServer.global_shader_parameter_get(“uniform_name”) 查询全局统一的值,但这会造成很大的性能损失,因为渲染线程需要与调用线程同步。
因此,不建议在脚本中频繁读取全局着色器 uniform 的取值。如果你需要在设值之后用脚本读取,请考虑创建一个自动加载,在设置需要查询的全局 uniform 的同时保存对应的值。
单实例 uniform
备注
每个实例的统一在 canvas_item (2D) 和空间 (3D) 着色器中可用。
有时,你想使用材质修改每个节点上的参数。例如,在长满树木的森林中,当您希望每棵树都有略有不同的颜色时,可以手动编辑。
如果没有每个实例的制服,这需要为每棵树创建唯一的材质(每种树的色调略有不同)。
这使得材质管理更加复杂,并且由于场景需要更多独特的材质实例,因此也存在性能开销。
顶点颜色也可以在这里使用,但它们需要为每种不同的颜色创建网格体的唯一副本,这也会产生性能开销。
单实例 uniform 设置在每个 GeometryInstance3D 上,而不是在每个材质实例上。在处理指定了多种材质的网格或多重网格设置时,请考虑这一点。
1 | shader_type spatial; |
在保存着色器后,你可以在检查器中更改单实例 uniform 的值:
在检查器中的 GeometryInstance3D 部分设置单实例 uniform 的值
每个实例的统一值也可以在运行时使用 set_instance_shader_parameter 继承自 GeometryInstance3D 的节点上的方法:$MeshInstance3D.set_instance_shader_parameter("my_color", Color(0.3, 0.6, 1.0))
在使用单实例 uniform 时,你应该注意一些限制:
每个实例的统一不支持纹理或数组 ,仅支持常规标量和矢量类型。作为一种解决方法,您可以将纹理数组作为常规制服传递,然后使用每个实例的制服传递要绘制的纹理的索引。
每个着色器的最大实例统一的实际上限为 16 个。
如果您的网格体使用多种材质,则找到的第一个网格体材质的参数将“胜过”后续材质,除非它们具有相同的名称、索引和类型。在这种情况下,所有参数都会受到正确影响。
如果遇到上述情况,可以通过手动避免冲突 指定实例的索引 (0-15) 统一 instance_index 提示:instance uniform vec4 my_color : source_color, instance_index(5);
从代码设置制服
您可以使用 GDScript 中的 set_shader_parameter() 方法:
1 | material.set_shader_parameter("some_value", some_value) |
备注
set_shader_parameter() 的第一个参数是着色器中制服的名称。它必须与着色器中制服的名称完全匹配,否则将无法识别。
GDScript 使用的变量类型与 GLSL 不同,所以当把变量从 GDScript 传递到着色器时,Godot 会自动转换类型。以下是相应类型的表格:
| GLSL 类型 | GDScript 类型 | 注意 |
|---|---|---|
| bool 布尔语 | bool 布尔语 | |
| bvec2 | int 整数 | 按位打包整数,其中位 0 (LSB) 对应 x。 例如,值为 (bx, by) 的 bvec2 可以按以下方式创建: `bvec2_input: int = (int(bx)) |
| bvec3 | int 整数 | 按位打包整数,其中位 0 (LSB) 对应 x。 |
| bvec4 | int 整数 | 按位打包整数,其中位 0 (LSB) 对应 x。 |
| int 整数 | int 整数 | |
| ivec2 艾维 C2 | Vector2i 矢量 2i | |
| ivec3 艾维 C3 | Vector3i 矢量 3i | |
| ivec4 | Vector4i 矢量 4i | |
| uint 因特 | int 整数 | |
| uvec2 紫外膜2 | Vector2i 矢量 2i | |
| uvec3 | Vector3i 矢量 3i | |
| uvec4 | Vector4i 矢量 4i | |
| float 浮 | float 浮 | |
| vec2 | Vector2 矢量2 | |
| vec3 | Vector3、Color 矢量 3、 颜色 |
使用 Color 时会将其解释为 (r, g, b)。 |
| vec4 | Vector4、Color、Rect2、Plane、Quaternion 矢量 4、 颜色 、 矩形 2、 平面 、 四元数 |
使用 Color 时会将其解释为 (r, g, b, a)。 使用 Rect2 时会将其解释为 (position.x, position.y, size.x, size.y)。 使用 Plane 时会将其解释为 (normal.x, normal.y, normal.z, d)。 |
| mat2 食物2 | Transform2D 变换 2D | |
| mat3 食物3 | Basis 基础 | |
| mat4 垫4 | Projection、Transform3D 投影 、 变换 3D |
使用 Transform3D 时,向量 w 为单位向量。 |
| sampler2D 采样器 2D | Texture2D 纹理 2D | |
| isampler2D | Texture2D 纹理 2D | |
| usampler2D | Texture2D 纹理 2D | |
| sampler2DArray 采样器 2DArray | Texture2DArray 纹理 2DArray | |
| isampler2DArray | Texture2DArray 纹理 2DArray | |
| usampler2DArray | Texture2DArray 纹理 2DArray | |
| sampler3D 采样器 3D | Texture3D 纹理 3D | |
| isampler3D | Texture3D 纹理 3D | |
| usampler3D | Texture3D 纹理 3D | |
| samplerCube 采样器立方体 | Cubemap 立方体贴图 | 有关导入立方体贴图以在 Godot 中使用的说明,请参阅 更改导入类型 。 |
| samplerCubeArray 采样器立方体阵列 | CubemapArray 立方体贴图数组 | 仅在 Forward+ 和移动版中受支持,不支持兼容性。 |
| samplerExternalOES 采样器外部 OES | ExternalTexture 外部纹理 | 仅在兼容性/Android 平台中受支持。 |
备注
从 GDScript 设置着色器统一时要小心,因为如果类型不匹配,则不会引发错误。
着色器将仅表现出未定义的行为。具体来说,这包括将 GDScript int/float(64 位)设置为 Godot 着色器语言 int/float(32 位)。
在需要高精度的情况下,这可能会导致意想不到的后果。
Uniform limits统一限制
可以在单个着色器中使用的着色器制服的总大小是有限制的。在大多数桌面平台上,此限制为 65536 字节,或 4096 VEC4 制服。
在移动平台上,限制通常为 16384 字节,或 1024 个 vec4 制服。
小于 vec4 的矢量均匀性(例如 vec2 或 vec3)被填充为 vec4 的大小。
标量均匀性,例如 int 或 float 没有填充,并且 bool 被填充到 int 的大小。
数组计为其内容的总大小。如果需要大于此限制的统一数组,请考虑将数据打包到纹理中,因为纹理的内容不计入此限制,仅计入采样器均匀的大小。
内置变量
有大量内置变量可用,例如 UV、COLOR 和 顶点 。
可用的变量取决于着色器的类型( 空间 、canvas_item、 粒子等)和使用的函数( 顶点 、 片段 、 光源 、 开始 、 进程 、 天空或雾 )。
有关可用内置变量的列表,请参阅相应的页面:
空间着色器
画布物品着色器
粒子着色器
天空着色器
雾效着色器
内置函数
支持大量内置功能,符合 GLSL ES 3.0。有关详细信息,请参阅内置函数页面。
你的第一个着色器
你的第一个 2D 着色器
着色器是在 GPU 上运行,用来渲染图像的一种特殊程序。
引导使用顶点和片段函数编写着色器的整个流程.
如果着色器有一定的经验,只想知道着色器在 Godot 中是如何运作,请参阅着色器参考。
场景布置
CanvasItem 着色器在 Godot 中是用来绘制所有 2D 对象的,而 Spatial 着色器则用于绘制所有 3D 对象。
从 CanvasItem 派生的所有对象都具有 material 属性。
这包括所有 GUI 元素 、Sprite2D、TileMapLayers、MeshInstance2D 等。
他们还可以选择继承父母的材料。如果你有大量节点想要使用相同的材质,这会很有用。
首先,创建一个 Sprite2D 节点(可以使用任何 CanvasItem)。
在“检查器”中,点击“Texture”旁边写着“[空]”的地方然后选择“加载”,再选择“Icon.svg”。
这个就是新项目中 Godot 的图标。你现在就会在视口中看到这个图标了。
接下来,在“检查器”下的 CanvasItem 部分中,在“Material”旁点击并选择“新建 ShaderMaterial”。
这会创建一个新的材质资源。然后点击新出现的球体。
Godot 目前还不知道你是要写 CanvasItem 着色器还是 Spatial 着色器,它显示 Spatial 着色器的输出预览,所以你看到的是默认的 Spatial 着色器的输出。
单击“着色器”旁边的 ,然后选择“新建着色器”。最后,单击您刚刚创建的着色器,着色器编辑器将打开。现在,你已准备好开始编写第一个着色器。
备注
继承自 “材料” 资源的材料,例如StandardMaterial3D 和 ParticleProcessMaterial,可以转换为 ShaderMaterial 它们的现有属性将转换为随附的文本着色器。
为此,请右键单击文件系统停靠栏中的材质,然后选择 转换为 ShaderMaterial。您也可以通过右键单击检查器中包含对材质的引用的任何属性来执行此作。
你的第一个 CanvasItem 着色器
在Godot中, 所有的着色器第一行都是指定着色器类型的, 格式如下:shader_type canvas_item;
因为我们正在编写 CanvasItem 着色器,所以我们在第一行中指定 canvas_item。我们所有的代码都将位于此声明之下。
这一行告诉游戏引擎要提供你哪些内置变量以及函数.
在 Godot 中,您可以覆盖三个函数来控制着色器的运行方式; 顶点 、 片段和光源 。
本教程将引导你编写具有顶点和片段函数的着色器。Light 函数比顶点和片段函数复杂得多,因此这里不会介绍。
你的第一个片段函数
fragment 函数针对 Sprite2D 中的每个像素运行,并确定该像素应为何种颜色。
它们仅限于 Sprite2D 覆盖的像素,这意味着您不能使用它来创建轮廓,例如,围绕 Sprite2D 创建轮廓。
最基础的片段函数仅仅给每个像素赋予一个颜色.
我们通过向内置变量 COLOR 写入 vec4 来做到这一点。vec4 是构造具有 4 个数字的向量的简写。
有关向量的更多信息,请参阅向量数学教程 。COLOR 既是片段函数的输入变量,也是片段函数的最终输出。
1 | void fragment(){ |
恭喜你!你成功在 Godot 中写出了你的第一个着色器。
接着, 我们来讨论更复杂的事情.
片段函数有许多输入可用于计算 颜色 。 紫外线就是其中之一。
UV 坐标在 Sprite2D 中指定(在您不知情的情况下!),它们告诉着色器从网格体每个部分的纹理中读取的位置。
在片段函数中你只能从 UV 中读取, 但是你可以在其他函数中使用, 或者直接对 COLOR 赋值.
UV 取值在0-1之间, 从左到右, 由上到下.
1 | void fragment() { |
使用内置变量 TEXTURE
默认的片段函数会读取 Sprite2D 设置的纹理并将其显示出来。
想要调整 Sprite2D 中的颜色时,你可以像下面的代码那样手动修改纹理中的颜色。
1 | void fragment(){ |
Sprite2D 等节点存在专门的纹理变量,在着色器中可以通过 TEXTURE 访问。使用 Sprite2D 纹理时如果需要与其他颜色组合,你可以使用 UV 配合 texture 函数来访问这个变量,重绘 Sprite2D 的纹理。
1 | void fragment(){ |
Uniform 输入
Uniform 输入是用来向着色器传递数据的,这些数据在整个着色器中都是一致的。
你可以像这样通过在着色器顶部定义来使用 Uniform 值:uniform float size;
用法的更多详情请参见着色语言文档。
添加一个 Uniform 值来改变 Sprite2D 中蓝色量。
1 | uniform float blue = 1.0; // you can assign a default value to uniforms |
现在你可以在编辑器中改变这个 Sprite2D 的蓝色量。
回头看看“检查器”中你创建着色器的地方,你应该会看到一个叫做“Shader Param”的部分。
展开这个部分就会看到你刚刚声明的 Uniform。如果在编辑器中改变这个值,就会覆盖你在着色器中提供的默认值。
代码与着色器的交互
在代码中,你可以对该节点的材质资源使用 set_shader_parameter() 函数,从而修改 Uniform。
对于 Sprite2D 节点的话,使用下面的代码就可以设置 blue 这个 Uniform。
1 | var blue_value = 1.0 |
注意,uniform值的名称是一个字符串. 字符串必须与它在着色器中的书写方式完全匹配, 包括拼写和大小写.
你的第一个顶点函数
现在我们有了一个片段函数, 我们再写一个顶点函数.
使用顶点函数计算屏幕上每个顶点的结束位置.
顶点函数中最重要的变量是 VERTEX。它最初指定的是模型中的顶点坐标,但你也会通过往里面写值来决定把这些顶点画到哪里。VERTEX 是一个 vec2,最初使用的是局部空间(即与摄像机、视口、父节点无关)。
你可以通过直接调整 VERTEX 来偏移顶点。
1 | void vertex() { |
结合内置变量 TIME 就可以制作简单的动画。
1 | void vertex() { |
总结
着色器的核心部分:对 VERTEX 和 COLOR 的计算。
可以制定更复杂的数学策略来给这些变量赋值。
一些更高级的着色器教程可以给你启发, 如 Shadertoy 和 着色器之书 .
你的第一个 3D 着色器
如何编写空间着色器
空间着色器比CanvasItem着色器有更多的内置功能.
对空间着色器的期望是:Godot为常见的用例提供了功能, 用户仅需在着色器中设置适当的参数. 这对于PBR(基于物理的渲染)工作流来说尤其如此.
这个教程分为两个部分。
在第一部分中,我们会使用在 vertex 函数中根据高度图进行顶点位移,从而制作地形。
在第二部分中,我们会使用这个脚本中涉及的概念在片断着色器中设置自定义材质,编写海洋水体着色器。
在本教程中,我们将在网格体本身上设置材质,而不是利用 MeshInstance3D 覆盖材质的能力。
设置
向场景添加一个新的 MeshInstance3D 节点。
在检查器选项卡中,将 MeshInstance3D 的 Mesh 属性设置为新的 PlaneMesh 资源,通过单击
然后通过单击出现的平面图像来扩展资源。
这为我们的场景添加了一个平面。
然后,在视口中,单击左上角的 透视(Perspective) 按钮。
在出现的菜单中,选择 “显示线框”(Display Wireframe)。
这将允许你查看构成平面的三角形.
现在,将平面网格体的 细分宽度(Subdivide Width) 和 细分深度(Subdivide Depth) 设置为 32。
PrimitiveMesh 和 PlaneMesh 一样,只有一个 表面,因此只有一种材料而不是材料数组。
将 Material 添加到新的 ShaderMaterial,然后通过单击出现的球体来扩展材质。
备注
继承自 “材料” 资源的材料,例如StandardMaterial3D 和 ParticleProcessMaterial,可以转换为 ShaderMaterial 它们的现有属性将转换为随附的文本着色器。 为此,请右键单击文件系统停靠栏中的材质,然后选择 转换为 ShaderMaterial。您也可以通过右键单击检查器中包含对材质的引用的任何属性来执行此作。
现在,通过单击
单击检查器中的着色器,着色器编辑器现在应该会弹出。你已准备好开始编写你的第一个空间着色器!
新着色器已经使用 shader_type 变量生成,即 vertex() 函数和 fragment() 函数。
戈多的第一件事 着色器需要的是它们是什么类型的着色器的声明。
在本例中, shader_type 设置为空间,因为这是一个空间着色器。shader_type spatial;
vertex() 函数确定 MeshInstance3D 的顶点位置 出现在最后的场景中。我们将使用它来偏移每个顶点的高度 并使我们的平面看起来像一个小地形。
由于 vertex() 函数中没有任何内容,Godot 将使用其默认的顶点着色器。我们可以通过添加一行来开始进行更改:
1 | void vertex() { |
VERTEX 的 y 值正在增加。
我们将 VERTEX 的 x 和 z 分量作为参数传递给 cos() 和 sin(); 这使我们在 X 轴和 Z 轴上呈现出波浪状外观。
我们想要实现的是小山丘的外观;
毕竟。cos() 和 sin() 看起来已经有点像山丘了。我们通过将输入缩放到 cos() 和 sin() 函数。
1 | void vertex() { |
看起来效果好了一些, 但它仍然过于尖锐和重复, 让我们把它变得更有趣一点.
噪声高度图
噪声是一种非常流行的伪造地形的工具. 可以认为它和余弦函数一样生成重复的小山, 只是在噪声的影响下每个小山都拥有不同的高度.
Godot 提供了 NoiseTexture2D 资源,用于生成可从着色器访问的噪声纹理。
要在着色器中访问纹理,请在着色器顶部附近、vertex() 函数外部添加以下代码。uniform sampler2D noise;
这将允许您将噪点纹理发送到着色器。现在查看材料下方的检查器。
您应该会看到一个名为 着色器参数(Shader Parameters) 的部分。如果你打开它,你会看到一个名为“噪音”的参数。
将此 噪点(Noise) 参数设置为新的 NoiseTexture2D。
然后在 NoiseTexture2D 中,将其 Noise 属性设置为新的 FastNoiseLite。NoiseTexture2D 使用 FastNoiseLite 类来生成高度图。
现在,使用 texture() 函数访问噪声纹理:
1 | void vertex() { |
texture() 将纹理作为第一个参数,将纹理上位置的 vec2 作为第二个参数。我们使用 VERTEX 的 x 和 z 通道,以确定在纹理上查找的位置。
由于 PlaneMesh 坐标在 [-1.0, 1.0] 范围内(对于大小为 2.0),而纹理坐标在 [0.0, 1.0] 范围内,因此要重新映射坐标,我们将 PlaneMesh 的大小除以 2.0 并添加 0.5 .
texture() 返回该位置的 r、g、b、a 通道的 vec4。由于噪点纹理是灰度的,因此所有值都是相同的,因此我们可以使用任何一个通道作为高度。在这种情况下,我们将使用 r 或 x 通道。
备注
xyzw 与 GLSL 中的 rgba 相同,因此而不是 texture().x 上面,我们可以使用 texture().r。有关更多详细信息,请参阅 OpenGL 文档 。
使用此代码后, 你可以看到纹理创建了随机外观的山峰.
目前它还很尖锐, 我们需要稍微柔化一下山峰. 这将用到uniform值. 你在之前已经使用了uniform 值来传递噪声纹理, 现在让我们来学习一下其中的工作原理.
Uniform均匀
统一变量允许您传递数据 从游戏到着色器。他们是 对于控制着色器效果非常有用。制服几乎可以是任何数据类型 这可以在着色器中使用。要使用制服,请在 使用关键字 uniform 的着色器 。
让我们做一个改变地形高度的uniform.
Godot 允许您使用值初始化制服;此处,height_scale 设置为 0.5。您可以通过调用函数 set_shader_parameter() 在与着色器对应的材质上。从 GDScript 传递的值 优先于用于在着色器中初始化它的值。
1 | # called from the MeshInstance3D |
备注
在基于空间的节点中更改制服与基于 CanvasItem 的节点不同。
在这里,我们在 PlaneMesh 资源中设置材质。在其他网格体资源中,你可能需要先通过调用 surface_get_material() 来访问材质。
在 MeshInstance3D,您将使用 get_surface_material() 或 material_override。
请记住,传递给 set_shader_parameter() 的字符串必须与着色器中统一变量的名称匹配。
您可以在着色器内的任何位置使用 uniform 变量。在这里,我们将用它来设置高度值,而不是任意乘以 0.5。VERTEX.y += height * height_scale;
现在它看起来好多了.
使用制服,我们甚至可以更改每一帧的值,以动画化地形的高度。与补间结合使用,这对于动画特别有用。
与光交互
首先,关闭线框。为此,请再次打开视口左上角的 透视(Perspective) 菜单,然后选择 显示法线(Display Normal) 。此外,在 3D 场景工具栏中,关闭预览阳光。
注意网格颜色是如何变得平滑的. 这是因为它的光线是平滑的. 让我们加一盏灯吧!
首先,我们将向场景添加一个 OmniLight3D,并将其向上拖动,使其位于地形上方。
你会看到光线影响了地形, 但这看起来很奇怪. 问题是光线对地形的影响就像在平面上一样. 这是因为光着色器使用 网格 中的法线来计算光.
法线存储在网格中, 但是我们在着色器中改变网格的形状, 所以法线不再正确.
为了解决这个问题, 我们可以在着色器中重新计算法线, 或者使用与我们的噪声相对应的法线纹理.Godot让这一切变得很简单.
你可以在顶点函数中手动计算新的法线,然后只需设置法线 NORMAL。
设置好 NORMAL 后,Godot 将为我们完成所有困难的光照计算。我们将在本教程的下一部分介绍这种方法,现在我们将从纹理中读取法线。
将第二个均匀纹理设置为另一个 NoiseTexture2D,并使用另一个 FastNoiseLite。但这一次,选中 As Normal Map。
当我们有与特定顶点相对应的法线时,我们设置 NORMAL,但 如果你有一个来自纹理的法线贴图,请使用 NORMAL_MAP fragment() 函数中。
这样,Godot 将自动处理将纹理包裹在网格周围。
最后,为了确保我们从噪点纹理和法线贴图纹理上的相同位置读取,我们将传递 VERTEX.xz 从 vertex() 函数到 fragment() 函数的位置。
我们使用一个变化来做到这一点。
完整代码
1 | shader_type spatial; |
法线就位后, 光线就会对网格的高度做出动态反应.
我们甚至可以把灯拖来拖去, 灯光会自动更新.
这就是这部分的全部内容. 希望你现在已了解Godot中顶点着色器的基本知识.
在本教程的下一部分中, 我们将编写一个片段函数来配合这个顶点函数, 并且我们将介绍一种更高级的技术来将这个地形转换成一个移动的波浪海洋.
你的第二个 3D 着色器
从高层次来看,Godot 所做的是给用户一堆可以选择设置的参数(AO、SSS_Strength、RIM 等)。
这些参数对应于不同的复杂效果(环境光遮蔽、次表面散射、边缘光照等)。
如果未写入代码,则代码在编译之前会被丢弃,因此着色器不会产生额外功能的成本。
这使得用户可以轻松获得复杂的 PBR 正确着色,而无需编写复杂的着色器。
当然,Godot 还允许您忽略所有这些参数并编写完全自定义的着色器。
有关这些参数的完整列表, 请参见 空间着色器 参考文档.
顶点函数和片段函数之间的区别在于,顶点函数按顶点运行并设置 VERTEX 等属性 (position) 和 NORMAL, 而片段着色器按像素运行,最重要的是,设置 MeshInstance3D 的 ALBEDO 颜色。
第一个空间片段函数
如本教程的前一部分所述。
Godot 中 fragment 函数的标准用法是设置不同的材质属性,让 Godot 处理其余的。
为了提供更大的灵活性,Godot 还提供了称为渲染模式的东西。
渲染模式设置在着色器的顶部,即 shader_type 的正下方,它们指定您希望着色器的内置方面具有的功能类型。
例如,如果不想让灯光影响对象,请将渲染模式设置为 unshaded:render_mode unshaded;
你还可以将多个渲染模式堆叠在一起。
例如,如果你想使用卡通材质而不是更真实的 PBR 材质,将漫反射模式和镜面反射模式设置为卡通:render_mode diffuse_toon, specular_toon;
这个内置功能模型允许你通过更改几个参数来编写复杂的自定义着色器.
有关渲染模式的完整列表, 请参见空间着色器参考 Spatial shader reference.
在本教程的这一部分中, 我们将介绍如何将前一部分的崎岖地形变成海洋.
首先让我们设置水的颜色. 我们通过设置 ALBEDO 来做到这一点.
ALBEDO 是一个 vec3 , 包含物体的颜色.
我们把它调成蓝色.
1 | void fragment() { |
我们将其设置为深蓝色, 因为水的大部分蓝色来自天空的反射.
Godot 使用的 PBR 模型依赖于两个主要参数:METALLIC 和 粗糙度 。
粗糙度指定材质表面的光滑度/粗糙度。
低 粗糙度会使材质看起来像闪亮的塑料,而高粗糙度会使材质的颜色看起来更纯色。
METALLIC 指定该物体有多像金属, 它最好设置为接近 0 或 1 . 把 METALLIC 看作是改变反射和 ALBEDO 颜色之间的平衡. 高的 METALLIC 几乎完全忽略了 ALBEDO , 看起来像天空的镜子. 而低的 METALLIC 对天空的颜色和 ALBEDO 的颜色有一个更平实的表现.
粗糙度从左到右从 0 增加到 1,而 METALLIC 从上到下从 0 增加到 1。
备注
METALLIC 应接近 0 或 1,以获得适当的 PBR 着色。仅将其设置在它们之间以进行材质之间的混合。
水不是金属,所以我们将其 METALLIC 属性设置成 0.0。水的反射性也很高,因此我们将其ROUGHNESS 属性也设置得非常低。
1 | void fragment() { |
现在,我们有了光滑的塑料外观表面。现在该考虑要模拟的水的某些特定属性了。
这里有两种主要的方法可以把诡异的塑料表面变成好看的水。首先是镜面反射(Specular)。
镜面反射是那些来自太阳直接反射到你眼里的明亮斑点。第二个是菲涅耳反射(Fresnel)。
菲涅尔反射是物体在小角度下更具反射性的属性。这就是为什么你可以看见自己身下的水,却在更远处看见天空倒影的原因。
为了增强镜面反射,我们需要做两件事。首先,由于卡通渲染模式具有更高的镜面反射高光,我们将更改镜面反射为卡通渲染模式。render_mode specular_toon;
其次, 我们将添加边缘照明. 边缘照明增加了掠射角度的光线效果.
通常, 它用于模拟光线穿过对象边缘上的织物的路径, 但是我们将在此处使用它来帮助实现良好的水润效果.
1 | void fragment() { |
为了添加菲涅尔反射率,我们将在我们的 片段着色器。
在这里,我们不会使用真正的菲涅耳术语来表示 性能原因。
相反,我们将使用 NORMAL 和 VIEW 向量。NORMAL 矢量指向远离网格体表面,
而 VIEW 矢量是眼睛与表面上该点之间的方向。它们之间的点积是一种方便的方法,可以判断您何时正面或以扫视角度观看表面。float fresnel = sqrt(1.0 - dot(NORMAL, VIEW));
并将其混合到 ROUGHNESS 和 ALBEDO 中。
这是 ShaderMaterials 相对于 StandardMaterial3Ds.
使用 StandardMaterial3D,我们可以使用纹理或将这些属性设置为平面数字。
但是使用着色器,我们可以根据我们能想到的任何数学函数来设置它们。
1 | void fragment() { |
而现在, 只需要5行代码, 你就可以拥有看起来很复杂的水.
现在, 我们有了照明, 这个水看起来太亮了. 让我们把它变暗.
这可以通过减少我们传入 ALBEDO 的 vec3 的值来轻松实现. 让我们把它们设置为 vec3(0.01, 0.03, 0.05) .
用 TIME 做动画
回到顶点功能,我们可以使用内置变量 TIME 对波浪进行动画处理。
TIME 是一个内置变量,可从顶点和片段函数访问。
在上一个教程中,我们通过从高度图读取来计算高度。对于本教程,我们将做同样的事情。将高度图代码放在一个名为 height() 的函数中。
1 | float height(vec2 position) { |
为了在 height() 函数中使用 TIME,我们需要将其传递进去。
1 | float height(vec2 position, float time) { |
确保其正确传递到顶点函数中.
1 | void vertex() { |
而不是使用法线贴图来计算法线。我们将在 vertex() 函数中手动计算它们。为此,请使用以下代码行。NORMAL = normalize(vec3(k - height(pos + vec2(0.1, 0.0), TIME), 0.1, k - height(pos + vec2(0.0, 0.1), TIME)));
我们需要手动计算 NORMAL,因为在下一节中,我们将使用数学来创建外观复杂的波形。
现在,我们要通过使 positon 偏移 TIME 的余弦来使 height() 函数更加复杂。
1 | float height(vec2 position, float time) { |
这会实现缓慢移动的波纹效果, 但显得有点不自然.
下一节将深入探讨, 通过加入更多的数学函数, 来用着色器实现更复杂的效果, 比如更加真实的波纹.
进阶效果:水波
利用数学, 着色器可以实现复杂的效果, 这是着色器的强大之处.
为阐述这一点, 我们将修改 height() 函数和引入新函数 wave() , 来让波纹效果更进一层.
wave() 有一个参数, position, 和在 height() 中一样.
我们将在 height() 函数中多次调用 wave() 函数, 来改变波纹的样子.
1 | float wave(vec2 position){ |
这在一开始会让人觉得很复杂, 所以我们一行一行地来实现.position += texture(noise, position / 10.0).x * 2.0 - 1.0;
通过 noise 纹理来偏移位置. 这将会使波浪成为曲线, 所以它们将不会是与网格所对齐的直线.vec2 wv = 1.0 - abs(sin(position));
用 sin() 和 position 定义一个类似波浪的函数.
通常 sin() 波是很圆的.
我们使用 abs() 去将其绝对化, 让它有一个尖锐波峰, 并将其约束于0-1的范围内. 然后我们再从 1.0 中减去, 将峰值放在上方.return pow(1.0 - pow(wv.x * wv.y, 0.65), 4.0);
将x方向的波乘以y方向的波, 并将其提高到使峰值变得尖锐的幂. 然后从 1.0 中减去它, 使山脊成为山峰, 并提高山脊锐化的能力.
现在我们可以用 wave() 代替 height() 函数的内容.
1 | float height(vec2 position, float time) { |
这样一来, 你会得到:
正弦曲线的形状太明显了. 所以让我们把波型分散一下. 我们通过缩放 位置 来实现.
1 | float height(vec2 position, float time) { |
如果我们将多个波以不同的频率和幅度彼此叠加, 则可以做得更好.
这意味着我们将按比例缩放每个位置, 以使波形更细或更宽(频率). 我们将乘以波的输出, 以使它们变低或变高(振幅).
下面以四种波形为例, 说明如何将四种波形分层, 以达到更漂亮的波形效果.
1 | float height(vec2 position, float time) { |
请注意,我们将时间添加到两个并从其他两个中减去它。这使得波向不同的方向移动,产生复杂的效果。
另请注意,振幅(结果乘以的数字)加起来都是 1.0。这使得波浪保持在 0-1 范围内。
有了这段代码, 你应该可以得到更复杂的波形, 而你所要做的只是增加一点数学运算!
有关空间着色器的更多信息, 请阅读 Shading Language 文档和 Spatial Shaders 文档. 也可以看看 Shading 部分 和 3D 部分的高级教程.
使用 VisualShader
VisualShaders 是创建着色器的可视化替代方案。
由于着色器本质上与视觉效果有联系, 与纯粹基于脚本的着色器相比, 基于图的方式, 有纹理, 材质等的预览, 提供了很多额外的便利.
另一方面,VisualShaders并没有暴露出着色器脚本的所有功能, 对于特定的效果, 并行使用两者可能是必要的.
创建 VisualShader
VisualShader 可以在任何 ShaderMaterial 中创建。
在选择的对象中创建一个新的 ShaderMaterial。
然后将着色器资源分配给着色器属性。
单击新的着色器资源,创建着色器对话框将自动打开。
将“类型”选项更改为 VisualShader ,然后为其命名。
单击刚刚创建的可视着色器以打开着色器编辑器。
着色器编辑器的布局由四个部分组成,左侧的文件列表、上方工具栏、图表本身和右侧可关闭的材质预览
在工具栏中从左到右:
- 箭头可用于切换文件面板的可见性。
- “ 文件 ”按钮会打开一个下拉菜单,用于保存、加载和创建文件。
- 添加节点 按钮会显示一个弹出式菜单,让你为着色器图添加节点。
- 下拉菜单是着色器类型. 顶点, 碎片和光线和脚本着色器一样, 它定义了哪些内置节点将是可用的.
- 下面的按钮和数字输入控制缩放级别, 网格捕捉和网格线之间的距离(单位为像素).
- 切换控制编辑器右下角的图形小地图是否可见。
- 自动排列所选节点按钮将尝试尽可能高效、干净地组织您选择的任何节点。
- 管理变化按钮会打开一个下拉列表,用于添加或删除变化。
- 显示生成的代码按钮显示与图表对应的着色器代码。
- 切换开关打开或关闭材质预览。
- “联机文档” 按钮可在 Web 浏览器中打开此文档页面。
- 最后一个按钮允许您将着色器编辑器放在它自己的窗口中,与编辑器的其余部分分开。
备注
虽然 VisualShader 不需要编码,但它们与脚本着色器有着相同的逻辑。建议学习这两者的基础知识,以便对着色管道有一个很好的理解。
可视化的着色器图形在场景后台转换为脚本着色器, 按下工具栏上的最后一个按钮就可以看到代码. 这可以方便理解特定节点的作用, 以及如何在脚本中呈现.
使用 Visual Shader 编辑器
默认情况下, 每个新的 VisualShader 都会有一个输出节点.
每个节点的连接都在输出节点的一个套接处结束. 节点是创建着色器的基本单元.
要添加一个新的节点, 点击左上角的 添加节点 按钮, 或者在图形中的任何一个空的位置上右击, 就会弹出一个菜单.
此弹出窗口具有以下属性:
如果你在图形上单击右键, 这个菜单将在光标位置被调出, 创建的节点, 在这种情况下, 也将被放在该位置, 否则, 将在图形的中心位置创建.
它可以在水平和垂直方向上调整大小, 以允许显示更多的内容.
尺寸变换和树的内容位置在调用当中被保存, 所以如果突然关闭了弹出窗口, 可以很容易地恢复它以前的状态.
下拉选项菜单中的 展开全部 和 折叠全部 选项可用于轻松列出可用节点.
你也可以从弹出式菜单中拖放节点到图形上.
虽然弹出的节点是按类别分类的, 但一开始会不知所以. 试着添加一些节点, 将它们插入输出套接处, 观察会发生什么.
当把任何 scalar 输出连接到 vector 输入时, 向量的所有分量将取标量的值.
当把任何 vector 输出连接到 scalar 输入时, 标量的值将是向量分量的平均值.
可视化着色器节点界面
视觉着色器节点具有输入和输出端口。
输入端口位于节点的左侧,
输出端口位于节点的右侧。
这些端口采用颜色以区分端口类型,所有类型都用于计算着色器中的顶点、片段和光源。
例如:矩阵乘法、向量加法或标量除法。
可视化着色器节点
以下是一些值得了解的特殊节点. 该清单并非详尽无遗, 可能会增加更多的节点和示例.
Expression 节点
Expression 节点允许你在视觉着色器中编写 Godot 着色语言(GLSL-like)表达式.
该节点具有添加任意数量的所需输入和输出端口的按钮, 并且可以调整其大小.
你还可以设置每个端口的名称和类型. 输入的表达式将立即应用于材质(焦点离开表达式文本框后).
任何解析或编译错误都将打印到 “输出” 选项卡. 默认情况下, 输出初始化为零值. 该节点位于 “特殊” 选项卡下, 可用于所有着色器模式.
这个节点的可能性几乎无穷无尽 —— 你可以编写复杂的过程, 并使用基于文本的着色器的全部力量, 例如循环, 关键字 discard , 扩展类型, 等等. 例如:
Reroute node 重新路由节点
“重新路由 ”节点纯粹用于组织目的。
在具有许多节点的复杂着色器中,您可能会发现节点之间的路径会使内容难以阅读。
顾名思义,重新路由允许您调整节点之间的路径,以使内容更易于阅读。
您甚至可以为单个路径设置多个重新路由节点,这些节点可用于形成直角。
要移动重新路由节点,请将鼠标光标移动到其上方,然后抓住出现的手柄。
Fresnel 节点
用于接受法线向量和视图向量, 并生成一个标量, 即它们之间的饱和点积.
此外, 你可以设置反转和方程的幂. Fresnel 节点非常适合为对象添加类似边缘的照明效果.
Boolean 节点
可以转换为或 Scalar 或 Vector , 分别表示 0 或 1 和 (0, 0, 0) 或 (1, 1, 1) .
该属性可用于一键启用或禁用某些效果部件.
If 节点
If 节点允许你设置一个向量, 它将返回 a 和 b 之间的比较结果.
有三个向量可以返回: a == b (在这种情况下, 容差参数是作为比较阈值提供的–默认情况下它等于最小值, 即 0.00001 ), a > b 和 a < b .
Switch 节点
如果布尔条件为 true 或 Switch 节点返回一个向量 false。
上面介绍了布尔值 。如果要将向量转换为真正的布尔值,则向量的所有分量都应为非零。
Mesh Emitter网格体发射器
网格体发射器(Mesh Emitter) 节点用于从网格体顶点发射粒子。这仅适用于处于粒子模式的着色器。
请记住,并非所有 3D 对象都是网格文件。glTF 文件无法拖放到图形中。
但是,您可以从中创建继承的场景,将该场景中的网格体保存为自己的文件,然后使用它。.
您还可以将 obj 文件拖放到图表编辑器中,以添加该特定网格体的节点,其他网格体文件将不适用于此。
使用计算着色器
本教程会带领你创建一个最简单的计算着色器。
但首先需要先介绍一下计算着色器的背景以及在 Godot 中的工作原理。
计算着色器是一种着重于通用编程特殊的着色器。
换句话说,它们相比于节点和片段着色器更加灵活,因为它们没有固定的用途(节点变换或图片着色)。
不同于节点和片段着色器,计算着色器的幕后工作非常少。GPU 运行的代码就是你编写的代码,此外几乎没有其他内容。
因此,计算着色器在将繁重计算转移到 GPU 上时非常有用。
现在我们以一个简短的计算着色器入手。
首先,用你选用的 外部 编辑器,在项目文件夹中创建一个命名为 compute_example.glsl 的新文件。
Godot 的计算着色器直接使用 GLSL 代码。
Godot 着色器语言基于 GLSL,如果你对 Godot 正常着色器熟悉,那么对以下语法也会比较熟悉。
我们把它调成蓝色:
1 | #[compute] |
这段代码接受一个 float 数组,将其中的每个元素和 2 相乘,并将结果存储回数组中。现在,我们来逐行观察这段代码。
1 | #[compute] |
这两行文本传达了以下的两件事:
- 如下的代码为计算着色器。这是 Godot 特有的提示文本,编辑器需要此文本才能正确导入着色器文件。
- 代码使用的是 GLSL 450。
在编写计算着色器时,你应当永远以这两行作为文件的开头。
1 | // Invocations in the (x, y, z) dimension |
接下来我们要传达每个工作组所使用的调用次数。“调用”指的是同一个工作组中运行的着色器实例。
从 CPU 启动计算着色器时,我们会告诉它需要运行多少个工作组。工作组之间是并行执行的。
运行时,一个工作组无法访问另一个工作组中的信息。不过同一个工作组中的不同调用可以相互进行有限的访问。
你可以将工作组和调用想象成巨型的嵌套 for 循环。
1 | for (int x = 0; x < workgroup_size_x; x++) { |
工作组与调用属于高阶内容。目前请需要记住我们在每个工作组中运行了两个调用。
1 | // A binding to the buffer we create in our script |
这里我们提供的是与计算着色器所能访问的内存相关的信息。
我们可以通过 layout 属性告诉着色器去哪里寻找缓冲,稍后我们需要在 CPU 一侧匹配这些 set 和 binding 的位置。
关键字 restrict 能够告诉着色器该缓冲只会在这个着色器中的某个单一位置进行访问。换句话说,我们不会将该缓冲绑定到其他 set 或 binding 索引。这一点非常重要,着色器编译器就能够借此对着色器代码进行优化。能使用 restrict 时请一定要使用。
这是一个未指明大小的缓冲,也就是说可以是任意大小。因此我们需要注意不要让用来读取的索引超过缓冲的大小。
1 | // The code we want to execute in each invocation |
最后,我们编写 main 函数,这是所有逻辑发生的地方。
我们使用 gl_GlobalInvocationID 访问存储缓冲区中的位置 内置变量。gl_GlobalInvocationID 为当前调用提供全局唯一 ID。
要继续,请将上述代码写入新创建的 compute_example.glsl 文件。
创建局部 RenderingDevice
若要与计算着色器交互并执行计算着色器,我们需要一个脚本。
使用你选择的语言创建一个新脚本,并将其附加到场景中的任何节点。
现在,要执行着色器,我们需要一个本地 RenderingDevice 可以使用 RenderingServer 创建:
1 | # Create a local rendering device. |
之后,我们可以加载新创建的着色器文件 compute_example.glsl 并使用以下命令创建它的预编译版本:
1 | # Load GLSL shader |
警告
本地渲染设备无法使用以下工具进行调试 渲染文档 。
提供输入数据
你可能还记得,我们想将一个输入数组传递给着色器,将每个元素乘以 2 然后获取结果。
我们需要创建一个缓冲区来将值传递给计算着色器。
我们处理的是一个浮点数数组,所以在这个示例中我们将使用存储缓冲区。
存储缓冲区接收一个字节数组,能够在 CPU 与 GPU 之间进行数据传输。
让我们初始化一个浮点数数组并创建一个存储缓冲区:
1 | # Prepare our data. We use floats in the shader, so we need 32 bit. |
有了缓冲区后,我们需要让渲染设备来使用这个缓冲区。
为此,我们需要创建一个 uniform(和普通着色器中一样)并将其分配给一个 uniform 集,稍后我们可以将其传递给着色器。
1 | # Create a uniform to assign the buffer to the rendering device |
定义计算管线
下一步需要创建一套 GPU 可以运行的指令。为此我们需要一个管线和一个计算列表。
需要执行以下步骤才能够得到计算结果:
- 新建管线。
- 开启需要让 GPU 执行的指令列表。
- 将计算列表绑定至管线
- 将缓冲区 uniform 绑定至管线
- 指定要使用的工作组数量
- 关闭指令列表
1 | # Create a compute pipeline |
请注意,我们在 X 轴上调度计算着色器,其中 5 个工作组位于其他轴上。
由于我们在 X 轴中有 2 个本地调用(在着色器中指定),因此总共将启动 10 个计算着色器调用。
如果读取或写入缓冲区范围之外的索引,则可能会访问着色器控制之外的内存或其他变量的一部分,这可能会导致某些硬件出现问题。
执行计算着色器
在所有这些之后,我们几乎完成了,但我们仍然需要执行我们的管道。
到目前为止,我们只记录了我们希望 GPU 做什么;我们实际上还没有运行着色器程序。
若要执行计算着色器,我们需要将管道提交到 GPU 并等待执行完成:
1 | # Submit to GPU and wait for sync |
理想情况下,您不会立即调用 sync() 来同步 RenderingDevice,因为这会导致 CPU 等待 GPU 完成工作。
在我们的示例中,我们立即同步,因为我们希望我们的数据可以立即读取。
通常,您需要在同步之前至少等待 2 或 3 帧,以便 GPU 能够与 CPU 并行运行。
警告
长时间的计算可能会导致 Windows 图形驱动程序“崩溃”,因为 由 Windows 触发的 TDR。
这是一种在经过一定时间而图形驱动程序没有任何活动(通常为 5 到 10 秒)后重新初始化图形驱动程序的机制。
根据计算着色器执行所需的持续时间,您可能需要将其拆分为多个调度,以减少每次调度所需的时间并减少触发 TDR 的机会。
鉴于 TDR 与时间相关,因此与较快的 GPU 相比,在运行给定计算着色器时,速度较慢的 GPU 可能更容易出现 TDR。
获取结果
您可能已经注意到,在示例着色器中,我们修改了存储缓冲区的内容。
换句话说,着色器从我们的数组中读取数据并再次将数据存储在同一个数组中,因此我们的结果已经存在。
让我们检索数据并将结果打印到我们的控制台。
1 | # Read back the data from the buffer |
释放内存
我们一直在使用的缓冲区 、 管道和 uniform_set 变量都是 RID。因为 RenderingDevice 是较低级别的 API,RID 不会自动释放。这意味着一旦您完成使用 buffer 或任何其他 RID 时,您负责释放其内存 手动使用 RenderingDevice 的 free_rid() 方法。
这样,您就拥有了开始使用计算着色器所需的一切。
参见
演示项目存储库包含一个 计算着色器高度图演示
该项目在 CPU 上执行高度图图像生成,并 GPU 单独,可让您比较类似算法的 以两种不同的方式实现(GPU 实现速度更快) 在大多数情况下)。
屏幕读取着色器
很多人想要让着色器在写屏幕的同时读取该屏幕的数据。
因为内部硬件限制,OpenGL 和 DirectX 等 3D API 都很难实现这一功能。
GPU 是极其并行的,所以同时进行读写会导致各种缓存和一致性问题。因此,即便是最新的硬件也对此进行无法正确的支持。
解决办法是将屏幕或屏幕的一部分复制到一个后台缓冲区,然后在绘图时从那里读取。Godot 提供了一些工具,可以使这一过程变得很容易。
屏幕纹理
Godot 着色语言 具有特殊的纹理来访问屏幕中已渲染的内容。
它通过在声明采样器时指定提示 2D uniform: hint_screen_texture 来使用。
一个特殊的内置变化 SCREEN_UV 可用于获取当前片段相对于屏幕的 UV。
因此,此 canvas_item 片段着色器会导致不可见的对象,因为它仅显示后面的内容:
1 | shader_type canvas_item; |
这里使用 textureLod,因为我们只想从底部 mipmap 读取。
如果你想从纹理的模糊版本中读取,你可以将第三个参数增加到 textureLod 并将提示 filter_nearest 更改为 filter_nearest_mipmap (或启用了 mipmap 的任何其他筛选器) 。
如果使用带有 mipmap 的过滤器,Godot 会自动为您计算模糊纹理。
警告
如果筛选模式未更改为名称中包含 mipmap 的筛选模式, LOD 参数大于 0.0 的 textureLod 将具有与 0.0 LOD 参数相同的外观。
屏幕纹理示例
屏幕纹理可以用来做很多事情。有一个针对屏幕空间着色器的特殊演示项目,你可以下载后查看学习。其中的一个例子就是用简单的着色器来调整亮度、对比度以及饱和度:
1 | shader_type canvas_item; |
幕后
虽然这看起来很神奇,但其实不然。
在 2D 中,第一次在即将绘制的节点中发现 hint_screen_texture 时,Godot 就会将整个屏幕拷贝到后台缓冲之中。
后续在着色器中使用它的节点将不会造成屏幕的复制,因为否则的话效率非常低。
在 3D 中,进行屏幕拷贝的时机是在不透明几何体阶段之后、透明几何体阶段之前,所以透明的物体不会被捕捉到屏幕纹理之中。
因此,在 2D 中,如果使用 hint_screen_texture 的着色器存在覆盖,那么后一个着色器使用的就不是第一个着色器的结果,会导致意外的图像:
在上图中,第二个球体(右上)所使用的屏幕纹理和第一个球体所使用的屏幕纹理的来源是一致的,所以第一个球体会“消失”,或者说不可见。
在 2D 中,这个问题可以通过 BackBufferCopy 节点修正,在这两个球体之间实例化即可。BackBufferCopy 可以指定屏幕上的某个区域进行复制,也可以复制整个屏幕:
正确复制后台缓冲之后,这两个球体就能够正确混合了:
警告
在 3D 中,使用 hint_screen_texture 的材质本身被认为是透明的,不会出现在其他材质的生成屏幕纹理中。
如果你计划实例化使用材质的场景 hint_screen_texture,则需要使用 BackBufferCopy 节点。
在 3D 中,这个问题解决起来就没有那么灵活,因为屏幕纹理只会捕捉一次。
在 3D 中使用屏幕纹理时请多加小心,因为它并不会捕获到透明的对象,反而可能捕获到位于使用屏幕纹理的对象之前的不透明对象。
要在 3D 中重现后台缓冲的逻辑,可以创建一个 Viewport 并在对象的位置创建一个相机,然后就可以使用该 Viewport 的纹理来代替屏幕纹理。
后台缓冲逻辑
好的,想要对后台缓冲有更清晰的理解的话,Godot 在 2D 中后台缓冲复制的原理是这样的:
如果某个节点使用了 hint_screen_texture,那么绘制该节点之前就会将整个屏幕复制到后台缓冲之中。
只有第一次才会这么做,后续的节点不会触发。
如果上述情况发生前遇到过 BackBufferCopy 节点(即便尚未使用过 hint_screen_texture),那么也不会执行相关的行为。
换句话说,自动复制整个屏幕的条件只有:某个节点中首次使用 hint_screen_texture 并且按照树顺序不存在更早的(未被禁用的)BackBufferCopy 节点。
BackBufferCopy 可以选择复制整个屏幕或者只复制某个区域。
如果设置为区域(非整个屏幕),但是着色器使用了复制区域之外的像素,那么读取到的结果就是未定义的(很可能是上一帧残留的垃圾数据)。
换句话说,你确实能够使用 BackBufferCopy 复制屏幕上的某个区域,然后读取屏幕纹理上的其他区域。但请避免这样的行为!
深度纹理
3D 着色器也可以访问屏幕深度缓冲,使用 hint_depth_texture 提示即可。
该纹理不是线性的;必须通过逆投影矩阵进行转换。
以下代码会获取正在绘制的像素所在的 3D 位置:
1 | uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest; |
法线-粗糙度纹理
法线粗糙度纹理仅在 Forward+ 渲染方法中受支持,不支持 移动(Mobile) 或 兼容性(Compatibility)。
类似的,如果对象在深度预阶段中进行了渲染,就可以用法线-粗糙度纹理来读取该对象的法线和粗糙度。
法线存储在 .xyz 通道中(映射到了 0-1 范围内),而粗糙度则存储在 .w 通道中。
1 | uniform sampler2D normal_roughness_texture : hint_normal_roughness_texture, repeat_disable, filter_nearest; |
重定义屏幕纹理
可以对多个 uniform 使用屏幕纹理提示(hint_screen_texture、hint_depth_texture、hint_normal_roughness_texture)。
例如,你可能会想要使用不同的重复标志和过滤标志多次读取该纹理。
下面的例子中,着色器在读取屏幕空间法线时使用的就是线性过滤,而读取屏幕空间粗糙度时使用的就是最邻近过滤。
1 | uniform sampler2D normal_roughness_texture : hint_normal_roughness_texture, repeat_disable, filter_nearest; |
将 GLSL 转换为 Godot 着色器
GLSL
Godot使用基于GLSL的着色语言, 增加了一些生活质量特征.
因此,GLSL中提供的大多数功能都可以使用Godot的着色语言.
着色器程序
在GLSL中, 每个着色器使用一个单独的程序.
你有一个用于顶点着色器的程序和一个用于片段着色器的程序.
在Godot中, 你有一个包含 vertex 和/或 fragment 函数的单一着色器. 如果你只选择写一个,Godot会提供另一个.
Godot允许通过在一个文件中定义片段和顶点着色器来共享uniform的变量和函数. 在GLSL中, 顶点和片段程序不能共享变量, 除非是使用varyings的时候.
顶点属性
在 GLSL 中,您可以使用属性传入每个顶点的信息,并可以根据需要灵活地传入任意数量或尽可能少的信息。
在 Godot 中,您有一定数量的输入属性,包括 VERTEX(位置)、COLOR、 UV、UV2、 正常 。
文档的着色器参考部分中的每个着色器页面都附带其顶点属性的完整列表。
gl_Position
gl_Position 接收在顶点着色器中指定的顶点的最终坐标.
它是由用户在裁剪空间中指定的.
通常, 在GLSL中, 模型空间的顶点位置是通过一个名为 position 的顶点属性来传递的, 你可以手动处理从模型空间到裁剪空间的转换.
在Godot中, VERTEX 指定了 vertex 函数开始时在模型空间的顶点位置. 在用户定义的 vertex 函数运行后,Godot也会处理最终转换到裁剪空间的过程. 如果你想跳过从模型空间到视图空间的转换, 你可以将 render_mode 设置为 skip_vertex_transform . 如果你想跳过所有的转换, 将 render_mode 设置为 skip_vertex_transform 并将 PROJECTION_MATRIX 设置为 mat4(1.0) , 以便使从视图空间到裁剪空间的最终转换失效.
Varying变化
varyings是一种变量, 可以从顶点着色器传递到片段着色器.
在现代GLSL(3.0及以上版本)中, 变量是通过 in 和 out 关键字来定义的.
一个从顶点着色器出来的变量在顶点着色器中用 out 定义, 在片段着色器中用 in 定义.
主要
在GLSL中, 每个着色器程序看起来都像是一个独立的C风格程序.
因此, 主要入口点是 main .
如果要复制顶点着色器, 请将 main 重命名为 vertex , 如果要复制片段着色器, 请将 main 重命名为 fragment .
宏
Godot 着色器预处理器支持以下宏:
- #define / #undef
- #if、#elif、#else、#endif、defined()、#ifdef、#ifndef
#if、#elif、#else、#endif、defined()、#ifdef、#ifndef - #include (仅支持 .gdshaderinc 文件,最大深度为 25)
- #pragma disable_preprocessor ,对文件剩余部分禁用预处理
变量
GLSL 有许多内置的硬编码变量。这些变量不是 uniform,因此它们不能从主程序中编辑。
坐标
GLSL中的 gl_FragCoord 和Godot着色语言中的 FRAGCOORD 使用相同的坐标系. 如果在Godot中使用UV, 则y坐标将颠倒翻转.
精度
在GLSL中,你可以用 precision 关键字在着色器的顶部定义一个给定类型的精度(float 或 int)。
在 Godot 中,你可以在定义变量时将精度限定词 lowp、mediump、highp 放在类型前,根据需要设置单个变量的精度。更多信息请参见着色器语言参考。
Shadertoy
Shadertoy 是一个网站, 它使编写片段着色器和创建 纯正的魔法 变得容易.
Shadertoy 并没有让用户完全控制着色器。它处理所有的输入和 uniform,只让用户编写片段着色器。
类型
Shadertoy使用的是webgl规范, 所以它运行的GLSL版本略有不同. 然而, 它仍然有常规的类型, 包括常量和宏.
mainImage
Shadertoy着色器的主要入口点是 mainImage 函数.
mainImage 有两个参数, fragColor 和 fragCoord, 分别对应Godot中的 COLOR 和 FRAGCOORD.
这些参数在Godot中是自动处理的, 所以你不需要自己把它们作为参数. 移植到Godot时, mainImage 函数中的任何内容都应复制到 fragment 函数中.
变量
为了让编写片段着色器变得简单明了,Shadertoy为你处理了从主程序传递到片段着色器中的许多有用信息.
其中有一些在Godot中没有对应的信息, 因为Godot选择不在默认情况下提供这些信息.
这没关系, 因为Godot让你有能力制作自己的 uniform。
对于那些等价物被列为 “Provide with Uniform” 的变量, 用户有责任自己创建该uniform .
该描述给了读者一个提示, 告诉他们可以传入什么作为替代物.
坐标
fragCoord 的行为与 gl_FragCoord 相同 GLSL 和Godot中的 FRAGCOORD .
着色之书
与 Shadertoy 类似,The Book of Shaders 提供了在网络浏览器中访问片段着色器的机会,
用户可以与之互动,但只限于编写片段着色器代码,其中有一组传入的 uniform 列表,不能添加额外的 uniform。
有关将着色器移植到各种框架的进一步帮助,The Book of Shaders在各种框架中运行着色器时提供了一个 page .
类型
The Book of Shaders使用webgl规范, 因此它运行的GLSL略有不同.
但是, 它仍然具有常规类型, 包括常量和宏.
主要
Book of Shaders片段着色器的入口点是 main , 就像在GLSL中一样.
使用着色器 main 函数编写的所有内容都应该复制到Godot的 fragment 函数中.
变量
着色书比Shadertoy更接近普通GLSL. 它也比Shadertoy实施更少的制服.
提供Uniform
鼠标在像素坐标中的位置.
坐标
Shaders使用相同的坐标系 GLSL.
着色器风格指南
编码与特殊字符
- 使用换行符(LF)换行,而非 CRLF 或 CR。(编辑器默认)
- 在每个文件的末尾使用一个换行符。(编辑器默认)
- 使用不带字节顺序标记的 UTF-8 编码。(编辑器默认)
- 使用制表符代替空格进行缩进。(编辑器默认)
缩进
每个缩进级别应比包含它的区块多一个制表符.
命名规定
- 函数与变量使用 snake_case 命名
- 常量使用 CONSTANT_CASE
- 着色器预处理器指令应用 CONSTANT__CASE 编写。指令的编写时不应有任何缩进,即使嵌套在函数中也是如此。
此代码顺序遵循两个经验法则:
- 先是元数据和属性, 然后是方法.
- “公共”在“私有”之前。在着色器语言的语境中,“公共”指的是用户可以轻易调整的东西(uniform)。
局部变量
局部变量的声明位置离首次使用该局部变量的位置越近越好,让人更容易跟上代码的思路,而不需要上翻下找该变量的声明位置。
使用 SubViewport 作为纹理
本教程将介绍如何使用子视口作为可应用于 3D 对象的纹理。
为此,它将引导您完成制作如下所示程序行星的过程:
设置场景
创建一个新场景并添加以下节点,如下所示。
!
进入 MeshInstance3D 并将网格设置为 SphereMesh
点击 SubViewport 节点并将其大小设置为 (1024, 512).
这 SubViewport 实际上可以是任何大小,只要宽度是高度的两倍。
宽度需要是高度的两倍,以便图像能够准确地映射到球体上,因为我们将使用等距柱状投影,但稍后会详细介绍。
接下来禁用 3D。我们将使用 ColorRect 来渲染表面,因此我们也不需要 3D。
选择 ColorRect,然后在检查器中将锚点预设设置为 “完整矩形”。这将确保 ColorRect 占据整个 SubViewport。
接下来, 我们为 Shader Material 添加一个 ColorRect (ColorRect > CanvasItem > Material > Material > New ShaderMaterial).
点击着色器材质的下拉菜单按钮,点击 / 编辑(Edit) 。从这里转到着色器 > 新着色器 。
给它一个名字,然后单击“创建”。单击检查器中的着色器以打开着色器编辑器。删除默认代码并添加以下内容:
1 | shader_type canvas_item; |
保存着色器代码,您将在检查器中看到上面的代码渲染了如下所示的渐变。
现在,我们有了渲染到的子视口的基础知识,并且我们有一个可以应用于球体的独特图像。
应用纹理
现在进入 MeshInstance3D 并添加一个StandardMaterial3D 到它。
不需要特殊的着色器材质 (尽管对于更高级的效果(如上例中的大气)来说,这将是一个好主意)。
MeshInstance3D > GeometryInstance > Geometry > Material Override > 新建 StandardMaterial3D
然后点开 StandardMaterial3D 的下拉菜单,点击“编辑”
找到“Resource”部分并勾选 Local to scene 复选框。
然后找到“Albedo”部分,在“Texture”属性旁单击,添加反照率纹理。
这里我们要使用自己创建的纹理,请选择“新建 ViewportTexture”
在检查器中点击刚才创建的 ViewportTexture,然后点击“分配”。
接下来,在弹出的菜单中选择之前用于渲染的 Viewport。
现在, 你的球体应使用我们渲染到视口的颜色进行着色.
注意到在纹理环绕的地方形成的丑陋缝隙吗?
这是因为我们是根据UV坐标来选取颜色的, 而UV坐标并不会环绕纹理.
这是二维地图投影中的一个典型问题.
游戏开发人员通常有一个二维贴图, 他们想投射到一个球体上, 但是当它环绕时, 将有接缝.
这个问题有一个优雅的解决方法, 我们将在下一节中说明.
制作行星纹理
那么现在我们往 SubViewport 里渲染的东西就会神奇地出现在球体上。
不过由于纹理坐标的原因,球体上会存在一条很丑的缝隙,我们该如何让坐标优雅地环绕球体呢?
一种解决方法是使用在纹理域内重复的函数,比如 sin 和 cos。让我们把它们应用到纹理上,看看会发生什么。
请将着色器中的已有颜色代码替换成下面的内容:COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
还凑合吧。现在球体的四周就再也看不到缝隙了,不过取而代之的是两个极点的地方会有收缩的现象。
这种收缩的现象是 Godot 使用 StandardMaterial3D 将纹理映射到球体表面的方式造成的。
这里使用的是一种叫做“等距柱状投影”的将球面图形转化为 2D 平面的技术。
备注
如果你对技术方面的一些额外信息感兴趣,我们将从球面坐标转换为直角坐标。球面坐标映射的是球体的经度和纬度,而直角坐标则是从球体中心到点的一个向量。
对于每个像素,我们将计算其在球体上的 3D 位置。由此,我们将使用 3D 噪声来确定颜色值。通过计算 3D 噪声,我们解决了两极的夹紧问题。要理解原因,请想象在球体表面而不是 2D 平面上计算噪声。当您在球体表面上进行计算时,您永远不会碰到边,因此您永远不会在极点上创建接缝或夹点。以下代码将 UV 转换为笛卡尔坐标。
1 | float theta = UV.y * 3.14159; |
如果使用 unit 作为输出 COLOR 值,我们可以得到:
现在我们可以计算出球体表面的3D位置, 可以使用3D噪声来制作球体. 直接从 Shadertoy 中使用这个噪声函数:
1 | vec3 hash(vec3 p) { |
现在使用 noised,将以下内容添加到 fragment 函数中:
1 | float n = noise(unit * 5.0); |
备注
为了突出显示纹理, 我们将材质设置为无阴影.
你现在可以看到, 尽管这看起来完全不像所承诺的球体, 但噪音确实无缝地包裹着球体. 对此, 让我们进入一些更丰富多彩的东西.
为星球着色
现在来制作行星的颜色. 虽然有很多方法可以做到这一点, 但目前, 我们将使用水和陆地之间的梯度.
要在 GLSL 中创建渐变, 我们使用 mix 函数. mix 需要两个值来插值和第三个参数来选择在它们之间插入多少, 实质上它将两个值 混合 在一起.
在其他API中, 此函数通常称为 lerp . 虽然 lerp 通常用于将两个浮点数混合在一起, 但 mix 可以取任何值, 无论它是浮点数还是向量类型.COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), n * 0.5 + 0.5);
第一种颜色是蓝色, 代表海洋.
第二种颜色是一种偏红的颜色, 因为所有外星球都需要红色的地形. 最后, 它们 n * 0.5 + 0.5 混合在一起. n 在 -1 和 1 之间平滑变化.
所以我们把它映射到 mix 预期的 0-1 范围内. 现在你可以看到, 颜色在蓝色和红色之间变化.
这比我们想要的还要模糊一些.
行星通常在陆地和海洋之间有一个相对清晰的分隔.
为了做到这一点, 我们将把最后一项改为 smoothstep(-0.1, 0.0, n) . 整条线就变成了这样:COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), smoothstep(-0.1, 0.0, n));
smoothstep 所做的是, 如果第三个参数低于第一个参数, 则返回 0 , 如果第三个参数大于第二个参数, 则返回 1 , 如果第三个数字在第一个和第二个之间, 则在 0 和 1 之间平滑地混合. 所以在这一行中, 当 n 小于 -0.1 时, smoothstep 返回 0 , 当 n 高于 0 时, 它返回 1 .
还有一件事, 使其更像一个行星.
这片土地不应该是圆球状的;让我们把边缘变得更粗糙一些.
在着色器中经常使用的一个技巧是在不同的频率下将不同层次的噪声叠加在一起, 使地形看起来粗糙.
我们使用一个层来制作大陆的整体球状结构. 然后, 另一层将边缘打碎, 然后是另一层, 以此类推.
我们要做的是用四行着色器代码来计算 n , 而不是只有一行. n 变成了:
1 | float n = noise(unit * 5.0) * 0.5; |
现在这个星球看起来像:
制作海洋
让这个看起来更像是一颗行星的最后一件事. 海洋和陆地以不同的方式反射光线.
因此, 我们希望海洋比陆地更加闪耀. 我们可以通过将第四个值传递到输出 COLOR 的 alpha 通道并将其用作粗糙度图来实现.COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);
该行对于水返回 0.3 , 对于土地返回 1.0 . 这意味着土地将变得很粗糙, 而水将变得非常光滑.
然后,在材质中,在“Metallic”(金属性)部分,请确保 Metallic 为 0、Specular 为 1。
这样做的原因是水对光线的反射非常好,但它不是金属的。这些值在物理上并不准确,但对于这个演示来说已经足够好了。
接下来,在“粗糙度”部分下,将粗糙度纹理设置为 Viewport 纹理指向我们的行星纹理 SubViewport。
最后,将 纹理通道(Texture Channel) 设置为 Alpha。 这指示渲染器使用 alpha 通道作为 粗糙度(Roughness) 值。
您会注意到除了行星不再反射天空之外几乎没有什么变化。发生这种情况是因为,默认情况下,当使用 alpha 值渲染某些内容时,它会在背景上绘制为透明对象。由于子视口的默认背景是不透明的,因此 视口纹理(Viewport Texture) 为 1,导致行星纹理的绘制颜色稍暗,并且所有位置的 粗糙度(Roughness) 值均为 1。为了纠正这个问题,我们进入 子视口(SubViewport) 并启用 “透明背景(Transparent Bg) ”属性。由于我们现在在另一个透明对象上渲染一个透明对象,因此我们希望启用 blend_premul_alpha:render_mode blend_premul_alpha;
这是将颜色预先乘以 alpha 值, 然后将它们正确地混合在一起.
通常情况下, 当在一个透明的颜色上混合另一个颜色时, 即使背景的 alpha 为 0 (如本例), 也会出现奇怪的颜色渗漏问题.
设置 blend_premul_alpha 可以解决这个问题.
现在,这颗行星看起来应该像是在海洋上反射光线,而不是在陆地上反射光线。在 OmniLight3D 中移动 ,这样你就可以看到海洋反射的效果。
现在你有它。使用子视口生成的程序化行星。
自定义后期处理
实现自定义后期处理着色器的最简单方法是使用Godot的内置功能从屏幕纹理中读取.
如果你不熟悉这个, 你应该先阅读 屏幕阅读着色器教程 .
后处理效果是在 Godot 渲染帧后应用于帧的着色器。
要将着色器应用于帧,请创建一个 CanvasLayer,并为其提供一个 ColorRect。
将新的 ShaderMaterial 分配给新创建的 ColorRect,并将 ColorRect 的锚点预设设置为 Full Rect:
另一种更有效的方法是使用 BackBufferCopy 将屏幕区域复制到缓冲区,并通过 sampler2D 在着色器脚本中访问它 hint_screen_texture。
截至撰写本文时,Godot 不支持同时渲染到多个缓冲区。
您的后处理着色器将无法访问 Godot 未公开的其他渲染通道和缓冲区(例如深度或法线/粗糙度)。
您只能访问 Godot 作为采样器公开的渲染帧和缓冲区。
将新的着色器分配给 ColorRect 的 ShaderMaterial。您可以使用 使用 hint_screen_texture 和内置 SCREEN_UV 的 sampler2D 制服。
将以下代码复制到着色器. 上面的代码是单通道边缘检测滤波器, Sobel 滤波器
1 | shader_type canvas_item; |
多阶段后期处理
模糊之类的后期处理效果属于资源密集型效果,将其拆分为多个阶段就可以大幅提升运行速度。
在多阶段材质中,每个阶段都会使用上一阶段的结果作为输入,从而进行处理。
要生成多通道后处理着色器,请堆叠 CanvasLayer 和 ColorRect 节点。
在上面的示例中,您可以使用 CanvasLayer 对象通过下面图层上的帧来渲染着色器。
除了节点结构外,步骤与单通道后处理着色器相同。
你的场景树应该类似这样:
例如,可以通过将下面的代码段附加到每个 ColorRect 上来编写全屏高斯模糊效果。
应用着色器的顺序取决于场景树中 CanvasLayer 的位置,越往上越早应用。对于这个模糊着色器而言,顺序是无所谓的。
1 | shader_type canvas_item; |
1 | shader_type canvas_item; |
使用上面的代码, 你应该得到如下所示的全屏模糊效果.
高级后期处理
全屏四边形
制作自定义后期处理效果的一种方法是使用视口。
但是,使用视口有两个主要缺点:
- 无法访问深度缓冲区
- 在编辑器中看不到后期处理着色器的效果
要绕过使用深度缓冲区的限制,请使用 MeshInstance3D 使用四网格图元。
这允许我们使用着色器并访问场景的深度纹理。
接下来,使用顶点着色器使四边形始终覆盖屏幕,以便始终应用后期处理效果,包括在编辑器中。
首先,创建一个新的 MeshInstance3D 并将其网格设置为 QuadMesh。
这将创建一个以位置 (0, 0, 0) 为中心的四边形,宽度和高度为 1。
将宽度和高度设置为 2,并启用 翻转面(Flip Faces) 。
目前,四边形在世界空间的原点占据一个位置。
但是,我们希望它随着相机移动,以便它始终覆盖整个屏幕。
为此,我们将绕过通过差坐标空间平移顶点位置的坐标变换,并将顶点视为它们已经位于裁剪空间中。
顶点着色器期望在剪辑空间中输出坐标,这些坐标的坐标范围从屏幕左侧和底部的 -1 到屏幕顶部和右侧的 1。
这就是为什么 QuadMesh 的高度和宽度需要为 2。
Godot 在幕后处理从模型到视图空间再到剪辑空间的转换,所以我们需要抵消戈多转换的影响。
我们通过设置 POSITION 内置到我们想要的位置。POSITION 绕过内置变换,直接设置剪辑空间中的顶点位置。
1 | shader_type spatial; |
备注
在 4.3 之前的 Godot 版本中,此代码建议使用POSITION = vec4(VERTEX, 1.0);
它隐式假设平面附近的裁剪空间为 0.0。该代码现在不正确,在 4.3+ 版本中不起作用,因为我们现在使用“反向 z”深度缓冲区,其中近平面为 1.0。
即使使用此顶点着色器,四边形也会不断消失。
这是由于在 CPU 上完成的视锥体剔除。
视锥体剔除使用摄像机矩阵和网格体的 AABB 来确定网格体是否可见,然后再将其传递给 GPU。
CPU 不知道我们在用顶点做什么,因此它假设指定的坐标是指世界位置,而不是裁剪空间位置,这导致当我们转身离开场景中心时,Godot 会剔除四边形。
为了防止四边形被剔除,有几种选择:
- 将 QuadMesh 作为子节点添加到相机,这样相机就会始终指向它
- 在 QuadMesh 中将几何属性 extra_cull_margin 设置得尽可能大
第二个选项会确保四边形在编辑器中可见,而第一个选项能够保证即使摄像机移出剔除边缘也它仍可见。你也可以同时使用这两个选项。
深度纹理
要从深度纹理中读取数据,我们首先需要使用 hint_depth_texture 创建设置为深度缓冲区的纹理统一。uniform sampler2D depth_texture : hint_depth_texture;
定义之后,深度纹理可以从 texture() 函数中读取。float depth = texture(depth_texture, SCREEN_UV).x;
备注
与访问屏幕纹理类似,访问深度纹理只有在从当前视口读取时才能进行。
深度纹理不能从你已经渲染的另一个视口中访问。
depth_texture 返回的值介于 1.0 和 0.0 之间(由于使用了“反向 z”深度缓冲区,分别对应于近平面和远平面)并且是非线性的。
当直接从 depth_texture 显示深度时,一切看起来几乎 黑色,除非由于非线性而非常接近。
为了使深度值与 world 或 模型坐标,我们需要将值线性化。
当我们将投影矩阵应用于 顶点位置,z 值是非线性的,因此为了线性化它,我们将其乘以 投影矩阵的逆,在 Godot 中,可以通过变量 INV_PROJECTION_MATRIX。
首先,获取屏幕空间坐标并将其转换为归一化设备坐标(NDC)。
使用 Vulkan 后端时,NDC 在 x 和 y 方向上运行 -1.0 到 1.0,在 z 方向上运行 0.0 到 1.0。
使用 x 轴和 y 轴的 SCREEN_UV 以及 z 的深度值重建 NDC。
1 | void fragment() { |
备注
本教程假设使用 Forward+ 或 Mobile 渲染器,它们都使用 Z 范围为 [0.0, 1.0] 的 Vulkan NDC。相比之下,兼容性渲染器使用 Z 范围为 [-1.0, 1.0] 的 OpenGL NDC。对于兼容性渲染器,请将 NDC 计算替换为以下内容:vec3 ndc = vec3(SCREEN_UV, depth) * 2.0 - 1.0;
您还可以使用 CURRENT_RENDERER 和 RENDERER_COMPATIBILITY built-in 定义了可在所有渲染器中运行的着色器:
1 | #if CURRENT_RENDERER == RENDERER_COMPATIBILITY |
通过将NDC乘以 INV_PROJECTION_MATRIX , 将NDC转换成视图空间.
回顾一下, 视图空间给出了相对于相机的位置, 所以 z 值将给我们提供到该点的距离.
1 | void fragment() { |
因为摄像机是朝向 z 轴的负方向, 所以坐标会有负的 z 值。为了得到可用的深度值, 我们必须对 view.z 取反。
可以使用以下代码从深度缓冲区构造世界位置,使用 INV_VIEW_MATRIX 将位置从视图空间转换为世界空间。
1 | void fragment() { |
示例着色器
一旦我们添加了一条线来输出到 ALBEDO,我们就有一个完整的着色器,如下所示。
此着色器允许您可视化线性深度或世界空间坐标,具体取决于注释掉的线。
1 | shader_type spatial; |
优化
你可以使用单个大三角形而不是使用全屏四边形. 解释的原因在 这里 .
但是, 这种好处非常小, 只有在运行特别复杂的片段着色器时才有用.
将 MeshInstance3D 中的网格设置为 ArrayMesh。ArrayMesh 是一种工具,可让您轻松地从数组构建顶点、法线、颜色等的网格体。
现在,将脚本附加到 MeshInstance3D 并使用以下代码:
1 | extends MeshInstance3D |
备注
三角形在规范化设备坐标中指定。回想一下,NDC 在 x 和 y 中都从 -1.0 运行到 1.0 方向。
这使得屏幕宽 2 个单位,高 2 个单位。为了用一个三角形覆盖整个屏幕,请使用一个宽 4 个单位、高 4 个单位的三角形,高度和宽度加倍。
从上面分配相同的顶点着色器, 所有内容应该看起来完全相同.
与使用四网格相比,使用 ArrayMesh 的一个缺点是 ArrayMesh 在编辑器中不可见,因为在场景运行之前不会构造三角形。要解决这个问题,请在建模程序中构造一个三角形网格,并在 MeshInstance3D 中使用它。
树木的制作
用顶点颜色绘制
您可能要做的第一件事是使用顶点颜色来绘制有风时树的摇摆程度。
只需使用您最喜欢的 3D 建模程序的顶点颜色绘画工具并绘制如下内容:
这有点夸张, 但这个想法是, 颜色表明了多少摇摆影响树的每个部分. 这个比例尺更能说明问题:
为叶子编写自定义着色器
这是树叶着色器的示例:
1 | shader_type spatial; |
这是一个空间着色器. 没有前/后剔除(所以可以从两边看到叶子), 并且使用了alpha预通道, 所以使用透明度(和叶子投射阴影)导致的深度伪影比较少. 最后, 对于摇摆的效果, 推荐使用世界坐标, 以使树可以被复制, 移动等, 并且仍然可以和其他树一起使用.
1 | uniform sampler2D texture_albedo : source_color; |
在这里, 纹理和透射颜色被读取, 透射颜色被用来给叶子添加一些背光, 模拟地下散射.
1 | uniform float sway_speed = 1.0; |
这是创建叶子摆动的代码.
它是基本的(只是使用正弦波乘以时间和轴的位置, 但工作得很好).
注意, 强度乘以颜色. 每个轴使用不同的小的接近1.0的乘法系数, 所以轴不同步出现.
最后, 剩下的就是片段着色器了:
1 | void fragment() { |
差不多就是这样.
主干着色器是类似的, 除了它不写到alpha通道(因此不需要alpha前置)和不需要传输工作.
这两个着色器都可以通过添加法线映射, AO和其他映射来改进.
改进着色器
还有更多的资源可以做到这一点, 你可以阅读.
现在你已经了解了基础知识, 建议阅读GPU Gems3中关于Crysis如何做到这一点的章节(主要关注摇摆代码, 因为许多其他技术都已经过时了):
用户界面(UI)
UI 构件
大小和锚点
不同分辨率摆放控件不容易,要考虑宽高比、分辨率和用户缩放比例。
通过编辑控件的Anchors Preset(锚点偏移量)来实现,选择自定义。
- Anchor Points(锚点):控件四个边距,左、右、底、顶。
- Anchor Offsets(锚点偏移):控件四个边距,左、右、底、顶。
- Grow Direction(伸长方向):
居中将锚点都设置为 0.5,锚点偏移为其相关尺寸的一半。
使用容器(Container 节点)
Anchors 是 GUI 中处理基本多分辨率时应对不同纵横比的有效方法。
需要更强大的类似操作系统的用户界面时用 Container 节点会更方便。
容器控制其所有派生的节点的位置。
容器的真正优势在于可以嵌套(作为节点), 允许创建非常复杂的布局, 调整毫不费力.
大小选项(Container Sizing)
- Horizontal(水平)
- Vertical(垂直)
- Stretch Ratio(拉伸率)
展开的控制占用彼此可用空间的比率。带有“2”的控件将占用的可用空间是带有“1”的控件的两倍。
选择自定义选项后:
- 填充 : 确保控件填充容器内指定的区域. 无论控件是否 expands扩展 (见下面), 当此选项被选中时(默认情况), 只填充指定区域.
- 居中收缩 : 尝试在容器内居中控件。
- 末端收缩 : 尝试在容器内将控件放置在容器的末端。
- 扩展 :尝试在父容器(每个轴)中使用尽可能多的空间。
不扩展的控件将被扩展的控件推开。在扩展控件之间,它们彼此占用的空间量由拉伸比(Stretch Ratio) 决定(见下文)。
仅当父容器类型正确时,此选项才可用,例如,HBoxContainer 具有用于水平大小调整的此选项。
容器类型
Godot提供了几种开箱即用的容器类型:
- 盒式容器
垂直或水平排列子控件(通过 HBoxContainer 和 VBoxContainer)。
在与指定方向相反的方向(如水平容器的垂直方向),它只是扩展了子项。
这些容器对设置了 Expand 标志的子项使用 Stretch Ratio 属性。 - 网格容器
将子控件按照网格排列(使用 GridContainer ,必须指定列数),会同时用到垂直和水平扩展选项。 - 边距容器
将子节点扩展到该控件的边界(使用 MarginContainer ),会根据主题的设置来添加不同大小的边距。
同样, 请记住, 边距是一个 Theme 值, 所以它们需要从每个控件的常量重写部分进行编辑:g - 选项卡容器
允许你将多个子控件堆叠在一起(使用 TabContainer ),只会显示 当前 控件。
点击容器顶部的选项卡可以更改 当前 控件:
标题默认是根据节点名称生成的(尽管可以通过 TabContainer 的 API 重写)。
可以在 TabContainer 的主题覆盖项中修改类似选项卡位置和 StyleBox 等设置。 - 拆分容器
只接受单个或者两个子控件,会将它们相邻放置,中间是分隔线(使用 HSplitContainer 和 VSplitContainer ),会使用到水平和垂直选项以及 Ratio 属性。
可以通过拖动分隔线来调整两个子节点所占区域的大小: - 面板容器
一个容器,用于绘制 StyleBox,然后展开子项以覆盖其整个区域(通过 PanelContainer,遵循 StyleBox 边距)。它同时尊重水平和垂直大小选项。
此容器用作顶级控件,或仅用于向布局的各个部分添加自定义背景。 - 可折叠容器
可以展开/折叠的容器(通过 FoldableContainer)。子控件折叠时隐藏。 - 滚动容器
接受单个子节点。如果子节点大于容器,则将添加滚动条以允许平移节点(通过 ScrollContainer)。
垂直和水平大小选项都受到尊重,并且可以在属性中按轴打开或关闭行为。
鼠标滚轮和触摸拖动(当触摸可用时)也是平移子控件的有效方法.
正如上面的例子中所展示的,使用此容器最常见的方法之一,是将 VBoxContainer 作为子容器一起使用。 - 纵横比容器
一种容器类型,其排列方式在调整容器大小时自动保留其比例。(通过 AspectRatioContainer)。
它具有多种拉伸模式,提供了调整子控件与容器相关的大小的选项:“填充”、“宽度控制高度”、“高度控制宽度”和“覆盖”。
当容器需要动态并响应不同的屏幕尺寸,并且希望子元素按比例缩放而不会丢失其预期形状时,它非常有用。 - 流容器
FlowContainer 是一个容器,它水平或垂直排列其子控件(通过 HFlowContainer 和 VFlowContainer)。
当可用空间用完时,它会将子项换行到下一行或下一列,类似于书中的文本换行方式。
它对于创建灵活的布局非常有用,其中子控件会自动调整到可用空间而不会重叠。 - CenterContainer
CenterContainer 是一个容器,它会自动将其所有子控件保持在其中的中心,其最小大小。
它确保子控件始终与中心对齐,从而更容易创建居中布局,而无需手动定位(通过 CenterContainer)。 - 子视口容器
这是一个特殊的控件,它只接受单个视口节点作为子节点,并且它会像图像一样显示它(通过 SubViewportContainer)。
创建自定义容器
可以使用脚本创建自定义容器。下面是一个适合子大小的容器示例:
1 | extends Container |
自定义 GUI 控件
检查控件的大小
与2D节点不同,控件的“大小”很重要,有助于将控件正确地组织在布局中。
为此,提供了 Control.size 属性。在 _draw() 中检查该属性至关重要,以确保所有内容都保持在边界范围内。
检查输入焦点
一些控件(如按钮或文本编辑器)可为键盘或手柄输入提供输入焦点.
例如输入文本或按下一个按钮. 这可以通过 Control.focus_mode 属性来控制.
绘制时, 如果控件支持输入焦点, 总是希望显示某种指示来表明(高亮, 方框等), 当前这是焦点控件.
为了检查这个状态, 存在一个 Control.has_focus() 的方法. 例子:
1 | func _draw(): |
调整大小
如前所述, 尺寸对控件是很重要的. 这可以让它们在设置网格, 容器或锚定时以正确布局.
控件, 在大多数情况下, 提供了一个 minimum size 以正确布局.
例如, 如果控件被垂直放置在彼此的顶部, 使用 VBoxContainer , 最小尺寸将确保你的自定义控件不会被容器中的其他控件挤压.
要提供此回调,只需覆盖 Control._get_minimum_size(),例如:
1 | func _get_minimum_size(): |
或者, 使用函数进行设置:
1 | func _ready(): |
输入
控件为输入事件的管理提供了一些辅助工具,比普通节点要方便一点。
控件有一个特殊的输入方法, 只有在以下情况下才起作用:
- 鼠标指针悬停在控件上.
- 鼠标按键在此控件上被按下 (控件始终捕获输入, 直到按钮被释放)
- 控件通过以下方式提供键盘和手柄焦点
Control.focus_mode.
这个函数是 Control._gui_input() 的。要使用在控制中覆盖。无需设置处理。
1 | extends Control |
有关事件本身的详细信息,请查看 使用 InputEvent 教程。
通知
控件也有许多有用的通知, 这些通知不存在专门的回调, 但可以用_notification回调来检查:
1 | func _notification(what): |
键盘/控制器导航和焦点
使用键盘或控制器在 UI 元素之间导航是通过更改主动选择的节点来完成的。(更改 UI 焦点)
Godot 中的每个 Control 节点都能够获得焦点。
默认情况下,一些控件节点有能力自动拾取焦点内置的 UI 动作,并做出反应,例如 ui_up, ui_down, ui_focus_next 等。
这些动作可以在项目的输入映射中的设置和修改。
警告
因为这些动作用于焦点,不应该被用于游戏代码。
节点设置
除了内置的逻辑之外,你还可以为每个单独的控件节点定义所谓的邻焦点。
这允许在你项目的用户界面上的路径微调UI焦点。
单个节点的设置可以在检查器栏的 “Control” 部分的 “Focus” 类别下找到。
邻近选项用于定义四向导航的节点,例如使用箭头键或控制器上的方向键。
如,当使用向下箭头向下导航或按下方向键时,将使用底部邻近。
“下一个”和“上一个”选项与焦点转移按钮一起使用,例如桌面操作系统上的 Tab。
如果节点被隐藏,它就会失去焦点。
模式设置定义了节点如何被聚焦。
- All 意味着节点可以通过用鼠标点击,或用键盘或控制器选择而被关注。
- Click 意味着只能通过点击来关注。
- None 意味着它根本不能被关注。
确保为焦点和导航正确配置场景。如果节点没有配置邻焦点,引擎将尝试自动猜测下一个控件。
这可能会导致意外行为,尤其是在没有明确定义垂直或水平导航操作的复杂用户界面中。
必要的代码
若要使键盘和控制器导航正常工作,必须在场景启动时使用代码聚焦任何节点。
可以使用 Control.grab_focus() 方法聚焦控件。下面是使用代码设置初始焦点的基本示例:
1 | func _ready(): |
现在,当场景启动时,“开始按钮”节点将被聚焦,键盘或控制器可用于在它和其他 UI 元素之间导航。
GUI 外观与主题
GUI 外观简介
Godot 引擎有用于 GUI 换肤(或主题化)的系统,自定义用户界面中每个控件的外观,包括自定义控件。
UI 主题以级联方式应用(即从父控件传播到其子控件)。
当然,这个系统也可以用于游戏:
基于英雄的游戏可以为选定的玩家角色改变其风格,或者你可以为基于团队的项目中的双方赋予不同的风格。
主题基础知识
皮肤系统由 Theme 资源驱动。
主题仅用于描述配置,并且每个单独控件的工作仍然是按照显示自身所需的方式使用该配置。
Godot 编辑器本身也依赖于默认主题。
主题项目
存储在一个主题中的配置由主题项目组成。每个项目都有一个唯一的名称,并且必须是以下数据类型之一:
- color 值
- 常量,整型值,可用于控件的数字类型属性或布尔值标记。
- 字体(font),常常被用于显示控件中的文字。字体的大小和颜色。用另个单独的控件来控制对齐属性和文字方向。
- 字体大小,整数值,与字体一同使用,决定文本显示的大小。
- 图标,纹理资源,通常用于显示图标。
- 样式盒(StyleBox)
是用来定义UI面板怎样展示的配置项集合。
不只是用于面板控件(Panel),它还常常用于许多控件的背景设置和遮罩设置。
不同的控件将以不同的方式应用 StyleBox。
尤其是焦点样式框被绘制为其他样式框的覆盖层(例如 normal 或按下)以允许基本样式框保持可见。
这意味着焦点样式框应设计为轮廓或半透明框,以便其背景保持可见。
主题类型
主题中的项目被划分为多个类型,并且每个项目只能属于单个类型。
每个主题项目由其名称、数据类型和主题中的类型这个三元组来定义。这个三元组在主题中必须是唯一的。
例如,Label 类型中不能有两个叫做 font_color 的颜色项目,但是在 LineEdit 类型中可以有另一个叫做 font_color 的项目。
Godot 的默认主题诞生之初就已经定义了众多的主题类型,它内建于每个使用了 UI 皮肤的控件节点中。
在默认主题里上述例子都是目前再用的主题项目。
你可以在每个控件的类参考手册中查看主题属性区域看看哪些项目是父类和子类都可用的。
子类可以使用为其父类定义的主题项,Button 及其派生类型就是很好实例。
牢记子类中,哪些过程是自动执行的很重要.不论什么时候内建控件在主题里面请求主题项目时,我们可以忽略主题类型仅通过它的类名知悉。
之后呢,下次时我们能根据它的父级类名来使用.可以通过改变父级类,例如 Button,来影响所有派生类,而不是调整每一个类来实现。
还可以定义自己的主题类型,并另外自定义内置控件和自己的控件。
由于内置控件不知道自定义主题类型,因此必须使用脚本来访问这些项。
所有控制节点都有多种方法,允许您从应用于它们的主题中获取主题项。
这些方法接受主题类型作为参数之一。
1 | var accent_color = get_theme_color("accent_color", "MyType") |
为了提供更多的自定义可能性,类型还能够链接在一起成为变种。
这是自定义主题类型的另一种使用场景。
例如,主题可以包含 Header 类型,标记为基础 Label 类型的变种。
那么各个 Label 控件就可以将其类型设为使用 Header 变种,主题请求主题项目的时候,这个变种都会先于其他类型使用。
这样就可以在同一个 Theme 资源里为同样使用某个类的控件节点保存不同主题项目的预设值。
警告
只有默认主题或者自定义项目主题中的变种才会在“检查器”中列为可选项。
在这两处之外定义的变种名称仍然可以手动输入,但是建议把所有变种都放到项目主题里面。
自定义控件
可以不用主题直接对各个控件节点进行自定义。这种方式称为本地重载。
控件的类参考手册中列出的每个主题属性,无论是通过检查器面板还是脚本,都可以在该控件上直接重载。
这样就可以针对 UI 中的特定部份进行精细的修改,不影响项目中包括该控件子类在内的其他内容。
本地重载对于提升用户界面的美观程度意义不大,如果你注重一致性的话就更是如此。
然而,本地重载对于布局节点而言是不可或缺的。
BoxContainer 和 GridContainer 等节点通过主题常量定义其子节点的间隙大小,MarginContainer 用主题项目来保存自定义边距。
控件存在本地主题项目重载时,会直接使用这个值,主题中所提供的值会被忽略。
自定义项目
所有全新项目使用的都是 Godot 提供的默认项目主题。
默认主题本身是常量,无法修改,但可以通过自定义主题进行覆盖。
设置自定义主题有两种方法:修改项目设置,或者修改场景树控件节点的节点属性。
可以调整两个项目设置以影响整个项目:
GUI > 主题 > 自定义允许您设置自定义项目范围的主题,以及 GUI > 主题 > 自定义字体 对默认回退字体执行相同的作。
当控件请求主题项时 节点自定义项目主题(如果存在)将首先检查。只有当它没有 选中默认主题的项目。
在一个单独的主题资源中,你可以设置所有 Godot 控件的默认样式与外观,但是你可以做更多的细节调整.
每一个控件节点同样拥有一个主题属性,通过这个属性你可以为一个控件的所有节点分支设置一个自定义的主题.
那意味着那个控件与其所有的子类,和子类的子类,在回滚当前项目和默认主题之前自定义主题的资源将第一个被检查。
备注
计划设定作为一种变化的替代手段,可以让你通过设置自定义主题资源对几乎整个UI分支中的根控件节点做出相同的影响.
然而运行计划项目时可以充当预期效果展示,当单独场景直接预览或者运行时还将使用默认主题展示。
为了解决这个问题你可以为每一个单独场景中的根控件设置相同的主题资源.
例如,你可以在项目主题中为按钮设置特定的样式,希望在弹出对话框中的按钮又有不同的外观。
你可以为弹出窗口的根控件设置自定义主题资源,并在该资源中为按钮定义不同的样式。
只要弹出窗口的根控件和按钮之间的节点链不中断,这些按钮就会使用最接近它们的主题资源中定义的样式。
所有其他控件仍将使用整个项目的主题和默认的主题样式。
综上所述,对于任意控件,其主题项的查找会是这样的:
- 检查相同数据类型和名称的本地重写。
- 使用控件的类型变体、类名和父类名:
从自身开始检查每个控件,看看它是否设置了主题属性;
如果设置了,就在该主题中查找名称、数据、主题类型都相同的项目;
如果没有自定义主题,或者主题中没有匹配的条目,就前往父控件;
重复步骤 a 至 c,到场景树的根节点或者非控件节点为止。 - 如果存在项目范围的主题,就在这个主题中查找控件的类型变体、类名和父类名。
- 在默认主题中查找控件的类型变体、类名和父类名。
即便所有主题中都不存在对应的项目,也会返回一个针对该数据类型的默认值。
超越控件
主题是一种用来保存视觉效果配置的理想资源,也非常合理。
虽然其他节点并没有像控件节点一样内置针对主题的支持,但还是可以和使用其他资源一样来使用主题。
举个非控件使用主题的例子:
在策略游戏中,相同单位需要根据队伍的不同而使用不同颜色的精灵。
可以在主题资源中定义颜色的合集,精灵(在脚本的帮助下)就可以使用这些颜色来绘制纹理。
这样做的最大好处是可以为红绿蓝队制作不同的主题但使用相同的主题项目,切换队伍只需要替换资源就可以了。
使用主题编辑器
主题编辑器是底部面板工具,当一个 Theme 资源被选中进行编辑时,面板会自动激活。
面板包含了添加、删除和调整主题类型和主题项目的必要用户界面。
面板有一个预览区,用于测试你做出的变化,以及一个窗口对话框,用于对主题项目进行批量操作。
创建主题
和其他任何资源一样,主题可以直接在文件系统面板中创建,也可以在任何控件节点中创建主题。
请记住,这样创建的资源默认是和场景绑定的。可以使用上下文菜单将新建的主题保存为单独的文件。
虽然主题编辑器提供了管理主题类型和项目的工具,主题也包括默认的备用字体,你只能使用检查器面板来编辑。
这同样适用于复杂的资源类型的内容,如样式盒和图标——它们会在检查器中打开编辑。
主题编辑器概览
主题编辑器有两个主要部分。
- 主题预览
主编辑器左侧。
场景的根节点必须是控件节点才能进行预览。
点击添加预览按钮然后从文件系统中选择已保存的场景,就可以添加一个新的选项卡。
对场景的修改不会自动反映到预览中。更新预览需要点击工具栏上的重新加载按钮。
预览还可用于快速选择要编辑的主题类型。从工具栏中选择选取器工具,并将鼠标悬停在预览区域上以突出显示控制节点。
突出显示的控制节点显示其类名称或类型变体(如果可用)。单击突出显示的控件会在右侧打开它以进行编辑。 - 主题类型与项目
主题编辑器右侧。
类型的项目列表分为几个选项卡,对应于主题中可用的每种数据类型(颜色、常量、样式等)。
如果启用了显示默认选项,则对于每个内置类型,其默认主题值都会显示为灰色。如果禁用该选项,则只显示编辑主题本身的可用项目。
通过点击项目旁边的Override按钮,可以将默认主题中的个别项目添加到当前主题中。
你也可以通过点击Override All按钮来覆盖所选主题类型的所有默认项目。
然后可以用Remove Item按钮移除被覆盖的属性。
属性也可以用Rename Item按钮重命名,完全自定义的属性可以用它下面的文本字段添加到列表中。
覆盖的主题项目可以直接在右侧面板中编辑,除非它们是资源。资源具有可用的基本控件,但必须在检查器栏中进行编辑。
样式框有一个独特的功能,您可以在其中从列表中固定单个样式框。
固定样式框就像包的领导者一样,当您更改其属性时,所有相同类型的样式框都会与它一起更新。这允许您同时编辑多个样式框的属性。
虽然可以从预览中选择主题类型,但也可以手动添加。
单击类型列表旁边的加号按钮可以打开添加项目类型菜单。
在菜单中,可以从列表中选择一种类型,也可以输入任意名称来创建自定义类型。文本字段过滤控件节点列表。
管理与导入项目
单击“管理项目”按钮将弹出“管理主题项目”对话框。
在 “编辑项 ”选项卡中,您可以查看和添加主题类型,以及查看和编辑所选类型的主题项。
你可以通过点击相应的 Add X Item 并指定其名称,创建、重命名和删除单个主题项目。
你也可以按数据类型(使用列表中的画笔图标)或按质量批量删除主题项。
Remove Class Items将删除你为一个控制节点类型定制的所有内置主题项目。
Remove Custom Items将删除所选类型的所有自定义主题项目。
Remove All Items将删除该类型的所有项目。
从导入项目选项卡中,您可以从其他主题导入主题项目。
您可以从默认的 Godot 主题、Godot 编辑器主题或其他自定义主题导入项目。
您可以导入单个或多个项目,也可以决定是复制还是省略其数据。
您可以通过多种方式选择和取消选择项目,包括手动、按层次结构、按数据类型以及所有内容。
选择包含数据会将所有主题项按原样复制到您的主题中。
省略数据将创建相应数据类型和名称的项目,但会将它们留空,从而在某种程度上创建主题模板。
主题类型变种
主题类型变种
设计用户界面时,有时候会想要让某个 Control 节点看起来和 Theme 中定义的一般样式不同。
每个控件节点都有主题属性覆盖项,可以让你针对单独的 UI 元素定义不同的样式。
如果您需要在多个控件之间共享相同的自定义外观,则这种方法很快就会变得难以管理。
想象一下,您使用灰色、蓝色和红色的 Button 变体 在整个项目中。每次向界面添加新按钮元素时进行设置 是一项繁琐的任务。
为了方便组织,更好地发挥主题的威力,你可以使用主题类型变种。
它们用起来就像普通的主题类型,但无法自给自足,不是独立的,扩展自其他类型,称作基础类型。
还是上面的例子,你的主题可以为 Button 类型定义样式、颜色、以及字体,UI 中的所有按钮元素都会得到自定义。
如果要再有灰色、红色、蓝色按钮,你就会创建一个新的类型,例如 GrayButton,然后把它标记为基础 Button 类型的变种。
类型变种可以在替换掉基础类型的某些内容的同时保留其他方面。它们还可以定义基础样式没有定义的属性。
例如,你的 GrayButton 可以覆盖基础 Button 的 normal 样式,加上 Button 里没有定义的 font_color。
控件会使用这两个类型的组合,并且优先使用类型变种。
备注
控件如何确定使用哪个类型、哪个主题、哪个主题项目,在《GUI 皮肤简介》一文的自定义项目部分有更详尽的描述。
创建类型变种
要创建主题变种,请打开主题编辑器,然后点击编辑器右侧类型下拉框旁的加号图标。
在文本框中输入你给你的主题类型变种起的名字,然后点击添加类型。
类型下拉框的下方是属性选项卡。请切换到图标是扳手和螺丝刀的选项卡。
点击基础类型字段旁的加号。
你可以在此处选择基础类型,一般就是控件节点的类名(例如 Button、Label 等)。
类型变种还可以进行嵌套,扩展其他类型变种。这就和控件节点的继承基类风格一样。
例如,CheckButton 继承 Button 的风格,因为对应的节点类型存在扩展关系。
选好基础类型之后,你应该就能在主题编辑器的其他选项卡中看到对应的属性了。你可以像往常一样去编辑。
使用类型变种
现在已经创建好了一个类型变种,你可以将其应用到你的节点上了。
检查器面板中,在控件节点的 Theme 属性下,你可以找到 Theme Type Variation 属性。默认为空,表示只有基础类型会对这个节点起效。
您可以从下拉列表中选择类型变体,也可以手动输入其名称。仅当类型变体属于项目范围的主题时,变体才会显示在列表中,您可以在项目设置中进行配置。对于任何其他情况,您必须手动输入变体的名称。单击右侧的铅笔图标。然后键入类型变体的名称,然后单击复选标记图标或按 Enter 键。如果存在具有该名称的类型变体,则节点现在将使用它。
使用字体
使用 Open Sans SemiBold 作为默认的项目字体。
有三种不同的地方可以设置字体。
- 主题编辑器,选择要设置字体的节点后选择字体选项卡即可。
- 控件节点的检查器,在 Theme Overrides > Fonts 中设置。
- 主题的检查器,在 Default Font 中设置。
字体文件有两种:
- 动态字体(TTF/OTF/WOFF/WOFF2 格式)
最常用的,因为可以调整字体大小,即便很大也能保持清晰。
基于向量,所以可以在包含更多字形的同时保持合理的文件大小。
还支持位图字体无法支持的一些高级功能,例如合字(将若干字符转换为专门设计的单一字符)。 - 位图字体(BMFont .fnt 格式或等宽图像)
动态字体
Godot 支持以下动态字体格式:
- TrueType 字体或合集(.ttf、.ttc)
- OpenType 字体或合集(.otf、.otc)
- Web 开放字体格式 1(.woff)
- Web 开放字体格式 2(.woff2)
虽然 .woff 尤其是 .woff2 的文件往往更小,但并不存在普遍“更好”的字体格式。大多数情况下建议使用字体开发者网站上提供的字体格式。
位图字体
Godot 支持 BMFont(.fnt)位图字体格式。这种格式由 BMFont 程序创造。
与 BMFont 兼容的程序也有很多,比如 BMGlyph 和 Web 版的 fontcutter。
可以导入任何图像以用作位图字体。
为此,请在 FileSystem 停靠栏中选择图像,转到 Import 停靠栏,将其导入类型更改为 Font Data (Image Font), 然后单击 Reimport:
字体可以使用任意顺序的字符集布局,但建议使用与标准 Unicode 一致的顺序,这样导入所需的配置就会少很多。
例如,下面的位图字体包含了 ASCII 字符,与标准 ASCII 顺序一致:
使用以下导入选项即可成功导入上述字体图像:
字符范围选项是一个数组,对应图像上的各个位置(单位为图块坐标,并非像素)。
字体图集的遍历顺序是从左到右、从上到下。
字符的指定方式可以是十进制数组(127)、十六进制数字(0x007f)或使用西文单引号包围(’‘)。‘ 就等价于 32-127,代表可打印(可见)的 ASCII 字符范围。
可以用西文横杠指定字符之间的范围。
例如 0-127(或 0x0000-0x007f)指定的就是整个 ASCII 的范围。
再比如 ‘ ‘-‘
请确保字符范围不超过列数 × 行数所定义的数量。否则导入字体会失败。
如果字体图像中包含未用于字体字形的边距(如署名信息),请尝试调整图像边距。该边距只会在完整图像周围应用一次。
如果字体图像中包含辅助线(画在字形之间)或者字符间距看上去有问题,请尝试调整字符边距。每个导入的字形都会应用该边距。
如果你需要比 “字符边距” 选项提供的更精细的字符间距控制,你有更多选项。
首先, 字符范围支持在指定字符范围之后的 3 个附加参数。
这些附加参数控制它们的位置和间距。它们按此顺序表示空间前进、X 轴偏移和 Y 轴偏移。
它们将根据写入的像素量改变每个字符的空间前进和偏移量。
例如,如果小写字母比大写字母细,则空格提前最有用。
请注意,偏移量可能会导致文本从标签边界的边缘被裁剪掉。
其次,您还可以为单个字符设置字距调整对。
通过键入两组用空格分隔的字符来指定字距调整对,然后输入另一个空格,一个数字来指定当这两组字符并排放置时间隔多少个额外/更少的像素。
如果需要,可以通过输入 \uXXXX 来指定字距调整对字符,其中 XXXX 是 Unicode 字符的十六进制值。
加载字体文件
要加载字体文件(动态字体或位图字体),请使用字体属性旁资源下拉菜单中的快速加载或加载选项,然后找到要使用的字体文件:
你也可以将“文件系统”面板中的字体文件拖放至检查器中接受 Font 资源的属性。
警告
从 Godot 4.0 版本开始,纹理的过滤和重复属性由使用纹理的地方定义,不再由纹理自身定义,字体亦然(动态字体和位图字体都是这样)。
像素风格的字体应当禁用双线性过滤,做法是将项目设置 渲染 > 纹理 > 画布纹理 > 默认纹理过滤 改为 Nearest。
这种字体的大小必须是设计大小的整数倍(设计大小因字体而异),使用该字体的 Control 节点也必须采用整数倍缩放,否则字体看上去就会很模糊。Godot 中的字体大小使用像素(px)为单位,不使用点(pt)为单位。在不同软件之间比对字体大小时请务必注意这一点。
继承自 CanvasItem 的节点也可以单独设置纹理过滤模式,使用 CanvasItem.texture_filter 即可。
字体轮廓与阴影
如果无法提前预知背景色,那么使用字体轮廓和阴影就可以提升可读性。
例如在 2D/3D 场景上绘制 HUD 元素时就是这样的情况。
大多数继承自 Control 的节点以及 Label3D 节点都可以使用字体轮廓功能。
在节点上启用字体轮廓的方法是在检查器中配置主题覆盖项 Font Outline Color 和 Outline Size。结果应该类似这样:
如果将字体与 MSDF 渲染一起使用,则将其 MSDF 像素范围导入选项设置为至少两倍于轮廓大小值,以便轮廓渲染看起来正确。否则,轮廓可能会比预期更早被切断。
对字体阴影的支持更加有限:它们仅在 Label 和 RichTextLabel。此外,字体阴影总是有硬边(但你可以降低它们的不透明度,使它们看起来更微妙)。要在给定节点上启用字体阴影,请相应地在 Label 或 RichTextLabel 节点中配置 字体阴影颜色 、 阴影偏移 X 和阴影偏移 Y 主题覆盖:
小技巧
您可以通过创建 LabelSettings 资源。此资源优先于主题属性。
高级字体特性
抗锯齿
您可以通过调整来调整渲染时字体的平滑方式 抗锯齿和提示 。这些是不同的属性,具有不同的用例。
抗锯齿控制栅格化字体时应如何平滑字形边缘。默认的抗锯齿方法( 灰度 )适用于每种显示技术。
但是,在较小的尺寸下,灰度抗锯齿可能会导致字体看起来模糊。
抗锯齿清晰度可以通过使用 LCD 子像素优化来提高,该优化通过在每个通道(红/绿/蓝)上偏移字体抗锯齿来利用大多数 LCD 显示器的子像素模式。
缺点是这可能会在边缘上引入“边缘”,尤其是在不使用标准 RGB 子像素的显示技术(例如 OLED 显示器)上。
在大多数游戏中,建议坚持使用默认的灰度 抗锯齿。对于非游戏应用,LCD 子像素优化是值得的 探索。
MSDF 渲染的字体无法修改抗锯齿——这种字体始终使用灰度抗锯齿进行渲染。
微调
微调控制的是对字体进行栅格化的时候,字形边缘吸附到像素的程度。
None看上去最平滑,字体较小时会看上去比较模糊。
Light(默认值)只会在 Y 轴上对字形的边缘进行吸附,看上去会比较锐利,
而 Full 则更加锐利,X 轴和 Y 轴都会进行边缘的吸附。对微调模式的选择取决于你个人的口味。
备注
如果更改提示模式在单击后没有明显效果 重新导入 ,通常是因为字体不包含提示说明。
这可以通过查找包含提示说明的字体文件版本或在导入停靠栏中启用 强制自动提示器(Force Autohinter) 来解决。
这将使用 FreeType 的自动提示器自动向导入的字体添加提示指令。
次像素定位
可以调整子像素定位。这是一个 FreeType 允许字形更接近其预期形式的呈现功能。
默认设置“ 自动” 会自动启用小尺寸的子像素定位,但在大字体时禁用它以提高光栅化性能。
您可以强制子像素定位模式为禁用 、 二分之一像素或四分之一像素 。 四分之一的像素提供最佳质量,但代价是光栅化时间更长。
更改抗锯齿、提示和子像素定位在较小的字体大小下具有最明显的效果。
警告
具有像素艺术外观的字体应将其子像素定位模式设置为 “禁用”。否则,字体可能会显示像素大小不均匀。
位图字体不需要此步骤,因为子像素定位仅与动态字体(通常由矢量元素组成)相关。
Mipmap
默认情况下字体不会生成 Mipmap,这样就能够降低内存占用、加速栅格化。
但这样一来,缩小后的字体就会变成一坨。3D 文本 不启用 Fixed Size 的时候尤为明显。
如果在 Control 节点中使用传统的栅格化字体(非 MSDF 字体)显示文本,并且该节点的缩放比 (1, 1) 要小,也会出现这种情况。
在“文件系统”面板中选中字体后,你可以在“导入”面板中启用 Mipmap,从而改善字体缩小渲染后的外观。
MSDF 字体也可以启用 Mipmap。在字体大小小于默认值时,这可以稍稍改善字体的渲染质量,但 MSDF 字体在放大后原本就是没有颗粒度问题的。
MSDF 字体渲染
多通道带符号距离场(Multi-channel signed distance field,MSDF)字体渲染能够将字体渲染为任意大小,无需在大小发生变化时重新栅格化。
与 Godot 默认使用的传统字体栅格化相比,MSDF 字体渲染有两个优点:
- 即便文字非常巨大,字体看上去也总是清晰的。
- 首次渲染大字号字体的字符时卡顿更短,因为无须执行栅格化。
MSDF 字体渲染的缺点有: - 字体渲染的基础开销较高。桌面平台上通常无法察觉,但是会影响低端移动设备。
- 由于缺少微调,较小的字体没有栅格化字体清晰。
- 与传统的栅格化字体相比,首次为新字形渲染小字号字体的开销可能更大。可以使用 字体预渲染 缓解。
- 无法为 MSDF 字体启用 LCD 次像素优化。
- MSDF 模式下无法正确渲染轮廓自相交的字体。如果使用从 Google Fonts 等处下载到的字体时出现渲染问题,请尝试改为从作者的官方网站下载。
要启用某个字体的 MSDF 渲染,请在“文件系统”面板中选中,然后在“导入”面板中启用多通道带符号距离场,然后点击重新导入:
使用 Emoji
Godot 对 Emoji 字体的支持有限:
- 支持 CBDT/CBLC(内嵌 PNG)和 SVG Emoji 字体。
- 不支持 COLR/CPAL Emoji 字体(自定义矢量格式)。
- 不支持 EMJC 位图压缩(iOS 系统 Emoji 字体需要用到)。这意味着如果要在 iOS 上支持 Emoji,你就必须改用自定义的使用 SVG 或 PNG 位图压缩的字体。
为了让 Godot 能够显示表情符号,使用的字体(或其 后备 )需要包括它们。否则,将不会显示表情符号,而是会出现占位符“豆腐”字符:
添加字体以显示表情符号后,例如 Noto Color Emoji,你会得到预期的结果:
要将常规字体与表情符号一起使用,建议指定 指向常规字体的高级导入选项中的表情符号字体的后备字体。
如果您希望在显示表情符号时使用默认项目字体,请保留基本字体 FontVariation 中的属性为空,同时添加指向 表情符号字体:
小技巧
表情符号字体的大小相当大,因此您可能希望加载系统字体以提供表情符号字形,而不是将其与项目捆绑在一起。
这允许在项目中提供完整的表情符号支持,而无需增加其导出的 PCK 的大小。
缺点是表情符号会因平台而异,并且并非所有平台都支持加载系统字体。
也可以将系统字体用作回退字体。
使用图标字体
Fontello 等工具可用于生成包含从 SVG 文件导入的矢量的字体文件。
这可用于将自定义矢量元素渲染为文本的一部分,或使用 3D 文本和 TextMesh 创建拉伸的 3D 图标。
备注
Fontello 目前不支持创建多色字体(Godot 可以渲染)。截至 2022 年 11 月,图标字体生成工具中对多色字体的支持仍然很少。
根据您的用例,与在 RichTextLabel 中使用 img 标签相比,这可能会带来更好的结果。
与位图图像(包括 Godot 导入时栅格化的 SVG)不同,真正的矢量数据可以调整为任何大小而不会损失质量。
下载生成的字体文件后,将其加载到您的 Godot 项目中,然后将其指定为 Label、RichTextLabel 或 Label3D 节点的自定义字体。
切换到 Fontello Web 界面,然后通过选择字符然后按 Ctrl + C ( Cmd + C 在 macOS 上)来复制字符。将字符粘贴到 Text 属性。
该字符将在检查器中显示为占位符字形,但它应该在 2D/3D 视口中正确显示。
若要在同一 Control 中将图标字体与传统字体一起使用,可以将图标字体指定为回退字体。
这之所以有效,是因为图标字体使用 Unicode 专用区域 ,该区域保留供自定义字体使用,并且设计上不包含标准字形。
备注
几种现代图标字体,例如 Font Awesome 6 具有使用连字指定图标的桌面变体。
这允许您通过直接在任何可以显示字体的节点的 Text 属性中输入图标名称来指定图标。一旦图标的名称完全输入为文本(例如房屋 ),它将被图标替换。
虽然更易于使用,但此方法不能与字体回退一起使用,因为主字体的字符将优先于回退字体的连字。
字体回退
Godot 支持定义一个或更多的回退字体,会在主字体缺失要显示的字形时使用。定义回退字体主要有两种用途:
使用仅支持拉丁字符集的字体,需要显示西里尔字母等其他字符集的文本时使用另一种字体。
使用一种字体渲染文本,使用另一种字体渲染 emoji 和图标。
通过双击文件系统停靠栏中的字体文件打开“高级导入设置”对话框。您还可以在“文件系统”停靠栏中选择字体,转到“导入停靠栏”,然后选择底部的 “高级…”:
在出现的对话框中,找到右侧的 回退 部分,点击 Array[Font](大小 0)字样展开属性,然后点击添加元素:
点击新元素上的下拉箭头,然后使用快速加载或加载选项选择字体文件:
使用默认项目字体时也可以添加回退字体,做法是将 Base Font 留空,同时添加一个或多个字体回退。
字体回退也可以单独定义,做法和 OpenType 字体特性 类似,这里不再赘述。
可变字体
Godot 提供了对可变字体的完整支持,可变字体能够用单个字体文件表示不同的字重和样式(常规、加粗、斜体等)。
该功能需要字体文件本身支持可变字体。
使用可变字体时,请在需要使用字体的地方创建 FontVariation 资源,然后在该 FontVariation 资源中加载字体文件:
向下滚动到 FontVariant 的 Variation 部分,然后点击 变体坐标 字样展开可调节轴的列表:
能够调整哪些轴取决于加载的字体。有些可变字体仅支持单轴调整(通常是字重或倾斜),有些可变字体则会支持多轴调整。
例如,Inter V 字体将字重设置为 900、倾斜设置为 -10 时是这样的:
小技巧
虽然可变字体轴名称和比例没有标准化,但字体设计者通常遵循一些常见的约定。
权重轴在 OpenType 中标准化,工作原理如下:
| Axis value 轴值 | Effective font weight 有效字体粗细 |
|---|---|
| 100 | Thin (Hairline) 细(发际线) |
| 200 | Extra Light (Ultra Light) 超轻(超轻) |
| 300 | Light 光 |
| 400 | Regular (Normal) 常规(正常) |
| 500 | Medium 中等 |
| 600 | Semi-Bold (Demi-Bold) 半粗体(半粗体) |
| 700 | Bold 大胆 |
| 800 | Extra Bold (Ultra Bold) 超粗体(Ultra Bold) |
| 900 | Black (Heavy) 黑色(重) |
| 950 | Extra Black (Ultra Black) 超黑(超黑) |
| 你可以将 FontVariation 保存为 .tres 资源文件,这样就能够在其他地方重复使用了: |
假粗体和假斜体
在使用粗体和斜体字时,使用专门设计的字体变体可以产生更好的视觉效果。
粗体字体中的字形间距更为一致,而斜体字体中的某些字形与正常字体完全不同(对比 “a” 和 “a”)。
然而,使用真正的粗体和斜体字体需要更多的字体文件,导致发布文件大小增大。也可以使用单个 可变字体 文件,但该文件将会大于正常的单个不可变字体文件。文件大小在桌面版项目上通常不足为虑,但在意图保持分配文件大小尽可能小的移动 / web 版项目上可能成为问题。
为了在不需要发布额外字体(或者使用单个更大的字体文件)的情况下支持粗体和斜体,Godot 中支持 假 粗体和斜体。
如果没有为粗体和斜体提供自定义字体,则 RichTextLabel 中的粗体和斜体标签将自动使用假粗体和斜体。
若要使用假粗体,在需要 Font 资源的栏目中创建 FontVariation 资源。将 Variation > Embolden 设定为正值会加粗字体,而设定为负值则会让字体变细。建议采用 0.5 和 1.2 之间的值,具体视字体而定。
假斜体由歪斜字体创建,通过修改每个字符的变换实现。该功能同样由 FontVariation 提供,使用的是 Variation > Transform 属性。将字符变换中的 yx 分量设为一个正值会创建斜体效果。建议采用 0.2 和 0.4 之间的值,具体视字体而定。
调整字体间距
为了某些艺术效果,或提高可阅读性,你可能会想要调整 Godot 中显示字体的方式。
在需要 Font 资源的栏目中创建 FontVariation 资源。其中 Variation > Extra Spacing 部分有 4 个可用属性,均可接受正值和负值:
- Glyph: 每个字形之间的额外间距。
- Space: 单词之间的额外间距。
- Top: 字形上方的附加间距。这用于多行文本,也用于计算控件的最小大小,例如 Label 和按钮 。
- Bottom: 字形下方的附加间距。这用于多行文本,也用于计算控件的最小大小,例如 Label 和按钮 。
还可以调节 Variation > Transform 来对字符进行拉伸。具体方式是调节 xx (横向缩放) and yy (纵向缩放) 分量。
由于字形变换不会影响每个字形在文本中所占据的空间,使用时应切记进行相应的字形间距调整。
因为大部分字体并非是为了在被拉伸环境下显示而设计,这种非均一的拉伸应谨慎使用。
OpenType 字体特性
Godot 支持启用 OpenType 字体特性,这是一种标准化的方式,可以在不切换完整字体文件的情况下进行替代字符的切换。
虽然其名称是 OpenType 字符特性,但同样也支持 TrueType (.ttf) 和 WOFF/WOFF2 字体文件。
对 OpenType 特性的支持高度取决于所使用的字体。某些字体完全不支持 OpenType 特性,而另一些可以支持数十个可切换的特性。
使用 OpenType 字体特性有 2 种方法:
- 针对字体文件全局设置
通过双击文件系统停靠栏中的字体文件打开“高级导入设置”对话框。您还可以在“文件系统”停靠栏中选择字体,转到“导入停靠栏”,然后选择底部的 “高级…”:
在出现的对话框中,找到右侧侧边栏中的 元数据覆盖 > OpenType 特性 部分,点击 特性(0/N) 字样展开属性,然后点击 添加特性: - 针对字体用例设置(FontVariantion)
使用字体特性时,请和使用可变字体时一样创建 FontVariant 资源,然后在 FontVariation 资源中加载字体文件:
滚动到 FontVariation 的 OpenType Features 部分,点击 特性(0/N) 字样展开属性,然后点击 添加特性,在下拉列表中选择所需的特性:
以 Inter 字体为例,下图中展示的是不带 Slashed Zero 特性(上)和启用 Slashed Zero OpenType 特性(上)的效果:
可以通过添加 OpenType 特性后在检查器中取消勾选相应特性,为特定字体禁用连字和 / 或字偶剧:
系统字体
只有 Windows、macOS、Linux、Android、iOS 支持加载系统字体。
然而,Android 上加载系统字体不可靠,因为没有官方 API 提供支持。
Godot 需要依靠解析系统配置文件,而这些文件可能会被第三方 Android 供应商修改。这可能导致系统字体加载不起作用。
与导入的字体相比,系统字体是一种不同类型的资源。它们从未实际导入到项目中,而是在运行时加载。这有 2 个好处:
- 这些字体不包含在导出的 PCK 文件中,使得导出项目的文件大小更小。
- 这些字体不包含在导出项目中,可以避免将专有系统字体随项目发布所导致的许可问题。
引擎会自动使用系统字体作为回退字体,因此不加载自定义字体也能够显示中日韩字符以及 emoji。
不过仍然会有一些限制,见使用 emoji 中的相关内容。
需要使用系统字体的地方,请创建 SystemFont 资源:
你可以显式指定若干字体名称(例如 Arial),也可以指定字体的别名,后者会映射到系统中的“标准”默认字体:
| 字体类别 | 字体别名 | 操作系统 | 备注 |
|---|---|---|---|
| sans-serif | Arial | Windows | - |
| Helvetica (黑体) | macOS/iOS | - | |
| Roboto / Noto Sans (Roboto / 能登无名) | Android | 由 fontconfig 处理 | |
| serif | Times New Roman (时代新罗马) | Windows | - |
| Times (次) | macOS/iOS | 由 fontconfig 处理 | |
| Noto Serif (诺托衬线) | Linux 的 | - | |
| monospace | Courier New (快递 新) | Windows | - |
| Courier (邮差) | macOS/iOS | - | |
| Droid Sans Mono (无单声道机器人) | Android | 由 fontconfig 处理 | |
| cursive | Comic Sans MS (无漫画 MS) | Windows | - |
| Apple Chancery (苹果大法官院) | macOS/iOS | - | |
| Dancing Script (舞蹈剧本) | Linux 的 | 由 fontconfig 处理 | |
| fantasy | Gabriola (加布里奥拉) | Windows | - |
| Papyrus (纸草) | macOS/iOS | - | |
| Droid Sans Mono (无单声道机器人) | Android | 由 fontconfig 处理 | |
| 请注意,表中的“操作系统”列表示字体通常与哪个操作系统相关联,并不代表该字体不能在其他操作系统上使用。 | |||
| “由 fontconfig 处理” 表示在Linux系统中,这些字体的管理是由fontconfig来处理的。 |
在 Android 上,拉丁、西里尔文本会使用 Roboto,中日韩等其他语言的字形会使用 Noto Sans。
如果是第三方 Android 发行版,实际的字体可能会不同。
如果指定了多个字体,则会使用系统中(按从上至下的顺序)找到的第一个字体。
所有平台上的字体名称和别名都不区分大小写。
和字体变体一样,你可以将 SystemFont 配置保存为资源文件,在其他地方使用。
请注意,不同系统字体的度量系统不同,也就是说,在一个平台上能够放进某个矩形的一段文本可能在另一个平台上就放不进。
请在开发过程中保留足够的空间,让文本标签能够在按需扩展。
备注
与 Windows 和 macOS/iOS 不同,Linux 系统上默认提供哪些字体取决于发行版。
也就是说,同样的系统字体名称和别名在不同的 Linux 发行版上可能会使用不同的字体显示。
运行时也可以加载字体,即便是未在系统中安装的字体也可以加载。详见《运行时加载与保存》。
字体预渲染
使用传统的栅格字体时,Godot 会针对不同字体的不同尺寸进行字形的缓存。
这样做能够减轻卡顿,但卡顿仍然会在项目的运行过程中首次显示某个字形时发生。
如果使用的是较大的字体大小,或者是在移动设备上运行,就会尤为明显。
使用 MSDF 字体时,只需要执行一次特殊的带符号距离场纹理栅格化。
这样就可以单纯针对字体进行缓存,无需考虑字体大小。
不过 MSDF 字体的首次渲染相对于中等大小的传统栅格字体要慢。
为了避免与字体渲染相关的卡顿问题,可以对特定的字形进行预渲染。
可以针对所有需要使用的字形进行预渲染(得到最优的效果),也可以只针对游戏中可能出现的常见字形进行预渲染(降低文件尺寸)。
没有预渲染的字形会照常进行即时栅格化。
备注
无论是传统字体还是 MSDF 字体,栅格化都是在 CPU 上进行的。也就是说 GPU 的性能并不会影响字体栅格化的耗时。
通过双击文件系统停靠栏中的字体文件打开“高级导入设置”对话框。您还可以在“文件系统”停靠栏中选择字体,转到“导入停靠栏”,然后选择底部的 “高级…”:
前往“高级导入设置”对话框的预渲染配置选项卡,单击“加号”添加配置:
添加配置后,请单击对应的名称,确保选中该配置。双击名称可以重命名该配置。
在配置中添加字形的方法有两种。两种方法可以同时使用,效果会累积:
使用翻译中的文本
对于大多数项目而言,使用这个方法最方便,因为可以从语言翻译中自动提取文本。
缺点是只有项目支持国际化时才能使用。否则请使用下面“使用自定义文本”的方法。
将翻译添加到项目设置后,使用 翻译选项卡中的字形 ,通过双击来检查翻译,然后单击翻译中的“塑造所有字符串”并单击底部的“添加字形”:
备注
翻译更新时,预渲染字形列表不会自动更新,因此,如果您的翻译发生了重大更改,则需要重复此过程。
使用自定义文本
虽然它需要手动指定将出现在游戏中的文本,但对于不具有用户文本输入功能的游戏来说,这是最有效的方法。这种方法值得手机游戏探索,以减小分布式应用程序的文件大小。
要使用现有文本作为预渲染的基线,请从“高级导入设置”对话框的 “文本”子选项卡转到“字形 ”,在右侧窗口中输入文本,然后单击对话框底部的 “形状文本”和“添加字形”:
小技巧
如果你的项目支持国际化 ,你可以将 CSV 或 PO 文件的内容粘贴到上面的框中,以快速预渲染游戏过程中可能渲染的所有可能的角色(不包括用户提供的或不可翻译的字符串)。
通过启用字符集
如果游戏的文本发生变化,第二种方法需要更少的配置和更少的更新,并且更适合文本繁重的游戏或带聊天的多人游戏。
另一方面,它可能会导致游戏中从未出现的字形被预渲染,这在文件大小方面效率较低。
要使用现有文本作为预渲染的基线,请从“高级导入设置”对话框的 “字符映射表 ”子选项卡转到“字形”,然后 双击右侧要启用的字符集:
为确保完整的预渲染,您需要启用的字符集取决于游戏中支持的语言。
对于英语,只有基础拉丁语 需要启用。启用 Latin-1 补充也允许完全 涵盖更多语言,例如法语、德语和西班牙语。对于俄语, 需要启用西里尔字母 ,依此类推。
默认项目字体属性
在高级“项目设置”的 GUI > 主题部分中,可以对默认字体的渲染方式进行选择:
- 默认字体抗锯齿: 控制 用于默认项目字体的抗锯齿方法。
- 默认字体提示: 控制 用于默认项目字体的提示方法。
- 默认字体子像素定位: 控制 子像素定位 方法。
- 默认字体多通道有符号距离字段: 如果为 true,则使默认项目字体使用 MSDF 字体呈现而不是传统的光栅化。
- 默认字体生成 Mipmap: 如果为 true,则启用 默认项目字体的 mipmap 生成和用法。
备注
这些项目设置仅影响默认项目字体(在引擎二进制文件中硬编码的字体)。
自定义字体的属性由其各自的导入选项控制。
您可以使用“项目设置”对话框的“ 导入默认值 ”部分来覆盖自定义字体的默认导入选项。
控件节点教程(RichTextLabel 中的 BBCode)
前言
Label 节点不能改变文本局部属性。
要绕过这些限制使用RichTextLabel。
RichTextLabel 允许使用标记语法(BBCode)或内置 API 对文本进行复杂的格式设置。
BBCode 是一个为文本的一部分指定格式规则的标签系统。
“BBCode” 中的 “BB”(bulletin boards),就是指“公告板”。
RichTextLabel 还带有自己的垂直滚动条。
如果文本不适合控件的大小,则会自动显示此滚动条。
可以通过在 RichTextLabel 的检查器中取消选中 Scroll Active 属性来禁用滚动条。
请注意,BBCode 标签也可以在某种程度上用于其他用例:
BBCode 可用于在类引用的 XML 源中设置注释的格式。
BBCode 可以在 GDScript 文档注释中使用。
BBCode 可在将富文本打印到输出底部面板时使用。
使用 BBCode
默认 RichTextLabel 的功能类似于普通 Label。
具有 property_text 属性,您可以对其进行编辑以具有统一格式的文本。
为了能够使用 BBCode 进行富文本格式,您需要通过设置 bbcode_enabled 来打开 BBCode 模式。
之后,您可以编辑文本 属性。这两个属性都位于检查器的顶部 选择 RichTextLabel 节点后。
例如, BBCode [color=green]test[/color] 将单词“test”呈现为绿色。
大多数 BBCodes 由 3 个部分组成:开头标签、内容和结束标签。
开始标签分隔格式化部分的开头,也可以携带一些配置选项。
一些开始标记,如上面显示的颜色标记,也需要一个值才能工作。
其他开始标记可以接受多个选项(在开始标记内用空格分隔)。
结束标记分隔格式化部分的末尾。在某些情况下,可以省略结束标签和内容。
与 HTML 中的 BBCode 不同,前导/尾随空格在显示时不会被 RichTextLabel 删除。
重复的空格也会在最终输出中按原样显示。这意味着在 RichTextLabel 中显示代码块时,无需使用预格式化的文本标记。
1 | [tag]content[/tag] |
备注
RichTextLabel 不支持纠缠的 BBCode 标签。例如,而不是使用:[b]bold[i]bold italic[/b]italic[/i]
请使用:[b]bold[i]bold italic[/i][/b][i]italic[/i]
安全地处理用户输入
在用户可以自由输入文本的场景(例如多人游戏中的聊天)中,您应该确保用户不能使用将被 RichTextLabel 解析的任意 BBCode 标签。
这是为了避免不当使用格式,如果 [url] 标签由 RichTextLabel 处理,则可能会出现问题(因为玩家可能能够创建指向网络钓鱼网站或类似网站的可点击链接)。
使用 RichTextLabel 的 [lb] 和/或 [rb] 标签,我们可以用这些转义标签替换邮件中任何 BBCode 标签的左括号和/或右括号。
这可以防止用户使用将被解析为标签的 BBCode, 而是将 BBCode 显示为文本。
创建一个 Node 节点并附加下面的脚本:
1 | extends RichTextLabel |
剥离 BBCode 标签
对于某些用例,可能需要从字符串中删除 BBCode 标签。在另一个不支持 BBCode 的控件(例如工具提示)中显示 RichTextLabel 的文本时,这很有用:
1 | extends RichTextLabel |
备注
不建议完全删除 BBCode 标签以供用户输入,因为它可以 修改显示的文本,而用户不明白为什么他们的部分 消息已被删除。 转义用户输入 应该是首选。
性能
在大多数情况下,您可以直接按原样使用 BBCode,因为文本格式很少是一项繁重的任务。
但是,对于特别大的 RichTextLabels(例如跨数千行的控制台日志),当 RichTextLabel 的文本更新时,你可能会在游戏过程中遇到卡顿。
有几种方法可以缓解这种情况:
- 使用 append_text() 函数而不是附加到文本中 财产。
此函数将仅解析 BBCode 以获取添加的文本,而不是 从整个文本属性解析 BBCode。 - 使用
push_[tag]()和pop()函数向 RichTextLabel 添加标签,而不是使用 BBCode。 - 在 RichTextLabel 中启用线程 > 线程属性 。
这不会加快处理速度,但会防止主线程阻塞,从而避免游戏过程中出现卡顿。仅当项目中实际需要线程时才启用线程,因为线程会有一些开销。
使用 push_[标签]() 和 pop() 函数代替 BBCode
如果出于性能原因不想使用 BBCode,可以使用 RichTextLabel 提供的函数创建格式标签,而无需在文本中写入 BBCode。
每个 BBCode 标签(包括效果)都有一个 push_[tag]() 函数(其中 [tag] 是标签的名称)。
还有一些方便的函数可用,例如结合了 push_bold() 的 push_bold_italics() 和 push_italics() 到单个标签中。
请参阅 RichTextLabel 类参考 ,获取完整列表 push_[tag]() 函数。
pop() 函数用于结束任何标签。由于 BBCode 是一个标签堆栈 ,因此使用 pop() 将首先关闭最近启动的标签。
以下脚本将产生与使用 BBCode [color=green]test [i]example[/i][/color]:
1 | extends RichTextLabel |
警告
使用格式设置函数时 ,不要直接设置 text 属性。
附加到 text 属性将擦除使用 append_text()、push_[tag]() 和 pop() 对 RichTextLabel 所做的所有修改 功能。
参考
参见
其中一些 BBCode 标签可用于 @export 脚本变量的工具提示以及类引用的 XML 源代码中。有关详细信息,请参阅类引用 BBCode。
| 标签 | 示例 | 描述 |
|---|---|---|
b |
[b]{text}[/b] |
使 {text} 使用 RichTextLabel 的粗体(或粗体斜体)字体。 |
i |
[i]{text}[/i] |
使 {text} 使用 RichTextLabel 的斜体(或粗体斜体)字体。 |
u |
[u]{text}[/u] |
在 {text} 上显示下划线。 |
s |
[s]{text}[/s] |
在 {text} 上显示删除线。 |
code |
[code]{text}[/code] |
让 {text} 使用 RichTextLabel 的等宽字体。 |
char |
[char={codepoint}] |
将十六进制的 UTF-32 码位 {codepoint} 添加为 Unicode 字符。 |
p |
[p]{text}[/p][p {options}]{text}[/p] |
将 {text} 添加为新的段落。支持配置选项,见 段落选项。 |
br |
[br] |
在文本中添加换行符,而不添加新段落。如果在列表中使用,则不会创建新的列表项,而是在当前项中添加换行符。 |
hr |
[hr][hr {options}] |
添加新的水平线以分隔内容。支持配置选项,见 水平尺选项。 |
center |
[center]{text}[/center] |
使得 {text} 水平居中。等价于 [p align=center]。 |
left |
[left]{text}[/left] |
使得 {text} 左对齐。等价于 [p align=left]。 |
right |
[right]{text}[/right] |
使得 {text} 右对齐。等价于 [p align=right]。 |
fill |
[fill]{text}[/fill] |
使 {text} 填充 RichTextLabel 的整个宽度。等价于 [p align=fill]。 |
indent |
[indent]{text}[/indent] |
单次缩进 {text}。缩进宽度与 [ul] 或 [ol] 中相同,但不创建列表点。 |
url |
[url]{link}[/url][url={link}]{text}[/url] |
创建超链接(有下划线且可点击的文本)。可以包含可选的 {text} 或者原样显示 {link}。必须用“meta_clicked”信号处理才能产生效果,见 处理 [url] 标签点击。 |
hint |
[hint="{tooltip text displayed on hover}"]{text}[/hint] |
创建将鼠标悬停在文本上时显示的工具提示提示。建议将提示文本放在引号内。不能使用 \" 或 \' 转义引号,需用双引号包围包含撇号的字符串。 |
img |
[img]{路径}[/img][img={宽度}]{路径}[/img][img=<宽度>x<高度>]{路径}[/img][img={垂直对齐}]{路径}[/img][img {选项}]{路径}[/img] |
插入位于 {路径} 的图像(可以是任意有效的 Texture2D 资源)。可指定宽度、高度(支持百分比)和垂直对齐方式。支持配置选项,见 图像选项。 |
font |
[font={路径}]{文本}[/font][font {选项}]{文本}[/font] |
为 {text} 使用位于 {路径} 的字体资源。支持配置选项,见 字体选项。 |
font_size |
[font_size={大小}]{文本}[/font_size] |
为 {text} 使用自定义字体大小。 |
dropcap |
[dropcap font={font} font_size={size} color={color} outline_size={size} outline_color={color} margins={left},{top},{right},{bottom}]{text}[/dropcap] |
为 {text} 使用不同的字体大小和颜色,同时如果标签足够大,则使标签的内容跨越多行。边距值以逗号分隔,不可用空格。负边距可用于让段落其余部分显示在首字下沉下方。 |
opentype_features |
[opentype_features={list}]{text}[/opentype_features] |
为 {text} 启用自定义 OpenType 字体功能。功能必须以逗号分隔的 {list} 形式提供。值不得用空格分隔。 |
lang |
[lang={code}]{text}[/lang] |
替代由 RichTextLabel 中的 BiDi > 语言属性设置的 {text} 的语言。{code} 必须是 ISO 语言代码。可用于强制使用特定脚本,某些字体的脚本特定替代品会因此被使用。 |
color |
[color={code/name}]{text}[/color] |
修改 {text} 的颜色。必须使用通用名称(见 具名颜色)或十六进制格式(例如 #ff00ff,见 十六进制颜色代码)。 |
bgcolor |
[bgcolor={code/name}]{text}[/bgcolor] |
在 {text} 后面绘制颜色,用于突出显示文本。接受与 color 标签相同的值。默认有轻微填充(由主题项控制),可设为0避免重叠。 |
fgcolor |
[fgcolor={code/name}]{text}[/fgcolor] |
在 {text} 前面绘制颜色,用于“编辑”(遮盖)文本。接受与 color 标签相同的值。默认有轻微填充,可设为0避免重叠。 |
outline_size |
[outline_size={size}]{text}[/outline_size] |
为 {text} 使用自定义字体轮廓大小。 |
outline_color |
[outline_color={code/name}]{text}[/outline_color] |
为 {text} 使用自定义轮廓颜色。接受的值和 color 标签一致。 |
table |
[table={number}]{cells}[/table][table={number},{valign}]{cells}[/table][table={number},{valign},{alignment_row}]{cells}[/table] |
创建列数为 {number} 的表格。使用 cell 标签定义单元格。可提供垂直对齐方式和基线对齐行。 |
cell |
[cell]{text}[/cell][cell={ratio}]{text}[/cell][cell {options}]{text}[/cell] |
向表格中添加一个文本为 {text} 的单元格。可提供扩展比例 {ratio}。支持配置选项,见 单元格选项。 |
ul |
[ul]{items}[/ul][ul bullet={bullet}]{items}[/ul] |
添加无序列表。列表项 {item} 必须以一行一个的形式提供。项目符号可使用 {bullet} 参数自定义,见 无序列表项目符号。 |
ol |
[ol type={type}]{items}[/ol] |
添加有序(编号)列表,类型由 {type} 给定(见 有序列表类型)。列表项 {items} 必须以一行一个的形式提供。 |
lb, rb |
[lb]b[rb]text[lb]/b[rb] |
分别添加 [ 和 ]。用于转义 BBCode 标记。是自闭合标签,无需关闭。也可用于添加 Unicode 控制字符,如 [lrm], [rlm], [zwj] 等。 |
备注
粗体 ([b]) 和斜体 ([i]) 格式的标签效果最佳,如果 在 RichTextLabelNode 的主题中设置适当的自定义字体 重写。
如果未定义自定义粗体或斜体字体, 人造粗体和斜体字体 将由 Godot 生成。与手工制作的粗体/斜体字体变体相比,这些字体很少看起来不错。
等宽 ([code]) 标记仅在 RichTextLabel 节点的主题覆盖中设置了自定义字体时才有效。否则,等宽文本将使用常规字体。
目前还没有用于控制文本垂直居中的BBCode标签.
可以跳过所有标签的选项。
段落选项
align
bidi_override、st
justification_flags、jst
direction、dir
language、lang
tab_stops
处理 [url] 标签点击
默认情况下,[url] 标签在单击时不执行任何操作.这是为了允许灵活使用 [url] 标签,而不是限制它们在Web浏览器中打开URL.
要处理单击的 [url] 标记,请连接 RichTextLabel 节点的 meta_clicked 向脚本函数发出信号。
例如,可以将以下方法连接到 meta_clicked,以使用用户的默认 Web 浏览器打开点击的 URL:
1 | # This assumes RichTextLabel's `meta_clicked` signal was connected to |
对于更高级的用例,还可以将 JSON 存储在 [url] 中 标签的选项,并在处理 meta_clicked 信号的函数中对其进行解析。例如:[url={"example": "value"}]JSON[/url]
Horizontal rule options
水平尺选项
color 颜色
height 高度
width 宽度
align 对齐
图像选项
color 颜色
height 高度
width 宽度
region 地区
pad 垫
tooltip 工具提示
图像和表格的垂直对齐
当使用 [img] 或 [table] 标签提供垂直对齐值时,图像/表格将尝试将自身与周围的文本对齐。
使用图像的垂直点和文本的垂直点执行对齐。
图像上有 3 个可能的点( 顶部 、 中心和底部 ),文本和表格上有 4 个可能的点( 顶部 、 中心 、 基线和底部 ),可以任意组合使用。
要指定这两个点,请使用它们的完整名称或短名称作为 image/table 标记的值:
1 | text [img=top,bottom]...[/img] text |
1 | text [table=3,center]...[/table] text # Center to center. |
您还可以仅指定一个值( 顶部 、 中心或底部 )以使用相应的预设( 顶部-顶部 、 中心-中心和底部-底部 ) 分别)。
值的简称是 t(顶部)、c(中心)、l(基线)和 b(底部)。
字体选项
name、n
size、s
glyph_spacing、gl
space_spacing, sp
top_spacing、top
bottom_spacing、bt
embolden、emb
face_index、fi
slant、sln
opentype_variation、otv
opentype_features、otf
具名颜色
对于允许按名称指定颜色的标记,可以使用内置 Color 类中的常量名称。
可以使用不同的大小写以多种样式指定命名类:DARK_RED、DarkRed 和 darkred 将给出相同的结果。
有关颜色常量的列表,请参见此图:
十六进制颜色代码
对于不透明的 RGB 颜色,支持任何有效的 6 位十六进制代码,例如 [color=#ffffff]white[/color]。
速记 RGB 颜色代码,例如 #6f2 (相当于 #66ff22)也受支持。
对于透明 RGB 颜色,可以使用任何 RGBA 8 位十六进制代码,例如 [color=#ffffff88]translucent white[/color] .
请注意,alpha 通道是颜色代码的最后一个组件,而不是第一个组件。
还支持短 RGBA 颜色代码,例如 #6f28(相当于 #66ff2288)。
单元格选项
expand 扩大
border 边境
bg 背景
padding 填充
无序列表项目符号
默认情况下,[ul] 标记使用 U+2022 “Bullet” Unicode 字形作为项目符号字符。此行为类似于 Web 浏览器。
可以使用 [ul bullet={bullet}] 自定义项目符号。
如果提供,则此 {bullet} 参数必须是没有封闭引号的字符串(例如, [bullet=*])。
可以在项目符号字符后添加尾随空格,以增加项目符号和列表项文本之间的间距。
常用项目符号字符列表见维基百科上的《项目符号》,你可以直接粘贴过来用作 bullet 参数。
有序列表类型
有序列表可以使用数字或字母自动升序标记条目。该标签支持以下类型选项:
- 1 - 数字,会尽量使用语言对应的数字系统。
- a、A - 小写和大写拉丁字母。
- i、I - 小写和大写罗马数字。
文本效果
BBCode 还可用于创建不同的文本效果,这些效果可以选择 动画。
开箱即用地提供了五种可自定义的效果,您可以 轻松创建您自己的。
默认情况下,动画效果将暂停 当场景树暂停时 。
可以通过调整 RichTextLabel 的 Process > Mode 属性来更改此行为。
备注
移动字符位置的文本效果可能会导致字符被 RichTextLabel 节点边界裁剪。
您可以通过在选择 RichTextLabel 节点后在检查器中禁用 Control > Layout > Clip Contents 来解决此问题,
或者使用效果在行上方和下方使用换行符来确保在文本周围添加足够的边距。
脉冲
Pulse 创建动画脉冲效果,使每个角色的不透明度和颜色成倍增加。
它可用于引起对特定文本的注意。它的标签格式是 [pulse freq=1.0 color=#ffffff40 ease=-2.0]{text}[/pulse] 。
Freq 控制半脉冲周期的频率(越高越快)。一个完整的脉冲周期需要 2 * (1.0 / 频率) 秒。
颜色是闪烁的目标颜色乘数。默认值主要淡出文本,但不是完全淡出。ease 是要使用的缓动函数指数。
负值提供进出缓和,这就是默认值为 -2.0 的原因。
波浪
Wave 使文本上下移动。它的标签格式是 [wave amp=50.0 freq=5.0 connected=1]{text}[/wave] .
“放大器 ”控制效果的高度和低低,“ 频率 ”控制文本上下移动的速度。 频率值为 0 将导致不可见波浪,负频率值也不会显示任何波浪。
如果 connected 为 1(默认),则带有连字的字形将一起移动。
如果 connected 为 0,则每个字形将单独移动,即使它们通过连字连接。这可以解决字体连字的某些渲染问题。
旋风
Tornado 使文本绕圈移动。它的标签格式是 [tornado radius=10.0 freq=1.0 connected=1]{text}[/tornado] .
半径是控制偏移的圆的半径, 频率是文本在圆中移动的速度。 频率值为 0 将暂停动画,而负频率将向后播放动画。
如果 connected 为 1(默认),则带有连字的字形将一起移动。
如果 connected 为 0,则每个字形将单独移动,即使它们通过连字连接。这可以解决字体连字的某些渲染问题。
抖动
摇动使文本摇动。它的标签格式是 [shake rate=20.0 level=5 connected=1]{text}[/shake] .
rate 控制文本摇动的速度,level 控制文本与原点的偏移距离。
如果 connected 为 1(默认),则带有连字的字形将一起移动。
如果 connected 为 0,则每个字形将单独移动,即使它们通过连字连接。这可以解决字体连字的某些渲染问题。
渐隐
淡入淡出会创建静态淡入淡出效果,使每个字符的不透明度成倍增加。
它的标签格式是 [fade start=4 length=14]{text}[/fade] 。
开始控制衰减相对于插入淡入淡出命令的位置的起始位置,长度控制应发生淡出的字符数。
彩虹
Rainbow 为文本提供随时间变化的彩虹色。
它的标签格式是 [rainbow freq=1.0 sat=0.8 val=0.8 speed=1.0]{text}[/rainbow] .
freq 确定彩虹在重复之前延伸了多少个字母, sat 是彩虹的饱和度,val 是彩虹的值。
速度 是每秒完整的彩虹周期数。正速度值将向前播放动画,值为 0 将暂停动画,负数 speed 值将向后播放动画。
字体轮廓不受彩虹效果的影响(它们保持其原始颜色)。 现有字体颜色被彩虹效果覆盖。
但是,CanvasItem 的 “ 调制 ”和 “自调制 ”属性将影响彩虹效果的外观,因为调制会使其最终颜色成倍增加。
自定义 BBCode 标签和文本效果
可以通过扩展 RichTextEffect 资源类型来创建自己的 BBCode 标签。
首先扩展 RichTextEffect 资源类型并为脚本提供 class_name,这样就能够在检查器中选择该效果。
如果想在编辑器中运行这些自定义效果,请在 GDScript 文件中添加 @tool 注解。
RichTextLabel 不需要附加脚本,也不需要在 tool 模式下运行。
注册效果的方法是将这个新建的效果在检查器中加入到 Markup > Custom Effects 数组中,或者在代码中使用 install_effect() 方法:
警告
如果自定义效果未在 RichTextLabel 的 标记 > Custom Effects 属性,则不可见任何效果,原始标记将保持原样。
您只需要扩展一个函数:_process_custom_fx(char_fx)。
或者,您还可以通过添加成员名 bbcode 来提供自定义 BBCode 标识符。
该代码将自动检查 bbcode 属性,或使用文件名来确定 BBCode 标签应该是什么。
_process_custom_fx
这是每个效果的逻辑发生的地方,在文本渲染的绘制阶段每个字形调用一次。这会传入 CharFXTransform 对象,其中包含一些变量来控制关联字形的呈现方式:
- 如果为绘制文本轮廓调用 effect,则 outline 为 true。
- 范围 告诉你作为索引在给定的自定义效果块中走多远。
- elapsed_time 是文本效果运行的总时间.
- 可见 将告诉您字形是否可见,并且还允许您隐藏给定的文本部分。
- 偏移量是相对于给定字形在正常情况下应呈现的位置的偏移位置。
- 颜色是给定字形的颜色。
- glyph_index 和字体是正在绘制的字形和用于绘制它的字体数据资源。
- 最后,env 是分配给给定自定义效果的参数字典。
如果用户指定,您可以使用带有可选默认值的 get() 来检索每个参数。
例如 [custom_fx spread=0.5 color=#FFFF00]test[/custom_fx] ,将有一个浮点传播和颜色颜色 参数。更多用法示例见下文。
关于这个函数,最后一点需要注意的就是需要返回布尔值 true 来确认效果已正确处理完毕。这样一来,如果在渲染某个字形时遇到了问题,就会跳出自定义效果的渲染,直到用户修正了自定义效果逻辑中所发生的错误。
以下是一些自定义效果的示例:
幽灵
1 | @tool |
矩阵
1 | @tool |
这将增加一些新的BBCode命令, 可以像这样使用:
1 | [center][ghost]This is a custom [matrix]effect[/matrix][/ghost] made in |
XR
暂不考虑基础教程
内置了一套模块化扩展现实(XR)系统,该系统通过抽象化不同 XR 平台的底层实现细节,以简化跨平台 XR 应用的开发流程。
该系统的核心是 XRServer 类,它作为整个 XR 架构的中枢接口,允许开发者通过该接口发现并连接各类 XR 运行时环境。


