· 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,全部满足锥角边界;另有轴线、锥边、零角、半球、整球、接缝与非法输入检查。耗时只记录本次执行,不用于比较发射器性能。
发射方向与屏幕密度需要分别验收
局部方向进入世界空间时,应使用正交旋转保持角度与长度。直接乘带非均匀缩放的对象矩阵,再归一化,虽然恢复了单位长度,却会重新扭曲方向分布。发射器若允许通过缩放拉扁锥形,需要把这项变形作为明确的艺术控制;它不再满足本文的等立体角合同。采样器负责局部密度,变换层负责是否保留它。
随机数状态也应留在发射调度器。每个粒子的输入来自稳定的发射编号或既定随机流,数值映射不主动取随机数,才能在录制回放、批量生成和重排计算时复现同一组方向。本文只验证映射,未实现随机流拆分或引擎并行发射,接入时需要单独检查重复种子和跨粒子相关性。
如果目标是喷枪在墙上形成均匀圆斑,应先在墙面圆盘上按面积采样,再从枪口指向该位置;如果目标是有意强化中心的火焰,可以选择另一条明确的累计分布。两种需求都合理,但资产参数必须说明控制的是方向概率还是平面覆盖。开篇的中心拥挤已有可复现的成因:均匀角度给更小的轴向球面区域分配了过多样本。将这份度量写进发射合同后,粒子数量、速度曲线与混合亮度才不必替采样偏差作补偿。