同一套高度公式用于不同地貌时,仍可能得到错误的结果。排查这四套请求时,几个主要差异出在数据交接上:查询读了哪一版高度,绘制按什么顺序执行,纹理上传后何时能用。本文沿生成过程说明这些问题的成因与修复。
第一篇沿一条苔原请求解释高度图计算,第二篇展开 POI、Stamp、植被与岩石布局。第三篇讨论这些数据如何进入 UE 的运行时地形、碰撞和联机流程。本篇回到生成端,记录 10 月 3 日至 9 日的一轮扩展:以四套地貌请求检查同一套算法,并把支撑排序、纹理行序和连续高度绘制进一步移入 C++。
截至本次整理,平原(Plain)、沙丘(Desert Dune)、离子赤原(Ionic Crimson)与焦灼荒原(Scorched Moor)四个案例的自然 Stamp 父记录,以及区域内部点和角点高度,已经有完整对照结果。以相同的显式启动配置重新生成,C++/HLSL 输出与冻结 Web 基线的四份 Vista 前、Location 高度纹理写入后的 4096² float32 高度场逐位一致。与参考目标的比较则仍保留毫米级残差,外围范围和引擎运行时另有验收要求。
这次扩展中,影响最大的错误集中在阶段之间:使用了错误版本的任务边界,把平台后的高度用于本应读取平台前高度的查询,混用了 CPU 记录顺序与 GPU 绘制顺序,以及没有随纹理驻留级别同步更新几何。下面先看四个案例的结果,再从请求查表开始,沿各阶段说明高度怎样生成,最后展开这些错误的定位和修复过程。
四种地貌,使用同一个比较范围
四组对比均使用中央半径 450 米的共同范围,采样间距为 0.5 米,每组统计 2,544,680 个像素。图中依次为参考目标高度、独立生成高度和绝对误差。圆形仅用于限定统计范围,不代表地图边界,也不代表外围融合半径。

图 1|数据绘图:沙丘(Desert Dune)案例。右栏的连续误差色标上限为 2 毫米,最大绝对误差约 0.5913 毫米。它显示数值残差,不是地表材质或光照差异。

图 2|数据绘图:平原(Plain)案例。相同中央范围内的最大绝对误差约 0.5569 毫米;本次统计没有超过 1 毫米的像素。

图 3|数据绘图:离子赤原(Ionic Crimson)案例。最大绝对误差约 1.5011 毫米,49 个像素超过 1 毫米,没有超过 1 厘米的像素。

图 4|数据绘图:焦灼荒原(Scorched Moor)案例。最大绝对误差约 1.3332 毫米,37 个像素超过 1 毫米。现有参考仅覆盖中央裁片,不能据此给出整图误差。
| 案例 | Seed | 平均绝对误差 | 最大绝对误差 | 超过 1 毫米 | 超过 1 厘米 |
|---|---|---|---|---|---|
| 平原(Plain) | 2763691705 | 0.0334 毫米 | 0.5569 毫米 | 0 | 0 |
| 沙丘(Desert Dune) | 2429929577 | 0.0084 毫米 | 0.5913 毫米 | 0 | 0 |
| 离子赤原(Ionic Crimson) | 930813242 | 0.0129 毫米 | 1.5011 毫米 | 49 | 0 |
| 焦灼荒原(Scorched Moor) | 2431905037 | 0.0258 毫米 | 1.3332 毫米 | 37 | 0 |
四个 Seed 用来标识本次请求。复现时仍需相同的任务条件、静态资源、配置版本和启动策略;只复制 Seed 不足以确定结果。中文地貌名称用于本文阅读,对应的英文配置名列在表中。
这些数字来自编码前的 float32 高度数组,未做均值、中位数或整体高度偏移补偿。误差图在缩小时,对每个 2×2 显示块保留最大误差,避免孤立差异被平均掉。图中的一个着色块因此可能代表多个原始像素,表里的数量始终按原分辨率统计。
这里测试的是四套固定请求,各自使用不同配置、执行不同分支。这四套请求已能独立生成对应结果;其他 Seed、任务条件和特殊分组仍需另外测试。
本轮结果与阅读路线
本轮结果
四套固定请求均能从请求重新生成至 Location 高度纹理写入阶段。
分别保持各案例基线的显式启动与查询配置时,C++/HLSL 与冻结 Web 的四份 Vista 前 4096² float32 数组逐位一致。
与参考目标比较,中央共同范围最大绝对误差约为 0.56~1.50 毫米。
外围 Vista、UE 运行时、物件装配与联机仍按各自范围独立验收。
这两类高度结果使用不同的比较对象:冻结 Web 用于检查迁移是否保留已有实现行为,参考目标用于衡量独立生成的数值残差。后文分别列出三种比较的对象与范围;与冻结基线逐位一致,不代表与参考目标一致。
正文从 demo 页面开始,依次说明配置、阶段结果和问题定位。需要查某个环节时,可以直接使用下面的导航。
| 阅读目标 | 对应章节 |
|---|---|
| 确认实际生成入口与高度版本 | demo 生成链 |
| 追踪请求、查表、两次抽选与地点类别 | 请求与配置 |
| 查阅高度参数、来源混合、类型与曲线 | Region 配方与连续高度 |
| 对照阶段图与具体实例 | 阶段产物与局部实例 |
| 定位间距、状态版本、行序、排序和资源问题 | 七类阶段一致性问题 |
| 核对迁移结果及比较范围 | C++ 迁移、验证口径 |
| 查看引擎接入状态与本文结论 | UE 验证、结论 |
图注按来源区分:运行截图来自实际 demo 页面;数据绘图读取已归档高度数组或布局记录;配置与机制示意解释读取关系。标为教学示例的数值只用于演算,不作为该 Seed 的实际区域记录。
demo 实际执行了哪条生成链
先看 demo 的完整页面。左侧保留请求条件、配置选择结果和阶段切换,右侧展示所选阶段的地面。阅读时先确认本局输入,再看选中了哪份配方,最后切换阶段观察高度变化。本例是焦灼荒原请求,选中 Region 36、profile 0;后面的配置说明、六阶段截图和两组局部实例图都围绕这一次作业展开。

图 5|运行截图:demo 完整页面截图。Seed、Planet、Scene、阵营、难度和任务输入保留在左侧,六个阶段可切换;右侧显示 Location 资源高度绘制后的结果。它展示内圈高度,尚未包含外围 Vista。
本篇对应的是四地貌独立生成 demo:Web 提交请求和展示结果,上游 C++ 生成布局、区域与实例,独立 D3D11 宿主执行自写 HLSL 生成高度。第三篇的 UE 运行时工程另有入口与验收状态,不能把这个 demo 的最新修复直接算作 UE 已同步完成。
当前生成后端先读取 200 字节请求模板,只替换指定 Seed,保留其余条件;再加载静态目录并执行上游 C++。因此,选择“平原(Plain)”或“焦灼荒原(Scorched Moor)”案例,会同时确定任务、场景与资源筛选的初始条件。页面显示的配方索引则是后续查表和抽选的结果,需要与请求输入分开阅读。

图 6|机制示意:按实际调用区分上游生成与像素绘制。自然 Stamp 父实例在上游已经形成,绘制时才展开静态高度资产。三类高度输入分别保存,不能按“最新高度”统一替换。
后文使用下表中的阶段名。Hbase 是平滑后的基础像素高度,Hplatform 是强制平整后的输出。某个请求没有平台写入时,两份数组可能完全相同,但查询与绘制仍要读取各自约定的版本。图 6 分开画出实例生成和像素写入,因为父实例生成后,还要等到后续阶段才参与高度绘制。
| 阶段名 | 输入与处理 | 保留的产物或用途 |
|---|---|---|
| 上游场景生成 | 请求、静态配置及布局、区域和实例处理 | 带高度三角网、自然 Stamp 父记录与地点记录 |
| 栅格化 Raster | 对带高度三角形逐像素插值 | 栅格高度数组 |
| 平滑 Smooth | 对栅格场执行多尺度滤波 | Hbase,供基础查询金字塔及后续处理使用 |
| 强制平整 Platform | 从 Hbase 查询平台目标,再写入平台 | Hplatform,供 Location 支撑绘制作为底图 |
| Location 支撑 | 辅助查询保留 Hbase,像素写入读取 Hplatform | 支撑调整后的地面及独立支撑参数 |
| 自然 Stamp 高度绘制 | 展开已接受父实例的静态高度资产 | 自然局部形状写入后的地面 |
| Location 高度纹理绘制 | 在当前地面上写入地点静态高度资产 | 本次内圈连续绘制的最后一份高度 |
| 外围 Vista | 独立的外围输入与融合处理 | 不包含在本文四份 Vista 前逐位回归中 |
上游先产生布局、区域与实例
初始化完成后,地点的候选与位置处理先提供本局空间输入。区域结构随后计算归属、高度组、类型和 Zone;地点与区域之间还会经过关联、附着和后续布局更新。当前入口中,同类操作会在不同阶段再次调用,不能把它们压缩成一次“地点生成完毕”并认为以后不再变化。
地点来源进入基础角点赋高;边界、类型和曲线继续进入细分高度,产生带高度的内部点和角点。自然 Stamp 循环随后结合区域成员、任务范围、障碍和查询数据接受父实例,当前入口再组装并输出用于栅格化的高度三角网。运行记录还分别保留地点绘制记录与自然 Stamp 父记录,后续资源适配再将它们展开为局部单元。
图 6 中,自然 Stamp 父实例属于上游输出,支撑、自然 Stamp 和 Location 高度纹理属于后续像素处理。父实例先被选出并保存,等支撑地面完成后,才将其静态高度资产写入地形。两条流程通过实例记录连接,不能把像素绘制顺序当成实例生成顺序。
三类高度输入不能互换
| 消费者 | 当前 demo 实际读取 | 用途 | 不能替换为 |
|---|---|---|---|
| 自然 Stamp 候选查询 | 基础角点高度、下坡邻接、外侧距离和空间索引 | 得到位置处的高度、坡度和范围条件 | 支撑后或最终 GPU 高度 |
| Location 支撑辅助查询 | 平滑后的基础像素高度 Hbase 及其查询金字塔 | 边界采样、高度包络、辅助高度 | 平台写入后的 Hplatform |
| 支撑像素绘制 | 平台后的 Hplatform 和已经计算的支撑参数 | 改写当前地表 | 只有查询高度、没有平台的旧底图 |
自然 Stamp 查询先通过空间索引找到角点编号,读取该角点高度并乘以调用方尺度。存在有效下坡邻点时,再由高度差除以水平距离,经过 atan 转成坡度角。它使用的是上游角点场及邻接关系,没有从最终高度纹理采样,也不等同于渲染网格的法线坡度。
Location 支撑辅助查询使用的是另一条路径。自产三角网经栅格化和平滑后形成 Hbase,发布为约定纹理行序,再构造八层查询金字塔。查询结果包含辅助高度、包络以及接受/剔除采样统计;后面还要计算准入、目标、父子协调、发射与邻接,不是把查询返回值直接当作最终支撑高度。
强制平整同样从 Hbase 计算平台目标并写入 Hplatform,但 Location 支撑查询仍保留 Hbase。支撑绘制再读取 Hplatform 作为底图。程序为这两份高度分别保存文件路径和哈希,查询与绘制绑定的输入也不同。后文离子赤原的修复,就是把误接的平台后查询改回 Hbase。
绘制准备可以提前,像素写入仍按顺序执行
静态 Level/Unit、纹理、patch 网格与驻留计划可以在像素绘制前准备好;“已经解析自然资源”不表示自然高度已写入地面。端到端验证器准备绘制包后,由连续 C++ 绘制入口依次执行支撑、自然 Stamp、Location,最后才产生用于比较的高度数组。
这个连续入口不是另一套从 Seed 开始的完整生成器。它接收调用方提供的支撑目录、平台高度和绘制记录,保证后继绘制顺序;端到端验证器另外保证这些输入来自本次上游生成。核对这两项结果时,需要分别检查绘制输出和整条输入来源。
原版分析已经核对了配置读取、父记录、阶段内核和共同范围高度。检查 demo 时,还要逐项确认这些规则是否接入了当前入口、测试请求是否执行了对应分支,以及输出是否完成参考对照。源码中存在一个函数,并不足以说明这次请求已经通过它生成了结果。
请求怎样确定 Region 与 profile
生成入口需要三部分数据:请求提供本局条件,静态表保存可选资源及规则,执行状态记录分支选择和随机序列。只保留 Seed,会丢失另外两部分对结果的影响。
下面沿焦灼荒原请求解释配置:Seed 2431905037 查到 Planet 索引 73、Scene 索引 23,最终选中区域配方 36 和 profile 0。先看各字段被哪一步读取,再回到同一次 demo 作业查看结果。三角网、栅格化和局部写入另用一组平原图说明,图注会标明对应请求。

