LOD 屏幕尺寸:半径除以深度为何会在近处过早降级

一个半径为 1 的球形包围体,中心位于相机前方 2 个单位,垂直视角 90°,视口高 1080 像素。LOD 规则规定:垂直尺寸小于 580 像素时使用粗模型。常见的“直径除以深度再乘焦距”给出 540,允许降级;真实投影轮廓却约为 623.54,仍该保留细模型。两个判断来自同一份 Bounds,区别只在尺寸公式。

切线轮廓与中心截面投影的定性比较

这张示意图强调切线与中心截面的区别,不承担尺寸测量。下面用独立 C# 几何工程固定这次判定:相机坐标的正 Z 轴朝前,Y 轴朝上,半径、深度和近面采用同一长度单位。验证范围是理想针孔透视下、未被视口裁剪的球体垂直跨度,没有运行完整引擎或实际 LOD 资源切换。

轮廓点不在球心所在的深度平面

把球心放在光轴上,半径记为 r,中心深度为 z。中心截面的最高点投影斜率为 r/z,但从相机发出的轮廓射线与球相切,切点更靠近相机。由相机、球心和切点构成的直角三角形,得到轮廓半角满足 tan(α)=r/sqrt(z²-r²)。因此,近似与精确投影的比例并不相同。

令视口高度为 H、垂直视角为 φ,像素焦距为 f=H/[2tan(φ/2)]。光轴上的完整高度是 2fr/sqrt(z²-r²),近似高度是 2fr/z。本例两者相差约 83.54 像素;精确值除以近似值为 1/sqrt(1-(r/z)²)。只有球相对深度足够小时,这个倍率才接近 1,不能把远处成立的误差预算直接带到近处。

球心偏离光轴后,只用距离还会掩盖另一个变化。考虑相机 YZ 平面,球心为 (y,z),一条投影斜率为 m 的射线满足 Y=mZ。射线与半径为 r 的圆相切,意味着它到圆心的距离等于半径:

(y - mz)² = r²(1 + m²)
m± = [yz ± r sqrt(z² + y² - r²)] / (z² - r²)
pixels = f (m+ - m-)

在球完全位于近面前方时,分母为正。取 q=r/z、a=y/z,可将跨度写成 2q·sqrt(1-q²+a²)/(1-q²)。同样的半径和深度,把 y 从 0 改为 2,未裁剪高度增加到约 952.47 像素。这里的深度是相机 Z 分量,不能替换成球心到相机的欧氏距离。X 偏移不进入垂直极值公式;它影响横向范围,不能据此推断整个屏幕矩形不变。

近面相交应返回策略,而不是巨大像素数

研究工程的 SphereProjection.cs 保存完整入口 SphereProjection.Measure。调用方交付同一帧的相机空间球、近面、视角与视口高度,函数返回 Footprint。采用归一化比值是为了减少直接平方大坐标的中间值,并不构成覆盖所有浮点量级的数值证明。

public readonly record struct Footprint(bool ForceFine, double Pixels);

public static class SphereProjection
{
    public static Footprint Measure(double y, double z, double radius,
        double near, double verticalFov, int height)
    {
        if (!double.IsFinite(y) || !double.IsFinite(z) ||
            !double.IsFinite(radius) || !double.IsFinite(near) ||
            !double.IsFinite(verticalFov) || radius < 0 || near <= 0 ||
            verticalFov <= 0 || verticalFov >= Math.PI || height <= 0)
            throw new ArgumentOutOfRangeException(nameof(radius));
        // 近面相交时不把未裁剪的投影公式交给 LOD 判定。
        if (z <= near + radius) return new(true, 0);
        double q = radius / z;
        double a = y / z;
        double denominator = 1 - q * q;
        double span = 2 * q * Math.Sqrt(denominator + a * a) / denominator;
        double pixels = height / (2 * Math.Tan(verticalFov / 2)) * span;
        if (!double.IsFinite(pixels))
            throw new ArithmeticException("Projection exceeds numeric range");
        return new(false, pixels);
    }
}

ForceFine=true 表示本次不以有限尺寸作降级判断,此时 Pixels=0 只是未使用字段,消费端必须先检查标记。球与近面相交或相切时,当前策略保留细节;它没有计算裁剪后的可见跨度。完全位于相机后方的球也会进入这条保守分支,可见性系统应另行排除它,不能把 ForceFine 理解成必须绘制。

这个分支还有可验证的数值细节:最初用 z-r<=near,在 z=1.1、r=1、near=0.1 的测试中,减法舍入让相切球落入普通分支。实现改成比较 z<=near+r,锁住这组边界。它仍不是任意输入的严格舍入上界;生产代码若要求数学上的保守包含,需要另外给出适用坐标范围与向外误差预算。极端比值造成非有限输出时,当前实现会拒绝结果。

同一阈值为什么会选出相反的模型

球体投影高度曲线、580像素阈值与近面决策关系

图中两条曲线来自运行结果的 81 组固定深度,其他参数保持不变。Program.cs 中的 PrematureCoarseCounterexample 检查精确高度大于 580,同时近似值小于 580。这个失败回归证明的是近似公式足以改变等级选择,没有把模型质量或帧率当作已测结论。

另一个对照直接枚举圆周:Y=y+sin(θ)、Z=z+cos(θ),分别求 fY/Z 的最大与最小值,而不复写切线公式。三组中心高度与四组深度组合,每组采样十万个角度,共 120 万点。断言既检查采样跨度没有超过解析跨度,也限制两者间隙,避免一个任意放大的包围范围蒙混通过。

2026-09-25 使用 .NET 9 Release 执行,$LAB 表示研究工程目录:

dotnet build "$LAB/SphereLab.csproj" -c Release
dotnet run --project "$LAB/SphereLab.csproj" -c Release --no-build
build_exit=0 warnings=0 errors=0
run_exit=0 passed=18 failed=0 skipped=0 surfaceSamples=1200000
elapsedMs=82.0636
centerPixels=623.538290724796
naivePixels=540
thresholdPixels=580
offAxisPixels=952.4704719832528
maxUnderestimate=0
maxDenseGap=3.242137154302327e-07

计时覆盖断言、轮廓采样和曲线数据收集,不含构建与序列化,不是 LOD 运行成本基准。其余检查覆盖镜像位置、视口与世界尺度变化、零半径、近面、相机后方和非法输入。密集采样为本组数据提供独立数值对照,解析推导负责连续轮廓的依据;有限采样不能替所有浮点输入作保证。

球的投影范围修正后,开头的 580 像素规则会保留细模型。接入时仍要说明阈值衡量什么:包围球跨度适合按对象占屏尺寸分级,但它不等于简化网格相对原网格的最大屏幕误差,也不说明对象有多少像素实际可见。长条物体的球可能过松,画面边缘的未裁剪范围也可能很大;这些情况会让策略多留细节,不能靠缩小公式来掩盖。

LOD 管线可以先固定“当前 Bounds、相机投影、实际渲染视口、近面策略”这一组输入,再选择尺寸阈值与滞回范围。动态分辨率改变时,应明确按渲染像素还是最终显示像素预算,并传入对应高度;正交相机则应使用独立映射。滞回能够减少边界往返切换,却无法修复这里的系统性低估。只有尺寸本身具有清楚的几何含义,等级阈值才值得被美术和运行时共同调节。