.composit.blk
什么是复合对象?
复合对象不是对象,而是“.blk”格式的文本文件,其中包含引用对象及其一些参数的列表。这些文件根据特定约定进行命名,并且始终包含以下后缀之一:
_cmp.composit.blk: 简单复合对象。_random.composit.blk: 具有随机性的复合对象(替换对象或使对象消失)。_gameobj.composit.blk: 包含游戏对象的复合对象。
复合对象的主要用途是简化关卡设计人员的工作。例如,他们可以用内部元素组装房屋,而无需每次都手动放置每个元素。
从游戏的角度来看,单独放置的对象或由复合对象放置的对象之间没有区别。在游戏中,复合文件中的所有对象都作为独立对象加载。
Note
游戏引擎无法识别复合对象;它们仅用于方便在地图上排列和编辑对象集。只有对象本身(渲染实例、预制件等)才会导出到游戏关卡中。复合文件(’.blk’ 文件)不在编辑器之外使用。
Contents of .composit.blk 文件的内容
复合对象由具有特定参数的对象枚举组成:
对象名称。
放置坐标。
包含对象(或将其替换为另一个对象或空白区域)的概率。
声明单个对象的示例
className:t="composit"
node{
name:t="obj_name:rendInst"
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 0, 0]]
}
在此示例中:
className:t=”composit”: 指示此 ‘
.blk’ 文件将被引擎视为复合文件。node{}: 表示复合对象 (节点) 的单个构建块。确保所有大括号都正确匹配;缺少或多余的大括号将断开复合。
name:t=”obj_name:rendInst”: 指定节点的名称和类型。此处,“
obj_name” 是一个渲染实例。
如果没有 “重复项” (名称相同但类型不同的对象),你可以简单地指定名称而不指定类型:
name:t="obj_name"
这个简单的命名表示对象放置在指定坐标处,没有其他参数,如 disappear 或 replacement。
无论如何,您不应该有任何需要类型规范的情况:
仅当存在不同类型的全名时,例如”asset:rendinst” (asset.lod00.dag), “asset:composit” (asset.composit.blk) 和 “asset:prefab” (asset.dag),才需要这样做。
如果没有 nameakes,则无需指定类型即可录制: name:t="obj_name"。
这是一个简单的对象名称。简单来说,我们不能用它做任何事情(设置消失的概率、被另一个对象替换等)。
我们只是说在这样的坐标处有这样一个天体。如果缺少此字符串或 name:t="",则节点将为 “空” – 不会在其中提取任何资产。
我们可以在此处设置 rendInst、复合或游戏对象(如下所述)。Composite 并不关心它指的是什么 - 它只是一个文本文件,其中包含放置在某些位置的对象列表。名称不带扩展名。
也就是说,您应该只指定 table_a,而不是 table_a.lod00.dag。或者,我们设置了 table_a_plates_cmp,而不是 table_a_plates_cmp.composit.blk。
Warning
对于这些工具,优先选择之前找到的 资产。例如,从同一目录table_a.composit.blk 和 table_a.lod00.dag中的两个对象中,引擎将调用table_a.composit.blk
(按字母顺序,.composit将位于 .lod之前),但如果合成对象位于子目录”/composits/”中,则第一个对象将是.lod。因此,简单地以新的方式重新排列资源可能会破坏引用 “namesakes” 而没有明确指定类型的合成器。
因此,当复合引用同名的 .dag 时,通常会出错。也就是说,在 table_a.composit.blk 中指定了 table_a rendInst。引擎不会调用 table_a rendInst – 它会调用 table_a 复合。因此,我们进入了递归(当一个对象无限次引用自己时)。
因此,请始终按内容调用 composites。例如,is_table_a_dinner_chairs_cmp.composit.blk(一张带椅子的餐桌)或 is_book_case_shelf_0_7m_a_cmp.composit.blk (一个 0.7 米高的书架)。
但是,在某些情况下,您仍需要设置相同的名称。例如,我们已经在多个位置有table_a(rendInst)。而且,2 年后,有必要在上面放盘子。
我们可以制作一个复合 table_a_plates_cmp.composit.blk,在其中添加印版并在这些多个位置替换它(然后用将来的编辑覆盖它)。
这是对象的矩阵:
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 0, 0]]
Note
这是一个矩阵,而不是表示对象的旋转(以度为单位)或刻度(以百分比为单位)。不要尝试通过文本编辑器获取参数。 它们应由脚本生成,或在其中一个编辑器中创建复合对象时获取。只有矩阵的最后一个块是对象在空间中的位置(以米为单位)。此块可以通过文本编辑器进行编辑。
如果根本没有指定矩阵,它将等于上面的矩阵。零偏移和旋转、单位大小。
Note
该矩阵指定了相对于节点的 parent 的转换。 在上面的示例中,父级是复合本身,即相对于整个复合的枢轴指定偏移量。然而,情况并非总是如此。稍后会详细介绍。
声明 Random 节点的示例
如果我们希望将多个变体中的一个放置在指定的坐标中,而不是一个特定的资产中,那么我们需要以不同的方式指定节点。
node{
ent{
name:t="obj_name1"; weight:r=1;
}
ent{
name:t="obj_name2"; weight:r=1;
}
}
在这里,我们不是总是拥有相同的资产,而是以相同的概率随机选择两者中的一个。node{}中还可以有其他参数,例如矩阵。只有 "name:t" 不能与 ent{} 同时指定。
ent{}
这就是 实体块 的分配方式 – 一个节点内可以有多个实体块,但一次只能输出一个。
因此,如果我们只指定一个 ent{} 块,我们将得到与第一个例子完全相同的结果。始终只有一个选项可供选择。
里面已经有我们熟悉的参数 name:t,它的工作方式与节点中完全相同。此外,还添加了新参数 weight:r。如果未指定该参数,则默认情况下它将等于 1。
与其他节点的权重相比,其值越高,random 选择此选项的概率就越高。重量可以用任何“维度”来表示——0.0001 或 1000——重要的不是数字本身,而是它比邻居的重量大/低多少。我们指定更容易感知的值。
Note
与节点不同,实体没有自己的矩阵和其他参数。只有名称和重量。其余参数取自它们所在的节点。
该语法允许您不仅使用分号分隔实体参数,还允许您跳转到新行。以下示例中的两种方法是等效的。制表仅用于提高可读性,事实上它可以在没有缩进的情况下工作,并且所有块仅由大括号分隔。
ent{name:t="obj_name"; weight:r=1;}
ent{
name:t="obj_name"
weight:r=1
}
在文本编辑器中创建随机节点时,如果实体名称很长,则第二个选项可能更方便。
如果我们有时想不生成任何东西,我们应该怎么做?添加前面提到的空实体。正如我们记得的那样,空名称或没有 name:t 参数会给我们一个空节点:
node{
ent{
name:t="obj_name1"; weight:r=1;
}
ent{
name:t="obj_name2"; weight:r=1;
}
ent{
name:t=""; weight:r=2;
}
}
现在有一半的时间没有绘制任何内容,因为空节点的权重等于其他实体的总权重。
Note
如果您构建了一个将单个节点随机化的合成(例如,将椅子随机化为类似的节点),则必须为其命名*_random.composit.blk,这是一个简单的复合对象。
没有它也能工作。但是这个组合将由人工安排,搜索需要一个有意义的名称来解释里面的内容,而不必打开每个.blk 并检查内容。
声明多个对象的示例
让我们通过多次执行相同的步骤来巩固我们的知识。在实践中,没有必要并排使用不同的节点格式(因为它会使可读性复杂化),但每种方法都有效。
className:t="composit"
// 标准对象声明。如果没有指定的矩阵,它将具有零旋转、零偏移和小数位数`1`:
node{
name:t="obj_name0"
}
// 具有单个选项的 Random 实体的行为与上面的块相同。
“default” 矩阵与它的缺失相同:
node{
ent{
name:t="obj_name1"; weight:r=1;
}
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 0, 0]]
}
// 以相等的概率选择其中一个对象或什么都没有选择:
node{
ent{
name:t="obj_name2" // 参数可以用换行符分隔。
weight:r=1
}
ent{
name:t="obj_name3"; weight:r=1; // 或者用分号。
}
ent{
name:t=""; weight:r=1;
}
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 0, 0]]
}
声明随机变换的示例
正如我们之前所讨论的,需要使用不同的格式才能从多个实体中进行选择。随机对象放置的过程类似——我们不使用特殊参数,而不是矩阵:
随机化的可用参数
node{
offset_x:p2=0, 0
offset_y:p2=0, 0
offset_z:p2=0, 0
rot_x:p2=0, 0
rot_y:p2=0, 0
rot_z:p2=0, 0
scale:p2=1, 0
yScale:p2=1, 0
}
所有参数都是可选的,但不能与矩阵同时使用。如果指定了矩阵,则将忽略其中任何一个。
offset_*:p2- 指定对象沿相应轴的位置(以米为单位)。将*替换为轴字母。第一个值是绝对偏移量,第二个值是之后允许的两个方向的偏差。 例如,值 ‘3, 0.5’ 表示最终坐标将介于 2.5 到 3.5 米之间。如果未指定,则默认为 ‘0, 0’。rot_*:p2- 同样指定以度为单位的旋转。要设置任何围绕轴的随机旋转,只需输入 ‘0, 180’ - 随机值将覆盖围绕轴的完整旋转(从 -180° 到 180°)。如果未指定,则默认为 ‘0, 0’。yScale:p2- 与 rotation 和 offset 不同,所有轴上的缩放不能单独随机化。例外是 Y-axis,它在 Dagor 中指向上方。第一个值是初始刻度,第二个值是偏差。 如果未指定,则默认为 ‘1, 0’ 。scale:p2- 所有轴上的统一缩放。默认为 ‘1, 0’,可与 Y 轴缩放一起使用。
同时对多个合成进行随机变换
在同时控制多个合成时,例如使用多个渲染实例创建随机略微打开的窗口以实现视觉多样性,上面的示例很不方便。每个快门都可以分配其随机旋转参数,但进行调整会很麻烦。要更改角度,您必须遍历每个合成中对这些快门的所有引用 - 速度缓慢且容易出错。为避免这种情况,可以将参数放在单独的 ‘.blk’ 中。
具有随机变换和命名约定的文件的结构
从技术上讲,复合将接受任何名称,但由于人们会使用它们,因此文件名应该是有意义的。在通用文件中,常见的后缀是 _rot.blk 表示旋转,_offset.blk 表示偏移量,或者 _transform.blk 表示两者。如果某些对象具有不同旋转方向的左和右版本,请相应地添加后缀 _l 和 _r 。
例如,对于左侧百叶窗,使用以下内容创建文件 _shutter_rot_l.blk 是有意义的:
rot_y:p2=85, 5
rot_z:p2=0, 0.8
对于正确的文件,_shutter_rot_r.blk 和右侧的相应旋转。然后,此文件可以包含在快门的合成中,例如 name_city_house_window_shutter_1200x1900_a_l_cmp.composit.blk:
className:t="composit"
node{
name:t="name_city_house_window_shutter_1200x1900_a_l"
include "_shutter_rot_l.blk"
}
Important
_shutter_rot_l.blk文件的路径是绝对路径。在上面的示例中,包含文件与复合文件位于同一目录中。如果将此文件放在其他位置,请以命令行格式指定完整路径。我们实际上是将快门的
.dag文件name_city_house_window_shutter_1200x1900_a_l.lod00.dag替换为具有随机旋转的合成图像name_city_house_window_shutter_1200x1900_a_l_cmp.composit.blk。这意味着我们将这个_shutter_rot_l.blk包含在单个快门的合成中 - 没有其他任何东西。这是稍后将放置在窗口中的合成图像,而不是百叶窗本身。
为什么在嵌套组合中使用 include?
矩阵的优先级:请记住,矩阵具有更高的优先级。如果节点不在合成的 “零” 处,则更改其位置的唯一方法是沿未使用的轴通过随机偏移参数。
避免在 “Outer” 复合中执行此作,尽管它有效:
className:t="composit"
node{
name:t="name_city_house_window_shutter_1200x1900_a_l"
include "_shutter_rot_l.blk"
offset_y:p2=0.7, 0
offset_x:p2=1, 0
offset_z:p2=2, 0
}
node{
name:t="name_city_house_window_shutter_1200x1900_a_l"
include "_shutter_rot_l.blk"
offset_y:p2=0.7, 0
offset_x:p2=1, 0
offset_z:p2=-2, 0
}
这将起作用并将快门移动到指定的坐标,但默认情况下,所有编辑器当前都使用矩阵 - 无法以这种方式导出转换。以这种方式定位只能通过盲目手动编辑 .blk 来实现 - 这不是最方便或最快的方法。
重复转换的可能性:如果看不到包含的
.blk的内容,则很容易复制不可见的转换,这可能会导致错误。每个 ‘include’ 都会动态替换为指定文件的内容。如果参数指定两次,则仅应用第一个实例。
以下是粗心大意如何破坏事情:
className:t="composit"
node{
name:t="name_city_house_window_shutter_1200x1900_a_l"
rot_y:p2=90, 0 // 在 include 之前指定的第一个转换,shutter 将
include "_shutter_rot_l.blk" // 始终保持在 90°,忽略 Y 随机化
}
node{
name:t="name_city_house_window_shutter_1200x1900_a_l"
include "_shutter_rot_l.blk"
rot_y:p2=90, 0 // 包含中已存在的参数的第二个声明 - 尝试在 Y 上额外旋转 90° 将被忽略。
}
复合中的节点层次结构示例
在 ‘node{}’ 块中,可以不仅仅是一个参数列表。
另一个 ‘node{}’ 可以放在里面。在这种情况下,内部节点被视为子节点,其转换不是从零开始计算的,而是从父节点的矩阵计算的。
className:t="composit"
node{
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 1, 0]]
node{
name:t="obj_name0"
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 0, 1]]
}
}
在上面的例子中,obj_name0 被写在另一个 ‘node{}’ 中,这意味着它继承了偏移量。上部节点是一个“空”节点,沿 Y 轴偏移 1 米。包含对象的节点在其矩阵中沿 Z 轴的偏移量为 ‘1’ 米,但请记住,矩阵是组合的。相对于合成的中心,它将沿 Y 和 Z 偏移。我们可以为它设置其他坐标,添加另一个旁边有不同对象的块,等等。
它不经常使用,但有可能。合成以这种方式从 daEditor 导出 – 所有节点都与具有默认矩阵的空节点建立父级。在文本形式中,此类合成更难感知,直到最近,我们还没有可视化编辑合成的能力,因此实际使用仍然很少见。
节点层次结构使用的实际示例
假设我们有两张 0.7 米高的桌子和两个杯子。我们希望在组合中随机选择其中一个表并旋转。杯子也应该随机选择,放在靠近一侧的位置,并且只能沿着桌子的长度移动。它们也应该旋转,也许有时根本不出现。我们可以按如下方式实现这一点:
className:t="composit"
node{
rot_y:p2=0, 10
ent{
name:t="table_a";
}
ent{
name:t="table_b";
}
node{
offset_y:p2=0.7, 0
offset_z:p2=0.2, 0.05
offset_x:p2=0, 0.25
rot_y:p2=0, 180
ent{
name:t="cup_a";
}
ent{
name:t="cup_b";
}
ent{
name:t="";
}
}
}
分解一下:
父节点从 −10° 到 10° 随机旋转。其中一个表的绘制概率相等,因为每个实体的权重为 1,因为我们没有手动设置不同的值。
cup 节点嵌套在 table 节点内,这意味着考虑到其旋转,它的偏移量是相对于 table 发生的。之后,添加向上 0.7 米的绝对偏移量 - 杯子应始终位于桌面上,无需随机化。但是由于我们想要随机化其他转换,因此我们不能使用矩阵,因此我们以这种方式设置它。
然后,我们将边缘靠近 0.2 米,使其偏离中心,并增加一点随机性 - 从 0.15 到 0.25,这样它就不会在极端情况下脱落。
沿着表格的长度,距离更重要 - 让它在两个方向上均匀随机化。绝对偏移量未设置,偏差范围为 −0.25 至 0.25 米。
最后,我们添加一个完全随机的旋转 – 从 −180° 到 180°。

