· 5 分钟阅读 · 2026 字
相机已经对准包围球,自动取景为何仍会裁掉轮廓
把半径为 1 的球放在相机正前方,纵向视场角设为 60°,再按 距离 = 半径 / tan(半视角) 拉远相机,球心截面恰好装进画面,球的上下轮廓却仍然越界。固定输入下,这个距离是 1.732051,真实投影半幅达到屏幕半幅的 1.224745 倍。问题出在取景对象:中心截面的圆周不构成透视轮廓,离相机更近的球面仍能投影得更远。
模型预览、装备展示和编辑器的“框选目标”都需要这类自动取景。若对象由包围球代表,距离应该由视线与球面的切线决定。当前 C# 实现把取景距离、屏幕比例和裁剪区间放进同一次求解,输出一个可检查的结果;它验证的是透视几何合同,没有把数学运行结果当成引擎截图。

中心截面恰好放下,球面还没有放下
设相机位置为 O、球心为 C、轮廓切点为 T,半径为 r,球心沿相机正向轴,距离为 d。切点半径 CT 垂直于视线 OT,所以直角三角形 OCT 的斜边是 d。球体的角半径 β 满足 sin(β) = r/d。限制半视角为 α 时,完整球体进入视锥要求 β ≤ α,也就是 d ≥ r/sin(α)。
中心截面公式中的 tan 连接的是球心平面上的半径与深度,漏掉了切点位于球心前方这一条件。60° 纵向视场角对应 α=30°,正确距离是 2;错误公式给出的距离恰好小了一个 cos(α) 因子。视角越宽,两者偏差越大,额外乘一个没有依据的经验系数只能掩盖某组输入。
还可以直接验证屏幕结果。在相机前方、d > r 时,球面轮廓的投影斜率为 r / sqrt(d²-r²);除以 tan(α),便得到相对于屏幕半幅的范围。这个值大于 1 就是越界,小于等于 1 才满足当前轴的取景约束。它与切线推导互相校验,避免仅仅对求解公式做同义断言。
竖屏先卡住的是水平方向
代码中的输入是纵向完整视场角,单位为度;宽高比 a=width/height,距离、半径和裁剪面采用同一长度单位。对称透视投影满足 tan(αx)=a·tan(αy)。求解先得到两个半视角,再取较小者。横屏通常受纵向限制,竖屏则可能受横向限制。
半径仍为 1、纵向视场角仍为 60°,16:9 的结果为 2,改为 9:16 后就要退到 3.237512。沿用横屏距离会让球体在左右两侧被裁掉;只更新投影矩阵、不重新求取景距离,同样破坏合同。
SphereFraming.cs 的 Fit 返回距离、两个半视角和限制来源。调用方用 cameraPosition = center - forward * Distance 安放相机,并使光轴经过球心;它应在目标包围体、视场角、有效视口比例或裁剪面变化时重算,而不是每帧把当前距离再乘一次修正系数。
public readonly record struct FrameResult(
double Distance, double HalfX, double HalfY, string Limiter);
public static class SphereFraming
{
public static FrameResult Fit(double radius, double verticalFovDegrees,
double aspect, double near, double far)
{
if (!double.IsFinite(radius) || radius <= 0 ||
!double.IsFinite(verticalFovDegrees) || verticalFovDegrees <= 0 ||
verticalFovDegrees >= 180 || !double.IsFinite(aspect) || aspect <= 0 ||
!double.IsFinite(near) || near <= 0 || !double.IsFinite(far) || far <= near)
throw new ArgumentOutOfRangeException(nameof(radius));
double halfY = verticalFovDegrees * Math.PI / 360;
double halfX = Math.Atan(Math.Tan(halfY) * aspect);
double angular = radius / Math.Sin(Math.Min(halfX, halfY));
double distance = Math.Max(angular, radius + near);
if (!double.IsFinite(distance) || distance + radius > far)
throw new InvalidOperationException("Sphere cannot fit the clipping interval.");
return new(distance, halfX, halfY,
distance > angular ? "near" : halfX < halfY ? "horizontal" : "vertical");
}
public static double ProjectedPeak(double radius, double distance, double halfAngle)
{
if (distance <= radius) throw new ArgumentOutOfRangeException(nameof(distance));
return radius / Math.Sqrt((distance - radius) * (distance + radius)) /
Math.Tan(halfAngle);
}
}
半径必须已经是世界空间的保守包围半径。局部半径不能直接与世界空间距离混用;父级变换包含缩放或剪切时,包围体生产者先负责给出能够覆盖最终几何的球。取景器只消费这个合同,无法修正偏小的输入球。若对象细长或偏心,球体会留出较大空白,这是包围表示的成本;更紧的构图需要改用实际包围顶点或其他几何约束。
退远相机,也可能把球推过远裁面
视锥侧面只约束角度,近远裁面约束的是深度。相机瞄准球心时,最近点深度为 d-r,最远点为 d+r。因此最终距离取 max(角度所需距离, r+near),然后检查 d+r ≤ far。若不成立,当前投影参数没有可行解:继续后退只能让远裁面问题更严重。
固定样本把 near 从 0.1 改为 2,取景距离从 2 变成 3,限制来源也变成 near。另一回归将 far 设为 2.99,求解明确拒绝;球心仍在裁剪区间内不代表完整球体可见。调用方应根据展示需求调整裁剪区间、视场角或目标包围表示,不能在异常后沿用一个声称已经装下全部目标的结果。
数学上的相等边界仍有浮点舍入。当前 double 计算得到的横屏距离为 2.0000000000000004,代码保持保守拒绝,远裁面边界测试由该计算距离加半径构造。实际接入浮点相机时应给构图和裁剪预留余量,并对转换后的矩阵复核;本文没有用数值容差悄悄扩大可见区间。