图 7|配置示意:焦灼荒原固定请求的读取主线:请求标识先查表,资格条件限制候选,Seed 再参与配方与 profile 抽选。下排列出后续读取这些配置的步骤。
请求进入当前入口时保存为 200 字节记录。汇报图展示的是便于阅读的逻辑字段,不是这 200 字节的完整内存布局。例如,Scene 的逻辑标识已经在解码时转换成内部索引。字段显示在哪个框里,不能代替对实际序列化格式的说明。
这里的“独立生成”指生成分支不读取参考目标的中间高度、实例列表或父变换。参考数组只在生成完成后进入误差比较;本次计算自产的阶段文件则可继续传给下一阶段。Web 负责提交作业和显示,高度来自 C++ 核心、自写 HLSL 和独立图形执行程序。冻结 Web 基线也是这套实现产生的文件,因此它与 C++ 输出的逐位比较属于实现回归,不能作为两套独立算法对原版的双重证明。
图 7 的连接线表示数据读取关系。Planet 决定从哪些集合寻找配方,并提供地点资格条件;Scene 限定 profile 类别,解释任务编号并提供场景规则。二者共同约束配方选择。选中的 Region 配方再进入区域分组、类型选择和资源处理,经过角点、三角网与局部绘制,才形成像素高度。后文按这一顺序说明各字段的用途。
先区分配置、实例和计算产物
一份 Region 配方可以被多个请求重复使用。同一配方在不同 Seed、任务和空间布局下,会产生不同的区域归属和地点实例;这些实例再参与来源赋高和局部纹理写入。配方本身保存的不是一张固定高度图,也不是已经摆好的建筑清单。
| 数据 | 本例的具体内容 | 读取阶段 | 产生的结果 |
|---|---|---|---|
| 逻辑请求 | Seed、Planet/Scene 标识、九项任务和条件列表 | 初始化及任务匹配 | 命中的静态记录、任务种类与执行条件 |
| Planet 记录 | 两路集合引用 23/23、类型 9、B0/D4 列表 | 区域入口和地点资格判断 | 候选集合、模板与地点的过滤条件 |
| Scene 记录 | 允许类别 [1,2]、32 槽任务表、规则引用 |
配方资格及任务处理 | 可选 profile 类别、逐项任务种类和派生模式 |
| 候选集合 | 七个配方引用及各自外层权重 | Region 抽选 | 本局使用的配方 36 |
| 配方内 profile | 类别、档位权重、标签和地点类别权重 | 档位选择及地点候选处理 | profile 0,以及本局后续使用的约束 |
| 高度与类型参数 | 四组权重、距离参数、噪声参数、类型表 | 区域分类、角点及细分高度 | 高度组、类型、Zone 引用和带高度的点 |
| 本局实例 | 地点位置、资源引用、自然 Stamp 父实例与朝向 | 来源计算及资产展开 | 来源高度、绘制变换、局部纹理输入 |
| 阶段高度数组 | 栅格化、平滑、支撑和局部写入结果 | 后续查询、绘制与最终比较 | 可查询、可编码的地表高度 |
表中的输出分为三类:查表和抽选确定记录,资格条件限制候选,高度计算产生数值。Planet 类型 9、profile 类别 1 和地点类别 1 分别用于兼容判断、档位资格和资源分配;它们与后面的高度组、像素高度属于不同字段。后面的配方图会逐项说明这些字段在哪一步使用。
实现中需要明确区分静态配方索引、profile 索引、区域编号、实例编号和纹理像素坐标。传递索引时若未注明所属数据表,接收模块可能从错误的表中读取记录。
Planet 查表与地点资格条件
Planet 在这里限定 Region 候选集合与 Location 资源资格,尚不计算高度。查表确定记录后,集合引用用于选择配方,资格条件用于筛选地点资源。
Planet 静态表有 438 条记录。本例通过请求标识查到从零计数的索引 73,即表中第 74 条。这个索引是标识匹配的结果,不能用请求数值直接做算术换算。Seed 不参与这一步查找。

图 8|配置示意:Planet 只按标识查找记录,再按分支读取集合引用。集合编号、类型和条件列表分别交给 Region 查找与地点资格检查。
命中记录保存两路区域候选集合引用,本例都是 23。当前独立调用显式使用分支 0;原版上游怎样设置这个状态,尚未完整确认。四套测试请求对应的两路引用分别为 23/23、3/3、21/21、24/24,因此这四个案例的候选集合选择不受这一个分支差异影响。它没有证明原版四局的状态值都是 0。
这条记录还提供地点资格判断。本例 Planet 的 B0 列表为空,D4 列表为 [9,9],类型值为 9。B0、D4 暂时沿用字段标识;已确认的是读取方式,还不能为它们附上未经核验的玩法名称。
候选地点的 B0 列表为空时通过这一项;非空时,需要与 Planet 的 B0 列表相交。因此,Planet 空列表不等于所有候选都通过。D4 判断则允许候选值为 0、候选值出现在 Planet 列表中,或者 Planet 列表包含 9。本例符合最后一个条件,但阵营、难度、模板等资格判断仍需继续。
请求中的 B0 控制字、Planet 的 B0 列表和候选地点的 B0 列表属于不同字段。即使它们在分析记录里使用相似名称,也不能交换输入。类型值 9 还参与模板兼容检查,对应模板掩码的 bit 7;通过地点资格不等于任意模板都可用。
Scene 的任务种类与允许类别
Scene 提供三类配置:允许的 profile 类别、任务种类和派生区域规则。下面先说明各自用途,再看九项请求如何匹配任务表。
Scene 静态表有 161 条记录,本例请求标识命中索引 23。图中上排依次展示输入、读出的配置字段及其使用位置,下排保留九次任务匹配的实际槽位与种类。Scene 的分类、允许 profile 类别和任务种类来自不同字段。

图 9|配置示意:Scene 的三类输出分别使用:允许类别用于配置档筛选,任务种类控制类别分配分支,规则引用派生的 Zone 模式用于后续子区域处理。
本例允许的 profile 类别是 [1,2]。它用于筛选区域配方中的配置档,不是“本局有两个任务”,也不是某个地点的最终类别。Scene 分类 1 参与默认任务半径配置;半径覆盖引用为 0,含义是没有覆盖,不能解释为半径为零。
Scene 还保存一个规则引用,经过构造逻辑得到 Zone 模式 3。表中保存的原始引用与执行时派生的模式需要分开;不能把模式 3 当成 Scene 表里直接存放的一个数值字段。对应分支在地点摆放后检查模式标志与子区域覆盖,再写入类型、Zone 和高度组信息。
九项请求怎样匹配三十二槽任务表
任务表有 32 个槽,每槽 24 字节。目前已确认的是任务编号和任务种类,分别位于槽内偏移 0 和 12;其余 16 字节不在本文中推断用途。

图 10|配置示意:任务匹配与地点生成。重复任务按请求顺序处理;跳过类别分配后仍有资源选择和摆放。
本例请求保留九项任务编号及其原始顺序,其中前两项相同,接下来的两项也相同。它们依次命中槽位 [0,0,1,1,4,6,5,9,13],取得种类 [0,0,0,0,3,3,3,3,2]。重复编号并没有在此处去重,九次匹配也不能改写成七个独立任务。
这些种类值影响执行路径。种类大于 1 时,跳过地点类别分配;后续的资源选择和摆放仍然继续。种类 0 则进入相应的类别分配逻辑。把“跳过类别分配”写成“不生成地点”,会遗漏后面的实际工作,也会错误估计随机数消耗。
目前种类 0、3、2 的业务名称仍未确认。可以解释它们控制了哪个分支,却不应根据编号、位置或视觉结果补成具体任务名称。任务种类、Scene 允许类别与 Location 类别虽然都使用整数,仍是三个不同的输入域。
Region 配方与 profile 分别抽选
这里先选 Region 配方,再从已选配方内部选 profile。两次分别用同一个 Seed 初始化随机状态,各自按总票数取模;它们不连续消费同一条状态序列。
Planet 命中的候选集合 23 在外部映射中称为 region_moor_arid。它保存七个配方索引 [16,36,19,46,68,34,63],对应权重为 [0,1,1,0,0,1,1]。这些值需要先结合 Scene 的类别限制判断资格,才能进入实际抽选。