Important
对于一次性使用,此方法比旧方法更快,旧方法为每个随机元素创建单独的子组合。一个用于选择杯子及其旋转。另一个用于选择表格。然后是将杯子放在桌子上偏移量。最后是顶层的,它稍微旋转带有杯子的桌子。但在这里 - 只有一个!但是,如果要将杯子放置在架子或橱柜中,则将它们存放在自己的子组合中更有意义,这样您就不必将第三个杯子一个一个地添加到桌子、橱柜和搁板组合中。如果出于某种原因仅在这些桌子上找到杯子,那么是的,不要创建不必要的实体。
因此,此方法的正确应用是针对整体的各个部分,而不应使用同一组转换找到这些部分。例如,随机打开车门、引擎盖或相对于车身坐标的整个/完整/缺失的挡风玻璃都可以通过这种方式完成。同样,橱柜门,除非它是一个模块化系统,可以安装在不同的坐标处。
如果其他位置可能需要复合零件,请将它们移动到子复合中,以简化重用和将来可能的编辑。如果具有某些转换和随机性的节点只需要一次,例如,作为唯一对象的一部分,则最好使用单一复合方法。
游戏对象
概述
游戏对象或“gameObj”是游戏中的交互式元素,它们是动态的,对游戏玩法有直接影响。此类物品的示例包括医疗包、弹药、武器和其他设备。这些对象是从称为“loot_box”的对象自动生成的。
通常,这些对象放置在各种水平表面上,例如桌子、椅子、架子和橱柜。
示例节点描述
node{
name:t="loot_box:gameObj"
tm:m=[[0.3, 0, 0] [0, 0.3, 0] [0, 0, 0.3] [0, 0.6, 0]]
}
在此块中,name:t="loot_box:gameObj" 指定对象名称及其类型。格式为必填项。
如前所述,虽然如果没有不同类型的冲突资产,省略类型可能会起作用,但在以文本格式编辑复合时指定类型至关重要。这确保了清晰度并防止错误。
创建过程
不幸的是,这些节点不是通过 dag2riRes 或 daEditor 自动生成的。必须手动编写脚本。
您可以(并且应该)将名为 ‘loot_box’ 的立方体放置在场景/编辑器中的所需位置,然后将它们导出到合成中。放置过程与常规渲染实例相同。但是,所有这些 cube 最初将被记录为:
name:t="loot_box"
因此,在导出后,您需要手动更新合成中的每个对象,并为它们分配如上所述的正确参数。
Important
将游戏对象添加到合成对象后,使用格式 *_gameobj.composit.blk 对其进行命名是 强制性的,以将其区分为简单的复合对象。
其他类型的游戏对象
“loot_box”并不是唯一需要放置在环境中的游戏对象类型。其他常用的游戏对象包括用于精确室内照明和反射的“indoor_wall”、“wall_hole”和“envi_probe”、垂直梯子、光源(如闪烁的灯光)等等。
链式销毁和复合节点
链销毁
我们可以在游戏的参数中为某些资源分配 parent 类型(而不是在模型中 - 这是我们内部的 ‘.blk’ 文件中完成的)。所有没有指定 parent 的对象都被视为 children。当 parent 被销毁时,所有碰撞与 parent 的边界框相交的 child 也将被销毁。
Important
系统严格按如下方式运行: 一个 parent 销毁其碰撞与其边界框相交的所有 child。
这意味着:
子 不能摧毁 父。
子项 不能 销毁其他 子项。只有那些碰撞与 parent 的边界框相交的 children 才会被销毁。
不会发生后续的销毁链;该系统是刚性的,不支持这些规则之外的其他 logic。
例子
让我们举个例子,我们有一个 urn 作为 parent:

