· 5 分钟阅读 · 1945 字
非均匀缩放后的 SDF:安全步长与命中精度需要两种界
把半径为 1 的圆沿横向拉长四倍,竖直射线从 (0,3) 向下走,第一次接触应在 y=1。若把局部 SDF 的距离乘最大缩放 4,第一步却会走 8 米,直接落到 y=-5。起点与落点都在形状外面,单看两次距离的正负,甚至发现不了中间穿过了整个物体。
程序化体积、隐式曲面和距离场特效允许对象非均匀缩放时,局部空间里的精确距离不再是世界空间的精确距离。本文用独立 C# 椭圆探针固定这条计算链:先逆缩放采样点,再查询单位圆距离,最后沿单位世界射线前进。二维截面足以暴露倍率错误;这里没有引擎或 GPU 的运行截图。

拉伸改变了距离的方向依赖
令世界点为 p,正对角缩放矩阵为 S,局部点为 q=S^-1 p,单位圆外部的局部距离为 dLocal=length(q)-1。真实世界距离是 p 到整个椭圆边界的最短距离,不能只沿逆变换后的某个方向量一次。
设最小、最大轴缩放分别为 sMin、sMax。对任意局部边界点 u,都有 sMin*|q-u| <= |S(q-u)| <= sMax*|q-u|。左侧对所有 u 取最小值,得到世界距离下界;右侧选取局部最近边界点,得到一个可到达的世界边界点,因此也得到上界:
sMin * dLocal <= distanceToWorldSurface <= sMax * dLocal
这个区间用于形状外部。球面追踪的步长不能超过最近表面距离,所以应使用下界;沿任意单位方向走这一步,都不会在到达终点前遇到表面。最大缩放给的是上界,把它当步长正好反转了保证。固定短轴样本中,局部距离为 2,下界是 2,上界是 8,而实际世界距离为 2。
同一个下界也不能直接用来确认命中。世界点 (4.0002,0) 到椭圆的真实距离为 0.0002 米,但下界只有 0.00005 米。如果容差是 0.0001 米,检查“下界小于容差”就会提前接受。采用 sMax*dLocal <= epsilon 才能证明点已经进入指定的表面邻域。安全前进与确认接近需要相反方向的界。