图 11|配置与抽选示意:区域候选资格与抽选。展示焦灼荒原请求选中配方 36、profile 0 的输入和票数。
每份配方包含八个 profile。选择前还有兼容归一化:若八项权重和为零,或旧标志、旧类别、旧权重不满足当前格式条件,就把各档权重清零,将 profile 0 的权重设为 1,并使用旧类别。资格判断读取的是这个处理后的投影,不能只看原始八档表中的某一行。
配方 36 的旧标志为 0、旧类别为 1、旧权重为 1,八档权重和为 1.8,因此没有触发重写。当前选择器实现了资格判断所需的兼容投影,但初始化入口尚未认证选中旧格式后整份 profile 负载的复制重写;若最终选中的配方需要这种重写,入口会拒绝。它能排除不合格候选,不代表所有旧格式都已经能继续生成。本例选中配方 36,不进入这个未集成分支。配方 34、63 的兼容投影类别分别为 3、5,不符合 Scene 的 [1,2]。另外三项 16、46、68 即使符合类别,集合权重也是零,不获得普通抽选票数。最后参与本例普通抽选的是 36 和 19。
权重先按 float32 乘以 1000,再转换为无符号整数票数。这里两个配方各有 1000 票,总计 2000。随机状态按下式计算,乘加保留 uint64 溢出语义:
state = uint64(seed) × 0x5851f42d4c957f2d + 0x14057b7ef767814f
target = state % totalTickets
本例状态为 0xb1509b2349729f98,对 2000 取模得到 520,选中配方 36。内部遍历跳过零票项,累计正票数,首次满足累计值大于等于 target 时返回。复现时应保留这个边界条件,而不是换成另一种常见的区间写法。
接下来从配方 36 的合格 profile 中再选一次。profile 0 为类别 1、权重 1;profile 1 为类别 2、权重 0.8,分别得到 1000 和 800 票。这两次选择各自从相同 Seed 初始化状态,不是连续消费同一个随机状态。 因而第二次仍使用上面的 state,对 1800 取模得到 720,选中 profile 0。
七十五份配方不是七十五种独立地貌

图 12|静态配置关系:配方与候选集合的引用关系。静态配方、候选集合与显示家族名称是不同层次。
静态表共 75 份配方,每份 3296 字节。配方 36 是从零计数的第 37 份,外部映射名为 region_moor_mountain;名称映射不是记录内直接保存的字符串。
34 个候选集合可以共享配方。现有汇总按 13 个家族标签归类,共引用 46 个不同配方,其中包含零权重引用;另外 29 份没有出现在这批集合引用中。未被引用不等于已经废弃,75 份记录也不等于 75 个星球或 75 种互不相关的生态。
到这里,两次抽选只确定了配方 36 与 profile 0,区域高度和地点资源尚未生成。集合权重已经完成配方选择,profile 权重已经完成档位选择;接下来读取的标签、地点类别权重和高度组权重,各自控制新的步骤。图 13 继续展开 profile 在选中后怎样被使用。
profile 怎样约束地点资源

图 13|配置示意:选中的 profile 继续提供标签和地点类别权重。地点类别分配还需要任务种类、配额和任务槽可用性,不能简化成逐任务按权重抽选。
profile 除了类别与抽选权重,还保存标签约束和地点类别权重。本例 profile 0 的标签槽为 [0,0,0,0],解析在第一个 0 结束,得到零标签掩码;它不是四个名为 0 的标签。标签的全部匹配、任意匹配和排除条件仍分别执行,零掩码不应被概括成整个 profile 无条件通过。
profile 0 对地点类别 1 至 6 的权重为 [1,0,0,0,0,0];profile 1 则为 [1,1,0,0,0,0]。这张表参与地点资格与类别处理,与前面 Scene 允许的 profile 类别 [1,2] 不是同一张表。任务种类决定是否进行类别分配,选中的 profile 再提供分配所用权重。

图 14|条件规则示意:兼容处理、特殊组和覆盖规则分别说明。配方 36 不触发旧格式重写;另外两列解释条件路径,不作为本局特殊规则命中记录。
图 14 另列出三类条件规则。兼容资格投影可以参与筛选,但选中旧格式后的完整负载重写尚未集成;特殊组只作用于具有相应标识的区域;覆盖匹配得到记录后,还需完整上下文才能应用。是否执行这些规则,还需检查本次请求的条件和调用记录。
地点类别权重怎样进入实际分配
profile 中的类别权重不是让每个任务独立按比例抽一个类别。当前目标类别分配器还读取各类别配额,以及每个任务槽已经筛出的可用类别。建立可用性时,候选资源先经过阵营、难度、模板、标签与列表约束;只有存在合格候选,任务槽才把相应类别标为可用。
分配器先产生类别遍历顺序,跳过权重不大于零的类别,再按配额尝试分配。一个任务槽必须同时满足:种类不大于 1、尚未分配 target、当前类别可用。若还有一个只支持该类别的未分配槽,则暂不把这个类别分给拥有多种选择的槽。这样能避免先用完唯一可行的分配位置。
例如,教学条件下槽 A 只支持类别 1,槽 B 支持类别 1、2,类别 1 和 2 的配额各为一。如果遍历先处理类别 1,A 的选择比 B 更受限制,分配器优先保留 A 的可行性;随后 B 仍可用于类别 2。这是配额与可用性的约束,不是把 B 的抽选概率简单改为 50%。本例 profile 0 的类别 2 权重为零,则这个教学条件并不成立,不能直接套成当前请求的实际分配结果。
同一组权重在不同步骤中用法不同。目标类别分配器只检查权重是否为正,据此决定要不要处理该类别;后面的候选资源选择还可能使用权重大小。核对实现时,需要分别检查这两个读取位置。
得到 target 后,任务槽只是有了类别目标,后面还要选择资源、提出位置、检查并摆放。类别分配成功时地点尚未生成;跳过类别分配的任务也可能继续执行资源选择和摆放。
Region 配方怎样成为连续高度
配方 36 进入高度计算后,先为区域选择初始高度组,再按低组距离调整分组;地点和区域边界提供来源,来源混合形成角点高度;类型对应的 Zone 曲线与噪声继续计算细分点高度。图 15 标出这些步骤各自读取哪些参数。下面按这个计算顺序解释数值,最终再与三角网和阶段截图对应。

图 15|机制示意:高度计算的读取关系。区域分组、类型和 Zone、基础来源及细分高度各自保留输出;带高度几何随后进入栅格化。
初始高度组使用四项权重 0.4、1.25、1、0,总和 2.65。通常先取得空间噪声样本;当随机值小于阈值 0.1 时,才改用随机取样。不能把阈值写成每个区域都会附加 10% 的高度扰动。
用一个教学样本说明分组:将样本归一到 [0,1] 后取 0.5,乘总权重得到 1.325。累计权重在组 1 后为 0.4,在组 2 后为 1.65,因此命中组 2。这不是本 Seed 某个实际区域的追踪记录,而是用真实配置值解释选择规则。
初始高度组的随机状态怎样变化
基础分组先检查特殊规则。如果该区域对应的特殊项启用强制标志,就直接写组 1,不进入普通取样。在普通分支中,程序先推进一次阶段随机状态,得到 draw;只有 draw 小于 0.1 时,再推进一次并把第二个样本作为 value。其余情况从区域中心位置读取空间噪声,并映射到 [0,1]。
因此空间噪声分支仍消耗第一次随机数,强制分组分支则不经过这段普通抽样。若重写时只在“需要随机样本”时调用随机函数,后面区域读到的序列就会发生偏移。基础分组和类型选择还各有按阶段初始化的随机状态,也不能接成任意共享的一串抽样。
基础分组用累计值严格大于 threshold 作为命中条件。前面 Region 票数抽选使用的是累计值大于等于 target;两个阶段的边界比较不同。教学样本 0.5 不处于边界,两种写法都会得到相同分组,但用它验收实现,检查不出这个差别。
按低组距离调整高度分组
初始噪声将区域分成组 1 至 4,此时只有组号,还没有地面高度。接下来根据区域到低组的距离调整分组,再用调整后的组号选择类型和曲线。
低组距离调整随后以组 1 为锚点;没有组 1 时改用组 2。这项回退只属于距离调整函数;后续地点角点赋高的当前实现仍要求存在组 1,没有低组锚点时会拒绝,不能把前一步的回退推广为整链都支持无组 1 的输入。计算到最近锚点的中心距离,减去间距后截为非负数,再以配置上限与本次最大距离的较小值作为 cap。这个阶段只读取组 2、3、4 的权重,总和为 2.25,组 1 的 0.4 不参与这里的累计。
d = max(nearestAnchorDistance − spacing, 0)
cap = min(configuredCap, maximumDistance)
target = min(d / cap, 1) × (weight2 + weight3 + weight4)
这段表达式省略了实现中的接近零判断。以 cap=425、d=300 为教学条件,target 约为 1.588;它超过组 2 的累计值 1.25,进入组 3。这里 cap=425 是配置上限实际生效的条件示例,并不是声明当前地图的最大距离必然大于 425。