和双耳瓶作为 子:

放置场景 1: 全部正确。子级的碰撞(底部的 “尖峰”)进入父级骨灰盒的边界框。摧毁 父级,双耳瓶也会被摧毁。

放置场景 2: 模棱两可。如果我们想摧毁双耳瓶 通过 URN,这是正确的 – 双耳瓶的侧面碰撞进入 URN 的边界框。但是如果我们不想这样(因为双耳瓶只是站在骨灰盒旁边,而不是放在骨灰盒上),那就不正确了。双耳瓶会因为骨灰盒而被毁坏。

放置场景 3: 不正确。父母骨灰盒悬在空中。 摧毁它,双耳瓶也会坍塌。但是如果我们毁坏了双耳瓶,母骨灰盒就会一直挂着。

放置场景 4:** 不正确。当 父 骨灰盒被销毁时,只有中间的双耳瓶会被销毁,因为它的碰撞进入了 父级 的边界框。顶部的双耳瓶将保持悬在空中。

放置场景 5: 作为一个例外,父级 可以按照相同的规则销毁其他 父级 (并且只能在直接的 “圆圈” 内 - 需要与第一个被销毁的 父级 发生碰撞接触)。必须在属性中手动定义一个特殊参数(在相同的 .blk 文件中)才能启用此功能。它不是所有 parent 的默认行为,而是一个特定的功能。

