Merge commit '841d923fed686700610a85aeab6289e44239aa6c'

This commit is contained in:
Pascal Serrarens 2026-01-07 12:20:59 +01:00
commit f8fc9dabe6
6 changed files with 420 additions and 68 deletions

View File

@ -73,8 +73,7 @@ namespace LinearAlgebra
return d;
}
public override readonly string ToString()
{
public override readonly string ToString() {
return $"Direction(h: {this.horizontal}, v: {this.vertical})";
}
@ -112,7 +111,7 @@ namespace LinearAlgebra
if (this.vertical > AngleFloat.deg90 || this.vertical < -AngleFloat.deg90)
{
this.horizontal += AngleFloat.deg180;
this.vertical = AngleFloat.deg180 - this.vertical;
this.vertical = AngleFloat.Degrees(180 - this.vertical.inDegrees);
}
}
@ -127,9 +126,10 @@ namespace LinearAlgebra
float radV = this.vertical.inRadians;
// Calculate Vector
float x = MathF.Cos(radV) * MathF.Cos(radH);
float cosV = MathF.Cos(radV);
float x = cosV * MathF.Cos(radH);
float y = MathF.Sin(radV);
float z = MathF.Cos(radV) * MathF.Sin(radH);
float z = cosV * MathF.Sin(radH);
return new UnityEngine.Vector3(x, y, z);
}
@ -152,22 +152,22 @@ namespace LinearAlgebra
/// Convert the direction into a carthesian vector
/// </summary>
/// <returns>The carthesian vector corresponding to this direction.</returns>
public Vector3Float ToVector3()
{
public readonly Vector3Float ToVector3() {
// Quaternion q = Quaternion.Euler(90 - this.vertical.inDegrees, this.horizontal.inDegrees, 0);
// Vector3Float v = q * Vector3Float.forward;
// return v;
// Convert degrees to radians
float radH = this.horizontal.inRadians;
float radV = this.vertical.inRadians;
// Calculate Vector
float x = MathF.Cos(radV) * MathF.Cos(radH);
float y = MathF.Sin(radV);
float z = MathF.Cos(radV) * MathF.Sin(radH);
float cosV = MathF.Cos(radV);
float sinV = MathF.Sin(radV);
return new Vector3Float(x, y, z);
float horizontal = cosV * MathF.Sin(radH);
float vertical = sinV;
float depth = cosV * MathF.Cos(radH);
return new Vector3Float(horizontal, vertical, depth);
}
/// <summary>
@ -229,8 +229,7 @@ namespace LinearAlgebra
return HashCode.Combine(horizontal, vertical);
}
public static AngleFloat UnsignedAngle(Direction d1, Direction d2)
{
public static AngleFloat UnsignedAngle(Direction d1, Direction d2) {
// Convert angles from degrees to radians
float horizontal1Rad = d1.horizontal.inRadians;
float vertical1Rad = d1.vertical.inRadians;

View File

@ -180,11 +180,7 @@ namespace LinearAlgebra {
/// <returns>The resulting quaternion</returns>
/// Rotation are appied in the order Z, X, Y.
public static Quaternion Euler(Vector3Float angles) {
// return Quaternion.FromEulerRad(angles * AngleFloat.Deg2Rad);
// }
// private static Quaternion FromEulerRad(Vector3Float euler) {
Vector3Float euler = angles * AngleFloat.Deg2Rad;
// euler.x = pitch, euler.y = yaw, euler.z = roll (radians)
float cx = MathF.Cos(euler.horizontal * 0.5f);
float sx = MathF.Sin(euler.horizontal * 0.5f);
float cy = MathF.Cos(euler.vertical * 0.5f);
@ -199,28 +195,6 @@ namespace LinearAlgebra {
q.y = sy * cx * cz - cy * sx * sz;
q.z = cy * cx * sz - sy * sx * cz;
return q;
// float yaw = euler.horizontal;
// float pitch = euler.vertical;
// float roll = euler.depth;
// float rollOver2 = roll * 0.5f;
// float sinRollOver2 = MathF.Sin(rollOver2);
// float cosRollOver2 = MathF.Cos(rollOver2);
// float pitchOver2 = pitch * 0.5f;
// float sinPitchOver2 = MathF.Sin(pitchOver2);
// float cosPitchOver2 = MathF.Cos(pitchOver2);
// float yawOver2 = yaw * 0.5f;
// float sinYawOver2 = MathF.Sin(yawOver2);
// float cosYawOver2 = MathF.Cos(yawOver2);
// Quaternion result;
// result.w = cosYawOver2 * cosPitchOver2 * cosRollOver2 +
// sinYawOver2 * sinPitchOver2 * sinRollOver2;
// result.x = sinYawOver2 * cosPitchOver2 * cosRollOver2 +
// cosYawOver2 * sinPitchOver2 * sinRollOver2;
// result.y = cosYawOver2 * sinPitchOver2 * cosRollOver2 -
// sinYawOver2 * cosPitchOver2 * sinRollOver2;
// result.z = cosYawOver2 * cosPitchOver2 * sinRollOver2 -
// sinYawOver2 * sinPitchOver2 * cosRollOver2;
// return result;
}
/// <summary>

View File

@ -87,13 +87,11 @@ namespace LinearAlgebra {
return v;
}
#else
public static Spherical FromVector3(Vector3Float v)
{
public static Spherical FromVector3(Vector3Float v) {
float distance = v.magnitude;
if (distance == 0.0f)
return Spherical.zero;
else
{
else {
float verticalAngle = (float)(Math.PI / 2 - Math.Acos(v.vertical / distance)) * AngleFloat.Rad2Deg;
float horizontalAngle = (float)Math.Atan2(v.horizontal, v.depth) * AngleFloat.Rad2Deg;
return Degrees(distance, horizontalAngle, verticalAngle);
@ -178,36 +176,202 @@ namespace LinearAlgebra {
return distance;
}
public static Spherical Average(List<Spherical> vectors) {
float sumSinPhiCosTheta = 0.0f;
float sumSinPhiSinTheta = 0.0f;
float sumCosPhi = 0.0f;
public static Spherical Average(Spherical v1, Spherical v2) {
const float EPS = 1e-6f;
int n = vectors.Count;
// Angles in radians
float a1 = v1.direction.horizontal.inRadians;
float a2 = v2.direction.horizontal.inRadians;
float e1 = v1.direction.vertical.inRadians;
float e2 = v2.direction.vertical.inRadians;
// Step 1: Accumulate sine and cosine components
foreach(Spherical v in vectors) {
float sinHorizontal = AngleFloat.Sin(v.direction.horizontal);
sumSinPhiCosTheta += v.distance * sinHorizontal * AngleFloat.Cos(v.direction.vertical);
sumSinPhiSinTheta += v.distance * sinHorizontal * AngleFloat.Sin(v.direction.vertical);
sumCosPhi += v.distance * AngleFloat.Cos(v.direction.horizontal);
// Fast path: exactly same direction (allowing wrap for azimuth) -> preserve exact angles
bool sameAz = MathF.Abs(MathF.IEEERemainder(a1 - a2, MathF.PI * 2f)) < EPS;
bool sameEl = MathF.Abs(e1 - e2) < EPS;
if (sameAz && sameEl) {
// Distances may differ; average distance but keep exact angles from v1
float rAvgExact = 0.5f * (v1.distance + v2.distance);
return new Spherical(rAvgExact, v1.direction);
}
// Step 2: Calculate average components
float avgSinPhiCosTheta = sumSinPhiCosTheta / n;
float avgSinPhiSinTheta = sumSinPhiSinTheta / n;
float avgCosPhi = sumCosPhi / n;
// Horizontal unit-circle sum
float cx = MathF.Cos(a1) + MathF.Cos(a2);
float cy = MathF.Sin(a1) + MathF.Sin(a2);
// Step 3: Calculate the magnitude of the average vector
float rAvg = MathF.Sqrt(avgSinPhiCosTheta * avgSinPhiCosTheta +
avgSinPhiSinTheta * avgSinPhiSinTheta +
avgCosPhi * avgCosPhi);
// Vertical as z = sin(el)
float z1 = MathF.Sin(e1);
float z2 = MathF.Sin(e2);
float cz = z1 + z2;
// Step 4: Calculate average angles
AngleFloat horizontalAvg = AngleFloat.Acos(avgCosPhi / rAvg); // Handle rAvg != 0 case
AngleFloat verticalAvg = AngleFloat.Atan2(avgSinPhiSinTheta, avgSinPhiCosTheta);
// Magnitude of summed unit-direction vectors
float sumX = cx;
float sumY = cy;
float sumZ = cz;
float magSum = MathF.Sqrt(sumX * sumX + sumY * sumY + sumZ * sumZ);
// If the two direction unit-vectors cancel (or nearly), return zero distance.
if (magSum < EPS) {
return Spherical.Radians(0f, 0f, 0f);
}
// Normalized averaged direction components
float ux = sumX / magSum;
float uy = sumY / magSum;
float uz = sumZ / magSum;
// Compute averaged angles from normalized vector
float azAvgRad = MathF.Atan2(uy, ux);
float elAvgRad = MathF.Asin(Float.Clamp(uz, -1f, 1f));
// Average distance (arithmetic mean)
float rAvg = 0.5f * (v1.distance + v2.distance);
return Spherical.Radians(rAvg, azAvgRad, elAvgRad);
}
public static Spherical Average(List<Spherical> vectors) {
// float sumSinPhiCosTheta = 0.0f;
// float sumSinPhiSinTheta = 0.0f;
// float sumCosPhi = 0.0f;
// int n = vectors.Count;
// // Step 1: Accumulate sine and cosine components
// foreach(Spherical v in vectors) {
// float sinHorizontal = AngleFloat.Sin(v.direction.horizontal);
// sumSinPhiCosTheta += v.distance * sinHorizontal * AngleFloat.Cos(v.direction.vertical);
// sumSinPhiSinTheta += v.distance * sinHorizontal * AngleFloat.Sin(v.direction.vertical);
// sumCosPhi += v.distance * AngleFloat.Cos(v.direction.horizontal);
// }
// // Step 2: Calculate average components
// float avgSinPhiCosTheta = sumSinPhiCosTheta / n;
// float avgSinPhiSinTheta = sumSinPhiSinTheta / n;
// float avgCosPhi = sumCosPhi / n;
// // Step 3: Calculate the magnitude of the average vector
// float rAvg = MathF.Sqrt(avgSinPhiCosTheta * avgSinPhiCosTheta +
// avgSinPhiSinTheta * avgSinPhiSinTheta +
// avgCosPhi * avgCosPhi);
// // Step 4: Calculate average angles
// AngleFloat horizontalAvg = AngleFloat.Acos(avgCosPhi / rAvg); // Handle rAvg != 0 case
// AngleFloat verticalAvg = AngleFloat.Atan2(avgSinPhiSinTheta, avgSinPhiCosTheta);
// return new Spherical(rAvg, new Direction(horizontalAvg, verticalAvg));
if (vectors == null || vectors.Count == 0)
throw new ArgumentException("vectors must contain at least one element", nameof(vectors));
float sumX = 0f, sumY = 0f, sumZ = 0f;
int n = vectors.Count;
foreach (var v in vectors) {
// AngleFloat -> radians; assume AngleFloat provides Radians property
float theta = v.direction.horizontal.inRadians; // azimuth
float phi = v.direction.vertical.inRadians; // elevation
float cosPhi = MathF.Cos(phi);
float sinPhi = MathF.Sin(phi);
float cosTheta = MathF.Cos(theta);
float sinTheta = MathF.Sin(theta);
float x = v.distance * cosPhi * cosTheta;
float y = v.distance * cosPhi * sinTheta;
float z = v.distance * sinPhi;
sumX += x;
sumY += y;
sumZ += z;
}
float avgX = sumX / n;
float avgY = sumY / n;
float avgZ = sumZ / n;
float rAvg = MathF.Sqrt(avgX * avgX + avgY * avgY + avgZ * avgZ);
if (rAvg == 0f) {
return new Spherical(0f, new Direction(AngleFloat.Radians(0f), AngleFloat.Radians(0f)));
}
// elevation = asin(z / r)
AngleFloat verticalAvg = AngleFloat.Asin(avgZ / rAvg); // -90..90
// azimuth = atan2(y, x) -> -pi..pi
AngleFloat horizontalAvg = AngleFloat.Atan2(avgY, avgX); // -180..180
return new Spherical(rAvg, new Direction(horizontalAvg, verticalAvg));
}
/*
public static Spherical Average(IEnumerable<Spherical> vectors) {
const float EPS = 1e-6f;
if (vectors == null) throw new ArgumentNullException(nameof(vectors));
float sumRx = 0f, sumRy = 0f, sumRz = 0f;
float sumDistances = 0f;
int count = 0;
bool firstSet = false;
float firstAz = 0f, firstEl = 0f;
bool allSameDirection = true;
foreach (var v in vectors) {
float az = v.direction.horizontal.inRadians; // horizontal (azimuth)
float el = v.direction.vertical.inRadians; // vertical (elevation)
if (!firstSet) {
firstSet = true;
firstAz = az;
firstEl = el;
}
else {
if (MathF.Abs(MathF.IEEERemainder(az - firstAz, MathF.PI * 2f)) >= EPS ||
MathF.Abs(el - firstEl) >= EPS) {
allSameDirection = false;
}
}
float cosEl = MathF.Cos(el);
float ux = cosEl * MathF.Cos(az); // x
float uy = cosEl * MathF.Sin(az); // y
float uz = MathF.Sin(el); // z
sumRx += v.distance * ux;
sumRy += v.distance * uy;
sumRz += v.distance * uz;
sumDistances += v.distance;
count++;
}
if (count == 0) throw new ArgumentException("Sequence contains no elements", nameof(vectors));
// All directions equal -> preserve exact angles, average distance
if (allSameDirection) {
float rAvg = sumDistances / count;
return new Spherical(rAvg, Direction.Radians(firstAz, firstEl));
}
// Total vector sum V
float Vx = sumRx;
float Vy = sumRy;
float Vz = sumRz;
float Vmag = MathF.Sqrt(Vx * Vx + Vy * Vy + Vz * Vz);
if (Vmag < EPS) {
// Directions cancel out -> zero distance, angles arbitrary
return Spherical.Radians(0f, 0f, 0f);
}
float azAvg = MathF.Atan2(Vy, Vx);
float elAvg = MathF.Asin(Float.Clamp(Vz / Vmag, -1f, 1f));
float rAvgFinal = Vmag / count;
return Spherical.Radians(rAvgFinal, azAvg, elAvg);
}
*/
}
}

View File

@ -24,6 +24,10 @@ namespace LinearAlgebra.Test {
a = AngleFloat.Degrees(angle);
Assert.AreEqual(-90, a.inDegrees);
angle = -270.0f;
a = AngleFloat.Degrees(angle);
Assert.AreEqual(90, a.inDegrees);
// Radians
angle = 0.0f;
a = AngleFloat.Radians(angle);

View File

@ -33,6 +33,13 @@ namespace LinearAlgebra.Test {
Assert.AreEqual(30, d.vertical.inDegrees, 0.0001f);
}
[Test]
public void DegreesNormalize1() {
Direction d = Direction.Degrees(112, 91);
Assert.AreEqual(-68, d.horizontal.inDegrees, 0.0001f);
Assert.AreEqual(89, d.vertical.inDegrees, 0.0001f);
}
[Test]
public void RadiansEquivalentToDegreesConversion() {
Direction d1 = Direction.Radians((float)Math.PI / 3, (float)Math.PI / 4);
@ -68,6 +75,15 @@ namespace LinearAlgebra.Test {
Assert.AreEqual(0, v.depth, 0.0001f);
}
[Test]
public void ToVector3Left() {
Direction d = Direction.left;
Vector3Float v = d.ToVector3();
Assert.AreEqual(-1, v.horizontal, 0.0001f);
Assert.AreEqual(0, v.vertical, 0.0001f);
Assert.AreEqual(0, v.depth, 0.0001f);
}
[Test]
public void FromVector3Forward() {
Vector3Float v = new(0, 0, 1);
@ -85,6 +101,15 @@ namespace LinearAlgebra.Test {
Assert.AreEqual(d1.vertical.inDegrees, d2.vertical.inDegrees, 0.0001f);
}
[Test]
public void ToVector3AndBack2() {
Direction d1 = Direction.Degrees(135, 85);
Vector3Float v = d1.ToVector3();
Direction d2 = Direction.FromVector3(v);
Assert.AreEqual(d1.horizontal.inDegrees, d2.horizontal.inDegrees, 0.0001f);
Assert.AreEqual(d1.vertical.inDegrees, d2.vertical.inDegrees, 0.0001f);
}
[Test]
public void Compare() {
Direction d1 = Direction.Degrees(45, 135);

View File

@ -1,5 +1,6 @@
#if !UNITY_5_6_OR_NEWER
using System;
using System.Collections.Generic;
using NUnit.Framework;
namespace LinearAlgebra.Test {
@ -48,6 +49,191 @@ namespace LinearAlgebra.Test {
Assert.AreEqual(45.0f, r.direction.horizontal.inDegrees, "Addition(1 0 90)");
Assert.AreEqual(45.0f, r.direction.vertical.inDegrees, 1.0E-05F, "Addition(1 0 90)");
}
[Test]
public void Average2_IdenticalVectors() {
Direction dir = Direction.Radians(MathF.PI / 4f, MathF.PI / 6f);
Spherical v = new(2.5f, dir);
Spherical avg = Spherical.Average(v, v);
Assert.AreEqual(2.5f, avg.distance, 1e-5f);
Assert.AreEqual(dir.horizontal, avg.direction.horizontal);
Assert.AreEqual(dir.vertical, avg.direction.vertical);
}
[Test]
public void Average2_OppositeUnitVectors() {
// Two opposite vectors: same distance, horizontal opposite (pi apart), same vertical
Spherical v1 = Spherical.Radians(1f, 0f, 0f);
Spherical v2 = Spherical.Radians(1f, MathF.PI, 0f);
Spherical avg = Spherical.Average(v1, v2);
Assert.AreEqual(0f, avg.distance, 1e-4f);
// When distance is zero, angles may be undefined; allow any angle but ensure near-zero magnitude
}
[Test]
public void Average2_WeightedByDistance() {
// Two vectors same direction but different distances -> weighted average distance
Direction dir = Direction.Radians(MathF.PI / 3f, MathF.PI / 4f);
Spherical a = new(1f, dir);
Spherical b = new(3f, dir);
Spherical avg = Spherical.Average(a, b);
// average distance should be (1+3)/2 = 2
Assert.AreEqual(2f, avg.distance, 1e-5f);
Assert.AreEqual(dir.horizontal.inRadians, avg.direction.horizontal.inRadians, 1e-5f);
Assert.AreEqual(dir.vertical.inRadians, avg.direction.vertical.inRadians, 1e-5f);
}
[Test]
public void Average2_OppositeButNotExact_NotZero() {
// Nearly opposite but not exact; expect a valid averaged direction and averaged distance
Direction d1 = Direction.Radians(0f, 0f);
Direction d2 = Direction.Radians(MathF.PI - 1e-3f, 0.0f); // slight offset
Spherical v1 = new(2.0f, d1);
Spherical v2 = new(4.0f, d2);
Spherical avg = Spherical.Average(v1, v2);
// Distance is arithmetic mean
Assert.AreEqual(3.0f, avg.distance, 1e-5f);
// Averaged azimuth should be near +pi/2 or -pi/2? we can check it's not NaN and unit-vector properties hold
float ux = MathF.Cos(avg.direction.horizontal.inRadians) * MathF.Cos(avg.direction.vertical.inRadians);
float uy = MathF.Sin(avg.direction.horizontal.inRadians) * MathF.Cos(avg.direction.vertical.inRadians);
float uz = MathF.Sin(avg.direction.vertical.inRadians);
float mag = MathF.Sqrt(ux * ux + uy * uy + uz * uz);
Assert.IsTrue(mag > 0.999f && mag < 1.001f);
}
[Test]
public void Average2_BasicAverageDirectionAndDistance() {
// Two different directions not cancelling: expect vector-average result
Direction d1 = Direction.Radians(MathF.PI / 6f, MathF.PI / 12f); // 30°, 15°
Direction d2 = Direction.Radians(MathF.PI / 3f, MathF.PI / 18f); // 60°, 10°
Spherical v1 = new(2.0f, d1);
Spherical v2 = new(4.0f, d2);
Spherical avg = Spherical.Average(v1, v2);
// Distance is arithmetic mean
Assert.AreEqual(3.0f, avg.distance, 1e-5f);
// Check averaged unit-vector equals normalized sum of unit vectors computed here
float a1 = d1.horizontal.inRadians;
float a2 = d2.horizontal.inRadians;
float e1 = d1.vertical.inRadians;
float e2 = d2.vertical.inRadians;
float cx = MathF.Cos(a1) + MathF.Cos(a2);
float cy = MathF.Sin(a1) + MathF.Sin(a2);
float z1 = MathF.Sin(e1);
float z2 = MathF.Sin(e2);
float cz = z1 + z2;
float mag = MathF.Sqrt(cx * cx + cy * cy + cz * cz);
Assert.IsTrue(mag > 1e-6f);
float ux = cx / mag;
float uy = cy / mag;
float uz = cz / mag;
// Reconstruct direction from avg result
float uxAvg = MathF.Cos(avg.direction.horizontal.inRadians) * MathF.Cos(avg.direction.vertical.inRadians);
float uyAvg = MathF.Sin(avg.direction.horizontal.inRadians) * MathF.Cos(avg.direction.vertical.inRadians);
float uzAvg = MathF.Sin(avg.direction.vertical.inRadians);
Assert.AreEqual(ux, uxAvg, 1e-4f);
Assert.AreEqual(uy, uyAvg, 1e-4f);
Assert.AreEqual(uz, uzAvg, 1e-4f);
}
[Test]
public void Average_IdenticalVectors() {
var dir = Direction.Radians(MathF.PI / 4f, MathF.PI / 6f);
var v = new Spherical(2.5f, dir);
var list = new List<Spherical> { v, v, v };
var avg = Spherical.Average(list);
Assert.AreEqual(2.5f, avg.distance, 1e-5f);
Assert.AreEqual(dir.horizontal, avg.direction.horizontal);
Assert.AreEqual(dir.vertical, avg.direction.vertical);
}
[Test]
public void Average_SingleElement() {
Spherical s = Spherical.Radians(1.234f, 0.3f, -0.7f);
Spherical avg = Spherical.Average([s]);
Assert.AreEqual(s.distance, avg.distance, 1e-5f);
Assert.AreEqual(s.direction.horizontal.inRadians, avg.direction.horizontal.inRadians, 1e-5f);
Assert.AreEqual(s.direction.vertical.inRadians, avg.direction.vertical.inRadians, 1e-5f);
}
[Test]
public void Average_OppositeUnitVectors() {
// Two opposite vectors: same distance, horizontal opposite (pi apart), same vertical
Spherical v1 = Spherical.Radians(1f, 0f, 0f);
Spherical v2 = Spherical.Radians(1f, MathF.PI, 0f);
Spherical avg = Spherical.Average([v1, v2]);
Assert.AreEqual(0f, avg.distance, 1e-4f);
// When distance is zero, angles may be undefined; allow any angle but ensure near-zero magnitude
}
[Test]
public void Average_WeightedByDistance() {
// Two vectors same direction but different distances -> weighted average distance
Direction dir = Direction.Radians(MathF.PI / 3f, MathF.PI / 4f);
Spherical a = new(1f, dir);
Spherical b = new(3f, dir);
Spherical avg = Spherical.Average([a, b]);
// average distance should be (1+3)/2 = 2
Assert.AreEqual(2f, avg.distance, 1e-5f);
Assert.AreEqual(dir.horizontal.inRadians, avg.direction.horizontal.inRadians, 1e-5f);
Assert.AreEqual(dir.vertical.inRadians, avg.direction.vertical.inRadians, 1e-5f);
}
[Test]
public void Average_AxisSymmetricAroundVertical() {
// Four vectors around azimuth 0, pi/2, pi, 3pi/2 at same elevation (vertical) angle phi
float phi = MathF.PI / 6f; // elevation from horizontal plane
var dirs = new List<Spherical> {
new(1f, Direction.Radians(0f, phi)),
new(1f, Direction.Radians(MathF.PI/2, phi)),
new(1f, Direction.Radians(MathF.PI, phi)),
new(1f, Direction.Radians(3*MathF.PI/2, phi))
};
Spherical avg = Spherical.Average(dirs);
// rAvg should equal r * sin(elevation) = sin(phi)
Assert.AreEqual(MathF.Sin(phi), avg.distance, 1e-4f);
// vertical angle undefined when horizontal xy components cancel; allow any angle but ensure r matches
}
[Test]
public void Average_AxisSymmetricAroundVertical2() {
// Four vectors around azimuth 0, pi/2, pi, 3pi/2 at same polar angle from vertical (alpha)
float alpha = MathF.PI / 6f; // polar angle from vertical
float elevation = MathF.PI / 2f - alpha; // convert polar-from-vertical to elevation
var dirs = new List<Spherical> {
new(1f, Direction.Radians(0f, elevation)),
new(1f, Direction.Radians(MathF.PI/2, elevation)),
new(1f, Direction.Radians(MathF.PI, elevation)),
new(1f, Direction.Radians(3*MathF.PI/2, elevation))
};
Spherical avg = Spherical.Average(dirs);
// rAvg should equal r * sin(elevation) which equals cos(alpha)
Assert.AreEqual(MathF.Cos(alpha), avg.distance, 1e-4f);
}
}
}
#endif