图 16|配置与教学示意:七项分组与距离参数。配置值来自配方 36;取样值 0.5 与距离 300 是教学条件,不代表本局具体区域。
前面分组演算使用四组权重,距离调整只使用组 2~4 的权重;其余参数在来源与细分阶段继续使用。下面保留完整十四项字段,供实现时逐项回查。偏移以这份 3296 字节记录的起点为零;英文名按已确认的读取用途命名,不表示找到了原版符号。图中分组展示读取关系,表中保留逐项值。噪声频率还经过内部缩放,距离字段也需要结合调用方坐标,因此不直接为每项输入追加“米”或“赫兹”。
| 偏移 | 参数用途及描述名 | 配置值 | 主要消费者 |
|---|---|---|---|
| 0x18 | 高度组 1 权重 heightGroup1Weight |
0.4 | 初始分组、细分噪声阈值 |
| 0x14 | 高度组 2 权重 heightGroup2Weight |
1.25 | 初始分组、低组距离调整、细分噪声阈值 |
| 0x10 | 高度组 3 权重 heightGroup3Weight |
1 | 同上 |
| 0x0C | 高度组 4 权重 heightGroup4Weight |
0 | 同上;本值不增加正权重 |
| 0x1C | 基础噪声频率输入 baseNoiseFrequencyInput |
5 | 初始分组、类型选择、细分主噪声 |
| 0x20 | 改用随机值的阈值 randomInsteadOfNoiseThreshold |
0.1 | 初始分组、类型选择的取样分支 |
| 0x24 | 高度幅度输入 heightAmplitudeInput |
75 | 地点来源、基础角点来源 |
| 0x28 | 另一层噪声频率输入 alternateNoiseFrequencyInput |
0.5 | 细分第二层噪声 |
| 0x2C | 混合噪声幅度 subdivisionMixedNoiseAmplitude |
7.5 | 细分混合项 |
| 0x30 | 基础噪声幅度 subdivisionBaseNoiseAmplitude |
0 | 细分主噪声独立项 |
| 0x34 | 径向高度项幅度 radialHeightAmplitude |
−500 | 径向候选及最大值比较 |
| 0x38 | 高度过渡宽度 heightWidth |
550 | 来源距离归一化 |
| 0x3C | 低组距离上限 lowGroupDistanceCap |
425 | 低组距离调整 |
| 0x40 | 高度边缘参数 heightMargin |
4 | 来源距离扣减 |

图 17|配置与教学示意:另外七项高度参数:来源贡献、细分噪声和径向候选。N、v、alt、mask、h 与 w 为计算变量,示例贡献尚未完成后续来源混合。
来源与角点计算先从边界距离中扣除 margin=4,再由 width=550 控制归一化距离,并乘以幅度输入 75 及调用方归一化因子。单个来源贡献可写成:
d = max(rawBoundaryDistance − 4, 0)
sourceContribution = min(d / 550, 1) × (75 / N)
其中 N 是调用方传入的归一化因子。教学条件 d=275 时,贡献是 37.5/N;后续还会与地点来源按权重混合,不能直接把它写成最终地面高度 37.5 米。37.5/N 只完成了距离到来源贡献这一步,后面还要计算混合权重。
细分高度中的噪声也不只有一个幅度。主频率输入为 5,另一频率输入为 0.5;主噪声映射到 [0,1] 后,先以高度组权重构造阈值,再产生 v:
k = (0.4 / 2.65) × 1.15
denominator = clamp(1 − k, 0.001, 1)
v = clamp((n − k) / denominator, 0, 1)
noiseContribution = (v × 0 + v × alt × 7.5) × (1 − mask)
本例基础噪声幅度为 0,但混合幅度为 7.5,仍可产生局部细节。mask 来自底图采样,控制这部分贡献,再与曲线结果合成。将“基础噪声幅度为零”解释为“整个阶段没有噪声”,会遗漏第二项。
这份底图也有明确来源:当前入口先将自产底图三角形栅格化,保留 512² 掩码、256²/128² 的最大值阶段和最终 128² R8 场,再将 R8 样本除以 255 后交给细分高度。它是本次布局的中间产物,不是参考目标高度,也不是后面最终 4096² 的平滑高度。旧诊断接口可以显式替换这个输入,但本文四请求端到端验证没有使用该外部替换参数。
径向幅度 −500 也不是从全图统一减去 500。代码先生成 candidate = w×(−500)+(1−w)×h,再返回 max(h,candidate)。它能否改变一个点,取决于该点原高度和径向权重;不能只根据参数的正负判断输出一定抬高或降低。
地点来源怎样参与角点高度
前面的 sourceContribution 只解释单项距离如何映射为高度。实际地点来源还先做空间查询:使用地点 XY 查到所属区域;被排除的资源选择,或者归属高度组 1 的地点,来源高度为零。其他地点查找最近的低组中心,再计算到该低组多边形边界的最近距离,扣除 margin 后进入幅度计算。这不是简单地用“地点到最近低组中心的距离”直接赋高。
地点内部的范围也参与后续权重。对于角点 P,计算 P 到地点中心的距离,再减去地点范围 extent,得到 t。当前入口传入 radius=512、gain=10、nearDistance=96;这些是这条已检查调用的参数,不是所有生态的通用常量。
t = distance(P, locationCenter) − extent
baseWeight = 1000 当 t < 0
baseWeight = (1 − t / radius)^6 当 0 ≤ t < radius
baseWeight = 0 当 t ≥ radius
weight = baseWeight
当 t < nearDistance:
weight *= (1 − t / nearDistance) × gain
六次幂让地点外部的影响迅速衰减。教学条件 t=256、radius=512 时,基础权重是 0.5^6=1/64;t=384 时只有 0.25^6=1/4096。处于地点范围内则使用另一条较强权重分支,不能把六次幂公式向内无限延长。
角点还保留自己的来源贡献和权重。若角点归属低组,直接写零;否则将所有有效地点来源与自身来源共同归一:
cornerHeight = (sum(locationWeight × locationSourceHeight)
+ ownWeight × ownSourceHeight)
/ (sum(locationWeight) + ownWeight)
例如,教学条件下两个地点的来源高度为 12、20,权重为 2、1;角点自身来源为 8、权重为 1,混合结果是 (2×12+1×20+1×8)/4=13。这些来源和权重是为解释公式选取的教学值,不是本局某个角点的运行记录。
零来源也可能参与分母。当前实现对部分无效选择写入零来源高度,但保留后续距离权重处理;如果迁移时将这些项整条删除,分母变小,其余来源的影响就会被放大。仅核对非零来源的坐标和高度,仍可能遗漏这种差异。
这个阶段输出的是角点高度数组、地点来源高度和自有距离等计算产物。它不决定建筑最终 Z,也不等于后面的 Location 支撑目标。地点先以 XY 和资源关系参与基础地形,再查询成形后的地面计算支撑,最后写入局部高度资产;同一个实例在三个阶段承担不同职责。
类型表把区域分类连接到后续曲线