Important
不要过度使用此功能,因为很难预测对象在游戏中的位置和位置。例如,我们曾经允许这个 parent 桶摧毁其他 parent。它成功地拆除了一个 parent 帐篷和 parent 弹药箱,如上所示。
对象放置规则
**为什么在本节中讨论销毁?
因为开发者经常忘记 游戏对合成一无所知。这一点在一开始就已经强调过很多次了,让我们再讨论一次。
只有对象及其矩阵会导出到游戏中。带有装饰器的房屋的巨大层次结构,直到最后一个随机复合花瓶,将作为 100 个带有矩阵的对象导出到游戏中。**游戏中不会存在任何合成物!
同样,对象不能随意销毁它们的邻居。只有 parents 会破坏 children (有时还有其他 parents)。
因此,不要创建对象无意识地堆叠在一起的合成图!它只是一个对象列表 - 它不会帮助破坏机制!
例子
放置场景 1: 橱柜架上的罐头。从逻辑上讲,这里唯一的 父 是橱柜。摧毁它 – 里面的一切都会被摧毁。事实证明,这些罐头是叠在一起的,它们不是父母。如果我们射击底部的罐子 - 顶部的罐子将保持悬在空中。

放置场景 2: 壁橱的顶部是单独的 非父级 物体:一个倒置的鞋盒盖、鞋盒盖内的鞋盒本身和鞋盒内的一只鞋。根据规则,这是不正确的,因为你可以尝试从盒子下面射击盖子,从鞋子下面射击盒子。但这里的厚度和尺寸是如此之小,以至于可以忽略不计。这是可以接受的违规行为,在大多数情况下永远不会被注意到。

放置场景 3: 问题已经解决,所以这里有一个旧的屏幕截图,它不能完全捕捉到正确的角度。在中间,我们可以观察到一个典型的问题,即 子 对象(一个盆)没有与它的 父 对象(一个衣柜)正确对齐,此时它已经被销毁了。然而,我们在这里的重点是右侧的布置——衣柜顶部盆的艺术堆叠。它看起来令人印象深刻,但是当较低的盆地在较高的盆地之前开始被破坏时,它将 100% 明显。

