HLOD 不该在一个距离阈值上反复换模

相机沿着山路接近一组城镇建筑时,距离阈值看起来很合理:小于 200 米加载细节簇,大于 200 米显示合并代理。问题出现在相机停在边界附近。角色呼吸、镜头跟随和平滑碰撞会让距离在阈值两侧轻微摆动,细节簇与代理于是连续互换。即使资源早已驻留,渲染状态仍会抖动;若每次切换还驱动流送,问题会进一步扩散到 CPU、显存和 I/O 队列。

远近两种城镇几何在受控边界上完成稳定切换

单一世界距离还隐含了另一个错误前提:相同距离等于相同视觉贡献。玩家切换瞄准镜视场,或者窗口高度改变后,同一个物体覆盖的像素数已经不同。HLOD 真正需要判断的不是相机走了多少米,而是这组几何在当前视口里有多大。

先把世界尺寸投影到屏幕

探针用包围球半径近似 HLOD 簇的尺寸。给定球半径 r、相机空间深度 z、垂直视场角 fov 和视口高度 h,投影直径为:

focalPixels = h / (2 * tan(fov / 2))
diameterPixels = 2 * r * focalPixels / z

这个近似不负责精确栅格覆盖,但它把视场和分辨率纳入同一量纲,足以成为稳定的层级选择输入。固定样本 r=10mz=200mfov=60degh=1080px 得到 93.531px。仍在 200 米处,把视场改成 45 度时是 130.368px,改成 90 度时只有 54px。若继续使用 200 米距离阈值,这三个画面会得到同一个 LOD;屏幕贡献却相差超过一倍。

$WORK_DIR/.tmp/daily-labs/2026-09-04-2000/Program.cs 中,ProjectedDiameterPixels 只接受有限、正数且视场位于 (0, 180) 的输入。无效投影返回 NaN,调用者不能借此改变当前层级:相机矩阵尚未就绪时,保留上一帧的可解释状态比默认为高模或低模更安全。

两个阈值让选择器拥有记忆

只把距离换成像素仍不能消除边界振荡。若 90px 以上为细节、以下为代理,89, 91, 89, 91 仍会逐帧换模。选择器需要保留当前层级,并让两个方向使用不同门槛:代理只有达到 96px 才进入细节,细节缩小到 80px 才退出。中间的 16 像素带不是第三种 LOD,而是一段由已有状态决定结果的迟滞区。

public SelectionResult Update(ProjectionSample sample)
{
    double pixels = ProjectedDiameterPixels(sample);
    if (!double.IsFinite(pixels) || pixels <= 0)
        return SelectionResult.InvalidProjection;

    if (Current == LodLevel.Proxy && pixels >= enterDetailPixels)
    {
        Current = LodLevel.Detail;
        return SelectionResult.SwitchedToDetail;
    }

    if (Current == LodLevel.Detail && pixels <= exitDetailPixels)
    {
        Current = LodLevel.Proxy;
        return SelectionResult.SwitchedToProxy;
    }

    return SelectionResult.Unchanged;
}

这里状态属于 HlodSelector,投影样本只是一次不可变输入。渲染系统可以读取 Current 决定提交哪组 draw,流送系统则应把 SwitchedToDetail 当作需求变化,而不是自行根据距离再做一次判定。若两个系统各自维护阈值,画面可能已经选择细节簇,资源层却仍认为代理足够,最终只能依赖偶然时序。

同一投影直径序列在单阈值和双阈值选择器中的状态轨迹

固定序列 78,84,92,99,94,86,91,87,81,79 刻意穿过边界。单一 90px 阈值发生 4 次切换;96/80px 迟滞只在 99 像素进入细节、79 像素退出细节,共 2 次。它没有把变化延后任意时长,而是要求视觉贡献真正越过另一侧门槛。

$ dotnet build $WORK_DIR/.tmp/daily-labs/2026-09-04-2000/HlodHysteresisLab.csproj \
    -c Release --no-restore
Build succeeded.
    0 Warning(s)
    0 Error(s)

$ dotnet run --project \
    $WORK_DIR/.tmp/daily-labs/2026-09-04-2000/HlodHysteresisLab.csproj \
    -c Release --no-build
fixed radius_m=10 depth_m=200 fov_deg=60 height_px=1080 diameter_px=93.531
sequence=78,84,92,99,94,86,91,87,81,79 single_threshold_transitions=4 hysteresis_transitions=2
passed=8 failed=0 skipped=0 elapsed_ms=28.085

八项检查还覆盖阈值等号、迟滞带内保持、视场变化和非法投影不改状态。这里没有测量实际换模成本,因此不能据此声称节省了多少帧时间;运行结果只证明给定序列的状态转换次数与输入边界。

选择稳定不等于资源已经可用

迟滞选择器解决的是“应当显示哪一级”,不是“对应资源是否已驻留”。进入细节后,若网格和材质仍在异步加载,渲染层必须继续显示可用代理,直到资源屏障提交完成;但期望层级仍应保持为细节,不能因为暂时缺资源而反向喂给选择器。否则加载延迟会成为另一个隐式阈值,状态又开始往返。

包围球近似也有适用边界。细长桥梁、贴近屏幕边缘的簇或遮挡严重的建筑群,投影直径会高估真实贡献;此时可以改用投影包围盒、误差度量或离线生成的几何误差,但它们仍应输出统一的屏幕空间量,并复用同一迟滞状态机。门槛还应按平台画质档配置,而不是随每帧负载直接漂移;动态预算若要调整门槛,需要限制变化速率并记录版本,否则选择器的“记忆”会被移动中的边界破坏。

对这类城镇 HLOD,屏幕像素直径修正了距离阈值对视场和分辨率的失真,双阈值则阻止边界微动反复触发换模。当前切片没有覆盖资源驻留、交叉淡化和遮挡反馈,但已经给出这些系统可以共同依赖的选择合同:只有投影贡献越过方向对应的门槛,期望层级才发生一次明确变化。