图 18|配置与教学示意:用“普通区域已属于高度组 2”的教学条件演示筛选与选择。候选值来自配方 36,样本 0.5 为教学取值;后续 Zone 和合并分别使用不同数据。
配方类型表有 16 槽,每槽 68 字节,目前解释了其中六个字段,其余 44 字节继续保留。一个槽不是一块实际区域;它描述类型候选及后续使用的 Zone、曲线倍率、权重和高度组约束。
普通抽选只使用满足高度组条件且权重大于零的记录。下表仍保留完整 16 槽,供实现时核对;零权重项和终止后的槽位不会因此进入本局普通抽选。
| 槽 | 偏移 | 类型键 | Zone | 曲线倍率 | 类型权重 | 高度组条件 | 合并概率 |
|---|---|---|---|---|---|---|---|
| 0 | 0x238 | 18 | 1 | 1 | 0 | 1 | 1 |
| 1 | 0x27C | 19 | 2 | 1 | 0 | 0 | 1 |
| 2 | 0x2C0 | 20 | 3 | 1 | 0 | 1 | 1 |
| 3 | 0x304 | 24 | 29 | 1 | 0 | 1 | 1 |
| 4 | 0x348 | 25 | 27 | 1 | 0 | 1 | 1 |
| 5 | 0x38C | 26 | 28 | 1 | 0 | 1 | 1 |
| 6 | 0x3D0 | 8 | 66 | 1 | 0.5 | 1 | 1 |
| 7 | 0x414 | 7 | 65 | 1 | 0.2 | 2 | 1 |
| 8 | 0x458 | 11 | 63 | 1 | 0.15 | 2 | 1 |
| 9 | 0x49C | 9 | 66 | 1 | 1 | 2 | 1 |
| 10 | 0x4E0 | 12 | 65 | 1 | 0.5 | 3 | 1 |
| 11 | 0x524 | 10 | 66 | 1 | 1 | 3 | 1 |
| 12 | 0x568 | 4 | 67 | 1 | 1 | 4 | 1 |
| 13 | 0x5AC | 0 | 0 | 1 | 1 | 0 | 1 |
| 14 | 0x5F0 | 0 | 0 | 1 | 1 | 0 | 1 |
| 15 | 0x634 | 0 | 0 | 1 | 1 | 0 | 1 |
例如槽 6 保存类型 8、Zone 66、曲线倍率 1、类型权重 0.5、要求高度组 1、合并概率 1。只有先满足条件再被选中,这条记录才影响实际区域。合并概率 1 取消的是概率拒绝,类型相同与边关系等条件仍然存在,不能说所有符合高度组的区域都会合并。
普通候选解析遇到类型键 0 时结束,本例在槽 13 停止;后面仍保存的槽 14、15 不因此成为两个实际区域。零类型权重项不参与普通加权抽选。特殊规则和覆盖表又有自己的条件与优先级,不能因为记录存在,就断言这次地图已执行了它们。
用配方 36 演算类型筛选
对普通区域,类型选择先读取已经计算的高度组,只保留要求该高度组或使用通配条件的正权重类型。配方 36 的普通正权重候选可整理如下,数值 0.2、0.15 为便于阅读的十进制显示,运行仍使用原始 float32:
| 要求高度组 | 类型 | Zone | 类型权重 | 曲线倍率 | 合并概率 |
|---|---|---|---|---|---|
| 1 | 8 | 66 | 0.5 | 1 | 1 |
| 2 | 7 | 65 | 0.2 | 1 | 1 |
| 2 | 11 | 63 | 0.15 | 1 | 1 |
| 2 | 9 | 66 | 1 | 1 | 1 |
| 3 | 12 | 65 | 0.5 | 1 | 1 |
| 3 | 10 | 66 | 1 | 1 | 1 |
| 4 | 4 | 67 | 1 | 1 | 1 |
以高度组 2 的普通区域为例,筛选后只留下类型 7、11、9,总权重约 1.35;高度组 1 的类型 8 此时不能加入分母。选一个归一化教学样本 value=0.5,缩放为约 0.675,依次减去 0.2 和 0.15 后仍未命中,最终落在类型 9,取得 Zone 66 及对应参数。
这个例子解释了“分类先限制候选,再使用权重”的顺序。若先从所有类型里抽一次,再发现高度组不符而改成默认类型,抽选分布和随机序列都可能变化。特殊区域则可以通过特殊表直接取得类型;已有类型的区域也不会按普通分支重新抽一次。
普通分支的样本来源沿用噪声或随机取样的条件结构,但有自己的阶段状态。相同坐标和相同主频率让空间噪声具有相关性,并不意味着两个阶段实际使用了同一个随机样本。
类型选定后,同类型的相邻区域才能进入合并检查。实现读取边标志,并在相应检查位置推进随机状态,即使合并概率为 1,也不能直接删掉随机调用。概率条件、阻隔标志和已经形成的分组范围共同影响结果。合并产物保存每个区域对应的成员范围,后续边界处理和自然 Stamp 遍历都要读取这个分组结构。
当前类型选择器对尚未集成的邻接排除条件会拒绝输入,不能因为普通表已经能算出类型,就宣称每种配置的邻接规则均已实现。
Zone 曲线怎样变成细分点高度
类型表中的 Zone 是后续曲线查找所用的引用,不是高度值。例如 Zone 66 不表示 66 米,它需要查询相应的静态曲线,再以当前细分点到关联边界采样的距离确定曲线位置。
距离计算先对关联采样点求平方距离,减去边宽平方,截为非负数,再取最小值和平方根。当前内核将距离乘以 1/64 并钳位到 [0,1],据此定位曲线段及段内比例。它并不是直接把“最近区域中心距”传进曲线。
curveHeight = curve[i]
+ (curve[i+1] − curve[i]) × fraction × multiplier
倍率只乘插值增量,不整体缩放曲线基值。教学条件下曲线端点为 10、18,fraction=0.25,倍率为 0.5,结果为 10+(18−10)×0.25×0.5=11;若将整段插值结果乘以 0.5,会得到 6,改变的已不只是增量。配方 36 表中倍率为 1,无法暴露这个错误,因此教学例特意采用 0.5,不能把它记成配方真实值。
细分高度再叠加前文的受 mask 控制噪声,执行径向候选比较,输出内部点和角点高度。随后三角网读取这些结果进行插值。Region 参数的作用在这里才逐步转换为可绘制高度;类型号、Zone 号和高度组号不会直接写成像素值。
自然资源权重还会被当前位置重新计算
区域类型提供 Zone 引用,Zone 再提供可遍历的 StampGroup;每个组保存自己的摆放参数、候选项和重叠策略。当前独立输入保留 96 字节组记录与 104 字节候选记录。记录长度已经确定,但部分字段的用途仍未解释。遍历还结合合并后的区域成员及调度配额,因此“Zone 内有三个组”不等于只生成三个实例。
组负责组织摆放过程,候选项负责描述可选资源及其资格,接受的父实例才保存本局的资源引用、位置和朝向。父实例随后经静态 Level/Unit 数据展开为高度绘制参数。同一个候选可以被接受多次,同一个父资源也可能展开多个局部单元;组数、候选数、父实例数和绘制项数需要分别统计。
配方与区域决定使用哪些资源集合,但集合内一项资源的静态权重不一定就是本次抽选权重。自然 Stamp 选择器还读取当前候选位置的坡度、高度、外侧距离及分类,通过启用的条件逐项调整权重。
每项范围使用四个值 [a,b,c,d]。低于 a 或高于 d 时权重为零;a 到 b 之间线性增加;b 到 c 保持;c 到 d 之间线性衰减。只有对应 gate 启用时,才读取这项范围参与计算,不能因为记录中保存了范围,就默认全部生效。
教学条件下静态权重为 2,某项启用范围为 [0,10,20,30]。输入为 5 时权重变为 1;输入为 15 时保留 2;输入为 25 时又变为 1。若另一项启用条件再使权重减半,最终权重继续乘以 0.5,而不是重新恢复静态值。
分类上下界还可先将候选直接排除。调整完成后,程序才累加有效权重并抽选;总权重还需超过“候选数乘以 2^-23”的阈值,不能简单改成大于零。这里的权重计算决定选哪份资源,后面的区域、碰撞与任务边界检查决定该实例能否接受,两者分属不同步骤。
这也解释了同一资源集合为什么在两个位置上可能选出不同内容:当前位置改变参与抽选的有效权重,实例检查又可能触发重试。因此,估算实例数量还需要追踪候选生成、权重调整和重试过程,仅看资源表中的原始权重不够。
上面说明了配方中的权重、距离、类型和 Zone 如何参与选择与高度计算。具体区域选中了哪项,仍以运行记录为准;特殊覆盖在完整调用上下文中的应用也还没有全部验收。下一节从布局和三角网开始,看这些计算在高度场中的结果。
从实例布局到连续像素高度
先用平原汇报页观察几何和高度数组的交接。这组请求的 Seed 为 2763691705,包含 39 个 Location、1174 个自然 Stamp 父实例,与前面的焦灼荒原案例分开。图 19 至图 22 采用已归档的自产 C++/HLSL 阶段数据;随后图 23、图 24 回到配方 36 的同一次 demo 作业。本文整理期间没有重新执行生成器。

图 19|布局数据图:平原实际父级布局,与前面的荒原配置案例分开。
地点与自然 Stamp 先形成父级位置和朝向。父实例再经静态资源中的 Level、Unit 和局部变换展开成绘制输入。图中的父级布局标出了资源的位置和朝向。父资源内部的纹理、网格和植被还要继续展开,各自的世界变换需要另行核对。

图 20|阶段数据绘图:实际自产阶段数据,三角网数量对应显示裁片。
这页从带高度的三角网继续展示栅格化和平滑。显示裁片包含 39,837 个三角形,但这个数不是完整地图的总三角形数。逐像素插值将几何变为高度数组,平滑改变连续地面的起伏。三角网、栅格场和平滑场各自保留,才能确认差异首先发生在赋高、覆盖还是滤波。
阶段高度图取相同的 900 米方形范围,使用共同的高度色带、方向和光照,色带范围约为 −42.018 至 68.225 米。这些顶视 relief 图由高度数据生成,不是 Web 三维截图。它们适合观察阶段形状,不能以画面中的颜色块数量代替原始 float32 像素统计。

图 21|阶段数据绘图:阶段输出与查询所读取的高度版本分别表达。
强制平整与 Location 支撑继续读取前面的高度,但“用于查询的基础高度”和“当前正在改写的 GPU 高度”需要分别保存。平台写入已经发生,不代表后续所有查询都应该改读平台结果。后文的离子赤原案例会具体说明这项阶段依赖如何产生误差。

图 22|阶段数据绘图:三幅真实阶段场使用共同裁切、色标与光照。
最后两次局部写入分别来自自然 Stamp 和 Location 静态高度资产。图中依次保留支撑后、自然 Stamp 后和 Location 高度纹理后的真实地面;统一范围便于对照局部形状的变化。两者使用不同实例来源和资源关系,也不能以最终高度近似代替实例接受顺序与绘制参数的核对。
平原图展示了从布局到局部资产写入的过程。下面回到配方 36,查看同一次生成的六阶段截图,再逐项分析细分、随机状态和数据绑定中的错误。
配方 36 在 demo 中的六阶段结果
回到焦灼荒原请求:Seed 为 2431905037,选中 Region 36、profile 0,生成 66 条 Location 记录和 1501 个自然 Stamp 父实例。Location 记录包含占位记录,因此 66 不能直接当作建筑数量;自然 Stamp 父实例数也不能当作山丘、树木或岩石数量。下列两页来自这一次作业,镜头、显示范围、色标、光照和高度倍率保持一致。

图 23|运行截图:从左向右为栅格化、平滑和平台阶段。本请求没有平台写入,第二、第三阶段的完整高度数组相同。
左图以带高度的三角网为输入,通过像素插值生成第一份高度数组,仍能看到局部三角形轮廓。中图对这份数组执行平滑,得到供后续查询使用的 Hbase;大尺度起伏保留,局部过渡更连续。右图是平台阶段的输出 Hplatform。本请求没有平台写入,因此它与 Hbase 完全相同;阶段执行到这里,并不要求地面一定发生变化。

图 24|运行截图:从左向右为 Location 支撑、自然 Stamp 和 Location 资源高度绘制,沿用图 23 的请求与显示条件。
图 24 的左图以 Hplatform 为底图,按 Location 支撑目标与过渡范围调整地面。目标计算仍查询图 23 的 Hbase,两份高度的职责没有因为本请求数值相同而合并。中图再将已接受的自然 Stamp 父实例展开成静态高度绘制,加入较密集的局部起伏;右图在此基础上写入 Location 资源的具体轮廓,形成这次内圈生成的最后一份阶段高度。
这两页展示了三类产物:配方与布局决定带高度的几何,栅格化和平滑形成连续底图,支撑及静态资产绘制继续调整局部地面。若最终结果出现差异,应先比较相邻阶段数组,找到差异首次出现的位置,再检查该阶段读取的参数、资源和执行顺序。后面的实例局部图沿用这六份数组,用更小的范围观察支撑与自然绘制的净变化。
这些截图展示内圈高度,不包含外围 Vista,也没有用地表材质、植被或岩石遮盖高度变化。Web 预览将 4096² 数组按 8×8 平均缩为 512²,再生成显示网格。预览清晰度与原始高度精度是两回事;后面的局部图直接读取完整阶段数组。
自然 Stamp 父实例与阶段高度的局部对照
沿用配方 36 的焦灼荒原作业,父实例 #496 使用资源 17720cadbae59a9e,位置为 (1257.093, 1109.809) 米,朝向约为 5.35195 rad。接受记录还保留 root 49、group 0、member 254 和 grid 10;父记录的区域类别字段为 66。这些字段用于追溯候选来自哪一轮区域遍历,不代表该资源只有一个绘制形状。图中箭头沿父矩阵的局部 +X 方向,按该矩阵的角度约定转换。