用球面投影检查求出的距离
Program.cs 先检查宽屏、竖屏和近裁面结果,再检查零半径、非有限输入、无效视场角及无可行裁剪区间等拒绝分支。随后对 5 个纵向视场角和 5 个宽高比组合,各取包含端点的 10001 个圆周截面点,以 横向坐标/深度/tan(限制半视角) 计算归一化范围。球体具有轴对称性,较小半视角所对应的截面约束了两个屏幕方向;这组测试不依赖某个固定网格的顶点恰好避开轮廓。
以下回归同时锁住正确结果和错误公式,避免只证明两个实现写出了相同表达式。源码与运行材料位于 $WORK_DIR/.tmp/daily-labs/2026-10-25-1930/。
var fit = SphereFraming.Fit(1, 60, 16.0 / 9, .1, 100);
double wrong = 1 / Math.Tan(Math.PI / 6);
Check(Math.Abs(fit.Distance - 2) < 1e-12);
Check(Math.Abs(SphereFraming.ProjectedPeak(1, fit.Distance, Math.PI / 6) - 1) < 1e-12);
Check(SphereFraming.ProjectedPeak(1, wrong, Math.PI / 6) > 1.22);
2026-10-25 的 .NET 9 Release 执行命令与决定性输出如下。耗时属于这次功能验证,不用于推断相机运行成本。
cd "$WORK_DIR"
dotnet build .tmp/daily-labs/2026-10-25-1930/SphereLab.csproj -c Release --no-restore
dotnet run --project .tmp/daily-labs/2026-10-25-1930/SphereLab.csproj -c Release --no-build
build: exit=0, warnings=0, errors=0
run: exit=0, passed=25, failed=0, skipped=0, elapsed_ms=66.7966
surface_samples=250025, max_sample_peak=1
max_near_violation=0, max_far_violation=0
wide_distance=2.0000000000000004, portrait_distance=3.237511618740771
near_distance=3, wrong_distance=1.7320508075688776
wrong_peak=1.224744871391589, correct_peak=1
这套距离求解适用于光轴经过球心、无镜头偏移的对称透视相机。它不处理遮挡、相机碰撞、动画中的包围体变化或非对称投影。展示系统可以把构图留白转换为更大的有效半径,再执行同一合同;相机平滑则应另外管理,并检查过渡期间的可见性。若需求要求始终完整展示,不能只验证平滑终点。把“得到可行距离”和“怎样移动到那里”拆成两个职责,才能让取景失败明确归因于几何、裁剪参数或移动路径。