圆环粒子发射:均匀半径为何让内圈过密

一个内半径为 1、外半径为 3 的地面圆环,要在整片区域散布粒子。半径从 1 到 3 均匀抽取,角度也均匀抽取,看起来两个方向都公平,内圈却明显更密。增加粒子数量不会消除这种偏向;它只会让错误分布更加稳定。

圆环内部偏密与面积密度均匀的定性对照

上图只表达分布差异,不承担粒子数量和半径比例的测量。下面用独立 C# 采样工程把发射器缩到局部 XY 平面,长度统一使用局部单位,正角度从正 X 轴转向正 Y 轴。目标是每单位面积具有相同的出生概率,不包含粒子移动、大小、透明混合或屏幕投影。

四块面积相同的环带,为什么装下不同数量

宽度同为 dr 的薄环,面积随半径增长,约为 2πr·dr。若每个半径小区间拿到相同概率,越靠内侧,这份概率就越集中。对于内外半径 a、b,均匀半径对应的单位面积密度为 1/[2πr(b-a)];在本例中,靠近内边缘的极限密度是靠近外边缘的三倍。

这也给出了检验方法:不能只统计等宽半径区间,那恰好会让错误算法看起来完美。把平方半径按 1、3、5、7、9 分割,四块环带的面积都为 2π。面积均匀时,它们应有相同的期望点数。

从内边界积累到半径 r 的面积比例是 (r²-a²)/(b²-a²)。令这个比例等于均匀变量 u,便得到 r=sqrt(a²+u(b²-a²))。角度仍为 2πv,其中 u、v 在理想随机合同下独立且均匀分布于 [0,1)。以本例的 u=0.5、v=0 求值,返回 (2.23606797749979,0);半面积处的半径是 sqrt(5),而不是半径区间中点 2。

采样器只消费两个数,不拥有随机流

核心实现位于研究工程的 AnnulusSampler.cs,入口为 AnnulusSampler.Sample。它接收两项无量纲输入,交付局部位置;调用者负责随机流、发射器变换和出生数量。这样更换空间分布时,不必把随机状态隐藏到数学函数内部。

public static class AnnulusSampler
{
    public static (double X, double Y) Sample(
        double inner, double outer, double u, double v)
    {
        if (!double.IsFinite(inner) || !double.IsFinite(outer) ||
            inner < 0 || outer <= inner || outer > 1e100)
            throw new ArgumentOutOfRangeException(nameof(outer));
        if (!double.IsFinite(u) || !double.IsFinite(v) ||
            u < 0 || u >= 1 || v < 0 || v >= 1)
            throw new ArgumentOutOfRangeException(nameof(u));

        // 在归一化平方半径上插值,避免直接计算巨大半径的平方。
        double q = inner / outer;
        double radius = outer * Math.Sqrt((1 - u) * q * q + u);
        double angle = 2 * Math.PI * v;
        return (radius * Math.Cos(angle), radius * Math.Sin(angle));
    }
}

代码按外半径归一化后插值,减少大数平方带来的溢出风险;1e100 是本实现明确采用的数值上限,并非发射器的推荐尺寸。相等半径会被拒绝,因为零面积圆周无法继续使用面积分布合同。如果美术需要沿圆周喷发,应提供圆周采样模式;不应悄悄把错误配置夹成一个近乎没有厚度的环。

内半径为零则是正常圆盘,公式自然退化成 r=b·sqrt(u)。靠近浮点极限的极薄环、极小数,以及转换成引擎 float 后的精度没有在此得到完整保证;u<1 也不承诺浮点结果严格小于外半径,舍入可能把结果放到边界上。需要严格几何排除规则的消费端,应另定容差,不能直接把概率端点合同当作数值排除判据。

固定样本暴露偏差,不能替随机发生器作证

Program.cs 调用采样器,再按输出坐标计算 x²+y²,统计等面积环带,并检查二阶矩、角度象限和合法范围。10,000 对输入使用 u=(i+0.5)/10000,v=((7919i mod 10000)+0.5)/10000。后者是角度分层的一个置换;这是一组确定性回归输入,不能称作独立随机试验。

固定输入的圆环坐标、等面积环带与计数对照

图中各绘制每十点抽一项得到的 1,000 点,因此会出现规则纹路;计数使用全部 10,000 点。绿色的四个 2,500 来自固定分层,不意味着随机发射每次都应严格相等。错误对照复用同一组输入,只把半径改成 1+2u,得到由内至外的 3660、2520、2049、1771。换成独立随机输入后,各环带会波动,但错误映射的系统偏向仍由面积公式决定。

两个决定性断言保留在运行工程中:

Check(correct.All(x => x == 2500), "EqualAreaBands");
Check(wrong[0] > 3600 && wrong[3] < 1800, "UniformRadiusCounterexample");

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

dotnet build "$LAB/AnnulusLab.csproj" -c Release
dotnet run --project "$LAB/AnnulusLab.csproj" -c Release --no-build
build_exit=0 warnings=0 errors=0
run_exit=0 passed=16 failed=0 skipped=0 samples=10000
elapsedMs=93.3391
areaBands=[2500,2500,2500,2500]
radiusBands=[3660,2520,2049,1771]
angularQuadrants=[2500,2500,2500,2500]
meanRadiusSquared=5.000000000000006

16 项检查包含正常环域、圆盘、内边界、零面积、反向半径、非有限数和非法参数;耗时包括断言与图示点收集,不含构建和 JSON 序列化,不是粒子系统性能基准。平均平方半径接近理论值 (1²+3²)/2=5,与四段计数一起验证映射;它们仍未检验任意随机发生器的二维独立性。

接入发射模块时,值得保留的是显式分布模式与固定的随机输入接口。当前映射每次消费两个数;拒绝采样虽然也能得到面积分布,却可能消耗不定数量的随机数。若与后续颜色、寿命共用一条流,更换算法便会改变这些属性,回放需要一并记录算法版本或分离随机流。

局部平面的非退化仿射变换具有常量面积倍率,因而均匀环域变成椭圆环后仍对那片平面面积均匀;投影到起伏地形则不具备这一保证。屏幕上的明暗还受视角、遮挡与粒子尺寸影响。对于开头的发射器,修复应落在出生位置的概率分布上;如果目标本来就是内侧更密的艺术效果,均匀半径可以保留,但应作为另一种明确分布交付,避免同一个“均匀”开关混合两套含义。