图 25|局部数据绘图:同作业的 207×102 米局部范围,直接绘自 4096² 阶段高度。橙框是父实例的支撑包围框,白箭头是局部 +X 方向。差值图比较整个自然绘制阶段,不是单独绘制 #496 的结果。
图 25 放大图 24 中自然绘制前后的同一处位置:左上是支撑地面,右上是全部自然实例写入后的地面,左下保留后续 Location 高度纹理结果。右下按“自然绘制后减自然绘制前”计算净变化,橙色表示抬高,蓝色表示降低。父记录提供资源、位置和朝向,静态资产展开提供具体局部形状;差值图则说明这一阶段实际改动了哪些地面。
这里没有根据最终山丘的外观反推父实例。橙框直接来自同一条 304 字节父记录中的支撑 AABB。它用于摆放检查,不能当作高度纹理的精确外边缘;图中也包含其他父实例,因此不能把框内全部起伏或差值峰值都归因于 #496。若要核对单个实例的独立贡献,还需要另行保留逐绘制输出。
Location 的辅助查询、支撑目标与局部结果
再看同一作业的 Location #1,它位于 (901.870, 1070.721) 米,资源键为 384cd0efc44cee6b。静态资源提供三个支撑圆,同作业的支撑输出记录提供它们变换后的世界位置与半径。下面按这些记录标出实际范围,四幅图使用同一裁片与 XY 比例。

图 26|局部数据绘图:同作业的 276×136 米局部范围。橙线为 Location #1 的三个支撑圆,白十字为父实例位置。支撑阶段净变化包含裁片内所有支撑的共同作用。
这次圆周查询保留 196 个采样点、剔除 116 个采样点,报告的高度包络为 24.8096~32.5587 米。计算得到的辅助高度约为 29.1874 米,经过家族协调与目标处理后,支撑目标约为 27.2404 米。两个值属于不同阶段,不能用同一个“地点高度”代称,也不能当作建筑最终 Z。
图 26 的左上与右上对应平台后、支撑后两份数组,显示三个支撑圆附近及过渡范围的地面调整。右下按同一位置相减,标出这次支撑阶段的抬高与降低。左下是后续自然 Stamp 和 Location 资源绘制完成的地面。辅助高度和支撑目标用于计算调整参数;前后差值显示地面实际改动的位置;最终阶段还包含后续写入的静态轮廓。
图 25、图 26 都直接读取本次作业的编码前浮点数组,使用统一的 0~83 米高度色标及相同方向的显示光照,差值图另用各自注明的对称量程。坐标以 2048 米计算域左下角为原点;读取原生纹理行序时只转换一次 Y 方向。它们是数据绘图,六阶段总览才是浏览器截图;两者都没有修改源高度。这些图沿用已经完成的作业,不代表重新执行了参考对照。
多地貌验证暴露出的阶段一致性问题
四套请求没有启用完全相同的分支。离子赤原的平台写入让查询版本差异产生实际高度变化;沙丘暴露行序和驻留绑定问题;焦灼荒原暴露支撑执行层序问题。下面按问题所在的接口组织案例,分别列出修复前后的数值、复现条件,以及实际完成的对照。
| 案例 | 地貌或验证对象 | 检查的阶段关系 |
|---|---|---|
| 细分间距 | 离子赤原(Ionic Crimson) | 配置间距进入不同采样分支时的缩放 |
| 任务范围版本 | 离子赤原(Ionic Crimson) | 初始化范围与地点布局后范围 |
| 查询高度版本 | 离子赤原(Ionic Crimson) | Hbase 查询与 Hplatform 绘制底图 |
| 二维行序 | 沙丘(Desert Dune) | CPU 数组发布与纹理查询约定 |
| 记录与执行顺序 | 焦灼荒原(Scorched Moor) | CPU self 编号与 GPU 层排序 |
| 驻留绑定 | 沙丘(Desert Dune) | mip 链、尺寸常量与叶片几何 |
| 异步查询发布 | 四地貌离线设备与状态机测试 | GPU 完成、行复制、描述符发布和所有权 |
下面各节按异常、原因和修复结果展开。最后一节检查的是离线 GPU 完成状态和查询发布状态机,游戏现场的并发行为仍未全部验证。
细分间距必须保留分支含义
基础高度的计算需要区域内部点、角点和连接关系。若细分间距错误,变化会沿着点数、拓扑、区域归属一路传到高度与实例摆放。此时调整最终高度纹理无法恢复上游已经改变的空间结构。
离子赤原首先暴露了规则网格分支的问题。配置中的间距进入细分阶段时,规则网格分支除以 2,另一分支除以 4。独立入口此前统一使用除以 4,导致该案例的采样过密,随后点编号与拓扑也发生变化。
用描述性变量表示,这段配置处理为:
effectiveSpacing = configuredSpacing / (regularGrid ? 2 : 4)
这个规则适用于本次恢复的细分阶段,不能推广成所有地图采样的通用缩放。间距进入不同阶段时,应分别核对调用方是否已做转换,避免再次相除。
规则网格的数量由 float32 尺寸与间距决定:
nx = ceil(float32(width / spacing))
ny = ceil(float32(height / spacing))
x = (i − floor(nx / 2)) × spacing + spacing / 2 + centerX
y = (j − floor(ny / 2)) × spacing + spacing / 2 + centerY
这组公式对应当前规则网格内核。实现按行输出,内层递增 X;半计数采用整数除法。奇数与偶数数量下的点列位置因此需要分别检查。规则网格分支不消耗随机数,随机采样分支则有独立的随机推进过程。
例如,一个以零为中心、宽度 80 米、有效间距 20 米的教学域得到 nx=4。四列 X 坐标依次为 −30、−10、10、30 米,首个点相对边界向内半个间距。若配置进入细分前少做了一次间距转换,列数和位置都会改变;后面再把坐标整体乘一个比例,也无法恢复原来的候选数量和编号。
修复间距后,离子赤原得到 11,413 个内部点和 22,541 个角点,共 33,954 个高度值。随后完整细分对照还检查了区域、边、候选、归属和空间桶:395 个区域、35,133 条边及相关连接信息通过对照。单独核对高度值还不够,因为错误的连接关系有时能在局部得到相近高度,却会改变后续搜索。
规则网格内核另外使用了 108 组合成输入、25,251 个点检查边界情况。这项测试说明内核在这些输入上符合参考规则,与离子赤原整套细分验收属于不同层级,数量不应合并成“验证了多少张地图”。
细分修复后,Stamp 数量仍未一致,说明剩余差异还出在其他环节。细分对照通过后,仍需继续核对 Stamp 循环,不能把两项验收合在一起。
任务范围应读取地点布局后的版本
自然 Stamp 的完整循环包含候选生成、资源选择、位置和朝向计算、碰撞检查、任务范围判断、重试与接受。某个候选失败后,程序怎样继续推进,也会影响后面的随机状态。因此,“实例总数接近”不足以判断循环是否还原。
一个候选即使通过碰撞检查,也可能在任务范围检查被拒绝。反过来,任务范围允许的位置,还可能因已有实例和边界约束失败。两种拒绝来自不同数据,不能在诊断中合并为一个“摆放失败”。
离子赤原曾生成 1,006 条父记录,参考为 1,012 条。首个遗漏候选的位置与参考相同,说明最初的坐标生成已经对上;进一步检查发现,该候选四次通过碰撞检查,却均被任务范围判断拒绝。问题转向了范围参数的来源。
独立入口使用了初始化阶段的任务范围比例,约为 0.38。地点布局完成后,上游会根据已布置的 Location 扩展这一范围;本次自产后继值约为 0.3930511。自然循环应读取后者,入口却仍传入初始化值。修复参数交接后,缺少的六条记录及随后发生变化的随机序列一起恢复。
这里没有按参考数量补六个实例。修复的是循环所读的阶段参数,之后从初始请求重新执行。位置、资源、接受顺序与随机状态随控制流重新生成。若只在末尾补足数量,后续记录仍会保留错误序列。
四案例最终对照使用完整的 304 字节自然 Stamp 父记录。为排除不同进程地址的影响,仅将两个地址指针槽规范化为零;坐标、角度、资源键、区域归属及其他字段保持原值。每套请求还独立生成两次,分别检查重复结果和参考结果。
| 案例 | 自然 Stamp 父记录 | 区域内部点 | 角点 | 本轮对照 |
|---|---|---|---|---|
| 平原(Plain) | 1,174 | 11,928 | 23,606 | 规范化完整记录一致,细分高度位模式一致 |
| 沙丘(Desert Dune) | 1,184 | 11,854 | 23,459 | 同上 |
| 离子赤原(Ionic Crimson) | 1,012 | 11,413 | 22,541 | 同上 |
| 焦灼荒原(Scorched Moor) | 1,501 | 11,798 | 23,391 | 同上 |
这些数量统计父记录,不是可见山丘数量,也不是树木或岩石数量。一份父资源可以包含多个局部高度形状和物件;多个父实例还可能在最终地形上重叠。
Location 支撑查询与绘制使用不同高度版本
Location 支撑先查询地面,再计算辅助高度、父子协调和外扩。如果查询读错高度版本,即使其他参数相同,目标也会变化。离子赤原的平台处理暴露了这个问题。
该案例有 23 处强制平整区域。平台目标按连通组计算:同一配置的相邻区域形成组,符合类别条件的外侧边界参与查询。对边界中点向外延伸 32 米,再以 32 米半径查询高度包络。组目标采用以下规则:
target = min(0.5 × low + 0.5 × high, low + limit)
low 和 high 来自有效查询包络,limit 来自区域配置。若没有有效包络,当前已恢复分支将两者置零。这个回退条件需要和目标公式一同实现;只复制公式会漏掉无有效查询的情况。
举一个教学数值:包络最低 6 米、最高 10 米、配置限幅 1 米时,中点高度为 8 米,但 low + limit 只有 7 米,最终目标取 7 米。这说明配置限幅会限制相对低侧的抬升;它不是对最终高度统一乘一个强度。这里的数字仅解释公式,与下文实测目标分开。
本案例 23 个区域形成一个连通组,13 次边界查询的位置及 23 个目标值通过对照,目标约为 7.0749998 米。随后平台写入在 GPU 高度图上改变了 802,760 个像素。这些结果分别证明目标计算与平台绘制,不代表后续 Location 支撑已经读取正确输入。
错误发生在平台之后:独立入口从平台输出重新构造 CPU 查询金字塔,再把它交给 Location 支撑。但当前恢复的参考路径保留了平台写入前的基础查询描述符。地点辅助高度继续从该描述符查询;GPU 上的当前地面则已经包含平台。
这里的错误来自高度版本绑定:Hbase 和 Hplatform 都是合法产物,却被当成同一个可覆盖的 height。查询公式本身即使正确,读取平台后的场也会改变辅助目标。用不同阶段变量保留这两份输入,接口关系就能直接检查:
baseHeight = Smooth(Rasterize(baseTriangles))
queryLevels = BuildQueryPyramid(baseHeight)
platformHeight = DrawPlatforms(baseHeight, platformTargets)
supportTarget = QuerySupport(queryLevels, location)
supportHeight = DrawSupport(platformHeight, supportTarget)
伪代码省略资源创建与异步等待,重点是两处输入没有被同名变量覆盖。queryLevels 属于保留的基础查询版本,platformHeight 属于当前绘制版本。若每一步都覆写一个 height 变量,就很难从接口上发现错误的阶段替换。
当前排序回归中,离子赤原中央 450 米内的最大误差约为 1.5011 毫米。此前修复查询高度版本的那一轮,最大误差从约 0.4278 米降至约 1.5030 毫米,超过 1 厘米的像素由 310,128 个降为零。两组毫米值属于不同后续版本;修前的 42.8 厘米峰值有明确的阶段来源,不能归因于一般浮点误差。
其余三个案例没有平台写入,平整前后的基础数组相同。这解释了同一处接线错误为何没有同时造成四套明显异常。增加地貌案例后,平台写入等不同分支才实际参与计算,原先两份数组恰好相同时隐藏的输入错误也随之暴露。
Location 支撑目标仍是高度处理的辅助量。它参与地面调整和家族影响,不能直接当作建筑最终 Z;建筑装配、地表采样和碰撞还需要自己的运行时步骤。
CPU 高度只在纹理发布时转换行序
沙丘曾出现内圈平均约 26.534 米的大差异。排查发现两处交接问题:CPU 高度发布为纹理时遗漏了行序转换,之后纹理行序进入查询金字塔时又被额外翻转。两处都处理二维数组,却不应各自凭经验添加一次翻转。
高度数组有二维含义,也有一维存储顺序。CPU 计算按照世界 +Y 方向枚举行,GPU 纹理按本管线约定的纹理 -Y 方向存行。两个数组都可能尺寸正确、数值范围合理,直接交换却会让查询位置与地形对不上。
这一约定属于当前生成链,不能解释为所有图形 API 天生要求同一种 Y 翻转。接口需要明确三件事:第零行对应地面的哪一侧,在哪一步转换,以及查询索引按哪种行序计算。
发布转换为:
texture[row, column] = cpu[N − 1 − row, column]
每个 float32 样本只移动位置,不做高度计算,不改变位模式。C++ 接口先拒绝零尺寸、样本数量不符及非有限数,再逐行复制。