Important
小结:
避免在没有明确定义哪个是 父级 和哪个是 子级 的情况下将对象堆叠在一起。
子对象只能放置在单个图层中的父对象上。 任何不与 父级 边界框碰撞的对象都不会被正确销毁。
如果您需要创建由于这些限制而无法使用当前设置实现的复杂合成,请请求权限以创建将使用销毁机制进行拆分的唯一渲染合成。
复合材料的层次结构
层次结构不佳的问题
创建合成时的一个常见问题是,所有对象通常都放置在单个大型合成中。这种方法背后的基本原理是可以理解的——将所有内容安排在一个场景中,导出它,然后忘记它更容易。
然而,问题在于,合成工作永远不会真正结束。错误是不可避免的,并且保证会进行修订。在某些时候,这些修订将直接发生在合成的 ‘.blk’ 文件中,而不是在最初排列所有内容的原始场景中。一旦发生这种情况,原始场景就会过时,所有工作都会转移到 ‘.blk’ 文件。
这可能会导致重大问题。例如,考虑一个复合:

想象一下,这里的每个对象都不是子合成的一部分,而是手动放置在这个合成中(例如,尽管实际上并非如此)。
让我们看看使用此设置可能出现的典型问题。以这个带花瓶的橱柜为例:

请注意,它位于多个位置:

为了提高效率,设计师可以简单地将整个设置复制并粘贴到场景中的其他位置。结果是整个房子的布置相同,也许一两个花瓶发生了变化,给人一种随机的错觉。
我们之前已经讨论过链销毁。现在,想象一下你没有把其中一个花瓶与橱柜完全对齐,当下面的橱柜被摧毁时,它最终会悬在空中。
这带来了几个问题:
请记住,此设置已在房屋的多个位置复制。
例如,一些花瓶也可能独立放置,散落在地板上。
您需要费力地搜索合成物,识别每个花瓶,并单独调整它们的位置。
现在,考虑需要将这个带有花瓶的橱柜放在外面,并考虑景观。花瓶要么穿过橱柜,要么当橱柜放在地上时,它们会漂浮在橱柜上方。
在这种情况下,程序上的旋转和移位几乎是不可能的。请记住,这些调整通常无法自动进行,需要在使用 daEditor 或 dag2riRes 等工具后手动调整。在大型合成中,这变得完全不切实际。
此外,执行简单的随机化(例如将一个机柜换成另一个机柜)会变得非常不方便。您将不得不筛选数百行。
这些问题的根本原因是复合的不可读性。在如此纠结的混乱中快速做出任何调整几乎是不可能的。在极端情况下,整个房屋的家具(不包括房屋本身)可能会塞进一个复合体中,如下所示:

通常,我们不会分配 “create a composite for a single house” 的任务。 更多时候,这些任务涉及“创建 15 栋家具齐全的房子”,由 5-8 个不同的团队成员处理。每个人最终都会花费大量时间为他们的房屋单独设置独特的渲染实例,而不是利用由专门的团队成员创建的低级随机合成,这种方法可以为每个人节省工作时间。
可以理解的是,每个艺术家都想创造一些独特的东西,也许是为了讲述一个故事或引导玩家朝着一个特定的想法前进,比如“这是房主喜欢欣赏这个橱柜上他们最喜欢的花瓶的地方”。但没有人会注意到这些细节,因为在在线射击游戏中,环境需要:
无缝融入整个场景;
不干扰游戏。
玩家会注意到一个放置不当的物体,由于碰撞问题而扰乱了他们的目标。当您尝试在包含数百行的合成中找到该对象时,您会注意到非常令人头疼。您的同事会注意到手动排列对象而不是使用预制集所浪费的时间。
Important
问题总结
错误的不受控制的传播。
无法将对象组与 terrain 对齐。
缺乏多样性。
手动向场景添加多样性所花费的时间过多。
不可读的复合文件,使得很难理解什么位于何处以及它控制什么。
无法重复使用复合材料,从而阻止在邻近房屋中使用现成的资产组。
为避免这些问题,遵守严格的复合层次结构规则至关重要。
适当的层次结构
合成的层次结构应分解为对象的最小程序修改。让我们以花瓶和橱柜为例:
为对象的每个小程序修改创建合成:
程序旋转/移动(例如,围绕其轴旋转花瓶)。
对象随机化(例如,将橱柜或花瓶换成类似的)。
从步骤 1 中创建的合成中,为对象构建合成 组。
创建排列在橱柜上的多种花瓶。
将这些变化组合成一个随机的组合,例如 “随机花瓶排列在橱柜上”。
从步骤 2 中创建的复合中,为“具有随机花瓶排列的橱柜”创建一个随机复合。
从步骤 3 中创建的复合材料中,组装用于布置大型对象的复合材料,例如“在对象周围放置花瓶的随机橱柜”。
最后,将步骤 3 中的复合材料包括在整个 “house” 复合材料中,以及用于 “windows-doors”、“exterior décor”、“interior décor” 等的类似复合材料。
Note
遗憾的是,没有通用的复合结构 - 这完全取决于您正在创建的内容。如果您需要单个箱体的随机合成,则可能只需要一个层次结构级别 - 该箱体的随机合成,您可以在其中指定偏移、旋转和随机化。
但是,如果您要创建一家家具和餐具商店,则可能需要 3-6 层复合层次结构。
关键是从最低级别的复合材料开始,然后逐步提高。这种方法可确保所有内容都尽可能保持可编辑和可修复。您只需在一个不显眼的小文件中调整几个数字,即可解决所有游戏地图中的任何放置问题。
关于复合材料的细微差别
关于命名约定的附加说明
正如前面在本章中多次提到的,包括开头,让我们澄清几个关键点:
合成与渲染实例: 与渲染实例不同,合成不占用游戏客户端中的空间(它们根本不包含在内)。 因此,如果您从其他地图复制成功的合成图并出于自己的目的重命名它们,这并不是一个大问题。如果原始合成的创建者决定对其进行修改,此方法可帮助您避免潜在问题,这可能会中断您的设置。
详细命名: 始终尽可能描述性地命名合成,包括为合成和渲染实例指定的所有相关前缀和后缀。这一点至关重要,因为例如,您可能会创建一个用于室内的合成图像,但未能包含
is_前缀。然后,同事可能会错误地在户外使用它并添加景观对齐,从而导致复合材料沉入所有建筑物的地下。正确使用复合材料: 放置现有复合材料的人还应密切注意其预期用途。如果复合材料明确标明供室内使用,不要将其放在室外。相反,请复制它,给它起一个新名字,避免任何问题。
随机复合: 如果合成涉及随机化,请在名称中包含随机化对象的数量。例如
is_table_abc_random.composit.blk.内容所有权: 在其名称中明确说明复合作品的主题所有权。例如,如果您正在创建一个由古代花瓶组成的组合物,请相应地命名它,以便同事不会在不知不觉中将其置于未来主义的环境中。
对象在 Terrain 上和彼此相对的放置
您可能已经注意到,从编辑器中导出合成时,它们通常在节点中包含一个附加参数 place_type:i=1。例如:
node{
name:t="fachwerk_horse_cart_a_cmp"
tm:m=[[0.999018, 0, -0.0443057] [0, 1, 0] [0.0443057, 0, 0.999018] [-6.7688, 0.0028134, -1.22241]]
place_type:i=1
}
node{
name:t="haystack_i"
tm:m=[[0.020657, 0, 0.999787] [0, 1, 0] [-0.999787, 0, 0.020657] [6.92909, 0, -1.47888]]
place_type:i=3
}
这些值的范围可以从 1 到 6(甚至可能更高)。
发生这种情况是因为您导出的对象是根据特定规则放置在 terrain 上的。

