· 5 分钟阅读 · 1859 字

锥形粒子越靠轴线越密:均匀极角不等于均匀立体角

锥形发射器已经把极角和方位角都设为均匀随机,粒子却仍向轴线聚拢。增加粒子数只会让中心更厚:半锥角 60° 时,本文的 10,000 个固定样本中,有 4,307 个落在最靠近轴线、只占总立体角 20% 的区域。预期的均匀方向分布应当在这里放入 2,000 个。

问题来自“均匀”所对应的度量。发射器要让等面积球面区域获得相同数量的方向,直接均分角度却没有均分球面面积。下面用独立 C# 探针固定发射轴、锥角与输入序列,比较两条采样路径;没有接入引擎粒子系统,计数来自方向向量,不是屏幕截图中的像素亮度。

木锥与竹签表达锥形发射方向的概念摄影,非采样实验

中心拥挤从一个角度映射开始

把发射器的局部前向规定为 +Z,半锥角为 α,方向的极角 θ 从 +Z 量起,方位角 φ 围绕 Z 轴旋转。单位方向写为 (sinθ cosφ, sinθ sinφ, cosθ)。若直接令 θ=αu、φ=2πv,独立均匀的 u、v 确实覆盖了全部角度范围,但靠近轴线的一圈比外侧一圈小得多。

单位球面面积元为 sinθ dθ dφ。从轴线积到 θ,面积为 2π(1-cosθ);整个球冠面积为 2π(1-cosα)。因此累计概率应满足 u=(1-cosθ)/(1-cosα),得到 z=cosθ=1-u(1-cosα)。均匀的是 z,而不是 θ。再由 sqrt(1-z²) 恢复横向半径,就不必调用反余弦求出中间角度。

这里采样的是球冠上的方向。所有粒子若具有相同速度与年龄,它们位于等半径球面上,这份面积关系可以直接适用;如果观察对象变成一块平面上的命中点,距离和投影会改变面积权重,不能照搬“均匀方向”作为“均匀落点”的承诺。

采样器只交付局部单位方向

关键实现位于 .tmp/daily-labs/2026-10-22-2000/ConeSample.cs,调用链为 Program → ConeSample.Sample → Validate。以下是完整核心实现,角度使用弧度:

public readonly record struct Direction(double X, double Y, double Z);

public static class ConeSample
{
    public static Direction Sample(double halfAngle, double u, double v)
    {
        if (!double.IsFinite(halfAngle) || halfAngle < 0 || halfAngle > Math.PI)
            throw new ArgumentOutOfRangeException(nameof(halfAngle));
        Validate(u);
        Validate(v);
        double z = 1 - u * (1 - Math.Cos(halfAngle));
        double radius = Math.Sqrt(Math.Max(0, (1 - z) * (1 + z)));
        double azimuth = 2 * Math.PI * v;
        return new Direction(radius * Math.Cos(azimuth),
            radius * Math.Sin(azimuth), z);
    }

    private static void Validate(double value)
    {
        if (!double.IsFinite(value) || value < 0 || value > 1)
            throw new ArgumentOutOfRangeException(nameof(value));
    }
}

u 控制球冠面积分位,v 控制方位。生产调用者应交付相互独立的均匀样本,通常采用半开区间 [0,1);这里接受闭区间端点,便于直接检查轴线、锥边和方位接缝。v=0 与 v=1 表示同一方向,不应在离散随机表里给这个接缝重复权重。

半锥角允许从 0 到 π:零角退化为轴向,π/2 覆盖半球,π 覆盖整球。零角不是异常,不会触发面积分母为零,因为实现使用了已化简的映射。负角、超过 π 的角度、非有限值及区间外样本均被拒绝;无效输入不会悄悄变成一束轴向粒子。极窄锥角下 1-cosα 会受浮点消减影响,本文也没有把双精度结果当作任意窄角的精度保证。

横向长度采用 (1-z)(1+z),并在开平方前限制下界为零。这个限制只清理末位舍入,不能代替输入检查。方向没有携带速度大小、出生位置或世界变换,避免采样函数顺便改变发射能量或坐标空间。

用等面积分箱揭示错误路径

固定 α=60°、u=0.5、v=0.25 时,输出约为 (0,0.6614378277661477,0.75),长度为 1。均匀极角路径在同一个 u 上得到 θ=30°、z≈0.866025,更接近中心。输入相同,差异来自从样本到方向的映射。

探针输出的五个等立体角分箱及两种采样路径的方向计数

技术图按 z 从 1 到 0.5 划成五个等宽区间,每个区间对应相等立体角。测试使用 u=(i+0.5)/10000,方位样本为 v=(((i*7919)%10000)+0.5)/10000。这是一组确定的分层输入,不是随机试验;方位遍历全部分位点,断言重点是单位长度、锥内边界和 z 分布,没有据此证明任意随机数发生器的二维独立性。

正确路径得到 [2000,2000,2000,2000,2000],错误路径得到 [4307,1838,1450,1260,1145]。平均 z 分别为 0.75 和 0.8269933435;前者符合球冠的 (1+cosα)/2,后者符合均匀极角的 sinα/α。错误方向仍全部在锥内,因此仅测“没有越界”无法发现密度偏差。

Program.cs 保留正确分箱与错误集中两个回归断言:

Check("EqualSolidAngleBins", correctBins.All(x => x == 2000));
Check("PolarAngleBias", wrongBins[0] > 4000 && wrongBins[4] < 1200);

当天以 .NET SDK 9.0.202、Release 配置执行:

dotnet build "$PROBE_PROJECT/ConeLab.csproj" -c Release
dotnet run --project "$PROBE_PROJECT/ConeLab.csproj" -c Release --no-build

构建退出码 0,零警告、零错误;运行退出码 0,通过 24 项、失败 0、跳过 0,程序计时 30.6183 ms。10,000 个样本的最大长度平方误差为 4.440892098500626e-16,全部满足锥角边界;另有轴线、锥边、零角、半球、整球、接缝与非法输入检查。耗时只记录本次执行,不用于比较发射器性能。

发射方向与屏幕密度需要分别验收

局部方向进入世界空间时,应使用正交旋转保持角度与长度。直接乘带非均匀缩放的对象矩阵,再归一化,虽然恢复了单位长度,却会重新扭曲方向分布。发射器若允许通过缩放拉扁锥形,需要把这项变形作为明确的艺术控制;它不再满足本文的等立体角合同。采样器负责局部密度,变换层负责是否保留它。

随机数状态也应留在发射调度器。每个粒子的输入来自稳定的发射编号或既定随机流,数值映射不主动取随机数,才能在录制回放、批量生成和重排计算时复现同一组方向。本文只验证映射,未实现随机流拆分或引擎并行发射,接入时需要单独检查重复种子和跨粒子相关性。

如果目标是喷枪在墙上形成均匀圆斑,应先在墙面圆盘上按面积采样,再从枪口指向该位置;如果目标是有意强化中心的火焰,可以选择另一条明确的累计分布。两种需求都合理,但资产参数必须说明控制的是方向概率还是平面覆盖。开篇的中心拥挤已有可复现的成因:均匀角度给更小的轴向球面区域分配了过多样本。将这份度量写进发射合同后,粒子数量、速度曲线与混合亮度才不必替采样偏差作补偿。