图 27|教学示意:四行教学数组的发布转换。列内顺序和样本位模式保持不变;后续查询若再次翻转,就会恢复错误方向。
修复方式是把发布转换集中为一个 C++ 函数,输出明确标为纹理行序。后续步骤直接读取该定义,不再根据显示效果决定是否翻转。测试使用非对称数组,因为上下对称的地形可能掩盖整行反转;同时检查截断输入和非有限高度的拒绝行为。
坐标错误常常影响大面积,但不一定让画面完全失真。若地貌总体呈圆形,翻转后仍有山坡和低地,肉眼可能认为只是随机结果不同。数值诊断需要追踪一个世界位置在每个数组中的行列,而不能只比较两张缩略图。
记录编号与 GPU 执行顺序分别保存
焦灼荒原此前按 CPU 插入顺序直接绘制,遗漏了层排序。其支撑目标、邻接索引与许多缓冲数据已经正确,最终高度仍留下约 3.42 厘米的最大残差。仅检查参数表,可能将绘制顺序造成的差异误判为数值精度问题。
支撑数据包含圆、矩形、邻接范围和顶点中的自身编号。CPU 需要按 Location 组织记录,才能稳定引用这些表;GPU 还需要按高度绘制层执行。两种顺序服务不同用途。
当前 CPU 记录按 Location 编号升序排列,同一个 Location 内圆在矩形前,同类保持输入顺序。圆、矩形参数表仍分别保存。每条绘制记录的 self 编号对应 CPU 行,用于查找邻接范围及相关参数。
GPU 执行则按本次支撑 pass 的层号排序:矩形为 900,圆为 901,同层保持原顺序。绘制序列保存的是 CPU 行编号,不是圆参数表或矩形参数表的编号。调整执行顺序时,各参数表仍保持原来的索引对应关系。

图 28|教学示意:混合圆与矩形的教学记录。CPU 行编号稳定保留,GPU 只读取另一份执行排列。层号 900/901 属于当前支撑路径,不是通用材质优先级。
以两个 Location 为例,CPU 可以保存为“地点 A 的圆、地点 A 的矩形、地点 B 的圆、地点 B 的矩形”。GPU 则先执行两个矩形,再执行两个圆,但每次仍使用原来的 CPU 行编号读取参数。这样能同时保持记录组织和绘制层规则。
在当前接口中,CPU 行编号用来查记录,GPU 执行排列决定绘制次序。只改次序时,原编号和参数引用保持不变;如果连 CPU 行也重新排列,就要同步更新 self 与邻接跨度。这个编号只在当前缓冲及接口内有效,不能当成跨作业的永久标识。实例批处理和材质排序也要分别处理记录编号与执行次序。
恢复独立 GPU 执行排列后,重新执行支撑、自然 Stamp 和 Location 三个后续阶段,中央范围最大误差由 34.1530 毫米降为 1.3332 毫米;超过 1 厘米的像素由 4,826 个降为零,超过 1 毫米的像素由 14,493 个降为 37 个。参数缓冲保持不变,改变的是执行排列。
叠加操作依赖顺序,因此这里必须重跑完整后续链。只看支撑后某个点减少了多少,不能直接替代最终误差;后面的高度纹理可能覆盖或再次改变该点。
C++ 迁移进一步将两份排列同时输出:一份描述原始形状输入如何映射到 CPU 行,一份描述 GPU 应依次执行哪一行。迁移时还重建邻接跨度,并更新顶点里的 self 编号。缺少任意一项,都可能让排序正确而引用错误。
四案例共有 77、88、74、87 次支撑绘制。新 C++ 输出与已归档输入的缓冲和执行排列逐字节一致,重新绘制的支撑像素与已归档支撑输出逐位一致。这项回归证明排序迁移保留了已有行为,不应单独称为一次新的原版整图验收。
驻留 mip 必须与尺寸常量和几何同步
本轮沙丘中央共同范围的最大误差约为 0.5913 毫米。下面回溯这个结果对应的驻留修复,同时保留早先含 Vista 版本的整图记录;两者的比较范围仍分别标明。
沙丘修复行序后,仍有一个约 11.543 米的异常峰。逐阶段检查发现,基础高度、平台和支撑后该点都是约 49.189884 米;自然 Stamp 的第 3 次绘制将它改成约 60.733341 米,后面的 Location 和 Vista 没有改写它。由此可以把问题限定到一次确定的绘制,而非整张地图的高度基准。
高度纹理的 mip 通常被理解为精度选择。在这条链里,驻留级别还与资源尺寸、shader 常量和局部叶片几何相关。若只切换纹理字节起点,却保留旧尺寸和旧几何,生成的高度覆盖就会发生变化。
同一个静态资源的完整尺寸为 608×608,第一级 mip 为 304×304。受控测试分别选择 mip0、mip1、mip2,并同步更新纹理链、尺寸常量及叶片 root/center。mip1 下,该点保留底图高度,周围 3×3 邻域也重现了参考中的中心未覆盖和周边贡献;mip2 又产生不同结果。

图 29|机制示意:驻留策略同时决定像素链、尺寸常量和叶片几何。608²/304²来自本次沙丘诊断资源;图中分支解释绑定关系,不展示源纹理像素。
这项测试没有把目标像素手工改回去。它改变的是资源绑定策略,之后从请求重新生成整条链。在当时包含 Vista 的沙丘版本中,4096² 最大误差降至约 0.5913 毫米,原峰值点误差降至约 0.003815 毫米。该整图记录属于当时版本,最新排序版本的外围仍需重新合成核对。
本轮采用显式 max_mip=1 启动配置。它不是把全部资源统一裁为 mip1:固定加载类和流送类按各自描述符处理,生成器据此建立有效驻留链。不同资源的初始级别和目录状态不能用一个全局裁剪操作替代。
后期资源状态与该策略相符,是支持绑定诊断的一项证据;它没有证明历史启动配置原值,也没有证明每次绘制之前的异步升级已经完成。因此当前配置应作为可复现实验输入保留,而不能写成所有原版场景天然使用同一值。
静态纹理目录、升级请求、CPU 状态更新与 GPU 上传完成还属于不同阶段。某个 CPU 字段已经写为目标 mip,不足以说明设备上的新纹理可供绘制。资源升级中,新 mip 前缀需要上传,旧链还要复制到新的级别位置;测试这些操作参数,也不自动证明真实并发时序已经验收。
GPU 回读完成后才能发布查询数据
支撑查询读取多级高度,而 GPU 计算与 CPU 查询之间还有一次异步回读。提交计算后,即使纹理对象和请求记录已经存在,CPU 也不能立即读取像素。图 30 展示了后续步骤:等待 GPU 完成,Map 暂存纹理,逐行复制,再发布查询描述符。CPU 查询从发布完成后才可使用这份数据。
10 月 7 日的工作沿着请求创建、排队、GPU 计算、回读和描述符发布继续核对。在独立 D3D11 设备上,四地貌各八个查询层的实际 GPU 输出通过对照;CPU 行复制、完成轮询和发布路径也分别检查。