追踪器交付状态,而不把超预算解释为未命中
EllipseTrace.cs 的完整核心实现如下。Program.cs 直接调用 EllipseTrace.March,返回的 t 始终使用世界距离单位。方向只接受单位向量,避免方向长度暗中再次缩放步长。
public readonly record struct V2(double X, double Y)
{
public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y);
public static V2 operator *(V2 a, double s) => new(a.X * s, a.Y * s);
public double Length => Math.Sqrt(X * X + Y * Y);
public bool Finite => double.IsFinite(X) && double.IsFinite(Y);
}
public readonly record struct TraceResult(string Status, double T, int Steps);
public static class EllipseTrace
{
public static double LocalDistance(V2 p, V2 scale) =>
new V2(p.X / scale.X, p.Y / scale.Y).Length - 1;
public static TraceResult March(V2 origin, V2 direction, V2 scale,
double maxT, double epsilon, int budget)
{
if (!origin.Finite || !direction.Finite || !scale.Finite
|| scale.X < 1e-6 || scale.Y < 1e-6
|| scale.X > 1e6 || scale.Y > 1e6
|| origin.Length > 1e9 || Math.Abs(direction.Length - 1) > 1e-12
|| !double.IsFinite(maxT) || maxT <= 0 || maxT > 1e9
|| !double.IsFinite(epsilon) || epsilon < 1e-9 || budget <= 0 || budget > 1000000)
throw new ArgumentOutOfRangeException();
double lo = Math.Min(scale.X, scale.Y);
double hi = Math.Max(scale.X, scale.Y);
double t = 0;
for (int step = 1; step <= budget; step++)
{
double local = LocalDistance(origin + direction * t, scale);
if (local < -1e-12) return new("Inside", t, step);
// 上界证明接近表面;下界仅用于安全前进。
if (hi * Math.Abs(local) <= epsilon) return new("Hit", t, step);
double next = t + lo * Math.Max(0, local);
if (next > maxT) return new("Miss", t, step);
if (next <= t) return new("Stalled", t, step);
t = next;
}
return new("Budget", t, budget);
}
}
输入范围是这个小探针的数值合同:轴缩放限制在 1e-6 至 1e6,原点长度和追踪距离不超过 1e9,容差至少 1e-9,求值预算不超过一百万。这里没有用极小常量替换零缩放;不可逆变换直接拒绝。LocalDistance 是通过入口校验后使用的求值函数,不承担独立的外部输入校验。
Hit 表示到表面的距离上界满足容差;Miss 表示安全步长已经覆盖剩余查询范围;Budget 表示还没有足够证据;Stalled 表示浮点加法已经不能推进。起点明显处于内部则返回 Inside,不尝试沿负距离倒走。负值阈值只吸收接触附近的舍入误差。
同样从表面外 2 米出发,短轴样本只需 2 次求值,长轴样本需要 60 次。沿长轴推进时,下界只有该轴真实距离的四分之一,误差逐次乘以 0.75。形状越扁长,统一下界越保守。预算只有 2 次时,长轴样本返回 Budget;若调用端把它当作背景,物体仍会缺失,只是缺失原因从穿越变成了预算不足。
用交点和错误实现分别约束结果
当天在 .NET SDK 9.0.202、Release 配置下执行,构建与运行退出码均为 0,构建零警告、零错误。耗时来自检查程序计时,不包含构建,也不作为渲染性能结论。
dotnet build "$PROBE_PROJECT/SdfLab.csproj" -c Release
dotnet run --project "$PROBE_PROJECT/SdfLab.csproj" -c Release --no-build
passed=24 failed=0 skipped=0 elapsedMs=101.0601
randomCases=10000
maxTError=9.977022141782754E-08 m
maxStepExcess=0 m
shortAxis: Hit t=2 evaluations=2
longAxis: Hit t=1.999999914955832 evaluations=60
ShortAxisHit、LongAxisHit 对照轴端点;OffAxisHit 对照 x=2 竖直射线的解析交点 y=sqrt(0.75)。失败回归保留错误倍率:
var scale = new V2(4, 1);
double badStep = 4 * EllipseTrace.LocalDistance(new(0, 3), scale);
// badStep=8,落点y=-5,跨过y=1与y=-1两个边界。
LowerBoundFalseHit 与 UpperBoundRejectsFalseHit 使用前面的近表面样本,分别锁住错误接受与正确拒绝。其余检查覆盖均匀缩放、表面起点、内部起点、反向远离、有限射程、切线方向预算耗尽,以及非法缩放、方向、容差、预算和非有限输入。
固定种子 20261020 的一万组样本使用 0.5 至 4 的轴缩放,先取椭圆表面点,再从同一径向向外放大,沿径向射回。解析行程为“外扩倍率减一,再乘表面点长度”,与步进循环独立。全部返回 Hit,最大行程误差如上;首步下界未超过解析交点距离。径向测试没有覆盖所有近切线方向,切线样本单独检查有限预算返回值。
表面邻域容差也不等于射线交点深度误差:擦边时,点离表面很近,沿射线到真实交点仍可能很远,甚至没有严格交点。需要精确深度或碰撞接触时,应继续使用解析求交、根区间或明确的屏幕误差策略,不能把这个近表面 Hit 直接当成精确碰撞。
对于仅含正轴缩放的距离场实例,资源层可以同时保存 sMin 与 sMax,追踪层分别消费安全步长和终止上界,并将预算状态交给表现层选择降级。三维单位球可沿同一证明扩展到椭球;层级变换含剪切时,应改用完整线性变换的奇异值界,不能继续读取三个轴长度冒充这两个量。若预算长期耗尽,应缩紧距离估计或改用专用求交,而非把倍率调大来换取看似更快的前进。