放置类型:
place pivot - place_type:i=1: 对象枢轴在地形上的垂直放置(Dagger 中的枢轴位于 0.0.0)。适用于深入地下的大型高大物体(建筑物、电线杆、大堆碎片)——基本上不受地形高度变化影响的任何物体。

place pivot and use normal - place_type:i=2: 在地形上放置枢轴点,使用枢轴点处的法线(如果地形不平坦,对象将根据法线倾斜)。最适合需要符合地形形状的较小物品(例如,桶、桶、自行车、罐头)。如果大型对象的枢轴位于斜坡边缘,则可能无法正确对齐,从而导致部分对象漂浮在空中。仅对小型资产使用此选项。

place 3-point (bbox) - place_type:i=3: 使用对象边界框的三个点进行放置。边界框是根据对象的最大几何体限制构建的,对象被放置在地面上的三个极值点处,并考虑地形高度变化。此方法比类型 2 更精确,但对编辑器的要求略高。对于必须符合地形起伏的大型物体(例如,干草堆、成堆的木板、梯子、手推车、车辆),需要它。

Note
如果对象延伸到零以下(例如,桩、地基),它可能会被推高,因为放置是基于边界框的,该边界框建立在最外层的几何点上。对于此类对象,请使用类型 1 或 2。
place foundation (bbox) - place_type:i=4: 在地形上垂直放置,以便边界框的整个底部 不在 地形上方。对象可以部分延伸到地形下方,但绝不会漂浮在地形上方。

place on water (floatable) - place_type:i=5: 在水平面上垂直放置(是的,水有一个平面)。

place pivot with rendInst collision - place_type:i=6: 渲染实例的枢轴在另一个渲染实例的碰撞上的垂直放置。

适用于简单对象:

但难以处理复杂的 Alpha Surface,因为它可能无法正确识别碰撞表面:

Important
从 daEditor 导出合成时,请确保为未来合成的每个组件(例如,每个桶、桶和车辆)设置放置类型。
从 3ds Max 场景导出合成时,请手动将这些参数指定给最终合成中的相应对象。
不要导出没有这些参数的复合,然后再将它们应用于整个复合(例如,将房屋的整个外部装饰复合设置为 ‘place_type:i=3’)。这通常会导致错误,因为不清楚什么被用作枢轴或边界框。
始终为对象指定正确的放置类型。
对象放置精度
在任何游戏中,对象都根据其枢轴点放置在网格上(回想一下,Dagger 的枢轴位于 0.0.0)。这意味着对象的放置精度不能超过网格允许的最小单位,我们可以将其称为 Dagger 毫米。
因此,放置在非零位置的对象只有在合成没有旋转或偏移时才会完美对齐。
例如,带有 DP(动态定位)组件的房屋无论旋转或位移如何,都将保持完美对齐,因为房屋的底座及其 DP 组件都在正确的位置相对于零导出。它们以这种方式导出以确保最佳放置。
但是,当复合材料旋转或移动时,挂在由碎屑或砖块制成的墙壁上的窗户、门或绘画等物体可能会“漂移”,尤其是绘画。但别担心。
在 War Thunder 中,放置网格不够精确,以至于这会导致重大问题——我们有很大的误差余地。在基于 daNetGame 的游戏中,这种“漂移”仅发生在 Asset Viewer 和 daEditor 中。导出位置时,使用的精度级别比工具中的精度高得多,因此对象通常可以正确对齐。虽然您应该检查最终的位置,但您无需担心它会像在 daEditor 中出现的那样 “一样糟糕”。
示例
编辑器视图:

游戏内视图:

特殊参数
复合材料具有多个参数,这些参数专为特定的有限用例而设计。
War Thunder
quantizeTm:b=yes: 将渲染实例的几何体与合成中预制件的碰撞组合在一起时,此参数应应用于节点外部(紧跟在行 className:t="composit" 之后)。
这些对象类型在地图上具有不同的定位精度(预制件的精度较低)。此外,预制件在旋转方面的精度甚至更低。因此,在组合这两个对象时,碰撞几乎总是无法与视觉几何体对齐。
此参数强制预制件与渲染实例的坐标完全对齐。
className:t="composit"
quantizeTm:b=yes
node{
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 0, 0]]
node{
name:t="sheer_cliff_a"
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 0, 0]]
}
node{
name:t="sheer_cliff_a_prefab_collision:prefab"
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 0, 0]]
}
}
基于 daNetGame 的游戏
Random Object Seeds:例如,在基于 daNetGame 的游戏中,合成中的所有对象共享相同的配色方案。这意味着您不会找到带有不同颜色门窗的房屋 - 当您在地图上移动时颜色会有所不同,但每栋房屋的门窗将始终共享统一的颜色。
这是有意为之的 – 每个复合都只分配一个种子(本质上是随机选择的值)。此种子从顶级合成向下应用于嵌套最多的对象。
这种方法有利有弊。例如,房子内的所有家具、玩具和餐具都将采用统一的颜色。这使得创建像面料店这样的东西是不可能的,在那里你会期待不同颜色的面料,因为所有的面料都会共享一种颜色。同样,相同的家具物品将显示相同的磨损程度。
这可以通过 ignoreParentInstSeed:b=yes 参数来解决,该参数应用于应该与父复合具有不同种子的特定对象(或复合)。
例如,如果您希望商店中的每块织物具有不同的颜色,则需要将此种子应用于复合中的 每个织物节点,而不是整个“排列的织物”复合。否则,整个合成将具有不同的种子,但其中的对象将共享一个种子。
正确示例: 鞋子及其盒子会有不同的种子,因此颜色也不同。
node{
name:t="is_high_heel_shoe_a"
ignoreParentInstSeed:b=yes
tm:m=[[0.0888685, 0.939605, 0.205868] [0.49344, -0.244331, 0.902145] [0.896062, 0.0213662, -0.484326] [-2.40811, 1.9364, -31.9424]]
}
node{
name:t="is_high_heel_shoebox_a_opened"
ignoreParentInstSeed:b=yes
tm:m=[[0.0765914, 0, -0.997063] [0, 1, 0] [0.997063, 0, 0.0765914] [-4.67105, 2.93796, -24.8412]]
}
不正确的示例: 尽管整个组合项将具有不同的种子,但其中的所有项都将共享相同的种子。
node{
name:t="city_1_department_store_shops_stuff_cmp"
ignoreParentInstSeed:b=yes
tm:m=[[1, 0, 0] [0, 1, 0] [0, 0, 1] [0, 0, 0]]
}
Important
总结
将此参数应用于需要与主合成具有不同种子的特定渲染实例或合成。
不要 将其应用于整个合成,期望里面的所有内容都变得随机化 - 它不会。所有嵌套对象仍将共享一个公共种子,即使它与主合成的种子不同。
清单
在将复合图视为成品之前,您应该验证以下内容:
在 Asset Viewer 中正确显示复合对象:确保没有放错位置或缺失任何内容,并且所有内容都按预期显示。
使用右侧面板中的“Generate Random Seed”(生成随机种子)按钮正确生成变量对象:检查是否有任何内容重叠或意外消失,并且没有加载额外的对象。
Asset Viewer 日志中没有错误:监控控制台中是否存在由于命名错误、纹理缺失或其他未完成工作而导致的合成中资产显示问题相关的红色错误线。
复合日志通常仅指示对象未显示。 您需要打开有问题的对象本身,以查看日志对它的看法。
创建标准复合对象
在讨论了复合对象背后的结构和理论之后,现在让我们深入研究实际的创建过程。有几种方法可用于创建复合对象,每种方法都有自己的优点和缺点:
手动创建
使用daEditor
使用dag2riRes脚本
使用Asset Viewer中的Composite Editor
使用Blender中的dag4blend
在3ds Max中创建随机复合
手动创建
这里没有太多要详细说明的。您只需创建一个扩展名为 .composit.blk 的文件,然后手动定义一系列节点,指定对象名称及其坐标。
仅当需要添加单个对象的部分而没有任何偏移时,例如房屋的墙壁、室内家具、贴花、装饰和地面(所有内容都是以房屋的“零”点为中心的几何体或子合成),此方法才适用。
优点:
所有复合对象参数都可以在单个文本文件中访问。
不需要原始场景文件;您只需要一个新的或现有的
.composit.blk文件。
缺点:
盲工: 您在工作时看不到结果,因此很难正确对齐对象。
矩阵定义: 手动定义矩阵几乎是不可能的 - 让计算机来处理。
此方法通常用作管道的一部分来优化非随机 通过其他方式生成的复合对象。
使用 daEditor
此方法很简单:
根据需要将必要的对象放入编辑器中。
选择对象,从中心对象开始(稍后将成为合成对象的中心)。
在Landscape标签页中,选择Export as Composit选项:

优点:
易于应用。
与手动方法不同,它简化了编辑对象转换 - 矩阵是自动计算的。
缺点:
复合中心: 您无法控制复合的中心。当使用一个框架选择多个对象时,无法预测哪个对象将成为“零”点。按顺序选择对象时,第一个选定的对象将变为“零”坐标,这并不总是很方便。 不能在对象外部设置中心。
随机变换: 只允许使用固定矩阵和特定资产;您无法设置随机变换或对象。
解决方法是在所需中心添加其他对象,开始从中选择,然后在导出最终合成后删除此占位符。但是,这并不是特别方便。
此方法通常用于创建简单的对象集(例如,成堆的碎片、成排的桶、原木堆)。添加随机性需要通过其他方法进一步细化。
使用 dag2riRes 脚本
这是另一种相对简单的方法。虽然描述可能看起来很复杂,但尝试后,这些步骤将变得合乎逻辑和清晰,开发人员只需付出最少的努力。关键是按顺序执行这些步骤,因为缺少一个步骤将迫使您重新开始。
这种方法背后的想法是从 3ds Max 中的对象组合合成,将它们导出为.dag文件,然后使用脚本将.dag转换为.composit.blk。该脚本还允许您将导出到一个 .dag 文件中的所有对象拆分为单独的 .dag 文件。
优点:
实时预览: 直接在视口中移动节点,并提供即时反馈。
对齐选项: 允许在需要时更精确地放置对象。
缺点:
无随机化: 您不能定义随机转换或实体。
无 Include 语句:无法包含常规转换。
工作流:
将模型的所有基本级别 LOD 导入到您计划在合成中使用的一个场景中。
将每个模型附加到单个对象中(如果它们被分割到场景中的多个对象中)。
将每个模型命名为其对应的 DAG,减去
.lod00.dag后缀(例如,如果 DAG 为table_a.lod00.dag,则将模型命名为 ‘table_a’)。将模型的枢轴重置为 ‘
0.0.0’,并将旋转和缩放归零。否则,将导致最终合成中的矩阵计算不正确(非零参数将影响矩阵)。例如,如果枢轴不在 ‘
0.0.0’ 处,而是沿 X 轴保持一米,则模型将在合成中移动该距离。
为每个对象分配一个三位数后缀:“
*_000”(例如,“table_a”变为“table_a_000”)。克隆时,请确保三位数后缀保持不变:“
table_a_001”、“table_a_002”等。
将用作复合图中心的对象应具有后缀 ‘
*_origin’ 而不是 ‘_000’ (例如,’table_origin’)。根据需要定位对象,在场景中将它们克隆为参考或实例。确保旋转、位置和缩放与您预期的调整相匹配。
使用 *dag2riRes * 脚本将最终布局作为单个 ‘
.dag’ 导出到目录中。保持 DAG 名称一致更容易,以避免以后混淆。运行脚本,它将在 ‘
.dag’ 旁边生成一个 ‘composit.res.blk’ 文件(更多详细信息见下文)。重命名 ‘
composit.res.blk’ 文件并将其移动到所需位置。确保复合名称正确 – 请参阅文档开头的命名约定。
Important
如果在导出之前将 3ds Max 场景中的多个节点链接到一个层次中,则脚本会将整个层次视为以根对象命名的单个节点。
运行 dag2riRes 脚本:**
打开 FAR 或命令行。
导航到 ‘_
Dag2riRes’ 目录。输入以下命令:
dag2riRes-dev.exe -s:<name.dag> -d:simple_dags -no_tex
其中 ‘
<name.dag>’ 是 DAG 文件的名称。您可以指定任何名称。
命名提醒:
确保正确命名最终的合成图!不仅是名称,还有后缀。
*_cmp.composit.blk– 简单复合对象。*_random.composit.blk– 具有随机化的复合对象 (对象 替换,对象消失)。*_gameobj.composit.blk– 包含游戏对象的复合对象。
在 Asset Viewer 中使用合成编辑器
这是一个相对较新的选项,仍在开发中,因此在撰写本文时可能会缺少一些基本功能,并且会随着时间的推移而添加。
优点:
高度可视化: 您可以在视区中实时查看您的工作结果。
与 .composit.blk 配合使用: 允许编辑现有合成,而无需原始的 3ds Max 场景。
随机化: 节点可以通过随机实体选择来创建。
变换随机化: 可以使用随机变换创建节点。
缺点:
开发中: 编辑大型合成(数千个节点)时的性能问题,由于频繁使用多个嵌套结构,在基于 daNetGame 的游戏中尤其明显。
无多节点选择: 只能逐个移动节点。
No Undo: 如果您移动节点并改变主意,则必须手动还原它或从磁盘重新加载,从而丢失所有未保存的更改。
受限 Gizmo: 仅在世界坐标中可用 – 不能在视口中沿其自己的轴之一移动资源。
不支持
include: 目前,不支持 ‘include’ 语句。界面开发: 某些元素可能不直观。
除了添加 ‘includes’ 之外,它还允许您完全创建复合的层次结构。
在 Blender 中使用 dag4blend 插件
它是 dag4blend 附加组件的一部分,该附加组件以前是一个简单的导入器-导出器。这是一个新选项,也在开发中。
优点:
合成导入: 您可以导入合成,而无需原始的 混合 场景。您只需要
.composit.blk文件和资产 DAG(仅适用于具有几何导入的版本)。高视觉清晰度: 工作流程高度可视化,让您更容易查看和管理您的更改。
便捷的工作流程: 对齐、多节点选择等功能使其更易于使用,类似于 3ds Max 中的易用性。
随机实体节点: 您可以直接创建包含随机实体选择的节点。
Random Transform Nodes:您可以直接创建具有 randomized transforms 的节点。
子合成编辑: 您可以在父合成中的最终位置分解和编辑任何子合成,从而查看确切的最终节点位置。
合成到 DAG 转换: 提供将合成转换为 DAG 的功能。
游戏对象转换: 允许将
gameObj节点转换为网格,并将网格的边界框转换为gameObj节点,从而简化室内墙壁、墙洞、环境探测器等元素的放置。
缺点:
不支持 ‘
include’: 目前,不支持 ‘include’ 语句。有限的随机节点预览: 随机节点内实体的可见性必须手动切换;它们不会自动随机化。
游戏对象预览:必须手动配置
gameObj的预览;否则,它们将显示为空节点。无随机变换预览: 没有可用于随机变换的预览。
轴混淆: Blender 使用 Z 轴作为向上的方向,而 Dagor 引擎使用 Y 轴。在为随机变换定义参数时,必须考虑这种差异。
在 3ds Max 中创建随机合成
当计划在节点中使用随机对象时,最好在 3ds Max 中的合成集合期间提前做好准备。这对于避免以后的随机化问题至关重要。如果跳过此步骤,您可能会发现在最终合成中,椅子穿过桌子,或者桌子穿过梳妆台。
例如,假设您要创建围绕桌子放置的椅子的随机组合,这些椅子具有不同的大小和形状。您可以将它们合并为一个对象,并以最终的随机合成命名(例如, chairs_abc_random 用于复合的命名为 chairs_abc_random.composit.blk).

在放置过程中,您应该使用此组合的几何体来查看将在节点中随机化的对象的边界。
要配置随机化本身,需要通过其他方法进一步优化合成。