图 30|机制示意:查询层的生命周期。未就绪时保留待处理句柄;完成查询成功后才 Map、按 RowPitch 复制并发布稳定 CPU 数据。设备测试与游戏现场并发验收分开。
GPU 回读纹理具有自己的行距。每行实际占用的 RowPitch 可能大于有效像素字节数,复制时需要逐行读取,而不能把整片映射内存当作紧密数组直接搬运。本例 float32 单通道的有效行字节数为 width × 4。
完成查询成功后才映射暂存纹理,按行复制到 CPU 缓冲,随后解除映射并释放相应设备对象。发布步骤把有效描述符写入查询表,清除该层待处理句柄。当前八层状态机中,处理掉最后一批句柄的那次轮询仍返回“本轮遇到待处理项”;下一次空轮询才进入结束通知。
这个返回规则影响调用方怎样驱动状态机。如果把“本次处理后没有句柄”直接当成“本次应结束”,就会改变通知时机。用阻塞等待强行保证数据已经存在,可以帮助离线数值测试,却不能代替对原发布过程的核对。
描述符还涉及两种所有权:请求记录可以在发布后回收,像素存储仍需供 CPU 查询使用。若把两者作为同一个对象释放,数据可能在简单测试中暂时可读,后续分配复用时才出错。当前局部测试将已回收记录覆盖为不同字节,确认发布后的查询不再依赖请求记录;像素清理则另行检查。
现有证据包括实际离线 GPU 完成查询和 C++ 发布状态机,仍未覆盖游戏现场的资源注册、原 worker pool 并发和完整所有权生命周期。离线设备可以证明本次输出和调用顺序,不能替代真实运行中的全部调度条件。
C++ 迁移怎样保存阶段定义
此次进一步移入 C++ 的部分包括行序发布、支撑缓冲重排、GPU 执行排列和连续高度绘制入口。连续入口依次调用本实现的支撑、自然 Stamp 与 Location 绘制程序,实际高度像素仍由自写 HLSL 计算。
Python 继续承担部分静态资源解析、序列化和上游阶段编排。主要算法模块和后续绘制已有 C++ 实现及连续入口,阶段编排仍有 Python 参与,高度像素也仍由 GPU 计算。
| 部分 | 当前职责 | 本轮需要保留的信息 |
|---|---|---|
| C++ 场景核心 | 请求、布局、区域、细分、角点高度、自然 Stamp 父实例 | 配置分支、记录顺序与随机状态 |
| 静态资源适配 | Level/Unit、纹理目录、局部变换等输入组织 | 资源版本、尺寸及变换层级 |
| C++ 支撑模块 | 查询、目标、邻接、记录排列与执行排列 | 查询高度版本、CPU self 编号、GPU 层序 |
| C++/HLSL 高度绘制 | 栅格化、平滑、支撑、自然与地点高度写入 | 行序、mip 绑定、采样器与阶段输出 |
| Web 工具 | 作业提交、报告与图形预览 | 作业输入和输出身份,不另造高度结果 |
连续绘制入口接收调用者提供的阶段数据,本身不检查这些数据是否来自本次请求。端到端验证器负责追踪它们与请求、静态库的对应关系。单独给绘制器传入支撑高度或实例文件并执行成功,只能证明它能够处理这组输入。
验证入口使用新输出目录,拒绝已有结果与成功标记;某一阶段失败立即停止,后续结果不应继续产生。验证器由此避免复用旧高度图,确保每份输出都来自本次成功执行的阶段。
四次重新生成完成后,验证器才读取冻结 Web 的原始高度数组。第一次统一选择 GPU 查询后端时,平原与沙丘仍有微小差异;检查冻结基线配置后确认,这两套原本使用 CPU float32 查询,另外两套使用 GPU 查询。保持各自原配置重新生成后,四份 4096² 数组全部位模式相同,文件哈希也一致。
两种查询后端不应无条件互换。它们在本次配置下可以很接近,但浮点计算的执行方式仍会影响末位。迁移验收首先要复现选定基线的输入条件,再决定是否统一后端,以及统一后用什么误差标准验收。
逐位一致与毫米级残差对应不同的比较
这组实验同时包含模块对照、实现回归和目标对照。即使数组尺寸相同,这三类实验检查的内容也不同,需要分别说明比较双方。
| 比较 | 输入和对象 | 本轮结果 | 不能由此推出 |
|---|---|---|---|
| 同输入内核对照 | 自产阶段输入,在同一宿主运行自写和参考内核 | 对应已测阶段像素可逐位一致 | 全部上游参数及历史设备状态一致 |
| C++ 与冻结 Web 回归 | 同一请求、静态库、显式启动与查询配置;Vista 前 4096² float32 | 四案例均零失配、哈希一致 | 与参考目标整张地图零差异 |
| 独立生成与参考目标 | 生成结束后比较共同中央范围 | 最大误差约 0.56~1.50 毫米 | 所有 Seed、外围、碰撞及联机均通过 |
误差统计使用:
error(P) = abs(generated(P) − reference(P))
MAE = sum(error(P)) / sampleCount
maxError = max(error(P))
over1mm = count(error(P) > 0.001)
本文表中平均值是平均绝对误差 MAE,不是 RMS。第一、二篇的 R16 一致率、中央范围 RMS 和本轮 MAE 来自不同实验,不能直接拼接为一条“精度提高了多少”的曲线。

图 31|机制示意:外围 Vista 的输入与融合关系。四种地貌的外围整图验证范围另见正文。
中央共同范围以外,各案例的验证进度不同。平原与离子赤原已有半径 620 米内的比较;焦灼荒原只有中央参考裁片;沙丘先前的含 Vista 版本有整图记录,但最新排序版本没有重新完成整图 Vista 回归。四套 C++ 与 Web 的逐位一致范围则统一止于 Location 高度写入后、Vista 前。
剩余的毫米级差异还需要继续定位。排查中将两个案例的最大误差点逐次绘制回放,确认哪些自然 Stamp 和 Location 改写了该点;自写与参考内核在相同自产输入上的结果一致。这次回放缩小了排查范围,但还没找到造成残差的具体历史参数或计算环节。现有结果不足以把这些末位差异都归因于 GPU 精度。
报告仍保留 finalScene=false,因为完整场景入口、全部外围和运行期资源时序还没有按统一条件验收。父记录、支撑排序和已测像素的对照结果继续有效,但完整场景尚未通过。
接入 UE 前仍需补齐的验证
前三篇已经说明了 UE 内的高度会话、地形网格和网络摘要。此次 C++/HLSL 结果来自独立 Windows D3D11 验证程序,没有因此自动成为 UE RHI 中的同一条生成链。引擎侧仍需接入新的阶段定义和排序规则,再分别检查设备资源、打包与运行时行为。
接入时需要一并处理这次确认的数据依赖:基础查询高度与平台输出分别保存,CPU self 编号与 GPU 执行序列分别保存,纹理行序只转换一次,驻留 mip 与尺寸常量、叶片几何共同更新。如果只迁移计算函数,仍沿用旧入口的输入绑定方式,这些错误可能再次出现。
专用服务器也不能直接使用客户端的图形路径。若继续采用 CPU 高度实现,就需要逐阶段比较它与本轮 GPU 基线的数值;如果服务器读取离线产物,则应明确其生成与版本条件。选择哪条路线属于引擎实现问题,本次四案例像素对照没有替它完成验收。
树木、岩石和 POI 的运行时装配同样单列。本轮自然 Stamp 父记录通过,不等于该父资源内所有网格、植被、岩石的世界矩阵和碰撞都重新通过;Location 高度纹理通过,也不等于建筑最终位置与导航均正确。第二篇已有的物件验证保持原来的日期和范围,本篇没有新增整场景物件数量结论。
下一步可以把这套四请求基线作为引擎接入的回归输入,先核对阶段数组及资源绑定,再检查完整外围、物件变换、碰撞和联机。这样,出现差异时能够回到明确的输入和阶段,而不必从最终截图重新猜测。
结论
四套固定请求已能沿相同的配置读取、基础高度和局部绘制流程生成对应结果。扩展地貌也让此前没有执行的分支参与了测试:没有平台写入时,Hbase 与 Hplatform 相同,查询绑错版本也可能看不出差异;离子赤原写入平台后,这个错误才显现出来。
这些修复都涉及阶段之间的数据交接。间距改变了点列和拓扑,旧任务范围拒绝了本应接受的候选,错误的查询版本改变了支撑目标。行序、绘制次序、mip 绑定和 GPU 完成状态又分别影响后续写入。迁移时除了公式,还需要保留每一步的输入版本、索引关系和执行条件。
保持各案例的基线配置时,C++/HLSL 与冻结 Web 的四份 Vista 前 4096² float32 输出逐位一致,说明迁移保留了已有实现的结果。与参考目标相比,中央共同范围仍有毫米级残差。前者验证迁移是否保留结果,后者衡量独立生成与参考目标的差异。完整场景仍需另外验收。
接下来的工程工作是把已明确的阶段定义接入 UE,再分别检查外围 Vista、物件变换、碰撞与联机。单点残差仍可继续定位,但不能代替这些运行时验证。已归档的四套请求、阶段数组和资源绑定条件,将作为接入时追踪差异的依据。
AI 协作复盘
本次文章整理使用了近一周的阶段记录、完整记录对照报告、四份 C++/Web 原始高度回归报告,以及行序、支撑排序和查询发布的 C++ 源码。对比图沿用已有实际高度数组生成的结果,配置数据图依据近一周的汇报 SVG 重新编排,完整参数与类型表保留在正文;阶段图沿用汇报产物,行序、排序、mip 与回读图用于解释迁移细节;本文没有重新运行一次完整地图生成,也没有新增引擎性能数字。
整理过程中需要特别核对历史记录的时效。早期文档中的“只输出基础高度”、离子赤原的 1,006 条 Stamp,以及焦灼荒原的 3.42 厘米残差,都已被后续结果更新。如果只读取文档开头,或把每份报告的成功项相加,就会得到错误的当前状态。
“逐位一致”也需要明确比较对象。四份 Vista 前 C++/Web 4096² float32 数组已经逐位一致,与参考目标的高度比较却仍有残差。保留双方、配置、范围和数据格式,才能让后来的实现继续复现这些结果。因此正文保留了各阶段实际读取的数据,以及对应的实验记录,便于后续接入和复查。