Merge commit '8e87e4ea77308b51c3691bdad96e7f9707952821' as 'NanoBrain'

This commit is contained in:
Pascal Serrarens 2026-04-07 09:12:29 +02:00
commit 6f398ad4bf
185 changed files with 14014 additions and 19173 deletions

80
.gitignore vendored
View File

@ -1,80 +0,0 @@
# ---> Unity
# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
#
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/
# Recordings can get excessive in size
/[Rr]ecordings/
# Uncomment this line if you wish to ignore the asset store tools plugin
# /[Aa]ssets/AssetStoreTools*
# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*
# Visual Studio cache directory
.vs/
.vscode/
# Gradle cache directory
.gradle/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta
# Unity3D generated file on crash reports
sysinfo.txt
# Builds
*.apk
*.aab
*.unitypackage
*.unitypackage.meta
*.app
# Crashlytics generated file
crashlytics-build.properties
# Packed Addressables
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
# Temporary auto-generated Android Assets
/[Aa]ssets/[Ss]treamingAssets/aa.meta
/[Aa]ssets/[Ss]treamingAssets/aa/*
# Passer
#/Samples
/Samples.meta
/Samples~.meta

508
Cluster.cs Normal file
View File

@ -0,0 +1,508 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[Serializable]
public class Cluster : Nucleus {
public string baseName {
get {
int colonPositon = this.name.IndexOf(':');
if (colonPositon < 0)
return this.name;
return this.name[..colonPositon];
}
}
#region Init
public Cluster(ClusterPrefab prefab, Cluster parent) {
this.prefab = prefab;
this.name = prefab.name;
this.parent = parent;
this.parent?.clusterNuclei.Add(this);
ClonePrefab();
_ = this.inputs;
this.sortedNuclei = TopologicalSort(this.clusterNuclei);
}
public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) {
this.prefab = prefab;
this.name = prefab.name;
this.clusterPrefab = parent;
if (this.clusterPrefab != null)
this.clusterPrefab.nuclei.Add(this);
ClonePrefab();
_ = this.inputs;
this.sortedNuclei = TopologicalSort(this.clusterNuclei);
}
private void ClonePrefab() {
Nucleus[] prefabNuclei = this.prefab.nuclei.ToArray();
// first clone the nuclei without their connections
foreach (Nucleus nucleus in this.prefab.nuclei) {
nucleus.ShallowCloneTo(this);
}
Nucleus[] clonedNuclei = this.clusterNuclei.ToArray();
// Now clone the connections
for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) {
Nucleus prefabNucleus = prefabNuclei[nucleusIx];
if (prefabNucleus is not Neuron prefabNeuron)
continue;
Nucleus clonedNucleus = clonedNuclei[nucleusIx];
if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron)
continue;
// Copy the receivers, which will also create the synapses
// Clusters do not have receivers...
foreach (Nucleus receiver in prefabNeuron.receivers.ToArray()) {
int ix = GetNucleusIndex(prefabNuclei, receiver);
if (ix < 0)
continue;
if (clonedNuclei[ix] is not Nucleus clonedReceiver)
continue;
// Find the synapse for the weight
float weight = 1;
foreach (Synapse synapse in receiver.synapses) {
// Find the weight for this synapse
if (synapse.neuron == prefabNucleus) {
weight = synapse.weight;
break;
}
}
clonedNeuron.AddReceiver(clonedReceiver, weight);
}
}
// Copy nucleus arrays for receptors
for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) {
Nucleus prefabNucleus = prefabNuclei[nucleusIx];
if (prefabNucleus is not IReceptor prefabReceptor)
continue;
if (prefabReceptor.nucleiArray == null || prefabReceptor.nucleiArray.Length == 0)
continue;
IReceptor clonedNucleus = clonedNuclei[nucleusIx] as IReceptor;
if (prefabReceptor == prefabReceptor.nucleiArray[0]) {
// We clone the array only for the first entry
NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length, "array");
int arrayIx = 0;
foreach (Nucleus prefabArrayNucleus in prefabReceptor.nucleiArray) {
int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus);
if (arrayNucleusIx >= 0) {
Nucleus clonedArrayNucleus = clonedNuclei[arrayNucleusIx];
clonedArray.nuclei[arrayIx] = clonedArrayNucleus;
}
else {
Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones");
}
arrayIx++;
}
//clonedNucleus.array = clonedArray;
clonedNucleus.nucleiArray = clonedArray.nuclei;
}
else {
// The others will refer to the array created for the first nucleus in the array
int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabReceptor.nucleiArray[0]);
IReceptor clonedFirstNucleus = clonedNuclei[firstNucleusIx] as IReceptor;
clonedNucleus.nucleiArray = clonedFirstNucleus.nucleiArray;
}
}
foreach (Nucleus nucleus in this.clusterNuclei) {
if (nucleus is Cluster clonedSubCluster)
RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this);
}
}
// Sort the nuclei in a correct evaluation order
private List<Nucleus> TopologicalSort(List<Nucleus> nodes) {
Dictionary<Nucleus, int> inDegree = new();
foreach (Nucleus node in nodes)
inDegree[node] = 0; // Initialize in-degree to zero
// Calculate in-degrees
foreach (Nucleus node in nodes) {
if (node is Cluster cluster) {
foreach (Nucleus receiver in cluster.CollectReceivers())
inDegree[receiver]++;
}
else if (node is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers)
inDegree[receiver]++;
}
}
Queue<Nucleus> queue = new();
foreach (Nucleus node in nodes) {
if (inDegree[node] == 0) // Nodes with no dependencies
queue.Enqueue(node);
}
// The queue basically stores all input nuclei?
List<Nucleus> sortedOrder = new();
while (queue.Count > 0) {
Nucleus current = queue.Dequeue();
sortedOrder.Add(current); // Process the node
if (current is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers) {
inDegree[receiver]--;
if (inDegree[receiver] == 0) // If all dependencies resolved
queue.Enqueue(receiver);
}
}
else if (current is Cluster cluster) {
foreach (Nucleus receiver in cluster.CollectReceivers()) {
inDegree[receiver]--;
if (inDegree[receiver] == 0) // If all dependencies resolved
queue.Enqueue(receiver);
}
}
}
// Check for cycles in the graph
if (sortedOrder.Count != nodes.Count)
throw new InvalidOperationException("Graph is not a DAG; a cycle exists.");
return sortedOrder;
}
public override Nucleus Clone(ClusterPrefab parent) {
Cluster clone = new(this.prefab, parent);
foreach (Synapse synapse in this.synapses) {
Synapse clonedSynapse = clone.AddSynapse(synapse.neuron);
clonedSynapse.weight = synapse.weight;
}
foreach (Neuron output in this.outputs) {
foreach (Nucleus receiver in output.receivers) {
int ix = GetNucleusIndex(this.clusterNuclei.ToArray(), output);
if (ix < 0)
continue;
if (clone.clusterNuclei[ix] is not Neuron clonedOutput)
continue;
clonedOutput.AddReceiver(receiver);
}
}
return clone;
}
public override Nucleus ShallowCloneTo(Cluster parent) {
Cluster clone = new(this.prefab, parent) {
name = this.name,
clusterPrefab = this.clusterPrefab,
};
return clone;
}
private static void RestoreAllExternalReceivers(Cluster clonedCluster, ClusterPrefab prefabParent, Cluster clonedParent) {
int clonedClusterIx = GetNucleusIndex(clonedParent.clusterNuclei, clonedCluster);
if (prefabParent.nuclei[clonedClusterIx] is not Cluster sourceCluster)
return;
for (int nucleusIx = 0; nucleusIx < sourceCluster.clusterNuclei.Count; nucleusIx++) {
Nucleus sourceNucleus = sourceCluster.clusterNuclei[nucleusIx];
if (sourceNucleus is not Neuron sourceNeuron)
continue;
if (clonedCluster.clusterNuclei[nucleusIx] is not Neuron clonedNeuron)
continue;
// copy the receivers (and thus synapses) from the source to the clone
foreach (Nucleus receiver in sourceNeuron.receivers) {
int ix = GetNucleusIndex(prefabParent.nuclei, receiver);
if (ix < 0 || ix >= clonedParent.clusterNuclei.Count)
continue;
Nucleus clonedReceiver = clonedParent.clusterNuclei[ix];
// Find the synapse for the weight
float weight = 1;
foreach (Synapse synapse in receiver.synapses) {
// Find the weight for this synapse
if (synapse.neuron == sourceNucleus) {
weight = synapse.weight;
break;
}
}
clonedNeuron.AddReceiver(clonedReceiver, weight);
// Debug.Log($"external: {clonedReceiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}");
}
}
}
protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) {
for (int i = 0; i < nuclei.Length; i++) {
if (nucleus == nuclei[i])
return i;
}
return -1;
}
public static int GetNucleusIndex(List<Nucleus> nuclei, Nucleus nucleus) {
int i = 0;
foreach (Nucleus nucleiElement in nuclei) {
//for (int i = 0; i < nuclei.Length; i++) {
if (nucleus == nucleiElement)
return i;
i++;
}
return -1;
}
#endregion Init
public ClusterPrefab prefab;
[SerializeReference]
public List<Nucleus> clusterNuclei = new();
// the nuclei sorted using topological sorting
// to ensure that the cluster is computer in the right order
public List<Nucleus> sortedNuclei;
//public Dictionary<string, Nucleus> nucleiDict = new();
public List<Nucleus> _inputs = null;
public virtual List<Nucleus> inputs {
get {
if (this._inputs == null) {
this._inputs = new();
foreach (Nucleus nucleus in this.clusterNuclei) {
// inputs have no synapses
if (nucleus.synapses.Count == 0)
this._inputs.Add(nucleus);
}
ComputeOrders();
}
return this._inputs;
}
}
public Dictionary<Nucleus, List<Nucleus>> computeOrders = new();
private void ComputeOrders() {
foreach (Nucleus input in this._inputs)
computeOrders[input] = TopologicalSort2(input);
}
private List<Nucleus> TopologicalSort2(Nucleus startNode) {
Dictionary<Nucleus, int> inDegree = new();
HashSet<Nucleus> visited = new();
// Initialize in-degrees and mark all nodes as unvisited
foreach (Nucleus node in this.clusterNuclei)
inDegree[node] = 0;
// Calculate in-degrees for all nodes reachable from the start node
Queue<Nucleus> queue = new Queue<Nucleus>();
queue.Enqueue(startNode);
visited.Add(startNode);
while (queue.Count > 0) {
Nucleus current = queue.Dequeue();
List<Nucleus> receivers = null;
if (current is Neuron neuron)
receivers = neuron.receivers;
else if (current is Cluster cluster)
receivers = cluster.CollectReceivers();
// if (current is Neuron neuron) {
foreach (Nucleus receiver in receivers) {
if (!visited.Contains(receiver)) {
visited.Add(receiver);
queue.Enqueue(receiver);
}
inDegree[receiver]++;
}
// }
}
// Perform topological sort on all reachable nodes
queue.Clear();
foreach (Nucleus node in visited) {
if (inDegree[node] == 0)
queue.Enqueue(node);
}
List<Nucleus> sortedOrder = new List<Nucleus>();
while (queue.Count > 0) {
Nucleus current = queue.Dequeue();
sortedOrder.Add(current); // Process the node
List<Nucleus> receivers = null;
if (current is Neuron neuron)
receivers = neuron.receivers;
else if (current is Cluster cluster)
receivers = cluster.CollectReceivers();
//if (current is Neuron neuron) {
foreach (Nucleus receiver in receivers) {
if (visited.Contains(receiver)) {
inDegree[receiver]--;
if (inDegree[receiver] == 0) // If all dependencies resolved
queue.Enqueue(receiver);
}
}
//}
}
// Check for cycles in the graph
if (sortedOrder.Count != visited.Count)
throw new InvalidOperationException("Graph is not a DAG; a cycle exists.");
return sortedOrder;
}
public virtual Neuron defaultOutput {//=> this.nuclei[0] as Nucleus;
get {
if (this.clusterNuclei.Count > 0)
return this.clusterNuclei[0] as Neuron;
return null;
}
}
protected List<Neuron> _outputs = null;
public List<Neuron> outputs {
get {
if (this._outputs == null) {
this._outputs = new();
foreach (Nucleus nucleus in this.clusterNuclei) {
if (nucleus is Neuron neuron) // && neuron.receivers.Count == 0)
this._outputs.Add(neuron);
}
}
return this._outputs;
}
}
public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) {
foreach (Nucleus receptor in this.clusterNuclei) {
if (receptor is Nucleus nucleus)
if (nucleus.name == nucleusName) {
foundNucleus = nucleus;
return true;
}
}
foundNucleus = null;
return false;
}
public Nucleus GetNucleus(string nucleusName) {
int dotPosition = nucleusName.IndexOf('.');
if (dotPosition >= 0) {
string clusterName = nucleusName[..dotPosition];
string clusterName0 = clusterName + ": 0";
foreach (Nucleus nucleus in this.clusterNuclei) {
if (nucleus is Cluster cluster) {
if (cluster.name == clusterName || cluster.name == clusterName0) {
string subNucleusName = nucleusName[(dotPosition + 1)..];
return cluster.GetNucleus(subNucleusName);
}
}
}
return null;
}
else {
string nucleusName0 = nucleusName + ": 0";
foreach (Nucleus nucleus in this.clusterNuclei) {
if (nucleus is IReceptor receptor) {
if (nucleus.name == nucleusName | nucleus.name == nucleusName0)
return nucleus;
}
else if (nucleus.name == nucleusName)
return nucleus;
}
return null;
}
}
// [Obsolete("Use GetNucleus instead")]
// public IReceptor GetReceptor(string receptorName) {
// return GetNucleus(receptorName) as IReceptor;
// }
#region Receivers
public virtual List<Nucleus> CollectReceivers() {
List<Nucleus> receivers = new();
foreach (Neuron output in this.outputs) {
foreach (Nucleus receiver in output.receivers) {
// Only add receivers outside this cluster
if (receiver.clusterPrefab != this.prefab)
receivers.Add(receiver);
//receivers.AddRange(output.receivers);
}
}
return receivers;
}
#endregion Receivers
#region Update
public void UpdateFromNucleus(Nucleus startNucleus) {
// no bias+synapse input state calculation for now...
if (this.computeOrders.ContainsKey(startNucleus) == false) {
//Debug.LogError($"{this.name} compute orders does not contain an order for {startNucleus.name}");
return;
}
List<Nucleus> computeOrder = this.computeOrders[startNucleus];
if (startNucleus.trace)
Debug.Log($"Update from {startNucleus.name}");
foreach (Nucleus nucleus in computeOrder) {
nucleus.UpdateStateIsolated();
if (startNucleus.trace && nucleus is Neuron neuron)
Debug.Log($" {nucleus.name}[{nucleus.GetHashCode()}] = {neuron.outputValue}");
}
// continue in parent
this.parent?.UpdateFromNucleus(this);
UpdateNuclei();
}
public override void UpdateStateIsolated() {
throw new Exception("Cluster should not be updated!");
// float3 sum = this.bias;
// //Applying the weight factors
// foreach (Synapse synapse in this.synapses) {
// if (lengthsq(synapse.neuron.outputValue) > 0) {
// sum += synapse.weight * synapse.neuron.outputValue;
// }
// }
// foreach (Nucleus nucleus in this.sortedNuclei)
// nucleus.UpdateStateIsolated();
// UpdateNuclei();
}
public override void UpdateNuclei() {
foreach (Nucleus nucleus in this.clusterNuclei)
nucleus.UpdateNuclei();
}
#endregion Update
}

2
Cluster.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f13cdc4a175a9f379a00317ae68d8bea

116
ClusterPrefab.cs Normal file
View File

@ -0,0 +1,116 @@
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Passer/Cluster")]
public class ClusterPrefab : ScriptableObject {
// The ScriptableObject asset from which the runtime object has been created
[SerializeReference]
public List<Nucleus> nuclei = new();
public virtual Nucleus output => this.nuclei[0] as Nucleus;
public List<Nucleus> _inputs = null;
public virtual List<Nucleus> inputs {
get {
if (this._inputs == null) {
this._inputs = new();
foreach (Nucleus receptor in this.nuclei) {
if (receptor is Nucleus nucleus) {
// inputs have no incoming synapses yet.
if (nucleus.synapses.Count == 0)
this._inputs.Add(nucleus);
}
}
}
return this._inputs;
}
}
private List<Nucleus> _outputs = null;
public List<Nucleus> outputs {
get {
if (this._outputs == null)
RefreshOutputs();
return this._outputs;
}
}
public void RefreshOutputs() {
this._outputs = new();
foreach (Nucleus nucleus in this.nuclei) {
if (nucleus is Neuron neuron && neuron.receivers.Count == 0)
this._outputs.Add(nucleus);
}
}
public Nucleus GetNucleus(string nucleusName) {
foreach (Nucleus nucleus in this.nuclei) {
if (nucleus.name == nucleusName)
return nucleus;
}
return null;
}
// Call this function to ensure that there is at least one nucleus
// This is an invariant and should be ensured before the nucleus is used
// because output requires it.
public void EnsureInitialization() {
nuclei ??= new List<Nucleus>();
if (nuclei.Count == 0)
new Neuron(this, "Output"); // Every cluster should have at least 1 neuron
}
public void GarbageCollection() {
HashSet<Nucleus> visitedNuclei = new();
foreach (Nucleus output in this.outputs)
MarkNuclei(visitedNuclei, output);
//Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei");
this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false);
}
public void MarkNuclei(HashSet<Nucleus> visitedNuclei, Nucleus nucleus) {
if (nucleus is null)
return;
if (nucleus.parent != null && nucleus.parent.prefab != this)
visitedNuclei.Add(nucleus.parent);
else
visitedNuclei.Add(nucleus);
if (nucleus.synapses != null) {
HashSet<Synapse> visitedSynapses = new();
foreach (Synapse synapse in nucleus.synapses) {
if (synapse != null && synapse.neuron != null) {
visitedSynapses.Add(synapse);
if (synapse.neuron is Nucleus synapse_nucleus)
MarkNuclei(visitedNuclei, synapse_nucleus);
}
}
nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false);
}
if (nucleus is Neuron neuron && neuron.receivers != null) {
HashSet<Nucleus> visitedReceivers = new();
foreach (Nucleus receiver in neuron.receivers) {
if (receiver != null && receiver != null) {
visitedReceivers.Add(receiver);
visitedNuclei.Add(receiver);
}
}
neuron.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
}
}
public virtual void UpdateNuclei() {
foreach (Nucleus nucleus in this.nuclei)
nucleus.UpdateNuclei();
}
public int GetNucleusIndex(Nucleus receiver) {
int ix = 0;
foreach (Nucleus nucleus in this.nuclei) {
if (receiver == nucleus)
return ix;
ix++;
}
return -1;
}
}

2
ClusterPrefab.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 60a957541c24c57e78018c202ebb1d9b

214
ClusterReceptor.cs Normal file
View File

@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using System.Linq;
[Serializable]
public class ClusterReceptor : Cluster, IReceptor {
public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) {
this.name = name;
this.array = new NucleusArray(this);
if (this.name.IndexOf(":") < 0)
this.name += ": 0";
}
public ClusterReceptor(ClusterPrefab prefab, ClusterPrefab parent, string name) : base(prefab, parent) {
this.name = name;
this.array = new NucleusArray(this);
}
public string GetName() {
return this.name;
}
public override Nucleus ShallowCloneTo(Cluster parent) {
ClusterReceptor clone = new(this.prefab, parent, this.name) {
clusterPrefab = this.clusterPrefab,
};
return clone;
}
public override Nucleus Clone(ClusterPrefab parent) {
ClusterReceptor clone = new(prefab, parent, this.name) {
array = this._array
};
foreach (Synapse synapse in this.synapses) {
Synapse clonedSynapse = clone.AddSynapse(synapse.neuron);
clonedSynapse.weight = synapse.weight;
}
this._outputs = null; // Make sure the output are regenerated
foreach (Neuron output in this.outputs) {
int ix = GetNucleusIndex(this.clusterNuclei, output);
if (ix < 0 || clone.clusterNuclei[ix] is not Neuron clonedOutput)
continue;
foreach (Nucleus receiver in output.receivers)
clonedOutput.AddReceiver(receiver);
}
return clone;
}
public override List<Nucleus> CollectReceivers() {
List<Nucleus> receivers = new();
foreach (Nucleus element in this.nucleiArray) {
if (element is not Cluster clusterElement)
continue;
foreach (Nucleus outputNucleus in clusterElement.clusterNuclei) {
if (outputNucleus is not Neuron output)
continue;
// this should be clusterElement.outputs,
// but outputs is not updated when correctly and may contain old data...
foreach (Nucleus receiver in output.receivers) {
// Only add receivers outside clusterElement cluster
if (receiver.clusterPrefab != clusterElement.prefab &&
receivers.Contains(receiver) == false)
receivers.Add(receiver);
}
}
}
return receivers;
}
[SerializeReference]
private NucleusArray _array;
public NucleusArray array {
set { _array = value; }
}
public Nucleus[] nucleiArray {
get { return _array.nuclei; }
set { _array.nuclei = value; }
}
public void AddReceptorElement(ClusterPrefab prefab) {
IReceptorHelpers.AddReceptorElement(this, prefab);
}
public void RemoveReceptorElement() {
IReceptorHelpers.RemoveReceptorElement(this);
}
public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) {
IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight);
}
public override void UpdateStateIsolated() {
// Clusters don't do anything,
// The nuclei in them do the work
// and should be called directly, not from the cluster
}
public override void UpdateNuclei() {
foreach (Nucleus nucleus in this.clusterNuclei)
nucleus.UpdateNuclei();
}
public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
Debug.LogError("Process Stimulus was called on clusterreceptor without a neuron specified");
}
private readonly Dictionary<int, ClusterReceptor> thingReceivers = new();
public virtual void ProcessStimulus(Neuron input, Vector3 inputValue, int thingId = 0, string thingName = null) {
CleanupReceivers();
if (!thingReceivers.TryGetValue(thingId, out ClusterReceptor selectedReceiver))
selectedReceiver = FindReceiver2(thingId, inputValue, input);
if (selectedReceiver == null)
return;
if (thingName != null) {
string baseName = selectedReceiver.name;
int colonPos = selectedReceiver.name.IndexOf(":");
if (colonPos > 0)
baseName = selectedReceiver.name[..colonPos];
selectedReceiver.name = baseName + ": " + thingName;
}
int inputIx = GetNucleusIndex(this.clusterNuclei, input);
if (inputIx < 0)
return;
if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron)
selectedNeuron.ProcessStimulusDirect(inputValue);
}
private ClusterReceptor FindReceiver2(int thingId, float3 inputValue, Neuron input) {
// No existing nucleus for this thing
ClusterReceptor selectedReceiver = null;
float selectedMagnitude = 0;
foreach (ClusterReceptor receiver in this.nucleiArray.Cast<ClusterReceptor>()) {
if (thingReceivers.ContainsValue(receiver) == false) {
// We found an unusued receiver
thingReceivers.Add(thingId, receiver);
return receiver;
}
else if (receiver.defaultOutput.isSleeping) {
// A sleeping receiver is not active and can therefore always be used
thingReceivers.Add(thingId, receiver);
receiver.bias = float3(0, 0, 0);
return receiver;
}
else if (selectedReceiver == null) {
// If we haven't found a receiver yet, just start by taking the first
selectedReceiver = receiver;
selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue);
}
// Look for the receiver with the lowest output magnitude
else {
float magnitude = length(receiver.defaultOutput.outputValue);
if (length(receiver.defaultOutput.outputValue) < selectedMagnitude) {
selectedReceiver = receiver;
selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue);
}
}
}
if (selectedReceiver != null) {
// To re-initialize the cluster (esp. memory cells)
// we update the cluster neuron twice.
// Bit of a hack.....
int inputIx = GetNucleusIndex(this.clusterNuclei, input);
if (inputIx >= 0) {
if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron)
selectedNeuron.ProcessStimulusDirect(inputValue);
}
// Replace the receiver
// Find the thingId current associated with the receiver
int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key;
if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove))
thingReceivers.Remove(keyToRemove);
// And add the new association
thingReceivers.Add(thingId, selectedReceiver);
}
return selectedReceiver;
}
private void CleanupReceivers() {
// Remove a thing-receiver connection when the nucleus is inactive
List<int> receiversToRemove = new();
foreach (KeyValuePair<int, ClusterReceptor> item in thingReceivers) {
if (item.Value != null && item.Value.defaultOutput.isSleeping)
receiversToRemove.Add(item.Key);
}
foreach (int thingId in receiversToRemove) {
Nucleus selectedReceiver = thingReceivers[thingId];
thingReceivers.Remove(thingId);
int colonPos = selectedReceiver.name.IndexOf(":");
if (colonPos > 0)
selectedReceiver.name = selectedReceiver.name[..colonPos];
}
}
}

2
ClusterReceptor.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4f64f5d72a422a7c8bb9ace598432aad

View File

@ -1,54 +0,0 @@
using UnityEditor;
using UnityEditor.SceneManagement;
namespace Passer.CreatureControl {
[CustomEditor(typeof(Creature), true)]
public class Creature_Editor : Editor {
/// <summary>
/// The creature managed by this editor
/// </summary>
protected Creature creature;
#region Start
/// <summary>
/// Enable the creature editor
/// </summary>
public virtual void OnEnable() {
this.creature = target as Creature;
// Keep track if anything changed while enabling the creature editor
bool anythingChanged = false;
if (IsPrefab(this.creature) == false) {
// Only do this when it is not a prefab
anythingChanged |= this.creature.CheckTargetRig();
anythingChanged |= this.creature.CheckModel();
}
this.creature.targetRig.MatchTo(this.creature, ref anythingChanged);
// As the above functions do not use the serialized object
// We need to manually persist the changes.
if (anythingChanged) {
EditorUtility.SetDirty(this.creature);
AssetDatabase.SaveAssets();
}
}
/// <summary>
/// Check if the given creature is a prefab
/// </summary>
/// <param name="creature">The creature to check</param>
/// <returns>True when it is a prefab</returns>
public static bool IsPrefab(Creature creature) {
PrefabStage prefabStage = PrefabStageUtility.GetPrefabStage(creature.gameObject);
return prefabStage != null;
}
#endregion Start
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e21e842527e8292c1a7002c75825bc7b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,113 +0,0 @@
using UnityEditor;
using UnityEngine;
namespace Passer.CreatureControl {
[CustomEditor(typeof(Insect), true)]
public class Insect_Editor : Creature_Editor {
protected Insect insect;
public override void OnEnable() {
base.OnEnable();
insect = target as Insect;
bool anythingChanged = false;
anythingChanged |= insect.CheckTargetRig("InsectRig");
insect.insectRig.MatchTo(insect, ref anythingChanged);
if (anythingChanged) {
EditorUtility.SetDirty(creature);
AssetDatabase.SaveAssets();
}
}
#region Inspector
public override void OnInspectorGUI() {
EditorGUIUtility.wideMode = true;
serializedObject.Update();
TargetsInspector();
AnimatorInspector();
serializedObject.ApplyModifiedProperties();
}
static bool showTargets;
private void TargetsInspector() {
GUIContent text = new(
"Targets",
"The target transforms controlling the body parts"
);
showTargets = EditorGUILayout.Foldout(showTargets, text, true);
if (showTargets) {
EditorGUI.indentLevel++;
SerializedProperty leftFrontLegProp = serializedObject.FindProperty(nameof(Insect.leftFrontLeg));
Leg_Editor.Inspector(leftFrontLegProp);
SerializedProperty leftMiddleLegProp = serializedObject.FindProperty(nameof(Insect.leftMiddleLeg));
Leg_Editor.Inspector(leftMiddleLegProp);
SerializedProperty leftHindLegProp = serializedObject.FindProperty(nameof(Insect.leftHindLeg));
Leg_Editor.Inspector(leftHindLegProp);
SerializedProperty rightFrontLegProp = serializedObject.FindProperty(nameof(Insect.rightFrontLeg));
Leg_Editor.Inspector(rightFrontLegProp);
SerializedProperty rightMiddleLegProp = serializedObject.FindProperty(nameof(Insect.rightMiddleLeg));
Leg_Editor.Inspector(rightMiddleLegProp);
SerializedProperty rightHindLegProp = serializedObject.FindProperty(nameof(Insect.rightHindLeg));
Leg_Editor.Inspector(rightHindLegProp);
EditorGUI.indentLevel--;
}
}
private void AnimatorInspector() {
GUIContent text = new(
"Animator",
"Standard Unity Animator Controller for animating the character"
);
SerializedProperty targetRigProp = serializedObject.FindProperty(nameof(Insect.targetRig));
if (targetRigProp == null)
return;
SerializedObject targetRigObj = new(targetRigProp.objectReferenceValue);
SerializedProperty animatorControllerProp = targetRigObj.FindProperty(nameof(InsectRig.animator));
animatorControllerProp.objectReferenceValue = (Animator)EditorGUILayout.ObjectField(text, animatorControllerProp.objectReferenceValue, typeof(Animator), true);
EditorGUI.indentLevel++;
ForwardSpeedInspector();
RotationSpeedInspector();
EditorGUI.indentLevel--;
}
private void ForwardSpeedInspector() {
GUIContent text = new(
"Forward speed",
"The maximum forward speed of the ant"
);
SerializedProperty forwardSpeedProp = serializedObject.FindProperty(nameof(Insect.forwardSpeed));
forwardSpeedProp.floatValue = EditorGUILayout.FloatField(text, forwardSpeedProp.floatValue);
}
private void RotationSpeedInspector() {
GUIContent text = new(
"Rotation speed",
"The maximum rotation speed of the ant"
);
SerializedProperty rotationSpeedProp = serializedObject.FindProperty(nameof(Insect.rotationSpeed));
rotationSpeedProp.floatValue = EditorGUILayout.FloatField(text, rotationSpeedProp.floatValue);
}
#endregion Inspector
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: fd27aa19166e492b582b506dc25eb605
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,75 +0,0 @@
using UnityEditor;
using UnityEngine;
namespace Passer.CreatureControl {
public class Leg_Editor {
//private string label = "";
private static bool showfield = false;
public void Enable() {
}
public static void Inspector(SerializedProperty legProp) {
GUIStyle foldoutStyle = new(EditorStyles.foldout) {
margin = EditorStyles.objectField.margin
};
EditorGUILayout.BeginHorizontal();
string legName = ConvertCamelCase(legProp.name);
showfield = EditorGUILayout.Foldout(showfield, legName, true, foldoutStyle);
SerializedProperty femurProp = legProp.FindPropertyRelative(nameof(Leg.femur));
SerializedProperty tibiaProp = legProp.FindPropertyRelative(nameof(Leg.tibia));
SerializedProperty tarsusProp = legProp.FindPropertyRelative(nameof(Leg.tarsus));
Transform newFemur = (Transform)EditorGUILayout.ObjectField(femurProp.objectReferenceValue, typeof(Transform), true);
if (newFemur != femurProp.objectReferenceValue) {
femurProp.objectReferenceValue = newFemur;
if (newFemur != null) {
if (tibiaProp.objectReferenceValue == null && newFemur.childCount == 1)
tibiaProp.objectReferenceValue = newFemur.GetChild(0);
Transform tibia = (Transform)tibiaProp.objectReferenceValue;
if (tibia != null) {
if (tarsusProp.objectReferenceValue == null && tibia.childCount == 1)
tarsusProp.objectReferenceValue = tibia.GetChild(0);
}
}
}
EditorGUILayout.EndHorizontal();
if (femurProp.objectReferenceValue != null && tibiaProp.objectReferenceValue == null)
showfield = true;
if (showfield) {
EditorGUI.indentLevel++;
tibiaProp.objectReferenceValue = (Transform)EditorGUILayout.ObjectField("Lower Leg", tibiaProp.objectReferenceValue, typeof(Transform), true);
tarsusProp.objectReferenceValue = (Transform)EditorGUILayout.ObjectField("Foot", tarsusProp.objectReferenceValue, typeof(Transform), true);
EditorGUI.indentLevel--;
}
}
private static string ConvertCamelCase(string text) {
if (string.IsNullOrEmpty(text))
return text;
System.Text.StringBuilder result = new();
for (int i = 0; i < text.Length; i++) {
// Add a space before uppercase characters
if (char.IsUpper(text[i]) && i > 0)
result.Append(' ');
// Add the character to the result
result.Append(text[i]);
}
// Capitalize the first character of the result
if (result.Length > 0)
result[0] = char.ToUpper(result[0]);
return result.ToString();
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 32630932c64073f2d87d6502c6751fe1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: ffcfc0e0e1a219581bc4d2bb33d40bd7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,155 +0,0 @@
using UnityEngine;
namespace Passer.CreatureControl {
public class Creature : MonoBehaviour {
/// <summary>
/// The (hopefully rigged) 3D model of the creature
/// </summary>
public Transform model;
/// <summary>The target bones rig</summary>
/// The target bones rig contain the target pose of the creature
/// The creature movements will try to move the creature such that the target pose is reached
/// as closely as possible
public TargetRig targetRig;
/// <summary>
/// The positional different between the target rig and model root
/// </summary>
public Vector3 targetToModelTranslation;
/// <summary>
/// The rotational difference between the target rig and the model root
/// </summary>
public Quaternion targetToModelRotation;
#region Init
/// <summary>
/// Ensure a target rig is available
/// </summary>
/// <param name="targetRigResourceName">The name of the target rig resource</param>
/// <returns>True when the target rig has been updated</returns>
/// The parameter is used to instantiate a new target rig when none has been found.
public bool CheckTargetRig(string targetRigResourceName) {
if (this.targetRig != null)
return false;
// See if there is a target rig, but we just haven't found it
this.targetRig = this.GetComponentInChildren<TargetRig>();
if (this.targetRig == null) {
// Nope, there is no target rig, so instantiate it using the given resource name
GameObject targetsRigPrefab = Resources.Load<GameObject>(targetRigResourceName);
GameObject targetRig = Instantiate(targetsRigPrefab);
targetRig.name = "Target Rig";
targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation);
targetRig.transform.SetParent(this.transform);
this.targetRig = targetRig.GetComponent<TargetRig>();
}
return true;
}
/// <summary>
/// Ensure a target rig is available
/// </summary>
/// <returns>True when the target rig has been updated</returns>
/// This tries to instantiate the default target rig resource
public virtual bool CheckTargetRig() {
return CheckTargetRig("TargetRig");
}
/// <summary>
/// Ensure that the creature rig is available
/// </summary>
/// <returns>True when the creature rig has been updated</returns>
public bool CheckModel() {
if (this.model != null)
return false;
// We determine the model root as the parent of the renderers
SkinnedMeshRenderer[] skinnedMeshRenderers = this.GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (SkinnedMeshRenderer skinnedMeshRenderer in skinnedMeshRenderers) {
Transform rendererParent = skinnedMeshRenderer.transform.parent;
if (this.model == null || this.model == rendererParent)
this.model = rendererParent;
else
// Oops! There are multiple renders with different parents....
Debug.LogWarning("Unclear model root");
// We still return a model root, but this may not be the correct one...
}
return this.model != null;
}
#endregion Init
#region Start
/// <summary>
/// Start the creature
/// </summary>
protected virtual void Start() {
this.CheckTargetRig();
this.CheckModel();
this.targetRig.MatchTo(this);
}
#endregion Start
#region Update
/// <summary>
/// Update the creature
/// </summary>
public virtual void Update() {
if (this.targetRig == null)
// Without a target rig, the creature cannot move
return;
UpdatePose();
// copy animator root motion to the creature
this.transform.SetPositionAndRotation(targetRig.transform.position, targetRig.transform.rotation);
// As target rig is probably a child of this.transform,
// We need to restore the position/rotation of the targetsRig.
targetRig.transform.SetPositionAndRotation(this.transform.position, this.transform.rotation);
}
/// <summary>
/// Update the pose of the creature using the target rig
/// </summary>
public void UpdatePose() {
if (this.targetRig == null)
return;
this.targetRig.Pose();
UpdateModel();
}
/// <summary>
/// Update the bones of the creature's rig from the target rig pose
/// </summary>
public virtual void UpdateModel() {
Vector3 newPosition = this.targetRig.transform.position + this.targetToModelTranslation;
Quaternion newOrientation = this.targetRig.transform.rotation * this.targetToModelRotation;
this.model.SetPositionAndRotation(newPosition, newOrientation);
}
#endregion Update
#region Scene view
/// <summary>
/// Update the pose of the creature when the application is not running
/// </summary>
void OnDrawGizmos() {
// This ensures that the model is always following the target rig
if (Application.isPlaying == false) {
this.UpdatePose();
}
}
#endregion Scene view
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 77c2897bce0332255bd076aab62e46d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 85f3225e01cd157769e2faf0e722846f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,54 +0,0 @@
namespace Passer.CreatureControl {
public class Insect : Creature {
public InsectRig insectRig;
public float forwardSpeed = 1;
public float rotationSpeed = 1;
public Leg leftFrontLeg;
public Leg leftMiddleLeg;
public Leg leftHindLeg;
public Leg rightFrontLeg;
public Leg rightMiddleLeg;
public Leg rightHindLeg;
#region Init
public override bool CheckTargetRig() {
bool anythingChanged = base.CheckTargetRig("InsectTargetRig");
if (anythingChanged || this.insectRig == null) {
this.insectRig = this.targetRig as InsectRig;
return true;
}
else
return anythingChanged;
}
#endregion Init
#region Update
public override void UpdateModel() {
base.UpdateModel();
if (this.insectRig.leftFrontLeg != null)
this.insectRig.leftFrontLeg.UpdateBones(this.leftFrontLeg);
if (this.insectRig.leftMiddleLeg != null)
this.insectRig.leftMiddleLeg.UpdateBones(this.leftMiddleLeg);
if (this.insectRig.leftBackLeg != null)
this.insectRig.leftBackLeg.UpdateBones(this.leftHindLeg);
if (this.insectRig.rightFrontLeg != null)
this.insectRig.rightFrontLeg.UpdateBones(this.rightFrontLeg);
if (this.insectRig.rightMiddleLeg != null)
this.insectRig.rightMiddleLeg.UpdateBones(this.rightMiddleLeg);
if (this.insectRig.rightBackLeg != null)
this.insectRig.rightBackLeg.UpdateBones(this.rightHindLeg);
}
#endregion Update
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 01b14620016fc2bfdb8a58d81e0396a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,46 +0,0 @@
using UnityEngine;
namespace Passer.CreatureControl {
// An insect target rig....
public class InsectRig : TargetRig {
public TargetLeg leftFrontLeg;
public TargetLeg leftMiddleLeg;
public TargetLeg leftBackLeg;
public TargetLeg rightFrontLeg;
public TargetLeg rightMiddleLeg;
public TargetLeg rightBackLeg;
public override void Pose() {
this.leftBackLeg.PoseLimb();
this.leftMiddleLeg.PoseLimb();
this.leftFrontLeg.PoseLimb();
this.rightBackLeg.PoseLimb();
this.rightMiddleLeg.PoseLimb();
this.rightFrontLeg.PoseLimb();
}
public override void MatchTo(Creature creature, ref bool anythingChanged) {
base.MatchTo(creature, ref anythingChanged);
if (creature is not Insect insect)
return;
if (this.leftFrontLeg != null && insect.leftFrontLeg != null)
this.leftFrontLeg.MatchTo(insect.leftFrontLeg);
if (this.leftMiddleLeg != null && insect.leftMiddleLeg != null)
this.leftMiddleLeg.MatchTo(insect.leftMiddleLeg);
if (this.leftBackLeg != null && insect.leftHindLeg != null)
this.leftBackLeg.MatchTo(insect.leftHindLeg);
if (this.rightFrontLeg != null && insect.rightFrontLeg != null)
this.rightFrontLeg.MatchTo(insect.rightFrontLeg);
if (this.rightMiddleLeg != null && insect.rightMiddleLeg != null)
this.rightMiddleLeg.MatchTo(insect.rightMiddleLeg);
if (this.rightBackLeg != null && insect.rightHindLeg != null)
this.rightBackLeg.MatchTo(insect.rightHindLeg);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a25e8575da1d1cba48dc2b8da7a2b0ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
using UnityEngine;
[System.Serializable]
public class Leg {
public Transform femur; // UpperLeg, Thigh
public Transform tibia; // LowerLeg, Shank
public Transform tarsus; // Foot
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 90a2688565fe3b05aba7cc9f9d70168c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,39 +0,0 @@
using UnityEngine;
namespace Passer.CreatureControl {
public class LegTarget : MonoBehaviour {
public TargetLeg leg;
public Collider footCollider;
public Renderer sphereRenderer;
public void Start() {
sphereRenderer = this.GetComponentInChildren<Renderer>();
}
void OnTriggerEnter(Collider collider) {// (Collision collision) {
// Change color on collision
ChangeColor(Color.red);
}
//void OnCollisionExit(Collision collision) {
void OnTriggerExit(Collider other) {
// Reset to original color on exit
ChangeColor(Color.white);
}
void ChangeColor(Color newColor) {
if (sphereRenderer != null) {
sphereRenderer.material.color = newColor; // Change the color
}
}
public virtual void OnDrawGizmosSelected() {
if (leg != null)
leg.OnDrawGizmosSelected();
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: fda62ae0dd837da8cb37ba5c7396c6a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,156 +0,0 @@
using UnityEngine;
namespace Passer.CreatureControl {
[System.Serializable]
public class TargetLeg : MonoBehaviour {
public Transform femurTarget; // UpperLeg, Thigh
public Transform tibiaTarget; // LowerLeg, Shank
public Transform tarsusTarget; // Foot
public Transform target; // for the tarsus
protected LegTarget legTarget;
public Quaternion targetToBoneFemur;
public Quaternion targetToBoneTibia;
public float femurLength;
public float tibiaLength;
public float length;
/// <summary>
/// Update the lenghts of the leg bones
/// </summary>
private void CalculateLengths() {
this.femurLength = Vector3.Distance(this.femurTarget.position, this.tibiaTarget.position);
this.tibiaLength = Vector3.Distance(this.tibiaTarget.position, this.tarsusTarget.position);
this.length = femurLength + tibiaLength;
}
public void MatchTo(Leg leg) {
this.femurTarget.position = leg.femur.position;
this.tibiaTarget.position = leg.tibia.position;
this.tarsusTarget.position = leg.tarsus.position;
targetToBoneFemur = TargetRig.TargetToBoneRotation(leg.femur, leg.tibia);
targetToBoneTibia = TargetRig.TargetToBoneRotation(leg.tibia, leg.tarsus);
CalculateLengths();
// Put the end-effector target for IK in a sensible place
Vector3 legDirection = (this.tarsusTarget.position - this.femurTarget.position).normalized;
Vector3 targetPosition = this.femurTarget.position + 0.7f * this.length * legDirection.normalized;
Quaternion targetRotation = Quaternion.LookRotation(legDirection);
this.target.SetPositionAndRotation(targetPosition, targetRotation);
this.target.localPosition = new(this.target.localPosition.x, 0, this.target.localPosition.z);
}
public virtual void OnDrawGizmosSelected() {
if (this.enabled == false)
return;
if (target != null && legTarget == null) {
legTarget = target.GetComponent<LegTarget>();
if (legTarget == null)
legTarget = target.gameObject.AddComponent<LegTarget>();
legTarget.leg = this;
}
Gizmos.color = Color.white;
if (this.femurTarget != null && this.tibiaTarget != null)
Gizmos.DrawLine(this.femurTarget.position, this.tibiaTarget.position);
if (tibiaTarget != null && this.tarsusTarget != null)
Gizmos.DrawLine(this.tibiaTarget.position, this.tarsusTarget.position);
PoseLimb();
}
/// <summary>
/// Pose the target limb
/// </summary>
public void PoseLimb() {
if (target == null)
return;
Quaternion femurOrientation = FemurRotation(target.position);
Quaternion tibiaOrientation = TibiaRotation(target.position);
Quaternion tarsusOrientation = TarsusRotation(target.rotation);
femurTarget.rotation = femurOrientation;
tibiaTarget.rotation = tibiaOrientation;
tarsusTarget.rotation = tarsusOrientation;
}
public void UpdateBones(Leg leg) {
UpdateFemur(leg.femur);
UpdateTibia(leg.tibia);
}
protected Quaternion FemurRotation(Vector3 targetPosition) {
if (this.femurTarget == null || this.tibiaTarget == null || this.tarsusTarget == null)
return Quaternion.identity;
Vector3 toTarget = targetPosition - this.femurTarget.position;
// Debug.DrawRay(femur.position, toTarget, Color.magenta);
float targetDistance = toTarget.magnitude;
float femurLength = Vector3.Distance(this.femurTarget.position, this.tibiaTarget.position);
float tibiaLength = Vector3.Distance(this.tibiaTarget.position, this.tarsusTarget.position);
float hipAngle = CosineRule(targetDistance, femurLength, tibiaLength);
// NaN happens when the distance to the footTarget is longer than the length of the leg
// We will stretch the leg full then (angle = 0)
if (float.IsNaN(hipAngle))
hipAngle = 0;
Quaternion femurOrientation = Quaternion.LookRotation(toTarget, Vector3.up);
femurOrientation = Quaternion.AngleAxis(hipAngle, femurOrientation * Vector3.left) * femurOrientation;
// Debug.DrawRay(femur.position, femurOrientation * Vector3.forward, Color.blue);
// Debug.DrawRay(femur.position, femurOrientation * Vector3.up, Color.green);
return femurOrientation;
}
protected Quaternion TibiaRotation(Vector3 targetPosition) {
if (this.tibiaTarget == null)
return Quaternion.identity;
Vector3 directionToTarget = targetPosition - this.tibiaTarget.position;
Quaternion tibiaOrientation = Quaternion.LookRotation(directionToTarget, Vector3.up); // femur.up);
return tibiaOrientation; // In world space
}
protected Quaternion TarsusRotation(Quaternion targetRotation) {
return targetRotation;
}
public void UpdateFemur(Transform femurBone) {
if (femurBone == null || this.femurTarget == null)
return;
femurBone.rotation = this.femurTarget.rotation * targetToBoneFemur;
}
public void UpdateTibia(Transform tibiaBone) {
if (tibiaBone == null || this.tibiaTarget == null)
return;
tibiaBone.rotation = this.tibiaTarget.rotation * targetToBoneTibia;
}
#region Math
public static float CosineRule(float a, float b, float c) {
float a2 = a * a;
float b2 = b * b;
float c2 = c * c;
double angle = System.Math.Acos((a2 + b2 - c2) / (2 * a * b)) * Mathf.Rad2Deg;
if (double.IsNaN(angle))
angle = 0;
return (float)angle;
}
#endregion Math
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: f3394e8da3685a263901c13e2a231279
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,67 +0,0 @@
using UnityEngine;
namespace Passer.CreatureControl {
/// <summary>
/// A target rig for a creature
/// </summary>
public class TargetRig : MonoBehaviour {
public Animator animator;
/// <summary>
/// Pose the target rig using the IK targets
/// </summary>
public virtual void Pose() {
}
/// <summary>
/// Align the target rig with a creature
/// </summary>
/// <param name="creature">The creature to align to</param>
public void MatchTo(Creature creature) {
bool anythingChangedDummy = false;
MatchTo(creature, ref anythingChangedDummy);
}
/// <summary>
/// Align the target rig with a creature
/// </summary>
/// <param name="creature">The creature to align to</param>
/// <param name="anythingChanged">True when any property of the creature has changed</param>
public virtual void MatchTo(Creature creature, ref bool anythingChanged) {
Vector3 targetToModelTranslation = creature.model.position - this.transform.position;
bool changed = targetToModelTranslation != creature.targetToModelTranslation;
if (changed) {
anythingChanged = true;
creature.targetToModelTranslation = targetToModelTranslation;
}
Quaternion targetToModelRotation = Quaternion.Inverse(this.transform.rotation) * creature.model.rotation;
changed = targetToModelRotation != creature.targetToModelRotation;
if (changed) {
anythingChanged = true;
creature.targetToModelRotation = targetToModelRotation;
}
}
/// <summary>
/// Compute the rotation from the target bone to a creature's bone
/// </summary>
/// <param name="bone">The creature's bone for this target bone</param>
/// <param name="nextBone">The next bone in the creature's hierarchy</param>
/// <returns>The rotation from the target bone rotation to the creature bone rotation</returns>
/// The next bone is used to compute the direction of the bone.
/// The 'up'-direction of the bone is currently fixed to (world) up.
public static Quaternion TargetToBoneRotation(Transform bone, Transform nextBone) {
if (bone == null || nextBone == null)
return Quaternion.identity;
Vector3 direction = nextBone.position - bone.position;
Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);
Quaternion toBoneRotation = Quaternion.Inverse(targetRotation) * bone.rotation;
return toBoneRotation;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: d2609296f45aabe86b35997eeef1e59a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 1fa9ced5c1f8d9f45b173910d53bbe81
guid: 3aedf57a50b6dfa46a59457c87b8ef9d
folderAsset: yes
DefaultImporter:
externalObjects: {}

365
Editor/BrainEditorWindow.cs Normal file
View File

@ -0,0 +1,365 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
// Simple DAG data model
[System.Serializable]
public class DagNode {
public int id;
public string title;
public Vector2 position;
public float radius = 20f; // circle radius
}
[System.Serializable]
public class DagEdge {
public int fromId;
public int toId;
}
public class BrainEditorWindow : EditorWindow {
readonly List<DagNode> nodes = new();
readonly List<DagEdge> edges = new();
Vector2 pan = Vector2.zero;
float zoom = 1.0f;
const float minZoom = 0.5f;
const float maxZoom = 2.0f;
// Vector2 dragStart;
// bool draggingNode = false;
// int draggingNodeId = -1;
private readonly System.Type acceptedType = typeof(ClusterPrefab);
[MenuItem("Window/Brain Viewer")]
public static void ShowWindow() {
var w = GetWindow<BrainEditorWindow>("Brain Viewer");
w.minSize = new Vector2(500, 300);
}
void OnEnable() {
// if (nodes.Count == 0)
// CreateSampleGraph();
// Register callback so window updates when selection changes
Selection.selectionChanged += OnSelectionChanged;
RefreshSelection();
ComputeLeftToRightLayout();
}
private void OnDisable() {
Selection.selectionChanged -= OnSelectionChanged;
}
private void OnSelectionChanged() {
RefreshSelection();
ComputeLeftToRightLayout();
Repaint();
}
private void RefreshSelection() {
ClusterPrefab prefab = Selection.activeObject as ClusterPrefab;
if (prefab != null && acceptedType.IsAssignableFrom(prefab.GetType())) {
GenerateGraph(prefab);
}
}
private void GenerateGraph(ClusterPrefab prefab) {
nodes.Clear();
edges.Clear();
int ix = 0;
foreach (Nucleus nucleus in prefab.nuclei) {
nodes.Add(new DagNode() { id = ix, title = nucleus.name });
if (nucleus is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers) {
int receiverIx = prefab.GetNucleusIndex(receiver);
edges.Add(new DagEdge() { fromId = ix, toId = receiverIx });
}
}
ix++;
}
}
// void CreateSampleGraph() {
// nodes.Clear();
// edges.Clear();
// nodes.Add(new DagNode() { id = 0, title = "In1" });
// nodes.Add(new DagNode() { id = 1, title = "In2" });
// nodes.Add(new DagNode() { id = 2, title = "A" });
// nodes.Add(new DagNode() { id = 3, title = "B" });
// nodes.Add(new DagNode() { id = 4, title = "C" });
// nodes.Add(new DagNode() { id = 5, title = "Out1" });
// nodes.Add(new DagNode() { id = 6, title = "Out2" });
// edges.Add(new DagEdge() { fromId = 0, toId = 2 });
// edges.Add(new DagEdge() { fromId = 1, toId = 2 });
// edges.Add(new DagEdge() { fromId = 2, toId = 3 });
// edges.Add(new DagEdge() { fromId = 2, toId = 4 });
// edges.Add(new DagEdge() { fromId = 3, toId = 5 });
// edges.Add(new DagEdge() { fromId = 4, toId = 6 });
// }
void OnGUI() {
HandleInput();
Rect rect = new(0, 0, position.width, position.height);
EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f));
// compute window center
Vector2 windowCenter = new(position.width / 2f, position.height / 2f);
// compute graph bounds center (in graph space)
Rect bounds = GetGraphBounds();
Vector2 graphCenter = bounds.center;
// compute autoPan that recenters the graph (does not modify node positions)
Vector2 autoPan = -graphCenter; // moves graph center to origin
// total translation = windowCenter + autoPan + user pan
Matrix4x4 oldMatrix = GUI.matrix;
GUI.matrix = Matrix4x4.TRS(windowCenter + autoPan + pan, Quaternion.identity, Vector3.one * zoom) *
Matrix4x4.TRS(-windowCenter, Quaternion.identity, Vector3.one);
// Draw edges first
foreach (DagEdge e in edges) {
DagNode from = GetNodeById(e.fromId);
DagNode to = GetNodeById(e.toId);
if (from == null || to == null) continue;
DrawEdgeCircleNodes(from, to);
}
// Draw nodes (circles)
foreach (DagNode n in nodes)
DrawNucleus(n);
GUI.matrix = oldMatrix;
// Footer toolbar
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView();
if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout();
EditorGUILayout.EndHorizontal();
}
void HandleInput() {
Event e = Event.current;
// Zoom with scroll
if (e.type == EventType.ScrollWheel) {
float oldZoom = zoom;
float delta = -e.delta.y * 0.01f;
zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom);
Vector2 mouse = e.mousePosition;
pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom);
e.Use();
}
// Pan with middle or right+ctrl drag
if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) {
pan += e.delta;
e.Use();
}
}
DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id);
List<DagEdge> GetIncomingEdges(DagNode node) {
List<DagEdge> incoming = new();
foreach (DagEdge e in edges) {
if (e.toId == node.id)
incoming.Add(e);
}
return incoming;
}
List<DagEdge> GetOutgoingEdges(DagNode node) {
List<DagEdge> outgoing = new();
foreach (DagEdge e in edges) {
if (e.fromId == node.id)
outgoing.Add(e);
}
return outgoing;
}
void DrawNucleus(DagNode n) {
Vector3 position = n.position;
Handles.color = Color.white * 0.9f;
Handles.DrawSolidDisc(n.position, Vector3.forward, n.radius);
if (GetIncomingEdges(n).Count == 0)
DrawArrowHead(n.position - new Vector2(n.radius + 10, 0), n.position - new Vector2(n.radius + 5, 0), 10f / zoom, 12f / zoom, Color.white);
if (GetOutgoingEdges(n).Count == 0)
DrawArrowHead(n.position + new Vector2(n.radius + 10, 0), n.position + new Vector2(n.radius + 15, 0), 10f / zoom, 12f / zoom, Color.white);
Handles.color = Color.white;
GUIStyle style = new(EditorStyles.label) {
alignment = TextAnchor.UpperCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold,
};
Vector3 labelPos = position - Vector3.down * (n.radius + 10f); // below disc along up axis
Handles.Label(labelPos, n.title, style);
}
void DrawEdgeCircleNodes(DagNode from, DagNode to) {
Vector2 a = from.position;
Vector2 b = to.position;
if (a == b) return;
Handles.color = Color.white * 0.9f;
Handles.DrawLine(from.position, to.position);
// Vector2 dir = (b - a).normalized;
// Vector2 start = a + dir * from.radius;
// Vector2 end = b - dir * to.radius;
//DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white);
}
void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) {
Vector2 dir = (to - from).normalized;
if (dir == Vector2.zero) return;
Vector2 right = new Vector2(-dir.y, dir.x);
Vector3 p1 = to;
Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f;
Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f;
Handles.color = color;
Handles.DrawAAConvexPolygon(p1, p2, p3);
}
// Left-to-right layered layout (sources on the left, sinks on the right)
void ComputeLeftToRightLayout() {
// build adjacency and indegree
var adj = nodes.ToDictionary(n => n.id, n => new List<int>());
var indeg = nodes.ToDictionary(n => n.id, n => 0);
foreach (var e in edges) {
if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue;
adj[e.fromId].Add(e.toId);
indeg[e.toId]++;
}
// Kahn's algorithm to compute topological layers (horizontal layers)
Dictionary<int, int> layer = new();
Queue<int> q = new(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key));
foreach (var id in q) layer[id] = 0;
while (q.Count > 0) {
int u = q.Dequeue();
int l = layer[u];
foreach (var v in adj[u]) {
// prefer placing v at least one layer after u
if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1;
indeg[v]--;
if (indeg[v] == 0) q.Enqueue(v);
}
}
// Any unreachable nodes -> assign next layers
int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0;
foreach (var n in nodes) {
if (!layer.ContainsKey(n.id)) {
maxLayer++;
layer[n.id] = maxLayer;
}
}
// Group nodes by layer (left to right)
var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList();
// Layout parameters (horizontal spacing drives left->right)
float hSpacing = 150f;
float vSpacing = 100f;
// Place nodes: x increases with layer index, y spaced within layer
for (int li = 0; li < layers.Count; li++) {
var lst = layers[li];
float totalHeight = (lst.Count - 1) * vSpacing;
for (int i = 0; i < lst.Count; i++) {
int id = lst[i];
var n = GetNodeById(id);
if (n == null) continue;
float x = hSpacing + li * hSpacing;
float y = 400 - totalHeight / 2f + i * vSpacing;
// Debug.Log($"({li}, {i}) -> {x}, {y}");
n.position = new Vector2(x, y);
}
}
Repaint();
}
void FitToView() {
if (nodes.Count == 0) return;
// compute bounds including radii
Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f);
foreach (var n in nodes)
bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f));
// center graph at origin (0,0) then set pan so it appears centered in window
Vector2 graphCenter = bounds.center;
// move nodes so center is at origin
for (int i = 0; i < nodes.Count; i++)
nodes[i].position -= graphCenter;
// reset pan/zoom so centered
pan = Vector2.zero;
zoom = 1.0f;
Repaint();
}
static Rect RectUnion(Rect a, Rect b) {
float xMin = Mathf.Min(a.xMin, b.xMin);
float xMax = Mathf.Max(a.xMax, b.xMax);
float yMin = Mathf.Min(a.yMin, b.yMin);
float yMax = Mathf.Max(a.yMax, b.yMax);
return Rect.MinMaxRect(xMin, yMin, xMax, yMax);
}
Vector2 ScreenToGraph_old(Vector2 screenPos) {
Vector2 origin = new Vector2(position.width / 2, position.height / 2);
// invert the GUI.matrix transform (approx for current simple transforms)
return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom);
}
Vector2 ScreenToGraph(Vector2 screenPos) {
Vector2 windowCenter = new Vector2(position.width / 2f, position.height / 2f);
Rect bounds = GetGraphBounds();
Vector2 graphCenter = bounds.center;
Vector2 autoPan = -graphCenter;
// inverse of: screen -> translate by -(windowCenter+autoPan+pan), scale by 1/zoom, translate by windowCenter
return (screenPos - (windowCenter + autoPan + pan)) / zoom + windowCenter;
}
Rect GetGraphBounds() {
if (nodes == null || nodes.Count == 0) return new Rect(Vector2.zero, Vector2.one);
Rect bounds = new(
nodes[0].position - Vector2.one * nodes[0].radius,
2f * nodes[0].radius * Vector2.one);
foreach (var n in nodes)
bounds = RectUnion(bounds,
new Rect(n.position - Vector2.one * n.radius, 2f * n.radius * Vector2.one));
return bounds;
}
int HitTestNode(Vector2 graphPos) {
// returns node id under point or -1
for (int i = nodes.Count - 1; i >= 0; i--) {
var n = nodes[i];
if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id;
}
return -1;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f041740900808273ab006e7d276a78e9

View File

@ -0,0 +1,66 @@
using UnityEditor;
using UnityEngine;
using System;
using System.Linq;
public class ClusterPickerWindow : EditorWindow {
private Vector2 scroll;
private ClusterPrefab[] items = new ClusterPrefab[0];
private Action<ClusterPrefab> onPicked;
private string search = "";
public static void ShowPicker(Action<ClusterPrefab> onPicked, string title = "Select Cluster") {
var w = CreateInstance<ClusterPickerWindow>();
w.titleContent = new GUIContent(title);
w.minSize = new Vector2(360, 320);
w.onPicked = onPicked;
w.RefreshList();
w.ShowModalUtility(); // modal dialog
}
private void OnEnable() => RefreshList();
private void RefreshList() {
var guids = AssetDatabase.FindAssets("t:ClusterPrefab");
items = guids
.Select(g => AssetDatabase.LoadAssetAtPath<ClusterPrefab>(AssetDatabase.GUIDToAssetPath(g)))
.Where(b => b != null)
.OrderBy(b => b.name)
.ToArray();
}
private void OnGUI() {
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Choose Cluster:", EditorStyles.boldLabel);
if (GUILayout.Button("Refresh", GUILayout.Width(80))) RefreshList();
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
search = EditorGUILayout.TextField(search);
EditorGUILayout.Space();
scroll = EditorGUILayout.BeginScrollView(scroll);
foreach (var it in items) {
if (!string.IsNullOrEmpty(search) && it.name.IndexOf(search, StringComparison.OrdinalIgnoreCase) < 0)
continue;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(EditorGUIUtility.ObjectContent(it, typeof(ClusterPrefab)), GUILayout.Height(20));
if (GUILayout.Button("Select", GUILayout.Width(70))) {
onPicked?.Invoke(it);
Close();
return;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Cancel")) { onPicked?.Invoke(null); Close(); }
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9197e2d322d23b5798ab4aef729815b0

1100
Editor/ClusterInspector.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1fc1fb7db9f7ad54a87d31313e7f457d

393
Editor/DAGWindow.cs Normal file
View File

@ -0,0 +1,393 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
// Simple DAG data model
// [System.Serializable]
// public class DagNode
// {
// public int id;
// public string title;
// public Vector2 position;
// public float radius = 36f; // circle radius
// }
// [System.Serializable]
// public class DagEdge
// {
// public int fromId;
// public int toId;
// }
public class DAGEditorWindow : EditorWindow
{
List<DagNode> nodes = new List<DagNode>();
List<DagEdge> edges = new List<DagEdge>();
Vector2 pan = Vector2.zero;
float zoom = 1.0f;
const float minZoom = 0.5f;
const float maxZoom = 2.0f;
GUIStyle labelStyle;
int selectedNodeId = -1;
Vector2 dragStart;
bool draggingNode = false;
int draggingNodeId = -1;
[MenuItem("Window/DAG Viewer (LR, Circles)")]
public static void ShowWindow()
{
var w = GetWindow<DAGEditorWindow>("DAG Viewer (LR)");
w.minSize = new Vector2(500, 300);
}
void OnEnable()
{
labelStyle = new GUIStyle(EditorStyles.label);
labelStyle.alignment = TextAnchor.MiddleCenter;
labelStyle.normal.textColor = Color.white;
labelStyle.fontStyle = FontStyle.Bold;
if (nodes.Count == 0)
CreateSampleGraph();
ComputeLeftToRightLayout();
}
void CreateSampleGraph()
{
nodes.Clear();
edges.Clear();
nodes.Add(new DagNode() { id = 0, title = "In1" });
nodes.Add(new DagNode() { id = 1, title = "In2" });
nodes.Add(new DagNode() { id = 2, title = "A" });
nodes.Add(new DagNode() { id = 3, title = "B" });
nodes.Add(new DagNode() { id = 4, title = "C" });
nodes.Add(new DagNode() { id = 5, title = "Out1" });
nodes.Add(new DagNode() { id = 6, title = "Out2" });
edges.Add(new DagEdge() { fromId = 0, toId = 2 });
edges.Add(new DagEdge() { fromId = 1, toId = 2 });
edges.Add(new DagEdge() { fromId = 2, toId = 3 });
edges.Add(new DagEdge() { fromId = 2, toId = 4 });
edges.Add(new DagEdge() { fromId = 3, toId = 5 });
edges.Add(new DagEdge() { fromId = 4, toId = 6 });
}
void OnGUI()
{
HandleInput();
Rect rect = new Rect(0, 0, position.width, position.height);
EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f));
Matrix4x4 oldMatrix = GUI.matrix;
Vector2 origin = new Vector2(position.width / 2, position.height / 2);
GUI.matrix = Matrix4x4.TRS(origin + pan, Quaternion.identity, Vector3.one * zoom) *
Matrix4x4.TRS(-origin, Quaternion.identity, Vector3.one);
// Draw edges first
foreach (var e in edges)
{
var from = GetNodeById(e.fromId);
var to = GetNodeById(e.toId);
if (from == null || to == null) continue;
DrawEdgeCircleNodes(from, to);
}
// Draw nodes (circles)
foreach (var n in nodes)
{
DrawNodeCircle(n);
}
GUI.matrix = oldMatrix;
// Footer toolbar
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView();
if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout();
if (GUILayout.Button("Add Node", EditorStyles.toolbarButton))
{
AddNode("N" + nodes.Count);
ComputeLeftToRightLayout();
}
if (GUILayout.Button("Add Edge (selected->new)", EditorStyles.toolbarButton))
{
if (selectedNodeId != -1)
{
var newNode = AddNode("N" + nodes.Count);
edges.Add(new DagEdge() { fromId = selectedNodeId, toId = newNode.id });
ComputeLeftToRightLayout();
}
}
EditorGUILayout.EndHorizontal();
}
void HandleInput()
{
Event e = Event.current;
// Zoom with scroll
if (e.type == EventType.ScrollWheel)
{
float oldZoom = zoom;
float delta = -e.delta.y * 0.01f;
zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom);
Vector2 mouse = e.mousePosition;
pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom);
e.Use();
}
// Pan with middle or right+ctrl drag
if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control)))
{
pan += e.delta;
e.Use();
}
// Node dragging & selection (convert mouse to graph space)
Vector2 graphMouse = ScreenToGraph(e.mousePosition);
if (e.type == EventType.MouseDown && e.button == 0)
{
int hit = HitTestNode(graphMouse);
if (hit != -1)
{
selectedNodeId = hit;
draggingNode = true;
draggingNodeId = hit;
dragStart = graphMouse;
e.Use();
}
else
{
selectedNodeId = -1;
}
}
if (draggingNode && draggingNodeId != -1)
{
if (e.type == EventType.MouseDrag && e.button == 0)
{
Vector2 graphDelta = e.delta / zoom;
var n = GetNodeById(draggingNodeId);
if (n != null)
{
n.position += graphDelta;
Repaint();
e.Use();
}
}
if (e.type == EventType.MouseUp && e.button == 0)
{
draggingNode = false;
draggingNodeId = -1;
e.Use();
}
}
}
DagNode AddNode(string title)
{
int nextId = nodes.Count > 0 ? nodes.Max(n => n.id) + 1 : 0;
var n = new DagNode() { id = nextId, title = title, position = Vector2.zero };
nodes.Add(n);
return n;
}
DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id);
void DrawNodeCircle(DagNode n)
{
Vector2 center = n.position;
float r = n.radius;
Rect nodeRect = new Rect(center.x - r, center.y - r, r * 2, r * 2);
// circle background
Color bg = (n.id == selectedNodeId) ? new Color(0.15f, 0.5f, 0.9f) : new Color(0.2f, 0.2f, 0.2f);
EditorGUI.DrawRect(nodeRect, bg);
// anti-aliased circle outline
Handles.color = Color.white * 0.9f;
Handles.DrawAAPolyLine(3f / zoom, GetCircleOutlinePoints(center, r, 48).ToArray());
// label
Vector2 labelPos = center - new Vector2(0, 8);
GUI.Label(new Rect(labelPos.x - r, labelPos.y - 8, r * 2, 18), n.title, labelStyle);
}
List<Vector3> GetCircleOutlinePoints(Vector2 center, float radius, int segments)
{
var pts = new List<Vector3>(segments + 1);
for (int i = 0; i <= segments; i++)
{
float a = (float)i / segments * Mathf.PI * 2f;
pts.Add(new Vector3(center.x + Mathf.Cos(a) * radius, center.y + Mathf.Sin(a) * radius, 0));
}
return pts;
}
void DrawEdgeCircleNodes(DagNode from, DagNode to)
{
Vector2 a = from.position;
Vector2 b = to.position;
if (a == b) return;
// Compute edge line that starts/ends at circle circumferences
Vector2 dir = (b - a).normalized;
Vector2 start = a + dir * from.radius;
Vector2 end = b - dir * to.radius;
// Use a simple curved line: start -> control -> end (bezier)
Vector2 control = new Vector2((start.x + end.x) / 2f, (start.y + end.y) / 2f);
// Slight vertical offset to separate overlapping lines based on node ids
float offset = ((from.id * 7 + to.id * 11) % 7 - 3) * 6f / zoom;
control += new Vector2(0, offset);
Handles.color = Color.white * 0.9f;
Handles.DrawAAPolyLine(3f / zoom, 20, GetBezierPoints(start, control, end, 24).ToArray());
// Arrow at end pointing towards 'b'
DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white);
}
List<Vector3> GetBezierPoints(Vector2 p0, Vector2 p1, Vector2 p2, int seg)
{
var pts = new List<Vector3>(seg + 1);
for (int i = 0; i <= seg; i++)
{
float t = (float)i / seg;
Vector2 p = (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
pts.Add(new Vector3(p.x, p.y, 0));
}
return pts;
}
void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color)
{
Vector2 dir = (to - from).normalized;
if (dir == Vector2.zero) return;
Vector2 right = new Vector2(-dir.y, dir.x);
Vector3 p1 = to;
Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f;
Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f;
Handles.color = color;
Handles.DrawAAConvexPolygon(p1, p2, p3);
}
// Left-to-right layered layout (sources on the left, sinks on the right)
void ComputeLeftToRightLayout()
{
// build adjacency and indegree
var adj = nodes.ToDictionary(n => n.id, n => new List<int>());
var indeg = nodes.ToDictionary(n => n.id, n => 0);
foreach (var e in edges)
{
if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue;
adj[e.fromId].Add(e.toId);
indeg[e.toId]++;
}
// Kahn's algorithm to compute topological layers (horizontal layers)
Dictionary<int, int> layer = new Dictionary<int, int>();
Queue<int> q = new Queue<int>(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key));
foreach (var id in q) layer[id] = 0;
while (q.Count > 0)
{
int u = q.Dequeue();
int l = layer[u];
foreach (var v in adj[u])
{
// prefer placing v at least one layer after u
if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1;
indeg[v]--;
if (indeg[v] == 0) q.Enqueue(v);
}
}
// Any unreachable nodes -> assign next layers
int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0;
foreach (var n in nodes)
{
if (!layer.ContainsKey(n.id))
{
maxLayer++;
layer[n.id] = maxLayer;
}
}
// Group nodes by layer (left to right)
var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList();
// Layout parameters (horizontal spacing drives left->right)
float hSpacing = 220f;
float vSpacing = 120f;
// Place nodes: x increases with layer index, y spaced within layer
for (int li = 0; li < layers.Count; li++)
{
var lst = layers[li];
float totalHeight = (lst.Count - 1) * vSpacing;
for (int i = 0; i < lst.Count; i++)
{
int id = lst[i];
var n = GetNodeById(id);
if (n == null) continue;
float x = li * hSpacing;
float y = -totalHeight / 2f + i * vSpacing;
n.position = new Vector2(x, y);
}
}
Repaint();
}
void FitToView()
{
if (nodes.Count == 0) return;
Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f);
foreach (var n in nodes)
bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f));
Vector2 center = bounds.center;
pan = -center;
zoom = 1.0f;
Repaint();
}
static Rect RectUnion(Rect a, Rect b)
{
float xMin = Mathf.Min(a.xMin, b.xMin);
float xMax = Mathf.Max(a.xMax, b.xMax);
float yMin = Mathf.Min(a.yMin, b.yMin);
float yMax = Mathf.Max(a.yMax, b.yMax);
return Rect.MinMaxRect(xMin, yMin, xMax, yMax);
}
Vector2 ScreenToGraph(Vector2 screenPos)
{
Vector2 origin = new Vector2(position.width / 2, position.height / 2);
// invert the GUI.matrix transform (approx for current simple transforms)
return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom);
}
int HitTestNode(Vector2 graphPos)
{
// returns node id under point or -1
for (int i = nodes.Count - 1; i >= 0; i--)
{
var n = nodes[i];
if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id;
}
return -1;
}
}

2
Editor/DAGWindow.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 95393aed582b8b30d965400672aec4d8

View File

@ -0,0 +1,49 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[CustomEditor(typeof(NanoBrain))]
public class NanoBrainComponent_Editor : Editor {
protected static VisualElement mainContainer;
protected static VisualElement inspectorContainer;
protected NanoBrain component;
private SerializedProperty brainProp;
ClusterInspector.GraphView board;
public void OnEnable() {
component = target as NanoBrain;
if (Application.isPlaying == false && serializedObject != null) {
string propertyName = nameof(NanoBrain.defaultBrain);
brainProp = serializedObject.FindProperty(propertyName);
}
}
public override VisualElement CreateInspectorGUI() {
Cluster brain = component.brain;
if (Application.isPlaying == false)
serializedObject.Update();
VisualElement root = new();
if (Application.isPlaying == false) {
PropertyField brainField = new(brainProp) {
label = "Nano Brain"
};
root.Add(brainField);
}
if (brain != null)
ClusterInspector.CreateInspector(root, brain.prefab, brain.defaultOutput, component.gameObject);
if (Application.isPlaying == false)
serializedObject.ApplyModifiedProperties();
return root;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f05072314d39990639a2dbf99f322664

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: fd3f8dbf7199f6ffdaef4718aaaa4bb4
guid: 7b61a93fc9332d2adae74fe4abe92d53
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,12 @@
#content {
background-color: #2b2b2b;
}
#inspector {
border-left-width: 1px;
border-left-color: #000;
padding: 3px;
}
#title {
-unity-font-style: bold;
color: white;
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 28268b644fa8f3948851b25e41f5b03b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 3a999e12de71fa28494cf28175deb1d5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,35 +0,0 @@
using UnityEditor;
using UnityEngine;
namespace Passer.CreatureControl {
[CustomEditor(typeof(Ant))]
public class Ant_Editor : Insect_Editor {
protected Ant ant;
public override void OnEnable() {
this.ant = target as Ant;
base.OnEnable();
}
public override void OnInspectorGUI() {
base.OnInspectorGUI();
HomePheromonePrefabInspector();
FoodPheromonePrefabInspector();
}
private void HomePheromonePrefabInspector() {
SerializedProperty homePheromonePrefabProp = serializedObject.FindProperty(nameof(Ant.homePheromonePrefab));
homePheromonePrefabProp.objectReferenceValue = (GameObject) EditorGUILayout.ObjectField("Home Pheromone Prefab", homePheromonePrefabProp.objectReferenceValue, typeof(GameObject), true);
}
private void FoodPheromonePrefabInspector() {
SerializedProperty foodPheromonePrefabProp = serializedObject.FindProperty(nameof(Ant.foodPheromonePrefab));
foodPheromonePrefabProp.objectReferenceValue = (GameObject) EditorGUILayout.ObjectField("Food Pheromone Prefab", foodPheromonePrefabProp.objectReferenceValue, typeof(GameObject), true);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 6d342503e4a28bac9aa53dca1bb602e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

73
IReceptor.cs Normal file
View File

@ -0,0 +1,73 @@
using UnityEngine;
public interface IReceptor {
public string GetName();
public Nucleus[] nucleiArray { get; set; }
public void AddReceptorElement(ClusterPrefab prefab);
public void RemoveReceptorElement();
public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1);
public void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null);
}
public static class IReceptorHelpers {
public static void AddReceptorElement(IReceptor receptor, ClusterPrefab prefab) {
if (receptor.nucleiArray.Length == 0) {
Debug.LogError("Empty perceptoid array, cannot add");
}
int newLength = receptor.nucleiArray.Length + 1;
Nucleus[] newArray = new Nucleus[newLength];
string baseName = receptor.GetName();
int colonPos = baseName.IndexOf(":");
if (colonPos > 0)
baseName = baseName[..colonPos];
for (int i = 0; i < receptor.nucleiArray.Length; i++)
newArray[i] = receptor.nucleiArray[i];
if (receptor.nucleiArray[0] is Nucleus nucleus) {
newArray[newLength - 1] = nucleus.Clone(prefab);
newArray[newLength - 1].name = $"{baseName}: {newLength - 1}";
}
foreach (Nucleus element in receptor.nucleiArray) {
if (element is IReceptor receptorElement) {
receptorElement.nucleiArray = newArray;
}
}
}
public static void RemoveReceptorElement(IReceptor receptor) {
int newLength = receptor.nucleiArray.Length - 1;
if (newLength == 0) {
Debug.LogWarning("Perceptoid array cannot be empty");
}
Nucleus[] newArray = new Nucleus[newLength];
for (int i = 0; i < newLength; i++)
newArray[i] = receptor.nucleiArray[i];
// Delete the last perception
if (receptor.nucleiArray[newLength] is Nucleus nucleus)
Neuron.Delete(nucleus);
foreach (Nucleus element in receptor.nucleiArray) {
if (element is IReceptor receptorElement) {
receptorElement.nucleiArray = newArray;
}
}
}
public static void AddArrayReceiver(IReceptor receptor, Nucleus receiverToAdd, float weight = 1) {
foreach (Nucleus element in receptor.nucleiArray) {
if (element is Cluster cluster)
cluster.defaultOutput.AddReceiver(receiverToAdd, weight);
if (element is Neuron neuron)
neuron.AddReceiver(receiverToAdd, weight);
}
}
}

2
IReceptor.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 73f052292ad16bb53a3c07aa1694c705

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7e4dae7701b15cb018fbd090cc5c6c0b
guid: 885c5a70637820322b07e023ce18fdd5
folderAsset: yes
DefaultImporter:
externalObjects: {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,117 @@
fileFormatVersion: 2
guid: 288088fdc016525a59f83f1c608e514d
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,117 @@
fileFormatVersion: 2
guid: e16264b4b7305e5c5b5b1389d6b2f13e
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,117 @@
fileFormatVersion: 2
guid: 948c13386d926b7bbbca85239a974d85
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

59
Identity.asset Normal file
View File

@ -0,0 +1,59 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 60a957541c24c57e78018c202ebb1d9b, type: 3}
m_Name: Identity
m_EditorClassIdentifier: Assembly-CSharp::ClusterPrefab
nuclei:
- rid: 2262690531574022216
references:
version: 2
RefIds:
- rid: -2
type: {class: , ns: , asm: }
- rid: 2262690531574022216
type: {class: Neuron, ns: , asm: Assembly-CSharp}
data:
name: Output
clusterPrefab: {fileID: 11400000}
parent:
rid: -2
trace: 0
bias: {x: 0, y: 0, z: 0}
_synapses: []
combinator: 0
_curvePreset: 0
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 1
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1000
value: 1000
inSlope: 1
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
curveMax: 1
_receivers: []

View File

@ -1,8 +1,8 @@
fileFormatVersion: 2
guid: d4b9f32bef604abd5953647ad53ca0f7
guid: 5f4d2ea0d0115b3549f8e9aa5e669163
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

373
LICENSE
View File

@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: ead5f2201e9c95549acb5631aad16b67
guid: d98555a675e8e5e879de17db950b55fe
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,19 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
max_line_length = 80
[*.cs]
csharp_new_line_before_open_brace = none
# Suppress warnings everywhere
dotnet_diagnostic.IDE1006.severity = none
dotnet_diagnostic.IDE0130.severity = none

View File

@ -0,0 +1,37 @@
name: Build and Run C# Unit Tests
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '8.0.x' # Specify the .NET SDK version
- name: Check Current Directory
run: pwd # Logs the current working directory
- name: List Files
run: ls -la # Lists all files in the current directory
- name: Restore Dependencies
run: dotnet restore ./LinearAlgebra-csharp.sln # Restore NuGet packages
- name: Build the Project
run: dotnet build ./LinearAlgebra-csharp.sln --configuration Release # Build the C# project
- name: Run Unit Tests
run: dotnet test ./test/LinearAlgebra_Test.csproj --configuration Release # Execute unit tests

5
LinearAlgebra/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
DoxyGen/DoxyWarnLogfile.txt
.vscode/settings.json
**bin
**obj
**.meta

View File

@ -0,0 +1,30 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinearAlgebra", "src\LinearAlgebra.csproj", "{ECB58727-0354-924D-AE7B-22F6B21097EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinearAlgebra_Test", "test\LinearAlgebra_Test.csproj", "{715BB399-5FC4-2AC9-3757-177CA0C80774}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{ECB58727-0354-924D-AE7B-22F6B21097EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECB58727-0354-924D-AE7B-22F6B21097EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECB58727-0354-924D-AE7B-22F6B21097EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECB58727-0354-924D-AE7B-22F6B21097EB}.Release|Any CPU.Build.0 = Release|Any CPU
{715BB399-5FC4-2AC9-3757-177CA0C80774}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{715BB399-5FC4-2AC9-3757-177CA0C80774}.Debug|Any CPU.Build.0 = Debug|Any CPU
{715BB399-5FC4-2AC9-3757-177CA0C80774}.Release|Any CPU.ActiveCfg = Release|Any CPU
{715BB399-5FC4-2AC9-3757-177CA0C80774}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E93B8294-87D4-4887-83B7-182A623D5833}
EndGlobalSection
EndGlobal

341
LinearAlgebra/src/Angle.cs Normal file
View File

@ -0,0 +1,341 @@
using System;
namespace LinearAlgebra {
public struct AngleFloat {
public const float Rad2Deg = 360.0f / ((float)Math.PI * 2); //0.0174532924F;
public const float Deg2Rad = (float)Math.PI * 2 / 360.0f; //57.29578F;
private AngleFloat(float degrees) {
this.value = degrees;
}
private readonly float value;
public static AngleFloat Degrees(float degrees) {
// Reduce it to (-180..180]
if (float.IsFinite(degrees)) {
while (degrees < -180)
degrees += 360;
while (degrees >= 180)
degrees -= 360;
}
return new AngleFloat(degrees);
}
public static AngleFloat Radians(float radians) {
// Reduce it to (-pi..pi]
if (float.IsFinite(radians)) {
while (radians <= -Math.PI)
radians += 2 * (float)Math.PI;
while (radians > Math.PI)
radians -= 2 * (float)Math.PI;
}
return new AngleFloat(radians * Rad2Deg);
}
public static AngleFloat Revolutions(float revolutions) {
// reduce it to (-0.5 .. 0.5]
if (float.IsFinite(revolutions)) {
// Get the integer part
int integerPart = (int)revolutions;
// Get the decimal part
revolutions -= integerPart;
if (revolutions < -0.5)
revolutions += 1;
if (revolutions >= 0.5)
revolutions -= 1;
}
return new AngleFloat(revolutions * 360);
}
public readonly float inDegrees => this.value;
public readonly float inRadians => this.value * Deg2Rad;
public readonly float inRevolutions => this.value / 360.0f;
public override string ToString() {
return $"{this.inDegrees}\u00B0";
}
public static readonly AngleFloat zero = Degrees(0);
public static readonly AngleFloat deg90 = Degrees(90);
public static readonly AngleFloat deg180 = Degrees(180);
/// <summary>
/// Get the sign of the angle
/// </summary>
/// <param name="a">The angle</param>
/// <returns>-1 when the angle is negative, 1 when it is positive and 0 in all other cases</returns>
public static int Sign(AngleFloat a) {
if (a.value < 0)
return -1;
if (a.value > 0)
return 1;
return 0;
}
/// <summary>
/// Returns the magnitude of the angle
/// </summary>
/// <param name="a">The angle</param>
/// <returns>The positive magnitude of the angle</returns>
/// Negative values are negated to get a positive result
public static AngleFloat Abs(AngleFloat a) {
if (Sign(a) < 0)
return -a;
else
return a;
}
/// <summary>
/// Tests the equality of two angles
/// </summary>
/// <param name="a1"></param>
/// <param name="a2"></param>
/// <returns>True when the angles are equal, false otherwise</returns>
/// <remarks>The equality is determine within the limits of precision of a float</remarks>
public static bool operator ==(AngleFloat a1, AngleFloat a2) {
return a1.value == a2.value;
}
/// <summary>
/// Tests the inequality of two angles
/// </summary>
/// <param name="a1"></param>
/// <param name="a2"></param>
/// <returns>True when the angles are not equal, false otherwise</returns>
/// <remarks>The equality is determine within the limits of precision of a float</remarks>
public static bool operator !=(AngleFloat a1, AngleFloat a2) {
return a1.value != a2.value;
}
public override readonly bool Equals(object obj) {
if (obj is AngleFloat other) {
return this == other;
}
return false;
}
public override readonly int GetHashCode() {
return this.value.GetHashCode();
}
/// <summary>
/// Tests if the first angle is greater than the second
/// </summary>
/// <param name="a1"></param>
/// <param name="a2"></param>
/// <returns>True when a1 is greater than a2, False otherwise</returns>
public static bool operator >(AngleFloat a1, AngleFloat a2) {
return a1.value > a2.value;
}
/// <summary>
/// Tests if the first angle is greater than or equal to the second
/// </summary>
/// <param name="a1"></param>
/// <param name="a2"></param>
/// <returns>True when a1 is greater than or equal to a2, False otherwise</returns>
public static bool operator >=(AngleFloat a1, AngleFloat a2) {
return a1.value >= a2.value;
}
/// <summary>
/// Tests if the first angle is less than the second
/// </summary>
/// <param name="a1"></param>
/// <param name="a2"></param>
/// <returns>True when a1 is less than a2, False otherwise</returns>
public static bool operator <(AngleFloat a1, AngleFloat a2) {
return a1.value < a2.value;
}
/// <summary>
/// Tests if the first angle is less than or equal to the second
/// </summary>
/// <param name="a1"></param>
/// <param name="a2"></param>
/// <returns>True when a1 is less than or equal to a2, False otherwise</returns>
public static bool operator <=(AngleFloat a1, AngleFloat a2) {
return a1.value <= a2.value;
}
/// <summary>
/// Negate the angle
/// </summary>
/// <param name="a">The angle</param>
/// <returns>The negated angle</returns>
/// The negation of -180 is still -180 because the range is (-180..180]
public static AngleFloat operator -(AngleFloat a) {
AngleFloat r = new(-a.value);
return r;
}
/// <summary>
/// Subtract two angles
/// </summary>
/// <param name="a1">Angle 1</param>
/// <param name="a2">Angle 2</param>
/// <returns>The result of the subtraction</returns>
public static AngleFloat operator -(AngleFloat a1, AngleFloat a2) {
AngleFloat r = new(a1.value - a2.value);
return r;
}
/// <summary>
/// Add two angles
/// </summary>
/// <param name="a1">Angle 1</param>
/// <param name="a2">Angle 2</param>
/// <returns>The result of the addition</returns>
public static AngleFloat operator +(AngleFloat a1, AngleFloat a2) {
AngleFloat r = new(a1.value + a2.value);
return r;
}
/// <summary>
/// Multiplies the angle
/// </summary>
/// <param name="a">The angle to multiply</param>
/// <param name="factor">The factor by which the angle is multiplied</param>
/// <returns>The multiplied angle</returns>
public static AngleFloat operator *(AngleFloat a, float factor) {
return Degrees(a.inDegrees * factor);
}
public static AngleFloat operator *(float factor, AngleFloat a) {
return Degrees(factor * a.inDegrees);
}
/// <summary>
/// Clamp the angle between the given min and max values
/// </summary>
/// <param name="angle">The angle to clamp</param>
/// <param name="min">The minimum angle</param>
/// <param name="max">The maximum angle</param>
/// <returns>The clamped angle</returns>
/// Angles are normalized
public static float Clamp(AngleFloat angle, AngleFloat min, AngleFloat max) {
return Float.Clamp(angle.inDegrees, min.inDegrees, max.inDegrees);
}
/// @brief Calculates the cosine of an angle
/// @param angle The given angle
/// @return The cosine of the angle
public static float Cos(AngleFloat angle) {
return MathF.Cos(angle.inRadians);
}
/// @brief Calculates the sine of an angle
/// @param angle The given angle
/// @return The sine of the angle
public static float Sin(AngleFloat angle) {
return MathF.Sin(angle.inRadians);
}
/// @brief Calculates the tangent of an angle
/// @param angle The given angle
/// @return The tangent of the angle
public static float Tan(AngleFloat angle) {
return MathF.Tan(angle.inRadians);
}
/// @brief Calculates the arc cosine angle
/// @param f The value
/// @return The arc cosine for the given value
public static AngleFloat Acos(float f) {
return Radians(MathF.Acos(f));
}
/// @brief Calculates the arc sine angle
/// @param f The value
/// @return The arc sine for the given value
public static AngleFloat Asin(float f) {
return Radians(MathF.Asin(f));
}
/// @brief Calculates the arc tangent angle
/// @param f The value
/// @return The arc tangent for the given value
public static AngleFloat Atan(float f) {
return Radians(MathF.Atan(f));
}
/// @brief Calculates the tangent for the given values
/// @param y The vertical value
/// @param x The horizontal value
/// @return The tanget for the given values
/// Uses the y and x signs to compute the quadrant
public static AngleFloat Atan2(float y, float x) {
return Radians(MathF.Atan2(y, x));
}
/// <summary>
/// Rotate from one angle to the other with a maximum degrees
/// </summary>
/// <param name="fromAngle">Starting angle</param>
/// <param name="toAngle">Target angle</param>
/// <param name="maxAngle">Maximum angle to rotate</param>
/// <returns>The resulting angle</returns>
/// This function is compatible with radian and degrees angles
public static AngleFloat MoveTowards(AngleFloat fromAngle, AngleFloat toAngle, float maxDegrees) {
maxDegrees = Math.Max(0, maxDegrees); // filter out negative distances
AngleFloat d = toAngle - fromAngle;
float dDegrees = Abs(d).inDegrees;
d = Degrees(Float.Clamp(dDegrees, 0, maxDegrees));
if (Sign(d) < 0)
d = -d;
return fromAngle + d;
}
}
/// <summary>
/// %Angle utilities
/// </summary>
public static class Angles {
public const float pi = 3.1415927410125732421875F;
// public static float Rad2Deg = 360.0f / ((float)Math.PI * 2);
// public static float Deg2Rad = ((float)Math.PI * 2) / 360.0f;
/// <summary>
/// Determine the angle difference, result is a normalized angle
/// </summary>
/// <param name="a">First first angle</param>
/// <param name="b">The second angle</param>
/// <returns>the angle between the two angles</returns>
/// Angle values should be degrees
public static float Difference(float a, float b) {
float r = Normalize(b - a);
return r;
}
/// <summary>
/// Normalize an angle to the range -180 < angle <= 180
/// </summary>
/// <param name="angle">The angle to normalize</param>
/// <returns>The normalized angle in interval (-180..180] </returns>
/// Angle values should be in degrees
public static float Normalize(float angle) {
if (float.IsInfinity(angle))
return angle;
while (angle <= -180) angle += 360;
while (angle > 180) angle -= 360;
return angle;
}
/// <summary>
/// Map interval of angles between vectors [0..Pi] to interval [0..1]
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns>The resulting factor in interval [0..1]</returns>
/// Vectors a and b must be normalized
/// \deprecated Please use Vector2.ToFactor instead.
// [Obsolete("Please use Vector2.ToFactor instead.")]
// public static float ToFactor(Vector2Float v1, Vector2Float v2) {
// return (1 - Vector2Float.Dot(v1, v2)) / 2;
// }
}
}

View File

@ -0,0 +1,287 @@
using System;
namespace LinearAlgebra {
class QR {
// QR Decomposition of a matrix A
public static (Matrix2 Q, Matrix2 R) Decomposition(Matrix2 A) {
int nRows = A.nRows;
int nCols = A.nCols;
float[,] Q = new float[nRows, nCols];
float[,] R = new float[nCols, nCols];
// Perform Gram-Schmidt orthogonalization
for (uint colIx = 0; colIx < nCols; colIx++) {
// Step 1: v = column(ix) of A
float[] v = new float[nRows];
for (int rowIx = 0; rowIx < nRows; rowIx++)
v[rowIx] = A.data[rowIx, colIx];
// Step 2: Subtract projections of v onto previous q's (orthogonalize)
for (uint colIx2 = 0; colIx2 < colIx; colIx2++) {
float dotProd = 0;
for (int i = 0; i < nRows; i++)
dotProd += Q[i, colIx2] * v[i];
for (int i = 0; i < nRows; i++)
v[i] -= dotProd * Q[i, colIx2];
}
// Step 3: Normalize v to get column(ix) of Q
float norm = 0;
for (int rowIx = 0; rowIx < nRows; rowIx++)
norm += v[rowIx] * v[rowIx];
norm = (float)Math.Sqrt(norm);
for (int rowIx = 0; rowIx < nRows; rowIx++)
Q[rowIx, colIx] = v[rowIx] / norm;
// Store the coefficients of R
for (int colIx2 = 0; colIx2 <= colIx; colIx2++) {
R[colIx2, colIx] = 0;
for (int k = 0; k < nRows; k++)
R[colIx2, colIx] += Q[k, colIx2] * A.data[k, colIx];
}
}
return (new Matrix2(Q), new Matrix2(R));
}
// Reduced QR Decomposition of a matrix A
public static (Matrix2 Q, Matrix2 R) ReducedDecomposition(Matrix2 A) {
int nRows = A.nRows;
int nCols = A.nCols;
float[,] Q = new float[nRows, nCols];
float[,] R = new float[nCols, nCols];
// Perform Gram-Schmidt orthogonalization
for (int colIx = 0; colIx < nCols; colIx++) {
// Step 1: v = column(colIx) of A
float[] columnIx = new float[nRows];
bool isZeroColumn = true;
for (int rowIx = 0; rowIx < nRows; rowIx++) {
columnIx[rowIx] = A.data[rowIx, colIx];
if (columnIx[rowIx] != 0)
isZeroColumn = false;
}
if (isZeroColumn) {
for (int rowIx = 0; rowIx < nRows; rowIx++)
Q[rowIx, colIx] = 0;
// Set corresponding R element to 0
R[colIx, colIx] = 0;
Console.WriteLine($"zero column {colIx}");
continue;
}
// Step 2: Subtract projections of v onto previous q's (orthogonalize)
for (int colIx2 = 0; colIx2 < colIx; colIx2++) {
// Compute the dot product of v and column(colIx2) of Q
float dotProduct = 0;
for (int rowIx2 = 0; rowIx2 < nRows; rowIx2++)
dotProduct += columnIx[rowIx2] * Q[rowIx2, colIx2];
// Subtract the projection from v
for (int rowIx2 = 0; rowIx2 < nRows; rowIx2++)
columnIx[rowIx2] -= dotProduct * Q[rowIx2, colIx2];
}
// Step 3: Normalize v to get column(colIx) of Q
float norm = 0;
for (int rowIx = 0; rowIx < nRows; rowIx++)
norm += columnIx[rowIx] * columnIx[rowIx];
if (norm == 0)
throw new Exception("invalid value");
norm = (float)Math.Sqrt(norm);
for (int rowIx = 0; rowIx < nRows; rowIx++)
Q[rowIx, colIx] = columnIx[rowIx] / norm;
// Here is where it deviates from the Full QR Decomposition !
// Step 4: Compute the row(colIx) of R
for (int colIx2 = colIx; colIx2 < nCols; colIx2++) {
float dotProduct = 0;
for (int rowIx2 = 0; rowIx2 < nRows; rowIx2++)
dotProduct += Q[rowIx2, colIx] * A.data[rowIx2, colIx2];
R[colIx, colIx2] = dotProduct;
}
}
if (!float.IsFinite(R[0, 0]))
throw new Exception("invalid value");
return (new Matrix2(Q), new Matrix2(R));
}
}
class SVD {
// According to ChatGPT, Mathnet uses Golub-Reinsch SVD algorithm
// 1. Bidiagonalization: The input matrix AA is reduced to a bidiagonal form using Golub-Kahan bidiagonalization.
// This process involves applying a sequence of Householder reflections to AA to create a bidiagonal matrix.
// This step reduces the complexity by making the matrix simpler while retaining the essential structure needed for SVD.
//
// 2. Diagonalization: Once the matrix is in bidiagonal form,
// the singular values are computed using an iterative process
// (typically involving QR factorization or Jacobi rotations) until convergence.
// This process diagonalizes the bidiagonal matrix and allows extraction of the singular values.
//
// 3. Computing UU and VTVT: After obtaining the singular values,
// the left singular vectors UU and right singular vectors VTVT are computed
// using the accumulated transformations (such as Householder reflections) from the bidiagonalization step.
// Bidiagnolizations through Householder transformations
public static (Matrix2 U1, Matrix2 B, Matrix2 V1) Bidiagonalization(Matrix2 A) {
int m = A.nRows; // Rows of A
int n = A.nCols; // Columns of A
float[,] U1 = new float[m, m]; // Left orthogonal matrix
float[,] V1 = new float[n, n]; // Right orthogonal matrix
float[,] B = A.Clone().data; // Copy A to B for transformation
// Initialize U1 and V1 as identity matrices
for (int i = 0; i < m; i++)
U1[i, i] = 1;
for (int i = 0; i < n; i++)
V1[i, i] = 1;
// Perform Householder reflections to create a bidiagonal matrix B
for (int j = 0; j < n; j++) {
// Step 1: Construct the Householder vector y
float[] y = new float[m - j];
for (int i = j; i < m; i++)
y[i - j] = B[i, j];
// Step 2: Compute the norm and scalar alpha
float norm = 0;
for (int i = 0; i < y.Length; i++)
norm += y[i] * y[i];
norm = (float)Math.Sqrt(norm);
if (B[j, j] > 0)
norm = -norm;
float alpha = (float)Math.Sqrt(0.5 * (norm * (norm - B[j, j])));
float r = (float)Math.Sqrt(0.5 * (norm * (norm + B[j, j])));
// Step 3: Apply the reflection to zero out below diagonal
for (int k = j; k < n; k++) {
float dot = 0;
for (int i = j; i < m; i++)
dot += y[i - j] * B[i, k];
dot /= r;
for (int i = j; i < m; i++)
B[i, k] -= 2 * dot * y[i - j];
}
// Step 4: Update U1 with the Householder reflection (U1 * Householder)
for (int i = j; i < m; i++)
U1[i, j] = y[i - j] / alpha;
// Step 5: Update V1 (storing the Householder vector y)
// Correct indexing: we only need to store part of y in V1 from index j to n
for (int i = j; i < n; i++)
V1[j, i] = B[j, i];
// Repeat steps for further columns if necessary
}
return (new Matrix2(U1), new Matrix2(B), new Matrix2(V1));
}
public static Matrix2 Bidiagonalize(Matrix2 A) {
int m = A.nRows; // Rows of A
int n = A.nCols; // Columns of A
float[,] B = A.Clone().data; // Copy A to B for transformation
// Perform Householder reflections to create a bidiagonal matrix B
for (int j = 0; j < n; j++) {
// Step 1: Construct the Householder vector y
float[] y = new float[m - j];
for (int i = j; i < m; i++)
y[i - j] = B[i, j];
// Step 2: Compute the norm and scalar alpha
float norm = 0;
for (int i = 0; i < y.Length; i++)
norm += y[i] * y[i];
norm = (float)Math.Sqrt(norm);
if (B[j, j] > 0)
norm = -norm;
float r = (float)Math.Sqrt(0.5 * (norm * (norm + B[j, j])));
// Step 3: Apply the reflection to zero out below diagonal
for (int k = j; k < n; k++) {
float dot = 0;
for (int i = j; i < m; i++)
dot += y[i - j] * B[i, k];
dot /= r;
for (int i = j; i < m; i++)
B[i, k] -= 2 * dot * y[i - j];
}
// Repeat steps for further columns if necessary
}
return new Matrix2(B);
}
// QR Iteration for diagonalization of a bidiagonal matrix B
public static (Matrix1 singularValues, Matrix2 U, Matrix2 Vt) QRIteration(Matrix2 B) {
int m = B.nRows;
int n = B.nCols;
Matrix2 U = new(m, m); // Left singular vectors (U)
Matrix2 Vt = new(n, n); // Right singular vectors (V^T)
float[] singularValues = new float[Math.Min(m, n)]; // Singular values
// Initialize U and Vt as identity matrices
for (int i = 0; i < m; i++)
U.data[i, i] = 1;
for (int i = 0; i < n; i++)
Vt.data[i, i] = 1;
// Perform QR iterations
float tolerance = 1e-7f; //1e-12f; for double
bool converged = false;
while (!converged) {
// Perform QR decomposition on the matrix B
(Matrix2 Q, Matrix2 R) = QR.Decomposition(B);
// Update B to be the product Q * R //R * Q
B = R * Q;
// Accumulate the transformations in U and Vt
U *= Q;
Vt *= R;
// Check convergence by looking at the off-diagonal elements of B
converged = true;
for (int i = 0; i < m - 1; i++) {
for (int j = i + 1; j < n; j++) {
if (Math.Abs(B.data[i, j]) > tolerance) {
converged = false;
break;
}
}
}
}
// Extract singular values (diagonal elements of B)
for (int i = 0; i < Math.Min(m, n); i++)
singularValues[i] = B.data[i, i];
return (new Matrix1(singularValues), U, Vt);
}
public static (Matrix2 U, Matrix1 S, Matrix2 Vt) Decomposition(Matrix2 A) {
if (A.nRows != A.nCols)
throw new ArgumentException("SVD: matrix A has to be square.");
Matrix2 B = Bidiagonalize(A);
(Matrix1 S, Matrix2 U, Matrix2 Vt) = QRIteration(B);
return (U, S, Vt);
}
}
}

View File

@ -0,0 +1,261 @@
using System;
#if UNITY_5_3_OR_NEWER
using Vector3Float = UnityEngine.Vector3;
#endif
namespace LinearAlgebra
{
/// <summary>
/// A direction in 3D space
/// </summary>
/// A direction is represented using two angles:
/// * The horizontal angle ranging from -180 (inclusive) to 180 (exclusive)
/// degrees which is a rotation in the horizontal plane
/// * A vertical angle ranging from -90 (inclusive) to 90 (exclusive) degrees
/// which is the rotation in the up/down direction applied after the horizontal
/// rotation has been applied.
/// The angles are automatically normalized to stay within the abovenmentioned
/// ranges.
public struct Direction
{
/// @brief horizontal angle, range = (-180..180] degrees
public AngleFloat horizontal;
/// @brief vertical angle, range in degrees = (-90..90] degrees
public AngleFloat vertical;
/// <summary>
/// Create a new direction
/// </summary>
/// <param name="horizontal">The horizontal angle</param>
/// <param name="vertical">The vertical angle</param>
/// <remarks>The direction will be normalized automatically
/// to ensure the angles are within the allowed ranges</remarks>
public Direction(AngleFloat horizontal, AngleFloat vertical)
{
this.horizontal = horizontal;
this.vertical = vertical;
this.Normalize();
}
/// <summary>
/// Create a direction using angle values in degrees
/// </summary>
/// <param name="horizontal">The horizontal angle in degrees</param>
/// <param name="vertical">The vertical angle in degrees</param>
/// <returns>The direction</returns>
/// <remarks>The direction will be normalized automatically
/// to ensure the angles are within the allowed ranges</remarks>
public static Direction Degrees(float horizontal, float vertical)
{
Direction d = new()
{
horizontal = AngleFloat.Degrees(horizontal),
vertical = AngleFloat.Degrees(vertical)
};
d.Normalize();
return d;
}
/// <summary>
/// Create a direction using angle values in radians
/// </summary>
/// <param name="horizontal">The horizontal angle in radians</param>
/// <param name="vertical">The vertical angle in radians</param>
/// <returns>The direction</returns>
public static Direction Radians(float horizontal, float vertical)
{
Direction d = new()
{
horizontal = AngleFloat.Radians(horizontal),
vertical = AngleFloat.Radians(vertical)
};
d.Normalize();
return d;
}
public override readonly string ToString() {
return $"Direction(h: {this.horizontal}, v: {this.vertical})";
}
/// <summary>
/// A forward direction with zero for both angles
/// </summary>
public readonly static Direction forward = Degrees(0, 0);
/// <summary>
/// A backward direction with horizontal angle -180 and zero vertical
/// angle
/// </summary>
public readonly static Direction backward = Degrees(-180, 0);
/// <summary>
/// A upward direction with zero horizontal angle and vertical angle 90
/// </summary>
public readonly static Direction up = Degrees(0, 90);
/// <summary>
/// A downward direction with zero horizontal angle and vertical angle
/// -90
/// </summary>
public readonly static Direction down = Degrees(0, -90);
/// <summary>
/// A left-pointing direction with horizontal angle -90 and zero
/// vertical angle
/// </summary>
public readonly static Direction left = Degrees(-90, 0);
/// <summary>
/// A right-pointing direction with horizontal angle 90 and zero
/// vertical angle
/// </summary>
public readonly static Direction right = Degrees(90, 0);
private void Normalize()
{
if (this.vertical > AngleFloat.deg90 || this.vertical < -AngleFloat.deg90)
{
this.horizontal += AngleFloat.deg180;
this.vertical = AngleFloat.Degrees(180 - this.vertical.inDegrees);
}
}
#if UNITY_5_3_OR_NEWER
/// <summary>
/// Convert the direction into a carthesian vector
/// </summary>
/// <returns>The carthesian vector corresponding to this direction.</returns>
public readonly UnityEngine.Vector3 ToVector3() {
// Convert degrees to radians
float radH = this.horizontal.inRadians;
float radV = this.vertical.inRadians;
// Calculate Vector
float cosV = MathF.Cos(radV);
float sinV = MathF.Sin(radV);
float x = cosV * MathF.Sin(radH);
float y = sinV;
float z = cosV * MathF.Cos(radH);
return new UnityEngine.Vector3(x, y, z);
}
/// <summary>
/// Convert a carthesian vector into a direction
/// </summary>
/// <param name="v">The carthesian vector</param>
/// <returns>The direction</returns>
/// <remarks>Information about the length of the carthesian vector is not
/// included in this transformation</remarks>
public static Direction FromVector3(UnityEngine.Vector3 v) {
AngleFloat horizontal = AngleFloat.Atan2(v.x, v.z);
AngleFloat vertical = AngleFloat.deg90 - AngleFloat.Acos(v.y);
Direction d = new(horizontal, vertical);
return d;
}
#else
/// <summary>
/// Convert the direction into a carthesian vector
/// </summary>
/// <returns>The carthesian vector corresponding to this direction.</returns>
public readonly Vector3Float ToVector3() {
// Quaternion q = Quaternion.Euler(90 - this.vertical.inDegrees, this.horizontal.inDegrees, 0);
// Vector3Float v = q * Vector3Float.forward;
// return v;
float radH = this.horizontal.inRadians;
float radV = this.vertical.inRadians;
float cosV = MathF.Cos(radV);
float sinV = MathF.Sin(radV);
float horizontal = cosV * MathF.Sin(radH);
float vertical = sinV;
float depth = cosV * MathF.Cos(radH);
return new Vector3Float(horizontal, vertical, depth);
}
/// <summary>
/// Convert a carthesian vector into a direction
/// </summary>
/// <param name="v">The carthesian vector</param>
/// <returns>The direction</returns>
/// <remarks>Information about the length of the carthesian vector is not
/// included in this transformation</remarks>
public static Direction FromVector3(Vector3Float v)
{
AngleFloat horizontal = AngleFloat.Atan2(v.horizontal, v.depth);
AngleFloat vertical = AngleFloat.deg90 - AngleFloat.Acos(v.vertical);
Direction d = new(horizontal, vertical);
return d;
}
#endif
public static Direction operator -(Direction d) {
AngleFloat horizontal = d.horizontal + AngleFloat.deg180;
AngleFloat vertical = -d.vertical;
return new Direction(horizontal, vertical);
}
/// <summary>
/// Tests the equality of two directions
/// </summary>
/// <param name="d1"></param>
/// <param name="d2"></param>
/// <returns>True when the direction angles are equal, false otherwise.</returns>
public static bool operator ==(Direction d1, Direction d2)
{
bool horizontalEq = d1.horizontal == d2.horizontal;
bool verticalEq = d1.vertical == d2.vertical;
return horizontalEq && verticalEq;
}
/// <summary>
/// Tests the inequality of two directions
/// </summary>
/// <param name="d1"></param>
/// <param name="d2"></param>
/// <returns>True when the direction angles are not equal, false otherwise.</returns>
public static bool operator !=(Direction d1, Direction d2)
{
bool horizontalNEq = d1.horizontal != d2.horizontal;
bool verticalNEq = d1.vertical != d2.vertical;
return horizontalNEq || verticalNEq;
}
public override readonly bool Equals(object obj)
{
if (obj is not Direction d)
return false;
bool horizontalEq = this.horizontal == d.horizontal;
bool verticalEq = this.vertical == d.vertical;
return horizontalEq && verticalEq;
}
public override readonly int GetHashCode()
{
return HashCode.Combine(horizontal, vertical);
}
public static AngleFloat UnsignedAngle(Direction d1, Direction d2) {
// Convert angles from degrees to radians
float horizontal1Rad = d1.horizontal.inRadians;
float vertical1Rad = d1.vertical.inRadians;
float horizontal2Rad = d2.horizontal.inRadians;
float vertical2Rad = d2.vertical.inRadians;
// Calculate the cosine of the angle using the spherical law of cosines
float cosTheta = MathF.Sin(vertical1Rad) * MathF.Sin(vertical2Rad) +
MathF.Cos(vertical1Rad) * MathF.Cos(vertical2Rad) *
MathF.Cos(horizontal1Rad - horizontal2Rad);
// Clip cosTheta to the valid range for acos
cosTheta = Float.Clamp(cosTheta, -1.0f, 1.0f);
// Calculate the angle
AngleFloat angle = AngleFloat.Acos(cosTheta);
return angle;
}
}
}

View File

@ -0,0 +1,41 @@
namespace LinearAlgebra {
/// <summary>
/// Float number utilities
/// </summary>
public class Float {
/// <summary>
/// The precision of float numbers
/// </summary>
public const float epsilon = 1E-05f;
/// <summary>
/// The square of the float number precision
/// </summary>
public const float sqrEpsilon = 1e-10f;
/// <summary>
/// Clamp the value between the given minimum and maximum values
/// </summary>
/// <param name="f">The value to clamp</param>
/// <param name="min">The minimum value</param>
/// <param name="max">The maximum value</param>
/// <returns>The clamped value</returns>
public static float Clamp(float f, float min, float max) {
if (f < min)
return min;
if (f > max)
return max;
return f;
}
/// <summary>
/// Clamp the value between to the interval [0..1]
/// </summary>
/// <param name="f">The value to clamp</param>
/// <returns>The clamped value</returns>
public static float Clamp01(float f) {
return Clamp(f, 0, 1);
}
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
</ItemGroup>
</Project>

689
LinearAlgebra/src/Matrix.cs Normal file
View File

@ -0,0 +1,689 @@
using System;
#if UNITY_5_3_OR_NEWER
using Vector3Float = UnityEngine.Vector3;
using Vector2Float = UnityEngine.Vector2;
using Quaternion = UnityEngine.Quaternion;
#endif
namespace LinearAlgebra {
public readonly struct Slice
{
public int start { get; }
public int stop { get; }
public Slice(int start, int stop)
{
this.start = start;
this.stop = stop;
}
}
public class Matrix2 {
public float[,] data { get; }
public int nRows => data.GetLength(0);
public int nCols => data.GetLength(1);
public Matrix2(int nRows, int nCols)
{
this.data = new float[nRows, nCols];
}
public Matrix2(float[,] data) {
this.data = data;
}
public Matrix2 Clone() {
float[,] data = new float[this.nRows, nCols];
for (int rowIx = 0; rowIx < this.nRows; rowIx++) {
for (int colIx = 0; colIx < this.nCols; colIx++)
data[rowIx, colIx] = this.data[rowIx, colIx];
}
return new Matrix2(data);
}
public static Matrix2 Zero(int nRows, int nCols)
{
return new Matrix2(nRows, nCols);
}
public static Matrix2 FromVector3(Vector3Float v) {
float[,] result = new float[3, 1];
result[0, 0] = v.horizontal;
result[1, 0] = v.vertical;
result[2, 0] = v.depth;
return new Matrix2(result);
}
public static Matrix2 Identity(int size)
{
return Diagonal(1, size);
}
public static Matrix2 Identity(int nRows, int nCols)
{
Matrix2 m = Zero(nRows, nCols);
m.FillDiagonal(1);
return m;
}
public static Matrix2 Diagonal(Matrix1 v) {
float[,] resultData = new float[v.size, v.size];
for (int ix = 0; ix < v.size; ix++)
resultData[ix, ix] = v.data[ix];
return new Matrix2(resultData);
}
public static Matrix2 Diagonal(float f, int size)
{
float[,] resultData = new float[size, size];
for (int ix = 0; ix < size; ix++)
resultData[ix, ix] = f;
return new Matrix2(resultData);
}
public void FillDiagonal(Matrix1 v)
{
int n = (int)Math.Min(Math.Min(this.nRows, this.nCols), v.size);
for (int ix = 0; ix < n; ix++)
this.data[ix, ix] = v.data[ix];
}
public void FillDiagonal(float f)
{
int n = Math.Min(this.nRows, this.nCols);
for (int ix = 0; ix < n; ix++)
this.data[ix, ix] = f;
}
public static Matrix2 SkewMatrix(Vector3Float v) {
float[,] result = new float[3, 3] {
{0, -v.depth, v.vertical},
{v.depth, 0, -v.horizontal},
{-v.vertical, v.horizontal, 0}
};
return new Matrix2(result);
}
public Matrix1 GetRow(int rowIx) {
float[] row = new float[this.nCols];
for (int colIx = 0; colIx < this.nCols; colIx++) {
row[colIx] = this.data[rowIx, colIx];
}
return new Matrix1(row);
}
#if UNITY_5_3_OR_NEWER
public UnityEngine.Vector3 GetRow3(int rowIx) {
int cols = this.nCols;
UnityEngine.Vector3 row = new() {
x = this.data[rowIx, 0],
y = this.data[rowIx, 1],
z = this.data[rowIx, 2]
};
return row;
}
#endif
public void SetRow(int rowIx, Matrix1 v) {
for (uint ix = 0; ix < v.size; ix++)
this.data[rowIx, ix] = v.data[ix];
}
public void SetRow3(int rowIx, Vector3Float v) {
this.data[rowIx, 0] = v.horizontal;
this.data[rowIx, 1] = v.vertical;
this.data[rowIx, 2] = v.depth;
}
public void SwapRows(int row1, int row2) {
for (uint ix = 0; ix < this.nCols; ix++) {
float temp = this.data[row1, ix];
this.data[row1, ix] = this.data[row2, ix];
this.data[row2, ix] = temp;
}
}
public Matrix1 GetColumn(int colIx)
{
float[] column = new float[this.nRows];
for (int i = 0; i < this.nRows; i++) {
column[i] = this.data[i, colIx];
}
return new Matrix1(column);
}
public void SetColumn(int colIx, Matrix1 v) {
for (uint ix = 0; ix < v.size; ix++)
this.data[ix, colIx] = v.data[ix];
}
public void SetColumn(int colIx, Vector3Float v) {
this.data[0, colIx] = v.horizontal;
this.data[1, colIx] = v.vertical;
this.data[2, colIx] = v.depth;
}
public static bool AllClose(Matrix2 A, Matrix2 B, float atol = 1e-08f) {
for (int i = 0; i < A.nRows; i++) {
for (int j = 0; j < A.nCols; j++) {
float d = MathF.Abs(A.data[i, j] - B.data[i, j]);
if (d > atol)
return false;
}
}
return true;
}
public Matrix2 Transpose() {
float[,] resultData = new float[this.nCols, this.nRows];
for (uint rowIx = 0; rowIx < this.nRows; rowIx++) {
for (uint colIx = 0; colIx < this.nCols; colIx++)
resultData[colIx, rowIx] = this.data[rowIx, colIx];
}
return new Matrix2(resultData);
// double checked code
}
public Matrix2 transposed {
get => Transpose();
}
public static Matrix2 operator -(Matrix2 m) {
float[,] result = new float[m.nRows, m.nCols];
for (int i = 0; i < m.nRows; i++) {
for (int j = 0; j < m.nCols; j++)
result[i, j] = -m.data[i, j];
}
return new Matrix2(result);
}
public static Matrix2 operator -(Matrix2 A, Matrix2 B) {
if (A.nRows != B.nRows || A.nCols != B.nCols)
throw new System.ArgumentException("Size of A must match size of B.");
float[,] result = new float[A.nRows, B.nCols];
for (int i = 0; i < A.nRows; i++) {
for (int j = 0; j < A.nCols; j++)
result[i, j] = A.data[i, j] - B.data[i, j];
}
return new Matrix2(result);
}
public static Matrix2 operator +(Matrix2 A, Matrix2 B) {
if (A.nRows != B.nRows || A.nCols != B.nCols)
throw new System.ArgumentException("Size of A must match size of B.");
float[,] result = new float[A.nRows, B.nCols];
for (int i = 0; i < A.nRows; i++) {
for (int j = 0; j < A.nCols; j++)
result[i, j] = A.data[i, j] + B.data[i, j];
}
return new Matrix2(result);
}
public static Matrix2 operator *(Matrix2 A, Matrix2 B) {
if (A.nCols != B.nRows)
throw new System.ArgumentException("Number of columns in A must match number of rows in B.");
float[,] result = new float[A.nRows, B.nCols];
for (int i = 0; i < A.nRows; i++) {
for (int j = 0; j < B.nCols; j++) {
float sum = 0.0f;
for (int k = 0; k < A.nCols; k++)
sum += A.data[i, k] * B.data[k, j];
result[i, j] = sum;
}
}
return new Matrix2(result);
// double checked code
}
public static Matrix1 operator *(Matrix2 A, Matrix1 v) {
float[] result = new float[A.nRows];
for (int i = 0; i < A.nRows; i++) {
for (int j = 0; j < A.nCols; j++) {
result[i] += A.data[i, j] * v.data[j];
}
}
return new Matrix1(result);
}
public static Vector3Float operator *(Matrix2 A, Vector3Float v) {
return new Vector3Float(
A.data[0, 0] * v.horizontal + A.data[0, 1] * v.vertical + A.data[0, 2] * v.depth,
A.data[1, 0] * v.horizontal + A.data[1, 1] * v.vertical + A.data[1, 2] * v.depth,
A.data[2, 0] * v.horizontal + A.data[2, 1] * v.vertical + A.data[2, 2] * v.depth
);
}
public static Matrix2 operator *(Matrix2 A, float s) {
float[,] result = new float[A.nRows, A.nCols];
for (int i = 0; i < A.nRows; i++) {
for (int j = 0; j < A.nCols; j++)
result[i, j] = A.data[i, j] * s;
}
return new Matrix2(result);
}
public static Matrix2 operator *(float s, Matrix2 A) {
return A * s;
}
public static Matrix2 operator /(Matrix2 A, float s) {
float[,] result = new float[A.nRows, A.nCols];
for (int i = 0; i < A.nRows; i++) {
for (int j = 0; j < A.nCols; j++)
result[i, j] = A.data[i, j] / s;
}
return new Matrix2(result);
}
public static Matrix2 operator /(float s, Matrix2 A) {
float[,] result = new float[A.nRows, A.nCols];
for (int i = 0; i < A.nRows; i++) {
for (int j = 0; j < A.nCols; j++)
result[i, j] = s / A.data[i, j];
}
return new Matrix2(result);
}
public Matrix2 GetRows(Slice slice) {
return GetRows(slice.start, slice.stop);
}
public Matrix2 GetRows(int from, int to) {
if (from < 0 || to >= this.nRows)
throw new System.ArgumentException("Slice index out of range.");
float[,] result = new float[to - from, this.nCols];
int resultRowIx = 0;
for (int rowIx = from; rowIx < to; rowIx++) {
for (int colIx = 0; colIx < this.nCols; colIx++)
result[resultRowIx, colIx] = this.data[rowIx, colIx];
resultRowIx++;
}
return new Matrix2(result);
}
public Matrix2 Slice(Slice slice)
{
return Slice(slice.start, slice.stop);
}
public Matrix2 Slice(int from, int to)
{
if (from < 0 || to >= this.nRows)
throw new System.ArgumentException("Slice index out of range.");
float[,] result = new float[to - from, this.nCols];
int resultRowIx = 0;
for (int rowIx = from; rowIx < to; rowIx++)
{
for (int colIx = 0; colIx < this.nCols; colIx++)
{
result[resultRowIx, colIx] = this.data[rowIx, colIx];
}
resultRowIx++;
}
return new Matrix2(result);
}
public Matrix2 Slice(Slice rowRange, Slice colRange) {
return Slice((rowRange.start, rowRange.stop), (colRange.start, colRange.stop));
}
public Matrix2 Slice((int start, int stop) rowRange, (int start, int stop) colRange)
{
float[,] result = new float[rowRange.stop - rowRange.start, colRange.stop - colRange.start];
int resultRowIx = 0;
int resultColIx = 0;
for (int i = rowRange.start; i < rowRange.stop; i++)
{
for (int j = colRange.start; j < colRange.stop; j++)
result[resultRowIx, resultColIx] = this.data[i, j];
}
return new Matrix2(result);
}
public void UpdateSlice(Slice slice, Matrix2 m) {
UpdateSlice((slice.start, slice.stop), m);
}
public void UpdateSlice((int start, int stop) slice, Matrix2 m) {
// if (slice.start == slice.stop)
// Console.WriteLine("WARNING: no data is updates when start equals stop in a slice!");
int mRowIx = 0;
for (int rowIx = slice.start; rowIx < slice.stop; rowIx++, mRowIx++) {
for (int colIx = 0; colIx < this.nCols; colIx++)
this.data[rowIx, colIx] = m.data[mRowIx, colIx];
}
}
public void UpdateSlice(Slice rowRange, Slice colRange, Matrix2 m)
{
UpdateSlice((rowRange.start, rowRange.stop), (colRange.start, colRange.stop), m);
}
public void UpdateSlice((int start, int stop) rowRange, (int start, int stop) colRange, Matrix2 m)
{
for (int i = rowRange.start; i < rowRange.stop; i++)
{
for (int j = colRange.start; j < colRange.stop; j++)
this.data[i, j] = m.data[i - rowRange.start, j - colRange.start];
}
}
public Matrix2 Inverse() {
Matrix2 A = this;
// unchecked
int n = A.nRows;
// Create an identity matrix of the same size as the original matrix
float[,] augmentedMatrix = new float[n, 2 * n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
augmentedMatrix[i, j] = A.data[i, j];
augmentedMatrix[i, j + n] = (i == j) ? 1 : 0; // Identity matrix
}
}
// Perform Gaussian elimination
for (int i = 0; i < n; i++) {
// Find the pivot row
float pivot = augmentedMatrix[i, i];
if (Math.Abs(pivot) < 1e-10) // Check for singular matrix
throw new InvalidOperationException("Matrix is singular and cannot be inverted.");
// Normalize the pivot row
for (int j = 0; j < 2 * n; j++)
augmentedMatrix[i, j] /= pivot;
// Eliminate the column below the pivot
for (int j = i + 1; j < n; j++) {
float factor = augmentedMatrix[j, i];
for (int k = 0; k < 2 * n; k++)
augmentedMatrix[j, k] -= factor * augmentedMatrix[i, k];
}
}
// Back substitution
for (int i = n - 1; i >= 0; i--)
{
// Eliminate the column above the pivot
for (int j = i - 1; j >= 0; j--)
{
float factor = augmentedMatrix[j, i];
for (int k = 0; k < 2 * n; k++)
augmentedMatrix[j, k] -= factor * augmentedMatrix[i, k];
}
}
// Extract the inverse matrix from the augmented matrix
float[,] inverse = new float[n, n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++)
inverse[i, j] = augmentedMatrix[i, j + n];
}
return new Matrix2(inverse);
}
public float Determinant()
{
int n = this.nRows;
if (n != this.nCols)
throw new System.ArgumentException("Matrix must be square.");
if (n == 1)
return this.data[0, 0]; // Base case for 1x1 matrix
if (n == 2) // Base case for 2x2 matrix
return this.data[0, 0] * this.data[1, 1] - this.data[0, 1] * this.data[1, 0];
float det = 0;
for (int col = 0; col < n; col++)
det += (col % 2 == 0 ? 1 : -1) * this.data[0, col] * this.Minor(0, col).Determinant();
return det;
}
// Helper function to compute the minor of a matrix
private Matrix2 Minor(int rowToRemove, int colToRemove)
{
int n = this.nRows;
float[,] minor = new float[n - 1, n - 1];
int r = 0, c = 0;
for (int i = 0; i < n; i++) {
if (i == rowToRemove) continue;
c = 0;
for (int j = 0; j < n; j++) {
if (j == colToRemove) continue;
minor[r, c] = this.data[i, j];
c++;
}
r++;
}
return new Matrix2(minor);
}
public static Matrix2 DeleteRows(Matrix2 A, Slice rowRange) {
float[,] result = new float[A.nRows - (rowRange.stop - rowRange.start), A.nCols];
int resultRowIx = 0;
for (int i = 0; i < A.nRows; i++) {
if (i >= rowRange.start && i < rowRange.stop)
continue;
for (int j = 0; j < A.nCols; j++)
result[resultRowIx, j] = A.data[i, j];
resultRowIx++;
}
return new Matrix2(result);
}
internal static Matrix2 DeleteColumns(Matrix2 A, Slice colRange) {
float[,] result = new float[A.nRows, A.nCols - (colRange.stop - colRange.start)];
for (int i = 0; i < A.nRows; i++) {
int resultColIx = 0;
for (int j = 0; j < A.nCols; j++) {
if (j >= colRange.start && j < colRange.stop)
continue;
result[i, resultColIx++] = A.data[i, j];
}
}
return new Matrix2(result);
}
}
public class Matrix1
{
public float[] data { get; }
public int size => data.GetLength(0);
public Matrix1(int size)
{
this.data = new float[size];
}
public Matrix1(float[] data) {
this.data = data;
}
public static Matrix1 Zero(int size)
{
return new Matrix1(size);
}
public static Matrix1 FromVector2(Vector2Float v) {
float[] result = new float[2];
result[0] = v.horizontal;
result[1] = v.vertical;
return new Matrix1(result);
}
public static Matrix1 FromVector3(Vector3Float v) {
float[] result = new float[3];
result[0] = v.horizontal;
result[1] = v.vertical;
result[2] = v.depth;
return new Matrix1(result);
}
#if UNITY_5_3_OR_NEWER
public static Matrix1 FromQuaternion(Quaternion q) {
float[] result = new float[4];
result[0] = q.x;
result[1] = q.y;
result[2] = q.z;
result[3] = q.w;
return new Matrix1(result);
}
#endif
public Vector2Float vector2 {
get {
if (this.size != 2)
throw new System.ArgumentException("Matrix1 must be of size 2");
return new Vector2Float(this.data[0], this.data[1]);
}
}
public Vector3Float vector3 {
get {
if (this.size != 3)
throw new System.ArgumentException("Matrix1 must be of size 3");
return new Vector3Float(this.data[0], this.data[1], this.data[2]);
}
}
#if UNITY_5_3_OR_NEWER
public Quaternion quaternion {
get {
if (this.size != 4)
throw new System.ArgumentException("Matrix1 must be of size 4");
return new Quaternion(this.data[0], this.data[1], this.data[2], this.data[3]);
}
}
#endif
public Matrix1 Clone() {
float[] data = new float[this.size];
for (int rowIx = 0; rowIx < this.size; rowIx++)
data[rowIx] = this.data[rowIx];
return new Matrix1(data);
}
public float magnitude {
get {
float sum = 0;
foreach (var elm in data)
sum += elm;
return sum / data.Length;
}
}
public static Matrix1 operator +(Matrix1 A, Matrix1 B) {
if (A.size != B.size)
throw new System.ArgumentException("Size of A must match size of B.");
float[] result = new float[A.size];
for (int i = 0; i < A.size; i++) {
result[i] = A.data[i] + B.data[i];
}
return new Matrix1(result);
}
public Matrix2 Transpose() {
float[,] r = new float[1, this.size];
for (uint colIx = 0; colIx < this.size; colIx++)
r[1, colIx] = this.data[colIx];
return new Matrix2(r);
}
public static float Dot(Matrix1 a, Matrix1 b) {
if (a.size != b.size)
throw new System.ArgumentException("Vectors must be of the same length.");
float result = 0.0f;
for (int i = 0; i < a.size; i++) {
result += a.data[i] * b.data[i];
}
return result;
}
public static Matrix1 operator -(Matrix1 A, Matrix1 B) {
if (A.size != B.size)
throw new System.ArgumentException("Size of A must match size of B.");
float[] result = new float[A.size];
for (int i = 0; i < A.size; i++) {
result[i] = A.data[i] - B.data[i];
}
return new Matrix1(result);
}
public static Matrix1 operator *(Matrix1 A, float f)
{
float[] result = new float[A.size];
for (int i = 0; i < A.size; i++)
result[i] += A.data[i] * f;
return new Matrix1(result);
}
public static Matrix1 operator *(float f, Matrix1 A) {
return A * f;
}
public static Matrix1 operator /(Matrix1 A, float f) {
float[] result = new float[A.size];
for (int i = 0; i < A.size; i++)
result[i] = A.data[i] / f;
return new Matrix1(result);
}
public static Matrix1 operator /(float f, Matrix1 A) {
float[] result = new float[A.size];
for (int i = 0; i < A.size; i++)
result[i] = f / A.data[i];
return new Matrix1(result);
}
public Matrix1 Slice(Slice range)
{
return Slice(range.start, range.stop);
}
public Matrix1 Slice(int from, int to)
{
if (from < 0 || to >= this.size)
throw new System.ArgumentException("Slice index out of range.");
float[] result = new float[to - from];
int resultIx = 0;
for (int ix = from; ix < to; ix++)
result[resultIx++] = this.data[ix];
return new Matrix1(result);
}
public void UpdateSlice(Slice slice, Matrix1 v) {
int vIx = 0;
for (int ix = slice.start; ix < slice.stop; ix++, vIx++)
this.data[ix] = v.data[vIx];
}
}
}

View File

@ -0,0 +1,87 @@
using System;
namespace LinearAlgebra {
public class Quat32 {
public float x;
public float y;
public float z;
public float w;
public Quat32() {
this.x = 0;
this.y = 0;
this.z = 0;
this.w = 1;
}
public Quat32(float x, float y, float z, float w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
public static Quat32 FromSwingTwist(SwingTwist s) {
Quat32 q32 = Quat32.Euler(-s.swing.vertical.inDegrees, s.swing.horizontal.inDegrees, s.twist.inDegrees);
return q32;
}
public static Quat32 Euler(float yaw, float pitch, float roll) {
float rollOver2 = roll * AngleFloat.Deg2Rad * 0.5f;
float sinRollOver2 = (float)Math.Sin((float)rollOver2);
float cosRollOver2 = (float)Math.Cos((float)rollOver2);
float pitchOver2 = pitch * 0.5f;
float sinPitchOver2 = (float)Math.Sin((float)pitchOver2);
float cosPitchOver2 = (float)Math.Cos((float)pitchOver2);
float yawOver2 = yaw * 0.5f;
float sinYawOver2 = (float)Math.Sin((float)yawOver2);
float cosYawOver2 = (float)Math.Cos((float)yawOver2);
Quat32 result = new Quat32() {
w = cosYawOver2 * cosPitchOver2 * cosRollOver2 +
sinYawOver2 * sinPitchOver2 * sinRollOver2,
x = sinYawOver2 * cosPitchOver2 * cosRollOver2 +
cosYawOver2 * sinPitchOver2 * sinRollOver2,
y = cosYawOver2 * sinPitchOver2 * cosRollOver2 -
sinYawOver2 * cosPitchOver2 * sinRollOver2,
z = cosYawOver2 * cosPitchOver2 * sinRollOver2 -
sinYawOver2 * sinPitchOver2 * cosRollOver2
};
return result;
}
public void ToAngles(out float right, out float up, out float forward) {
float test = this.x * this.y + this.z * this.w;
if (test > 0.499f) { // singularity at north pole
right = 0;
up = 2 * (float)Math.Atan2(this.x, this.w) * AngleFloat.Rad2Deg;
forward = 90;
return;
//return Vector3(0, 2 * (float)atan2(this.x, this.w) * Angle.Rad2Deg, 90);
}
else if (test < -0.499f) { // singularity at south pole
right = 0;
up = -2 * (float)Math.Atan2(this.x, this.w) * AngleFloat.Rad2Deg;
forward = -90;
return;
//return Vector3(0, -2 * (float)atan2(this.x, this.w) * Angle.Rad2Deg, -90);
}
else {
float sqx = this.x * this.x;
float sqy = this.y * this.y;
float sqz = this.z * this.z;
right = (float)Math.Atan2(2 * this.x * this.w - 2 * this.y * this.z, 1 - 2 * sqx - 2 * sqz) * AngleFloat.Rad2Deg;
up = (float)Math.Atan2(2 * this.y * this.w - 2 * this.x * this.z, 1 - 2 * sqy - 2 * sqz) * AngleFloat.Rad2Deg;
forward = (float)Math.Asin(2 * test) * AngleFloat.Rad2Deg;
return;
// return Vector3(
// atan2f(2 * this.x * this.w - 2 * this.y * this.z, 1 - 2 * sqx - 2 * sqz) *
// Rad2Deg,
// atan2f(2 * this.y * this.w - 2 * this.x * this.z, 1 - 2 * sqy - 2 * sqz) *
// Rad2Deg,
// asinf(2 * test) * Angle.Rad2Deg);
}
}
}
}

View File

@ -0,0 +1,582 @@
using System;
#if UNITY_5_3_OR_NEWER
using Quaternion = UnityEngine.Quaternion;
#endif
namespace LinearAlgebra {
#if UNITY_5_3_OR_NEWER
public class QuaternionExtensions {
public static Quaternion Reflect(Quaternion q) {
return new(-q.x, -q.y, -q.z, q.w);
}
public static Matrix2 ToRotationMatrix(Quaternion q) {
float w = q.x, x = q.y, y = q.z, z = q.w;
float[,] result = new float[,]
{
{ 1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y) },
{ 2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x) },
{ 2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y) }
};
return new Matrix2(result);
}
public static Quaternion FromRotationMatrix(Matrix2 m) {
float trace = m.data[0, 0] + m.data[1, 1] + m.data[2, 2];
float w, x, y, z;
if (trace > 0) {
float s = 0.5f / (float)Math.Sqrt(trace + 1.0f);
w = 0.25f / s;
x = (m.data[2, 1] - m.data[1, 2]) * s;
y = (m.data[0, 2] - m.data[2, 0]) * s;
z = (m.data[1, 0] - m.data[0, 1]) * s;
}
else {
if (m.data[0, 0] > m.data[1, 1] && m.data[0, 0] > m.data[2, 2]) {
float s = 2.0f * (float)Math.Sqrt(1.0f + m.data[0, 0] - m.data[1, 1] - m.data[2, 2]);
w = (m.data[2, 1] - m.data[1, 2]) / s;
x = 0.25f * s;
y = (m.data[0, 1] + m.data[1, 0]) / s;
z = (m.data[0, 2] + m.data[2, 0]) / s;
}
else if (m.data[1, 1] > m.data[2, 2]) {
float s = 2.0f * (float)Math.Sqrt(1.0f + m.data[1, 1] - m.data[0, 0] - m.data[2, 2]);
w = (m.data[0, 2] - m.data[2, 0]) / s;
x = (m.data[0, 1] + m.data[1, 0]) / s;
y = 0.25f * s;
z = (m.data[1, 2] + m.data[2, 1]) / s;
}
else {
float s = 2.0f * (float)Math.Sqrt(1.0f + m.data[2, 2] - m.data[0, 0] - m.data[1, 1]);
w = (m.data[1, 0] - m.data[0, 1]) / s;
x = (m.data[0, 2] + m.data[2, 0]) / s;
y = (m.data[1, 2] + m.data[2, 1]) / s;
z = 0.25f * s;
}
}
return new Quaternion(x, y, z, w);
}
}
#else
public struct Quaternion {
public float x;
public float y;
public float z;
public float w;
/// <summary>
/// create a new quaternion with the given values
/// </summary>
/// <param name="x">x component</param>
/// <param name="y">y component</param>
/// <param name="z">z component</param>
/// <param name="w">w component</param>
public Quaternion(float x, float y, float z, float w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
/// <summary>
/// An identity quaternion
/// </summary>
public static readonly Quaternion identity = new(0, 0, 0, 1);
private readonly Vector3Float xyz => new(x, y, z);
private readonly float magnitude => MathF.Sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
private readonly float sqrMagnitude => x * x + y * y + z * z + w * w;
/// <summary>
/// Convert to unit quaternion
/// </summary>
/// This will preserve the orientation,
/// but ensures that it is a unit quaternion.
public readonly Quaternion normalized {
get {
float length = this.magnitude;
Quaternion q = new(this.x / length, this.y / length, this.z / length, this.w / length);
return q;
}
}
/// <summary>
/// Convert to unity quaternion
/// </summary>
/// <param name="q">The quaternion to convert</param>
/// <returns>A unit quaternion</returns>
/// This will preserve the orientation,
/// but ensures that it is a unit quaternion.
public static Quaternion Normalize(Quaternion q) {
return q.normalized;
}
/// <summary>
/// Convert to euler angles
/// </summary>
/// <param name="q">The quaternion to convert</param>
/// <returns>A vector containing euler angles</returns>
/// The euler angles performed in the order: Z, X, Y
public static Vector3Float ToAngles(Quaternion q) {
// Extract Euler angles in Unity order (X = pitch, Y = yaw, Z = roll), returned in degrees as (x,pitch),(y,yaw),(z,roll)
// Handle singularities/gimbal lock
float test = 2f * (q.w * q.x - q.y * q.z);
// clamp
if (test >= 1f) test = 1f;
if (test <= -1f) test = -1f;
float pitch = MathF.Asin(test); // X
float roll = MathF.Atan2(2f * (q.w * q.z + q.x * q.y),
1f - 2f * (q.x * q.x + q.z * q.z)); // Z
float yaw = MathF.Atan2(2f * (q.w * q.y + q.x * q.z),
1f - 2f * (q.y * q.y + q.x * q.x)); // Y
const float rad2deg = 180f / MathF.PI;
return new Vector3Float(pitch * rad2deg, yaw * rad2deg, roll * rad2deg);
// float test = q.x * q.y + q.z * q.w;
// if (test > 0.499f) // singularity at north pole
// return new Vector3Float(0, 2 * MathF.Atan2(q.x, q.w) * AngleFloat.Rad2Deg, 90);
// else if (test < -0.499f) // singularity at south pole
// return new Vector3Float(0, -2 * MathF.Atan2(q.x, q.w) * AngleFloat.Rad2Deg, -90);
// else {
// float sqx = q.x * q.x;
// float sqy = q.y * q.y;
// float sqz = q.z * q.z;
// return new Vector3Float(
// MathF.Atan2(2 * q.x * q.w - 2 * q.y * q.z, 1 - 2 * sqx - 2 * sqz) *
// AngleFloat.Rad2Deg,
// MathF.Atan2(2 * q.y * q.w - 2 * q.x * q.z, 1 - 2 * sqy - 2 * sqz) *
// AngleFloat.Rad2Deg,
// MathF.Asin(2 * test) * AngleFloat.Rad2Deg);
// }
}
/// <summary>
/// Create a rotation from euler angles
/// </summary>
/// <param name="x">The angle around the right axis</param>
/// <param name="y">The angle around the upward axis</param>
/// <param name="z">The angle around the forward axis</param>
/// <returns>The resulting quaternion</returns>
/// Rotation are appied in the order Z, X, Y.
public static Quaternion Euler(float x, float y, float z) {
return Quaternion.Euler(new Vector3Float(x, y, z));
}
/// <summary>
/// Create a rotation from a vector containing euler angles
/// </summary>
/// <param name="eulerAngles">Vector with the euler angles</param>
/// <returns>The resulting quaternion</returns>
/// Rotation are appied in the order Z, X, Y.
public static Quaternion Euler(Vector3Float angles) {
Vector3Float euler = angles * AngleFloat.Deg2Rad;
float cx = MathF.Cos(euler.horizontal * 0.5f);
float sx = MathF.Sin(euler.horizontal * 0.5f);
float cy = MathF.Cos(euler.vertical * 0.5f);
float sy = MathF.Sin(euler.vertical * 0.5f);
float cz = MathF.Cos(euler.depth * 0.5f);
float sz = MathF.Sin(euler.depth * 0.5f);
// Unity uses intrinsic Z, then X, then Y -> q = Qy * Qx * Qz
Quaternion q;
q.w = cy * cx * cz + sy * sx * sz;
q.x = cy * sx * cz + sy * cx * sz;
q.y = sy * cx * cz - cy * sx * sz;
q.z = cy * cx * sz - sy * sx * cz;
return q;
}
/// <summary>
/// Multiply two quaternions
/// </summary>
/// <param name="q1"></param>
/// <param name="q2"></param>
/// <returns>The resulting rotation</returns>
public static Quaternion operator *(Quaternion q1, Quaternion q2) {
return new Quaternion(
q1.x * q2.w + q1.y * q2.z - q1.z * q2.y + q1.w * q2.x,
-q1.x * q2.z + q1.y * q2.w + q1.z * q2.x + q1.w * q2.y,
q1.x * q2.y - q1.y * q2.x + q1.z * q2.w + q1.w * q2.z,
-q1.x * q2.x - q1.y * q2.y - q1.z * q2.z + q1.w * q2.w);
}
/// <summary>
/// Rotate a vector using this quaterion
/// </summary>
/// <param name="q">The rotation</param>
/// <param name="v">The vector to rotate</param>
/// <returns>The rotated vector</returns>
public static Vector3Float operator *(Quaternion q, Vector3Float v) {
float num = q.x * 2;
float num2 = q.y * 2;
float num3 = q.z * 2;
float num4 = q.x * num;
float num5 = q.y * num2;
float num6 = q.z * num3;
float num7 = q.x * num2;
float num8 = q.x * num3;
float num9 = q.y * num3;
float num10 = q.w * num;
float num11 = q.w * num2;
float num12 = q.w * num3;
float px = v.horizontal;
float py = v.vertical;
float pz = v.depth;
float rx =
(1 - (num5 + num6)) * px + (num7 - num12) * py + (num8 + num11) * pz;
float ry =
(num7 + num12) * px + (1 - (num4 + num6)) * py + (num9 - num10) * pz;
float rz =
(num8 - num11) * px + (num9 + num10) * py + (1 - (num4 + num5)) * pz;
Vector3Float result = new(rx, ry, rz);
return result;
}
/// <summary>
/// The inverse of quaterion
/// </summary>
/// <param name="quaternion">The quaternion for which the inverse is
/// needed</param> <returns>The inverted quaternion</returns>
public static Quaternion Inverse(Quaternion q) {
float n = MathF.Sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
return new Quaternion(-q.x / n, -q.y / n, -q.z / n, q.w / n);
}
/// <summary>
/// A rotation which looks in the given direction
/// </summary>
/// <param name="forward">The look direction</param>
/// <param name="upwards">The up direction</param>
/// <returns>The look rotation</returns>
public static Quaternion LookRotation(Vector3Float forward, Vector3Float up) {
Vector3Float nForward = forward.normalized;
Vector3Float nRight = Vector3Float.Normalize(Vector3Float.Cross(up, nForward));
Vector3Float nUp = Vector3Float.Cross(nForward, nRight);
float m00 = nRight.horizontal; // x;
float m01 = nRight.vertical; // y;
float m02 = nRight.depth; // z;
float m10 = nUp.horizontal; // x;
float m11 = nUp.vertical; // y;
float m12 = nUp.depth; // z;
float m20 = nForward.horizontal; // x;
float m21 = nForward.vertical; // y;
float m22 = nForward.depth; // z;
float num8 = (m00 + m11) + m22;
float x, y, z, w;
if (num8 > 0) {
float num = MathF.Sqrt(num8 + 1);
w = num * 0.5f;
num = 0.5f / num;
x = (m12 - m21) * num;
y = (m20 - m02) * num;
z = (m01 - m10) * num;
return new Quaternion(x, y, z, w);
}
if ((m00 >= m11) && (m00 >= m22)) {
float num7 = MathF.Sqrt(((1 + m00) - m11) - m22);
float num4 = 0.5F / num7;
x = 0.5f * num7;
y = (m01 + m10) * num4;
z = (m02 + m20) * num4;
w = (m12 - m21) * num4;
return new Quaternion(x, y, z, w);
}
if (m11 > m22) {
float num6 = MathF.Sqrt(((1 + m11) - m00) - m22);
float num3 = 0.5F / num6;
x = (m10 + m01) * num3;
y = 0.5F * num6;
z = (m21 + m12) * num3;
w = (m20 - m02) * num3;
return new Quaternion(x, y, z, w);
}
float num5 = MathF.Sqrt(((1 + m22) - m00) - m11);
float num2 = 0.5F / num5;
x = (m20 + m02) * num2;
y = (m21 + m12) * num2;
z = 0.5F * num5;
w = (m01 - m10) * num2;
return new Quaternion(x, y, z, w);
}
/// <summary>
/// Creates a quaternion with the given forward direction with up =
/// Vector3::up
/// </summary>
/// <param name="forward">The look direction</param>
/// <returns>The rotation for this direction</returns>
/// For the rotation, Vector::up is used for the up direction.
/// Note: if the forward direction == Vector3::up, the result is
/// Quaternion::identity
public static Quaternion LookRotation(Vector3Float forward) {
Vector3Float up = new(0, 1, 0);
return LookRotation(forward, up);
}
/// <summary>
/// Calculat the rotation from on vector to another
/// </summary>
/// <param name="fromDirection">The from direction</param>
/// <param name="toDirection">The to direction</param>
/// <returns>The rotation from the first to the second vector</returns>
public static Quaternion FromToRotation(Vector3Float fromDirection, Vector3Float toDirection) {
Vector3Float axis = Vector3Float.Cross(fromDirection, toDirection);
axis = axis.normalized;
AngleFloat angle = Vector3Float.SignedAngle(fromDirection, toDirection, axis);
Quaternion rotation = AngleAxis(angle, axis);
return rotation;
}
/// <summary>
/// Rotate form one orientation to anther with a maximum amount of degrees
/// </summary>
/// <param name="from">The from rotation</param>
/// <param name="to">The destination rotation</param>
/// <param name="maxDegreesDelta">The maximum amount of degrees to
/// rotate</param> <returns>The possibly limited rotation</returns>
public static Quaternion RotateTowards(Quaternion from, Quaternion to,
float maxDegreesDelta) {
float num = Quaternion.UnsignedAngle(from, to);
if (num == 0) {
return to;
}
float t = MathF.Min(1, maxDegreesDelta / num);
return SlerpUnclamped(from, to, t);
}
/// <summary>
/// Convert an angle/axis representation to a quaternion
/// </summary>
/// <param name="angle">The angle</param>
/// <param name="axis">The axis</param>
/// <returns>The resulting quaternion</returns>
public static Quaternion AngleAxis(AngleFloat angle, Vector3Float axis) {
if (axis.sqrMagnitude == 0.0f)
return Quaternion.identity;
float radians = angle.inRadians;
radians *= 0.5f;
Vector3Float axis2 = axis * MathF.Sin(radians);
float x = axis2.horizontal; // x;
float y = axis2.vertical; // y;
float z = axis2.depth; // z;
float w = MathF.Cos(radians);
return new Quaternion(x, y, z, w).normalized;
}
/// <summary>
/// Convert this quaternion to angle/axis representation
/// </summary>
/// <param name="angle">A pointer to the angle for the result</param>
/// <param name="axis">A pointer to the axis for the result</param>
public readonly void ToAngleAxis(out AngleFloat angle, out Vector3Float axis) {
Quaternion q1 = (MathF.Abs(this.w) > 1.0f) ? this.normalized : this;
angle = AngleFloat.Radians(2.0f * MathF.Acos(q1.w)); // angle
float den = MathF.Sqrt(1.0F - q1.w * q1.w);
if (den > 0.0001f) {
axis = Vector3Float.Normalize(q1.xyz / den);
}
else {
// This occurs when the angle is zero.
// Not a problem: just set an arbitrary normalized axis.
axis = Vector3Float.right;
}
}
/// <summary>
/// Get the angle between two orientations
/// </summary>
/// <param name="orientation1">The first orientation</param>
/// <param name="orientation2">The second orientation</param>
/// <returns>The smallest angle in degrees between the two
/// orientations</returns>
public static float UnsignedAngle(Quaternion q1, Quaternion q2) {
// float f = Dot(q1, q2);
// return MathF.Acos(MathF.Min(MathF.Abs(f), 1)) * 2 * AngleFloat.Rad2Deg;
float dot = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;
dot = MathF.Min(MathF.Max(dot, -1f), 1f);
return 2f * MathF.Acos(MathF.Abs(dot)) * (180f / MathF.PI);
}
/// <summary>
/// Sherical lerp between two rotations
/// </summary>
/// <param name="rotation1">The first rotation</param>
/// <param name="rotation2">The second rotation</param>
/// <param name="factor">The factor between 0 and 1.</param>
/// <returns>The resulting rotation</returns>
/// A factor 0 returns rotation1, factor1 returns rotation2.
public static Quaternion Slerp(Quaternion a,
Quaternion b, float t) {
if (t > 1)
t = 1;
if (t < 0)
t = 0;
return SlerpUnclamped(a, b, t);
}
/// <summary>
/// Unclamped sherical lerp between two rotations
/// </summary>
/// <param name="rotation1">The first rotation</param>
/// <param name="rotation2">The second rotation</param>
/// <param name="factor">The factor</param>
/// <returns>The resulting rotation</returns>
/// A factor 0 returns rotation1, factor1 returns rotation2.
/// Values outside the 0..1 range will result in extrapolated rotations
public static Quaternion SlerpUnclamped(Quaternion a,
Quaternion b, float t) {
// if either input is zero, return the other.
if (a.sqrMagnitude == 0.0f) {
if (b.sqrMagnitude == 0.0f) {
return identity;
}
return b;
}
else if (b.sqrMagnitude == 0.0f) {
return a;
}
Vector3Float axyz = a.xyz;
Vector3Float bxyz = b.xyz;
float cosHalfAngle = a.w * b.w + Vector3Float.Dot(axyz, bxyz);
Quaternion b2 = b;
if (cosHalfAngle >= 1.0f || cosHalfAngle <= -1.0f) {
// angle = 0.0f, so just return one input.
return a;
}
else if (cosHalfAngle < 0.0f) {
b2.x = -b.x;
b2.y = -b.y;
b2.z = -b.z;
b2.w = -b.w;
cosHalfAngle = -cosHalfAngle;
}
float blendA;
float blendB;
if (cosHalfAngle < 0.99f) {
// do proper slerp for big angles
float halfAngle = MathF.Acos(cosHalfAngle);
float sinHalfAngle = MathF.Sin(halfAngle);
float oneOverSinHalfAngle = 1.0F / sinHalfAngle;
blendA = MathF.Sin(halfAngle * (1.0F - t)) * oneOverSinHalfAngle;
blendB = MathF.Sin(halfAngle * t) * oneOverSinHalfAngle;
}
else {
// do lerp if angle is really small.
blendA = 1.0f - t;
blendB = t;
}
Vector3Float v = axyz * blendA + b2.xyz * blendB;
Quaternion result =
new(v.horizontal, v.vertical, v.depth, blendA * a.w + blendB * b2.w);
if (result.sqrMagnitude > 0.0f)
return result.normalized;
else
return Quaternion.identity;
}
/// <summary>
/// Convert this quaternion to angle/axis representation
/// </summary>
/// <param name="angle">A pointer to the angle for the result</param>
/// <param name="axis">A pointer to the axis for the result</param>
public readonly void ToAngleAxis(out float angle, out Vector3Float axis) {
ToAxisAngleRad(this, out axis, out angle);
angle *= AngleFloat.Rad2Deg;
}
private static void ToAxisAngleRad(Quaternion q,
out Vector3Float axis,
out float angle) {
Quaternion q1 = (MathF.Abs(q.w) > 1.0f) ? Quaternion.Normalize(q) : q;
angle = 2.0f * MathF.Acos(q1.w); // angle
float den = MathF.Sqrt(1.0F - q1.w * q1.w);
if (den > 0.0001f) {
axis = (q1.xyz / den).normalized;
}
else {
// This occurs when the angle is zero.
// Not a problem: just set an arbitrary normalized axis.
axis = new Vector3Float(1, 0, 0);
}
}
/// <summary>
/// Returns the angle of around the give axis for a rotation
/// </summary>
/// <param name="axis">The axis around which the angle should be
/// computed</param> <param name="rotation">The source rotation</param>
/// <returns>The signed angle around the axis</returns>
public static float GetAngleAround(Vector3Float axis, Quaternion rotation) {
Quaternion secondaryRotation = GetRotationAround(axis, rotation);
secondaryRotation.ToAngleAxis(out float rotationAngle, out Vector3Float rotationAxis);
// Do the axis point in opposite directions?
if (Vector3Float.Dot(axis, rotationAxis) < 0)
rotationAngle = -rotationAngle;
return rotationAngle;
}
/// <summary>
/// Returns the rotation limited around the given axis
/// </summary>
/// <param name="axis">The axis which which the rotation should be
/// limited</param> <param name="rotation">The source rotation</param>
/// <returns>The rotation around the given axis</returns>
public static Quaternion GetRotationAround(Vector3Float axis, Quaternion rotation) {
Vector3Float ra = new(rotation.x, rotation.y, rotation.z); // rotation axis
Vector3Float p = Vector3Float.Project(
ra, axis); // return projection ra on to axis (parallel component)
Quaternion twist = new(p.horizontal, p.vertical, p.depth, rotation.w);
twist = Normalize(twist);
return twist;
}
/// <summary>
/// Swing-twist decomposition of a rotation
/// </summary>
/// <param name="axis">The base direction for the decomposition</param>
/// <param name="q">The source rotation</param>
/// <param name="swing">A pointer to the quaternion for the swing
/// result</param> <param name="twist">A pointer to the quaternion for the
/// twist result</param>
static void GetSwingTwist(Vector3Float axis, Quaternion q,
out Quaternion swing, out Quaternion twist) {
twist = GetRotationAround(axis, q);
swing = q * Inverse(twist);
}
/// <summary>
/// Calculate the dot product of two quaternions
/// </summary>
/// <param name="rotation1">The first rotation</param>
/// <param name="rotation2">The second rotation</param>
/// <returns></returns>
public static float Dot(Quaternion q1, Quaternion q2) {
return q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;
}
}
#endif
}

View File

@ -0,0 +1,279 @@
using System;
using System.Collections.Generic;
#if UNITY_5_3_OR_NEWER
using Vector3 = UnityEngine.Vector3;
#endif
namespace LinearAlgebra {
/// <summary>
/// A spherical vector
/// </summary>
/// <remark>This is a struct such that it is a value type and cannot be null
public struct Spherical {
/// <summary>
/// Create a spherical vector
/// </summary>
/// <param name="distance">The distance in meters</param>
/// <param name="direction">The direction of the vector</param>
public Spherical(float distance, Direction direction) {
if (distance > 0) {
this.distance = distance;
this.direction = direction;
}
else {
this.distance = -distance;
this.direction = -direction;
}
}
/// <summary>
/// Create spherical vector. All given angles are in degrees
/// </summary>
/// <param name="distance">The distance in meters</param>
/// <param name="horizontal">The horizontal angle in degrees</param>
/// <param name="vertical">The vertical angle in degrees</param>
/// <returns></returns>
public static Spherical Degrees(float distance, float horizontal, float vertical) {
Direction direction = Direction.Degrees(horizontal, vertical);
Spherical s = new(distance, direction);
return s;
}
public static Spherical Radians(float distance, float horizontal, float vertical) {
Direction direction = Direction.Radians(horizontal, vertical);
Spherical s = new(distance, direction);
return s;
}
/// <summary>
/// The distance in meters
/// </summary>
/// @remark The distance should never be negative
public float distance;
/// <summary>
/// The direction of the vector
/// </summary>
public Direction direction;
/// <summary>
/// A spherical vector with zero degree angles and distance
/// </summary>
public readonly static Spherical zero = new(0, Direction.forward);
/// <summary>
/// A normalized forward-oriented vector
/// </summary>
public readonly static Spherical forward = new(1, Direction.forward);
#if UNITY_5_3_OR_NEWER
public static Spherical FromVector3(Vector3 v) {
float distance = v.magnitude;
Direction direction = Direction.FromVector3(v / distance);
return new Spherical(distance, direction);
}
public readonly Vector3 ToVector3() {
Vector3 v = this.direction.ToVector3();
v *= this.distance;
return v;
}
#else
public static Spherical FromVector3(Vector3Float v) {
float distance = v.magnitude;
if (distance == 0.0f)
return Spherical.zero;
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);
}
}
public readonly Vector3Float ToVector3() {
// float verticalRad = (AngleFloat.deg90 - this.direction.vertical).inRadians;
// float horizontalRad = this.direction.horizontal.inRadians;
// float cosVertical = (float)Math.Cos(verticalRad);
// float sinVertical = (float)Math.Sin(verticalRad);
// float cosHorizontal = (float)Math.Cos(horizontalRad);
// float sinHorizontal = (float)Math.Sin(horizontalRad);
// float x = this.distance * sinVertical * sinHorizontal;
// float y = this.distance * cosVertical;
// float z = this.distance * sinVertical * cosHorizontal;
// Vector3Float v = new(x, y, z);
Vector3Float v = this.direction.ToVector3();
v *= this.distance;
return v;
}
#endif
public override readonly string ToString() {
return $"Spherical({this.distance}, h: {this.direction.horizontal}, v: {this.direction.vertical})";
}
public readonly float magnitude => this.distance;
public Spherical normalized {
get {
Spherical r = new() {
distance = 1,
direction = this.direction
};
return r;
}
}
public static Spherical operator +(Spherical s1, Spherical s2) {
// let's do it the easy way...
// using vars to be compatible with both unity (Vector3) and native (Vector3Float)
var v1 = s1.ToVector3();
var v2 = s2.ToVector3();
var v = v1 + v2;
Spherical r = FromVector3(v);
return r;
}
public static Spherical operator *(Spherical v, float d) {
Spherical r = new(v.distance * d, v.direction);
return r;
}
public static bool operator ==(Spherical v1, Spherical v2) {
return (v1.distance == v2.distance && v1.direction == v2.direction);
}
public static bool operator !=(Spherical v1, Spherical v2) {
return (v1.distance != v2.distance || v1.direction != v2.direction);
}
public override readonly bool Equals(object o) {
if (o is Spherical s)
return this == s;
return false;
}
public override readonly int GetHashCode() {
return HashCode.Combine(this.distance, this.direction);
}
public static float Distance(Spherical v1, Spherical v2) {
// Convert degrees to radians
float thetaARadians = v1.direction.horizontal.inRadians;
float phiARadians = v1.direction.vertical.inRadians;// DegreesToRadians(phiA);
float thetaBRadians = v2.direction.horizontal.inRadians; // DegreesToRadians(thetaB);
float phiBRadians = v2.direction.vertical.inRadians; // DegreesToRadians(phiB);
// Calculate sine and cosine values
float sinPhiA = MathF.Sin(phiARadians);
float cosPhiA = MathF.Cos(phiARadians);
float sinPhiB = MathF.Sin(phiBRadians);
float cosPhiB = MathF.Cos(phiBRadians);
// Calculate the cosine of the difference in azimuthal angles
float cosThetaDifference = MathF.Cos(thetaARadians - thetaBRadians);
// Apply the spherical law of cosines
float distance = MathF.Sqrt(
v1.distance * v1.distance +
v2.distance * v2.distance -
2 * v1.distance * v2.distance * (sinPhiA * sinPhiB * cosThetaDifference + cosPhiA * cosPhiB)
);
return distance;
}
public static Spherical Average(Spherical v1, Spherical v2) {
const float EPS = 1e-6f;
// 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;
// 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);
}
// Horizontal unit-circle sum
float cx = MathF.Cos(a1) + MathF.Cos(a2);
float cy = MathF.Sin(a1) + MathF.Sin(a2);
// Vertical as z = sin(el)
float z1 = MathF.Sin(e1);
float z2 = MathF.Sin(e2);
float cz = z1 + z2;
// 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 Sum(List<Spherical> vectors) {
if (vectors == null || vectors.Count == 0)
throw new ArgumentException("vectors must contain at least one element", nameof(vectors));
#if UNITY_5_3_OR_NEWER
Vector3 sum = Vector3.zero;
#else
Vector3Float sum = Vector3Float.zero;
#endif
foreach (Spherical v in vectors)
sum += v.ToVector3();
return FromVector3(sum);
}
public static Spherical Average(List<Spherical> vectors) {
if (vectors == null || vectors.Count == 0)
throw new ArgumentException("vectors must contain at least one element", nameof(vectors));
#if UNITY_5_3_OR_NEWER
Vector3 sum = Vector3.zero;
#else
Vector3Float sum = Vector3Float.zero;
#endif
int n = 0;
foreach (Spherical v in vectors) {
sum += v.ToVector3();
n++;
}
var avg = sum / n;
// if (avg.sqrMagnitude == 0f)
// return new Spherical(0f, new Direction(AngleFloat.Radians(0f), AngleFloat.Radians(0f)));
// else
return FromVector3(avg);
}
}
}

View File

@ -0,0 +1,136 @@
// #if !UNITY_5_3_OR_NEWER
// using UnityEngine;
// #endif
namespace LinearAlgebra {
/// <summary>
/// An orientation using swing and twist angles
/// </summary>
/// <param name="swing">The swing rotation</param>
/// <param name="twist">The twist rotation</param>
public struct SwingTwist {
public Direction swing;
public AngleFloat twist;
public SwingTwist(Direction swing, AngleFloat twist) {
this.swing = swing;
this.twist = twist;
}
/// <summary>
/// Create a swing/twist rotation using angles in degrees
/// </summary>
/// <param name="horizontalSwing">The swing angle in the horizontal plane in degrees</param>
/// <param name="verticalSwing">The swing angle in the vertical plan in degrees</param>
/// <param name="twist">The twist angle in degrees</param>
/// <returns>The swing/twist rotation</returns>
public static SwingTwist Degrees(float horizontalSwing, float verticalSwing, float twist) {
Direction swing = Direction.Degrees(horizontalSwing, verticalSwing);
AngleFloat twistAngle = AngleFloat.Degrees(twist);
SwingTwist s = new(swing, twistAngle);
return s;
}
/// <summary>
/// Create a swing/twist rotation using angles in degrees
/// </summary>
/// <param name="horizontalSwing">The swing angle in the horizontal plane in degrees</param>
/// <param name="verticalSwing">The swing angle in the vertical plan in degrees</param>
/// <param name="twist">The twist angle in degrees</param>
/// <returns>The swing/twist rotation</returns>
public static SwingTwist Radians(float horizontalSwing, float verticalSwing, float twist) {
Direction swing = Direction.Radians(horizontalSwing, verticalSwing);
AngleFloat twistAngle = AngleFloat.Radians(twist);
SwingTwist s = new(swing, twistAngle);
return s;
}
#if UNITY_5_3_OR_NEWER
/// <summary>
/// A zero angle rotation
/// </summary>
public static readonly SwingTwist zero = Degrees(0, 0, 0);
public Spherical ToAngleAxis() {
UnityEngine.Quaternion q = this.ToQuaternion();
q.ToAngleAxis(out float angle, out UnityEngine.Vector3 axis);
Direction direction = Direction.FromVector3(axis);
Spherical r = new(angle, direction);
return r;
}
public static SwingTwist FromAngleAxis(Spherical r) {
UnityEngine.Vector3 vectorAxis = r.direction.ToVector3();
UnityEngine.Quaternion q = UnityEngine.Quaternion.AngleAxis(r.distance, vectorAxis);
return FromQuaternion(q);
}
/// <summary>
/// Convert a quaternion in a swing/twist rotation
/// </summary>
/// <param name="q">The quaternion to convert</param>
/// <returns>The swing/twist rotation</returns>
public static SwingTwist FromQuaternion(UnityEngine.Quaternion q) {
UnityEngine.Vector3 angles = q.eulerAngles;
SwingTwist r = Degrees(angles.y, -angles.x, -angles.z);
return r;
}
public UnityEngine.Quaternion ToQuaternion() {
UnityEngine.Quaternion q = UnityEngine.Quaternion.Euler(this.swing.vertical.inDegrees,
this.swing.horizontal.inDegrees,
this.twist.inDegrees);
return q;
}
#else
/// <summary>
/// A zero angle rotation
/// </summary>
public static readonly SwingTwist zero = Degrees(0, 0, 0);
public Spherical ToAngleAxis() {
LinearAlgebra.Quaternion q = this.ToQuaternion();
q.ToAngleAxis(out float angle, out Vector3Float axis);
Direction direction = Direction.FromVector3(axis);
Spherical r = new(angle, direction);
return r;
}
public static SwingTwist FromAngleAxis(Spherical r) {
Vector3Float vectorAxis = r.direction.ToVector3();
LinearAlgebra.Quaternion q = LinearAlgebra.Quaternion.AngleAxis(AngleFloat.Degrees(r.distance), vectorAxis);
return FromQuaternion(q);
}
/// <summary>
/// Convert a quaternion in a swing/twist rotation
/// </summary>
/// <param name="q">The quaternion to convert</param>
/// <returns>The swing/twist rotation</returns>
public static SwingTwist FromQuaternion(LinearAlgebra.Quaternion q) {
Vector3Float v = LinearAlgebra.Quaternion.ToAngles(q);
SwingTwist r = Degrees(v.vertical, v.horizontal, v.depth);
return r;
}
public LinearAlgebra.Quaternion ToQuaternion() {
LinearAlgebra.Quaternion q = LinearAlgebra.Quaternion.Euler(this.swing.vertical.inDegrees,
this.swing.horizontal.inDegrees,
this.twist.inDegrees);
return q;
}
public static SwingTwist FromQuat32(Quat32 q32) {
q32.ToAngles(out float right, out float up, out float forward);
SwingTwist r = Degrees(up, right, forward);
return r;
}
#endif
}
}

View File

@ -0,0 +1,479 @@
using System;
using System.Numerics;
namespace LinearAlgebra {
/*
public struct Vector2Int {
public int horizontal;
public int vertical;
public Vector2Int(int horizontal, int vertical) {
this.horizontal = horizontal;
this.vertical = vertical;
}
/// <summary>
/// A vector with zero for all axis
/// </summary>
public static readonly Vector2Int zero = new(0, 0);
/// <summary>
/// A vector with values (1, 1)
/// </summary>
public static readonly Vector2Int one = new(1, 1);
/// <summary>
/// A vector with values (0, 1)
/// </summary>
public static readonly Vector2Int up = new(0, 1);
/// <summary>
/// A vector with values (0, -1)
/// </summary>
public static readonly Vector2Int down = new(0, -1);
/// <summary>
/// A vector with values (0, 1)
/// </summary>
public static readonly Vector2Int forward = new(0, 1);
/// <summary>
/// A vector with values (0, -1)
/// </summary>
public static readonly Vector2Int back = new(0, -1);
/// <summary>
/// A vector3 with values (-1, 0)
/// </summary>
public static readonly Vector2Int left = new(-1, 0);
/// <summary>
/// A vector with values (1, 0)
/// </summary>
public static readonly Vector2Int right = new(1, 0);
/// <summary>
/// Tests if the vector has equal values as the given vector
/// </summary>
/// <param name="v1">The vector to compare to</param>
/// <returns><em>true</em> if the vector values are equal</returns>
public readonly bool Equals(Vector2Int v) => this.horizontal == v.horizontal && vertical == v.vertical;
/// <summary>
/// Tests if the vector is equal to the given object
/// </summary>
/// <param name="obj">The object to compare to</param>
/// <returns><em>false</em> when the object is not a Vector2 or does not have equal values</returns>
public override readonly bool Equals(object obj) {
if (obj is not Vector2Int v)
return false;
return (this.horizontal == v.horizontal && this.vertical == v.vertical);
}
/// <summary>
/// Tests if the two vectors have equal values
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns><em>true</em>when the vectors have equal values</returns>
/// Note that this uses a Float equality check which cannot be not exact in all cases.
/// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon
/// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon
public static bool operator ==(Vector2Int v1, Vector2Int v2) {
return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical);
}
/// <summary>
/// Tests if two vectors have different values
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns><em>true</em>when the vectors have different values</returns>
/// Note that this uses a Float equality check which cannot be not exact in all case.
/// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon.
/// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon
public static bool operator !=(Vector2Int v1, Vector2Int v2) {
return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical);
}
public readonly float magnitude {
get {
int h = this.horizontal;
int v = this.vertical;
return MathF.Sqrt(h * h + v * v);
}
}
public static float MagnitudeOf(Vector2Int v) {
return v.magnitude;
}
public static Vector2Int operator -(Vector2Int v1, Vector2Int v2) {
return new Vector2Int(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical);
}
public static Vector2Int operator +(Vector2Int v1, Vector2Int v2) {
return new Vector2Int(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical);
}
public static float Distance(Vector2Int v1, Vector2Int v2) {
return (v1 - v2).magnitude;
}
}
public struct Vector2Float {
public float horizontal;
public float vertical;
public Vector2Float(float horizontal, float vertical) {
this.horizontal = horizontal;
this.vertical = vertical;
}
public readonly float magnitude {
get {
float h = this.horizontal;
float v = this.vertical;
return MathF.Sqrt(h * h + v * v);
}
}
public static Vector2Float operator -(Vector2Float v1, Vector2Float v2) {
return new Vector2Float(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical);
}
public static float Distance(Vector2Float v1, Vector2Float v2) {
return (v1 - v2).magnitude;
}
}
*/
/// <summary>
/// 2-dimensional vectors
/// </summary>
public struct Vector2Float {
/// <summary>
/// The right axis of the vector
/// </summary>
public float horizontal; // left/right
/// <summary>
/// The upward/forward axis of the vector
/// </summary>
public float vertical; // forward/backward
// directions are to be inline with Vector3 as much as possible...
/// <summary>
/// Create a new 2-dimensional vector
/// </summary>
/// <param name="x">x axis value</param>
/// <param name="y">y axis value</param>
public Vector2Float(float x, float y) {
this.horizontal = x;
this.vertical = y;
}
/// <summary>
/// Convert a Vector2Int into a Vector2Float
/// </summary>
/// <param name="v">The Vector2Int</param>
public Vector2Float(Vector2Int v) {
this.horizontal = v.horizontal;
this.vertical = v.vertical;
}
/// <summary>
/// A vector with zero for all axis
/// </summary>
public static readonly Vector2Float zero = new Vector2Float(0, 0);
/// <summary>
/// A vector with values (1, 1)
/// </summary>
public static readonly Vector2Float one = new Vector2Float(1, 1);
/// <summary>
/// A vector with values (0, 1)
/// </summary>
public static readonly Vector2Float up = new Vector2Float(0, 1);
/// <summary>
/// A vector with values (0, -1)
/// </summary>
public static readonly Vector2Float down = new Vector2Float(0, -1);
/// <summary>
/// A vector with values (0, 1)
/// </summary>
public static readonly Vector2Float forward = new Vector2Float(0, 1);
/// <summary>
/// A vector with values (0, -1)
/// </summary>
public static readonly Vector2Float back = new Vector2Float(0, -1);
/// <summary>
/// A vector3 with values (-1, 0)
/// </summary>
public static readonly Vector2Float left = new Vector2Float(-1, 0);
/// <summary>
/// A vector with values (1, 0)
/// </summary>
public static readonly Vector2Float right = new Vector2Float(1, 0);
/// <summary>
/// The squared length of this vector
/// </summary>
/// <returns>The squared length</returns>
/// The squared length is computationally simpler than the real length.
/// Think of Pythagoras A^2 + B^2 = C^2.
/// This leaves out the calculation of the squared root of C.
public readonly float sqrMagnitude => horizontal * horizontal + vertical * vertical;
public static float SqrMagnitudeOf(Vector2Float v) {
return v.sqrMagnitude;
}
/// <summary>
/// The length of this vector
/// </summary>
/// <returns>The length of this vector</returns>
public readonly float magnitude => MathF.Sqrt(horizontal * horizontal + vertical * vertical);
public static float MagnitudeOf(Vector2Float v) {
return v.magnitude;
}
/// <summary>
/// Convert the vector to a length of a 1
/// </summary>
/// <returns>The vector with length 1</returns>
public Vector2Float normalized {
get {
float l = magnitude;
Vector2Float v = zero;
if (l > Float.epsilon)
v = this / l;
return v;
}
}
public static Vector2Float Normalize(Vector2Float v) {
return v.normalized;
}
/// <summary>
/// Add two vectors
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns>The result of adding the two vectors</returns>
public static Vector2Float operator +(Vector2Float v1, Vector2Float v2) {
Vector2Float v = new Vector2Float(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical);
return v;
}
/// <summary>
/// Subtract two vectors
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns>The result of adding the two vectors</returns>
public static Vector2Float operator -(Vector2Float v1, Vector2Float v2) {
Vector2Float v = new Vector2Float(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical);
return v;
}
/// <summary>
/// Negate the vector
/// </summary>
/// <param name="v1">The vector to negate</param>
/// <returns>The negated vector</returns>
/// This will result in a vector pointing in the opposite direction
public static Vector2Float operator -(Vector2Float v1) {
Vector2Float v = new Vector2Float(-v1.horizontal, -v1.vertical);
return v;
}
/// <summary>
/// Scale a vector uniformly down
/// </summary>
/// <param name="v">The vector to scale</param>
/// <param name="f">The scaling factor</param>
/// <returns>The scaled vector</returns>
/// Each component of the vector will be devided by the same factor.
public static Vector2Float operator /(Vector2Float v, float f) {
Vector2Float r = new(v.horizontal / f, v.vertical / f);
return r;
}
/// <summary>
/// Scale a vector uniformly up
/// </summary>
/// <param name="v1">The vector to scale</param>
/// <param name="f">The scaling factor</param>
/// <returns>The scaled vector</returns>
/// Each component of the vector will be multipled with the same factor.
public static Vector2Float operator *(Vector2Float v1, float f) {
Vector2Float v = new Vector2Float(v1.horizontal * f, v1.vertical * f);
return v;
}
/// <summary>
/// Scale a vector uniformly up
/// </summary>
/// <param name="f">The scaling factor</param>
/// <param name="v1">The vector to scale</param>
/// <returns>The scaled vector</returns>
/// Each component of the vector will be multipled with the same factor.
public static Vector2Float operator *(float f, Vector2Float v1) {
Vector2Float v = new Vector2Float(f * v1.horizontal, f * v1.vertical);
return v;
}
/// @brief Scale the vector using another vector
/// @param v1 The vector to scale
/// @param v2 A vector with the scaling factors
/// @return The scaled vector
/// @remark Each component of the vector v1 will be multiplied with the
/// matching component from the scaling vector v2.
public static Vector2Float Scale(Vector2Float v1, Vector2Float v2) {
return new Vector2Float(v1.horizontal * v2.horizontal, v1.vertical * v2.vertical);
}
/// <summary>
/// Tests if the vector has equal values as the given vector
/// </summary>
/// <param name="v1">The vector to compare to</param>
/// <returns><em>true</em> if the vector values are equal</returns>
//public readonly bool Equals(Vector2Float v1) => horizontal == v1.horizontal && vertical == v1.vertical;
/// <summary>
/// Tests if the two vectors have equal values
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns><em>true</em>when the vectors have equal values</returns>
/// Note that this uses a Float equality check which cannot be not exact in all cases.
/// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon
/// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon
public static bool operator ==(Vector2Float v1, Vector2Float v2) {
return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical);
}
/// <summary>
/// Tests if two vectors have different values
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns><em>true</em>when the vectors have different values</returns>
/// Note that this uses a Float equality check which cannot be not exact in all case.
/// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon.
/// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon
public static bool operator !=(Vector2Float v1, Vector2Float v2) {
return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical);
}
/// <summary>
/// Tests if the vector is equal to the given object
/// </summary>
/// <param name="obj">The object to compare to</param>
/// <returns><em>false</em> when the object is not a Vector2 or does not have equal values</returns>
public override readonly bool Equals(object obj) {
if (obj is not Vector2Float v)
return false;
return (horizontal == v.horizontal && vertical == v.vertical);
}
/// <summary>
/// Get an hash code for the vector
/// </summary>
/// <returns>The hash code</returns>
public override readonly int GetHashCode() {
return HashCode.Combine(horizontal, vertical);
}
/// <summary>
/// Get the distance between two vectors
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns>The distance between the two vectors</returns>
public static float Distance(Vector2Float v1, Vector2Float v2) {
float x = v1.horizontal - v2.horizontal;
float y = v1.vertical - v2.vertical;
float d = (float)Math.Sqrt(x * x + y * y);
return d;
}
/// <summary>
/// The dot product of two vectors
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns>The dot product of the two vectors</returns>
public static float Dot(Vector2Float v1, Vector2Float v2) {
return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical;
}
/// <summary>
/// Calculate the signed angle between two vectors.
/// </summary>
/// <param name="from">The starting vector</param>
/// <param name="to">The ending vector</param>
/// <param name="axis">The axis to rotate around</param>
/// <returns>The signed angle in degrees</returns>
public static float SignedAngle(Vector2Float from, Vector2Float to) {
//float sign = Math.Sign(v1.y * v2.x - v1.x * v2.y);
//return Vector2.Angle(v1, v2) * sign;
float sqrMagFrom = from.sqrMagnitude;
float sqrMagTo = to.sqrMagnitude;
if (sqrMagFrom == 0 || sqrMagTo == 0)
return 0;
//if (!isfinite(sqrMagFrom) || !isfinite(sqrMagTo))
// return nanf("");
float angleFrom = (float)Math.Atan2(from.vertical, from.horizontal);
float angleTo = (float)Math.Atan2(to.vertical, to.horizontal);
return -(angleTo - angleFrom) * AngleFloat.Rad2Deg;
}
public static float UnsignedAngle(Vector2Float from, Vector2Float to) {
return MathF.Abs(SignedAngle(from, to));
}
/// <summary>
/// Rotates the vector with the given angle
/// </summary>
/// <param name="v1">The vector to rotate</param>
/// <param name="angle">The angle in degrees</param>
/// <returns></returns>
public static Vector2Float Rotate(Vector2Float v1, AngleFloat angle) {
float sin = (float)Math.Sin(angle.inRadians);
float cos = (float)Math.Cos(angle.inRadians);
// float sin = AngleFloat.Sin(angle);
// float cos = AngleFloat.Cos(angle);
float tx = v1.horizontal;
float ty = v1.vertical;
Vector2Float v = new Vector2Float() {
horizontal = (cos * tx) - (sin * ty),
vertical = (sin * tx) + (cos * ty)
};
return v;
}
/// <summary>
/// Lerp between two vectors
/// </summary>
/// <param name="v1">The from vector</param>
/// <param name="v2">The to vector</param>
/// <param name="f">The interpolation distance [0..1]</param>
/// <returns>The lerped vector</returns>
/// The factor f is unclamped. Value 0 matches the *v1* vector, Value 1
/// matches the *v2* vector Value -1 is *v1* vector minus the difference
/// between *v1* and *v2* etc.
public static Vector2Float Lerp(Vector2Float v1, Vector2Float v2, float f) {
Vector2Float v = v1 + (v2 - v1) * f;
return v;
}
/// <summary>
/// Map interval of angles between vectors [0..Pi] to interval [0..1]
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns>The resulting factor in interval [0..1]</returns>
/// Vectors a and b must be normalized
public static float ToFactor(Vector2Float v1, Vector2Float v2) {
return (1 - Vector2Float.Dot(v1, v2)) / 2;
}
}
}

View File

@ -0,0 +1,185 @@
using System;
namespace LinearAlgebra {
public struct Vector2Int {
public int horizontal;
public int vertical;
public Vector2Int(int horizontal, int vertical) {
this.horizontal = horizontal;
this.vertical = vertical;
}
/// <summary>
/// A vector with zero for all axis
/// </summary>
public static readonly Vector2Int zero = new(0, 0);
/// <summary>
/// A vector with values (1, 1)
/// </summary>
public static readonly Vector2Int one = new(1, 1);
/// <summary>
/// A vector with values (0, 1)
/// </summary>
public static readonly Vector2Int up = new(0, 1);
/// <summary>
/// A vector with values (0, -1)
/// </summary>
public static readonly Vector2Int down = new(0, -1);
/// <summary>
/// A vector with values (0, 1)
/// </summary>
public static readonly Vector2Int forward = new(0, 1);
/// <summary>
/// A vector with values (0, -1)
/// </summary>
public static readonly Vector2Int back = new(0, -1);
/// <summary>
/// A vector3 with values (-1, 0)
/// </summary>
public static readonly Vector2Int left = new(-1, 0);
/// <summary>
/// A vector with values (1, 0)
/// </summary>
public static readonly Vector2Int right = new(1, 0);
/*
/// <summary>
/// Get an hash code for the vector
/// </summary>
/// <returns>The hash code</returns>
public override int GetHashCode() {
return (this.horizontal, this.vertical).GetHashCode();
}
/// <summary>
/// Tests if the vector has equal values as the given vector
/// </summary>
/// <param name="v1">The vector to compare to</param>
/// <returns><em>true</em> if the vector values are equal</returns>
public readonly bool Equals(Vector2Int v) => this.horizontal == v.horizontal && vertical == v.vertical;
*/
/// <summary>
/// Tests if the two vectors have equal values
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns><em>true</em>when the vectors have equal values</returns>
/// Note that this uses a Float equality check which cannot be not exact in all cases.
/// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon
/// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon
public static bool operator ==(Vector2Int v1, Vector2Int v2) {
return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical);
}
/// <summary>
/// Tests if two vectors have different values
/// </summary>
/// <param name="v1">The first vector</param>
/// <param name="v2">The second vector</param>
/// <returns><em>true</em>when the vectors have different values</returns>
/// Note that this uses a Float equality check which cannot be not exact in all case.
/// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon.
/// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon
public static bool operator !=(Vector2Int v1, Vector2Int v2) {
return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical);
}
/// <summary>
/// Tests if the vector is equal to the given object
/// </summary>
/// <param name="obj">The object to compare to</param>
/// <returns><em>false</em> when the object is not a Vector2 or does not have equal values</returns>
public override readonly bool Equals(object obj) {
if (obj is not Vector2Int v)
return false;
return (this.horizontal == v.horizontal && this.vertical == v.vertical);
}
/// <summary>
/// Get an hash code for the vector
/// </summary>
/// <returns>The hash code</returns>
public override readonly int GetHashCode() {
return HashCode.Combine(horizontal, vertical);
}
public readonly float sqrMagnitude => this.horizontal * this.horizontal + this.vertical * this.vertical;
public static float SqrMagnitudeOf(Vector2Int v) {
return v.sqrMagnitude;
}
public readonly float magnitude =>
MathF.Sqrt(this.horizontal * this.horizontal + this.vertical * this.vertical);
public static float MagnitudeOf(Vector2Int v) {
return v.magnitude;
}
/// @brief Convert the vector to a length of 1
/// @return The vector normalized to a length of 1
public readonly Vector2Float normalized {
get {
float l = magnitude;
Vector2Float v = Vector2Float.zero;
if (l > Float.epsilon)
v = new Vector2Float(this) / l;
return v;
}
}
/// @brief Convert the vector to a length of 1
/// @param v The vector to convert
/// @return The vector normalized to a length of 1
public static Vector2Float Normalize(Vector2Int v) {
float num = v.magnitude;
Vector2Float result = Vector2Float.zero;
if (num > Float.epsilon)
result = new Vector2Float(v) / num;
return result;
}
public static Vector2Int operator -(Vector2Int v) {
return new Vector2Int(-v.horizontal, -v.vertical);
}
public static Vector2Int operator -(Vector2Int v1, Vector2Int v2) {
return new Vector2Int(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical);
}
public static Vector2Int operator +(Vector2Int v1, Vector2Int v2) {
return new Vector2Int(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical);
}
public static Vector2Int operator /(Vector2Int v, int f) {
return new Vector2Int(v.horizontal / f, v.vertical / f);
}
public static Vector2Int operator *(Vector2Int v1, int d) {
return new Vector2Int(v1.horizontal * d, v1.vertical * d);
}
public static Vector2Int operator *(int d, Vector2Int v1) {
return new Vector2Int(d * v1.horizontal, d * v1.vertical);
}
public static Vector2Int Scale(Vector2Int v1, Vector2Int v2) {
return new Vector2Int(v1.horizontal * v2.horizontal, v1.vertical * v2.vertical);
}
/// @brief The dot product of two vectors
/// @param v1 The first vector
/// @param v2 The second vector
/// @return The dot product of the two vectors
public static int Dot(Vector2Int v1, Vector2Int v2) {
return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical;
}
public static float Distance(Vector2Int v1, Vector2Int v2) {
return (v1 - v2).magnitude;
}
}
}

View File

@ -0,0 +1,402 @@
//#if !UNITY_5_3_OR_NEWER
using System;
namespace LinearAlgebra {
/*
public struct Vector3Float {
public float horizontal;
public float vertical;
public float depth;
public Vector3Float(float horizontal, float vertical, float depth) {
this.horizontal = horizontal;
this.vertical = vertical;
this.depth = depth;
}
/// <summary>
/// A vector with zero for all axis
/// </summary>
public static readonly Vector3Float zero = new(0, 0, 0);
public readonly float magnitude {
get => (float)Math.Sqrt(this.horizontal * this.horizontal + this.vertical * this.vertical + this.depth * this.depth);
}
/// <summary>
/// Convert the vector to a length of a 1
/// </summary>
/// <returns>The vector with length 1</returns>
public readonly Vector3Float normalized {
get {
float l = magnitude;
Vector3Float v = zero;
if (l > Float.epsilon)
v = this / l;
return v;
}
}
public static Vector3Float operator *(Vector3Float v, float f) {
Vector3Float r = new(v.horizontal * f, v.vertical * f, v.depth * f);
return r;
}
public static Vector3Float operator /(Vector3Float v, float f) {
Vector3Float r = new(v.horizontal / f, v.vertical / f, v.depth / f);
return r;
}
public static float Dot(Vector3Float v1, Vector3Float v2) {
return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical +
v1.depth * v2.depth;
}
const float epsilon = 1E-05f;
public static Vector3Float Project(Vector3Float v, Vector3Float n) {
float sqrMagnitude = Dot(n, n);
if (sqrMagnitude < epsilon)
return zero;
else {
float dot = Dot(v, n);
Vector3Float r = n * dot;
r /= sqrMagnitude;
return r;
}
}
}
*/
/// <summary>
/// 3-dimensional vectors
/// </summary>
/// This uses the right-handed coordinate system.
public struct Vector3Float {
/// <summary>
/// The right axis of the vector
/// </summary>
public float horizontal; //> left/right
/// <summary>
/// The upward axis of the vector
/// </summary>
public float vertical; //> up/down
/// <summary>
/// The forward axis of the vector
/// </summary>
public float depth; //> forward/backward
/// <summary>
/// Create a new 3-dimensional vector
/// </summary>
/// <param name="horizontal">x axis value</param>
/// <param name="vertical">y axis value</param>
/// <param name="depth">z axis value</param>
public Vector3Float(float horizontal, float vertical, float depth) {
this.horizontal = horizontal;
this.vertical = vertical;
this.depth = depth;
}
public Vector3Float(Vector3Int v) {
this.horizontal = v.horizontal;
this.vertical = v.vertical;
this.depth = v.depth;
}
public static Vector3Float FromSpherical(Spherical s) {
float verticalRad = (AngleFloat.deg90 - s.direction.vertical).inRadians;
float horizontalRad = s.direction.horizontal.inRadians;
float cosVertical = MathF.Cos(verticalRad);
float sinVertical = MathF.Sin(verticalRad);
float cosHorizontal = MathF.Cos(horizontalRad);
float sinHorizontal = MathF.Sin(horizontalRad);
float horizontal = s.distance * sinVertical * sinHorizontal;
float vertical = s.distance * cosVertical;
float depth = s.distance * sinVertical * cosHorizontal;
return new Vector3Float(horizontal, vertical, depth);
}
public override string ToString() {
return $"({this.horizontal}, {this.vertical}, {this.depth})";
}
/// <summary>
/// A vector with zero for all axis
/// </summary>
public static readonly Vector3Float zero = new Vector3Float(0, 0, 0);
/// <summary>
/// A vector with one for all axis
/// </summary>
public static readonly Vector3Float one = new Vector3Float(1, 1, 1);
/// <summary>
/// A Vector3Float with values (-1, 0, 0)
/// </summary>
public static readonly Vector3Float left = new Vector3Float(-1, 0, 0);
/// <summary>
/// A vector with values (1, 0, 0)
/// </summary>
public static readonly Vector3Float right = new Vector3Float(1, 0, 0);
/// <summary>
/// A vector with values (0, -1, 0)
/// </summary>
public static readonly Vector3Float down = new Vector3Float(0, -1, 0);
/// <summary>
/// A vector with values (0, 1, 0)
/// </summary>
public static readonly Vector3Float up = new Vector3Float(0, 1, 0);
/// <summary>
/// A vector with values (0, 0, -1)
/// </summary>
public static readonly Vector3Float back = new Vector3Float(0, -1, 0);
/// <summary>
/// A vector with values (0, 0, 1)
/// </summary>
public static readonly Vector3Float forward = new Vector3Float(0, 1, 0);
/// @brief The vector length
/// @return The vector length
public readonly float magnitude => MathF.Sqrt(horizontal * horizontal + vertical * vertical + depth * depth);
/// <summary>
/// The vector length
/// </summary>
/// <param name="v">The vector for which you need the length</param>
/// <returns>The vector length</returns>
public static float MagnitudeOf(Vector3Float v) {
return v.magnitude;
}
/// @brief The squared vector length
/// @return The squared vector length
/// @remark The squared length is computationally simpler than the real
/// length. Think of Pythagoras A^2 + B^2 = C^2. This leaves out the
/// calculation of the squared root of C.
public readonly float sqrMagnitude => (horizontal * horizontal + vertical * vertical + depth * depth);
/// <summary>
/// The squared vector length
/// </summary>
/// <param name="v">The vector for which you need the squared length</param>
/// <returns>The squared vector length</returns>
/// <remarks>The squared length is computationally simpler than the real
/// length. Think of Pythagoras A^2 + B^2 = C^2. This leaves out the
/// calculation of the squared root of C.</remarks>
public static float SqrMagnitudeOf(Vector3Float v) {
return v.sqrMagnitude;
}
/// @brief Convert the vector to a length of 1
/// @return The vector normalized to a length of 1
public readonly Vector3Float normalized {
get {
float l = magnitude;
Vector3Float v = zero;
if (l > Float.epsilon)
v = this / l;
return v;
}
}
/// @brief Convert the vector to a length of 1
/// @param v The vector to convert
/// @return The vector normalized to a length of 1
public static Vector3Float Normalize(Vector3Float v) {
float num = v.magnitude;
Vector3Float result = zero;
if (num > Float.epsilon)
result = v / num;
return result;
}
/// <summary>
/// Negate te vector such that it points in the opposite direction
/// </summary>
/// <param name="v1"></param>
/// <returns>The negated vector</returns>
public static Vector3Float operator -(Vector3Float v1) {
Vector3Float v = new(-v1.horizontal, -v1.vertical, -v1.depth);
return v;
}
/// <summary>
/// Subtract two vectors
/// </summary>
/// <param name="v1"></param>
/// <param name="v2"></param>
/// <returns>The result of the subtraction</returns>
public static Vector3Float operator -(Vector3Float v1, Vector3Float v2) {
Vector3Float v = new(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical, v1.depth - v2.depth);
return v;
}
/// <summary>
/// Add two vectors
/// </summary>
/// <param name="v1"></param>
/// <param name="v2"></param>
/// <returns>The result of the addition</returns>
public static Vector3Float operator +(Vector3Float v1, Vector3Float v2) {
Vector3Float v = new(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical, v1.depth + v2.depth);
return v;
}
/// @brief Scale the vector using another vector
/// @param v1 The vector to scale
/// @param v2 A vector with the scaling factors
/// @return The scaled vector
/// @remark Each component of the vector v1 will be multiplied with the
/// matching component from the scaling vector v2.
public static Vector3Float Scale(Vector3Float v1, Vector3Float v2) {
return new Vector3Float(v1.horizontal * v2.horizontal, v1.vertical * v2.vertical, v1.depth * v2.depth);
}
public static Vector3Float operator *(Vector3Float v1, float d) {
Vector3Float v = new(v1.horizontal * d, v1.vertical * d, v1.depth * d);
return v;
}
public static Vector3Float operator *(float d, Vector3Float v1) {
Vector3Float v = new(d * v1.horizontal, d * v1.vertical, d * v1.depth);
return v;
}
public static Vector3Float operator /(Vector3Float v1, float d) {
Vector3Float v = new(v1.horizontal / d, v1.vertical / d, v1.depth / d);
return v;
}
//public bool Equals(Vector3Float v) => (horizontal == v.horizontal && vertical == v.vertical && depth == v.depth);
public static bool operator ==(Vector3Float v1, Vector3Float v2) {
return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical && v1.depth == v2.depth);
}
public static bool operator !=(Vector3Float v1, Vector3Float v2) {
return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical || v1.depth != v2.depth);
}
public override readonly bool Equals(object obj) {
if (obj is not Vector3Float v)
return false;
return (horizontal == v.horizontal && vertical == v.vertical && depth == v.depth);
}
public override readonly int GetHashCode() {
return HashCode.Combine(horizontal, vertical, depth);
}
/// @brief The distance between two vectors
/// @param v1 The first vector
/// @param v2 The second vector
/// @return The distance between the two vectors
public static float Distance(Vector3Float v1, Vector3Float v2) {
return (v2 - v1).magnitude;
}
/// @brief The dot product of two vectors
/// @param v1 The first vector
/// @param v2 The second vector
/// @return The dot product of the two vectors
public static float Dot(Vector3Float v1, Vector3Float v2) {
return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical + v1.depth * v2.depth;
}
/// @brief The cross product of two vectors
/// @param v1 The first vector
/// @param v2 The second vector
/// @return The cross product of the two vectors
public static Vector3Float Cross(Vector3Float v1, Vector3Float v2) {
return new Vector3Float(v1.vertical * v2.depth - v1.depth * v2.vertical, v1.depth * v2.horizontal - v1.horizontal * v2.depth,
v1.horizontal * v2.vertical - v1.vertical * v2.horizontal);
}
/// @brief Project the vector on another vector
/// @param v The vector to project
/// @param n The normal vecto to project on
/// @return The projected vector
public static Vector3Float Project(Vector3Float v, Vector3Float n) {
float sqrMagnitude = Dot(n, n);
if (sqrMagnitude < Float.epsilon)
return zero;
else {
float dot = Dot(v, n);
Vector3Float r = n * dot / sqrMagnitude;
return r;
}
}
/// @brief Project the vector on a plane defined by a normal orthogonal to the
/// plane.
/// @param v The vector to project
/// @param n The normal of the plane to project on
/// @return Teh projected vector
public static Vector3Float ProjectOnPlane(Vector3Float v, Vector3Float n) {
Vector3Float r = v - Project(v, n);
return r;
}
/// @brief The angle between two vectors
/// @param v1 The first vector
/// @param v2 The second vector
/// @return The angle between the two vectors
/// @remark This reterns an unsigned angle which is the shortest distance
/// between the two vectors. Use Vector3::SignedAngle if a signed angle is
/// needed.
public static AngleFloat UnsignedAngle(Vector3Float v1, Vector3Float v2) {
float denominator = MathF.Sqrt(v1.sqrMagnitude * v2.sqrMagnitude);
if (denominator < Float.epsilon)
return AngleFloat.zero;
float dot = Dot(v1, v2);
float fraction = dot / denominator;
if (float.IsNaN(fraction))
return AngleFloat.Degrees(
fraction); // short cut to returning NaN universally
float cdot = Float.Clamp(fraction, -1.0f, 1.0f);
float r = MathF.Acos(cdot);
return AngleFloat.Radians(r);
}
/// @brief The signed angle between two vectors
/// @param v1 The starting vector
/// @param v2 The ending vector
/// @param axis The axis to rotate around
/// @return The signed angle between the two vectors
public static AngleFloat SignedAngle(Vector3Float v1, Vector3Float v2,
Vector3Float axis) {
// angle in [0,180]
AngleFloat angle = UnsignedAngle(v1, v2);
Vector3Float cross = Cross(v1, v2);
float b = Dot(axis, cross);
float signd = b < 0 ? -1.0F : (b > 0 ? 1.0F : 0.0F);
// angle in [-179,180]
AngleFloat signed_angle = angle * signd;
return signed_angle;
}
/// @brief Lerp (linear interpolation) between two vectors
/// @param v1 The starting vector
/// @param v2 The ending vector
/// @param f The interpolation distance
/// @return The lerped vector
/// @remark The factor f is unclamped. Value 0 matches the vector *v1*, Value
/// 1 matches vector *v2*. Value -1 is vector *v1* minus the difference
/// between *v1* and *v2* etc.
public static Vector3Float Lerp(Vector3Float v1, Vector3Float v2, float f) {
Vector3Float v = v1 + (v2 - v1) * f;
return v;
}
}
}
//#endif

View File

@ -0,0 +1,273 @@
//#if !UNITY_5_3_OR_NEWER
using System;
namespace LinearAlgebra {
/// <summary>
/// 3-dimensional vectors
/// </summary>
/// This uses the right-handed coordinate system.
/// <remarks>
/// Create a new 3-dimensional vector
/// </remarks>
/// <param name="horizontal">x axis value</param>
/// <param name="vertical">y axis value</param>
/// <param name="depth">z axis value</param>
public struct Vector3Int {
/// <summary>
/// The right axis of the vector
/// </summary>
public int horizontal; //> left/right
/// <summary>
/// The upward axis of the vector
/// </summary>
public int vertical; //> up/down
/// <summary>
/// The forward axis of the vector
/// </summary>
public int depth; //> forward/backward
public Vector3Int(int horizontal, int vertical, int depth) {
this.horizontal = horizontal;
this.vertical = vertical;
this.depth = depth;
}
/// <summary>
/// A vector with zero for all axis
/// </summary>
public static readonly Vector3Int zero = new(0, 0, 0);
/// <summary>
/// A vector with one for all axis
/// </summary>
public static readonly Vector3Int one = new(1, 1, 1);
/// <summary>
/// A Vector3Int with values (-1, 0, 0)
/// </summary>
public static readonly Vector3Int left = new(-1, 0, 0);
/// <summary>
/// A vector with values (1, 0, 0)
/// </summary>
public static readonly Vector3Int right = new(1, 0, 0);
/// <summary>
/// A vector with values (0, -1, 0)
/// </summary>
public static readonly Vector3Int down = new(0, -1, 0);
/// <summary>
/// A vector with values (0, 1, 0)
/// </summary>
public static readonly Vector3Int up = new(0, 1, 0);
/// <summary>
/// A vector with values (0, 0, -1)
/// </summary>
public static readonly Vector3Int back = new(0, -1, 0);
/// <summary>
/// A vector with values (0, 0, 1)
/// </summary>
public static readonly Vector3Int forward = new(0, 1, 0);
/// @brief The vector length
/// @return The vector length
public readonly float magnitude => MathF.Sqrt(horizontal * horizontal + vertical * vertical + depth * depth);
/// <summary>
/// The vector length
/// </summary>
/// <param name="v">The vector for which you need the length</param>
/// <returns>The vector length</returns>
public static float MagnitudeOf(Vector3Int v) {
return v.magnitude;
}
/// @brief The squared vector length
/// @return The squared vector length
/// @remark The squared length is computationally simpler than the real
/// length. Think of Pythagoras A^2 + B^2 = C^2. This leaves out the
/// calculation of the squared root of C.
public readonly float sqrMagnitude => (horizontal * horizontal + vertical * vertical + depth * depth);
/// <summary>
/// The squared vector length
/// </summary>
/// <param name="v">The vector for which you need the squared length</param>
/// <returns>The squared vector length</returns>
/// <remarks>The squared length is computationally simpler than the real
/// length. Think of Pythagoras A^2 + B^2 = C^2. This leaves out the
/// calculation of the squared root of C.</remarks>
public static float SqrMagnitudeOf(Vector3Int v) {
return v.sqrMagnitude;
}
/// @brief Convert the vector to a length of 1
/// @return The vector normalized to a length of 1
public readonly Vector3Float normalized {
get {
float l = magnitude;
Vector3Float v = Vector3Float.zero;
if (l > Float.epsilon)
v = new Vector3Float(this) / l;
return v;
}
}
/// @brief Convert the vector to a length of 1
/// @param v The vector to convert
/// @return The vector normalized to a length of 1
public static Vector3Float Normalize(Vector3Int v) {
float num = v.magnitude;
Vector3Float result = Vector3Float.zero;
if (num > Float.epsilon)
result = new Vector3Float(v) / num;
return result;
}
/// <summary>
/// Negate te vector such that it points in the opposite direction
/// </summary>
/// <param name="v1"></param>
/// <returns>The negated vector</returns>
public static Vector3Int operator -(Vector3Int v1) {
Vector3Int v = new(-v1.horizontal, -v1.vertical, -v1.depth);
return v;
}
/// <summary>
/// Subtract two vectors
/// </summary>
/// <param name="v1"></param>
/// <param name="v2"></param>
/// <returns>The result of the subtraction</returns>
public static Vector3Int operator -(Vector3Int v1, Vector3Int v2) {
Vector3Int v = new(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical, v1.depth - v2.depth);
return v;
}
/// <summary>
/// Add two vectors
/// </summary>
/// <param name="v1"></param>
/// <param name="v2"></param>
/// <returns>The result of the addition</returns>
public static Vector3Int operator +(Vector3Int v1, Vector3Int v2) {
Vector3Int v = new(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical, v1.depth + v2.depth);
return v;
}
/// @brief Scale the vector using another vector
/// @param v1 The vector to scale
/// @param v2 A vector with the scaling factors
/// @return The scaled vector
/// @remark Each component of the vector v1 will be multiplied with the
/// matching component from the scaling vector v2.
public static Vector3Int Scale(Vector3Int v1, Vector3Int v2) {
return new Vector3Int(v1.horizontal * v2.horizontal, v1.vertical * v2.vertical, v1.depth * v2.depth);
}
public static Vector3Int operator *(Vector3Int v1, int d) {
Vector3Int v = new(v1.horizontal * d, v1.vertical * d, v1.depth * d);
return v;
}
public static Vector3Int operator *(int d, Vector3Int v1) {
Vector3Int v = new(d * v1.horizontal, d * v1.vertical, d * v1.depth);
return v;
}
public static Vector3Int operator /(Vector3Int v1, int d) {
Vector3Int v = new(v1.horizontal / d, v1.vertical / d, v1.depth / d);
return v;
}
public bool Equals(Vector3Int v) => (horizontal == v.horizontal && vertical == v.vertical && depth == v.depth);
public override bool Equals(object obj) {
if (!(obj is Vector3Int v))
return false;
return (horizontal == v.horizontal && vertical == v.vertical && depth == v.depth);
}
public static bool operator ==(Vector3Int v1, Vector3Int v2) {
return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical && v1.depth == v2.depth);
}
public static bool operator !=(Vector3Int v1, Vector3Int v2) {
return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical || v1.depth != v2.depth);
}
public override int GetHashCode() {
return (horizontal, vertical, depth).GetHashCode();
}
/// @brief The distance between two vectors
/// @param v1 The first vector
/// @param v2 The second vector
/// @return The distance between the two vectors
public static float Distance(Vector3Int v1, Vector3Int v2) {
return (v2 - v1).magnitude;
}
/// @brief The dot product of two vectors
/// @param v1 The first vector
/// @param v2 The second vector
/// @return The dot product of the two vectors
public static float Dot(Vector3Int v1, Vector3Int v2) {
return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical + v1.depth * v2.depth;
}
/// @brief The cross product of two vectors
/// @param v1 The first vector
/// @param v2 The second vector
/// @return The cross product of the two vectors
public static Vector3Int Cross(Vector3Int v1, Vector3Int v2) {
return new Vector3Int(v1.vertical * v2.depth - v1.depth * v2.vertical, v1.depth * v2.horizontal - v1.horizontal * v2.depth,
v1.horizontal * v2.vertical - v1.vertical * v2.horizontal);
}
/// @brief The angle between two vectors
/// @param v1 The first vector
/// @param v2 The second vector
/// @return The angle between the two vectors
/// @remark This reterns an unsigned angle which is the shortest distance
/// between the two vectors. Use Vector3::SignedAngle if a signed angle is
/// needed.
public static AngleFloat UnsignedAngle(Vector3Int v1, Vector3Int v2) {
float denominator = MathF.Sqrt(v1.sqrMagnitude * v2.sqrMagnitude);
if (denominator < Float.epsilon)
return AngleFloat.zero;
float dot = Dot(v1, v2);
float fraction = dot / denominator;
if (float.IsNaN(fraction))
return AngleFloat.Degrees(
fraction); // short cut to returning NaN universally
float cdot = Float.Clamp(fraction, -1.0f, 1.0f);
float r = MathF.Acos(cdot);
return AngleFloat.Radians(r);
}
/// @brief The signed angle between two vectors
/// @param v1 The starting vector
/// @param v2 The ending vector
/// @param axis The axis to rotate around
/// @return The signed angle between the two vectors
public static AngleFloat SignedAngle(Vector3Int v1, Vector3Int v2,
Vector3Int axis) {
// angle in [0,180]
AngleFloat angle = UnsignedAngle(v1, v2);
Vector3Int cross = Cross(v1, v2);
float b = Dot(axis, cross);
float signd = b < 0 ? -1.0F : (b > 0 ? 1.0F : 0.0F);
// angle in [-179,180]
AngleFloat signed_angle = angle * signd;
return signed_angle;
}
}
}
//#endif

View File

@ -0,0 +1,322 @@
using System;
namespace LinearAlgebra {
public class float16 {
//
// FILE: float16.cpp
// AUTHOR: Rob Tillaart
// VERSION: 0.1.8
// PURPOSE: library for Float16s for Arduino
// URL: http://en.wikipedia.org/wiki/Half-precision_floating-point_format
ushort _value;
public float16() { _value = 0; }
public float16(float f) {
//_value = f32tof16(f);
_value = F32ToF16__(f);
}
public float toFloat() {
return f16tof32(_value);
}
public ushort GetBinary() { return _value; }
public void SetBinary(ushort value) { _value = value; }
//////////////////////////////////////////////////////////
//
// EQUALITIES
//
/*
bool float16::operator ==(const float16 &f) { return (_value == f._value); }
bool float16::operator !=(const float16 &f) { return (_value != f._value); }
bool float16::operator >(const float16 &f) {
if ((_value & 0x8000) && (f._value & 0x8000))
return _value < f._value;
if (_value & 0x8000)
return false;
if (f._value & 0x8000)
return true;
return _value > f._value;
}
bool float16::operator >=(const float16 &f) {
if ((_value & 0x8000) && (f._value & 0x8000))
return _value <= f._value;
if (_value & 0x8000)
return false;
if (f._value & 0x8000)
return true;
return _value >= f._value;
}
bool float16::operator <(const float16 &f) {
if ((_value & 0x8000) && (f._value & 0x8000))
return _value > f._value;
if (_value & 0x8000)
return true;
if (f._value & 0x8000)
return false;
return _value < f._value;
}
bool float16::operator <=(const float16 &f) {
if ((_value & 0x8000) && (f._value & 0x8000))
return _value >= f._value;
if (_value & 0x8000)
return true;
if (f._value & 0x8000)
return false;
return _value <= f._value;
}
//////////////////////////////////////////////////////////
//
// NEGATION
//
float16 float16::operator -() {
float16 f16;
f16.setBinary(_value ^ 0x8000);
return f16;
}
//////////////////////////////////////////////////////////
//
// MATH
//
float16 float16::operator +(const float16 &f) {
return float16(this->toDouble() + f.toDouble());
}
float16 float16::operator -(const float16 &f) {
return float16(this->toDouble() - f.toDouble());
}
float16 float16::operator *(const float16 &f) {
return float16(this->toDouble() * f.toDouble());
}
float16 float16::operator /(const float16 &f) {
return float16(this->toDouble() / f.toDouble());
}
float16 & float16::operator+=(const float16 &f) {
*this = this->toDouble() + f.toDouble();
return *this;
}
float16 & float16::operator-=(const float16 &f) {
*this = this->toDouble() - f.toDouble();
return *this;
}
float16 & float16::operator*=(const float16 &f) {
*this = this->toDouble() * f.toDouble();
return *this;
}
float16 & float16::operator/=(const float16 &f) {
*this = this->toDouble() / f.toDouble();
return *this;
}
//////////////////////////////////////////////////////////
//
// MATH HELPER FUNCTIONS
//
int float16::sign() {
if (_value & 0x8000)
return -1;
if (_value & 0xFFFF)
return 1;
return 0;
}
bool float16::isZero() { return ((_value & 0x7FFF) == 0x0000); }
bool float16::isNaN() {
if ((_value & 0x7C00) != 0x7C00)
return false;
if ((_value & 0x03FF) == 0x0000)
return false;
return true;
}
bool float16::isInf() { return ((_value == 0x7C00) || (_value == 0xFC00)); }
*/
//////////////////////////////////////////////////////////
//
// CORE CONVERSION
//
float f16tof32(ushort _value) {
//ushort sgn;
ushort man;
int exp;
float f;
//Debug.Log($"{_value}");
bool sgn = (_value & 0x8000) > 0;
exp = (_value & 0x7C00) >> 10;
man = (ushort)(_value & 0x03FF);
//Debug.Log($"{sgn} {exp} {man}");
// ZERO
if ((_value & 0x7FFF) == 0) {
return sgn ? -0 : 0;
}
// NAN & INF
if (exp == 0x001F) {
if (man == 0)
return sgn ? float.NegativeInfinity : float.PositiveInfinity; //-INFINITY : INFINITY;
else
return float.NaN; // NAN;
}
// SUBNORMAL/NORMAL
if (exp == 0)
f = 0;
else
f = 1;
// PROCESS MANTISSE
for (int i = 9; i >= 0; i--) {
f *= 2;
if ((man & (1 << i)) != 0)
f = f + 1;
}
//Debug.Log($"{f}");
f = f * (float)Math.Pow(2.0f, exp - 25);
if (exp == 0) {
f = f * (float)Math.Pow(2.0f, -13); // 5.96046447754e-8;
}
//Debug.Log($"{f}");
return sgn ? -f : f;
}
public static uint SingleToInt32Bits(float value) {
byte[] bytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes); // If the system is little-endian, reverse the byte order
return BitConverter.ToUInt32(bytes, 0);
}
public ushort F32ToF16__(float f) {
uint t = BitConverter.ToUInt32(BitConverter.GetBytes(f), 0);
ushort man = (ushort)((t & 0x007FFFFF) >> 12);
int exp = (int)((t & 0x7F800000) >> 23);
bool sgn = (t & 0x80000000) != 0;
// handle 0
if ((t & 0x7FFFFFFF) == 0) {
return sgn ? (ushort)0x8000 : (ushort)0x0000;
}
// denormalized float32 does not fit in float16
if (exp == 0x00) {
return sgn ? (ushort)0x8000 : (ushort)0x0000;
}
// handle infinity & NAN
if (exp == 0x00FF) {
if (man != 0)
return 0xFE00; // NAN
return sgn ? (ushort)0xFC00 : (ushort)0x7C00; // -INF : INF
}
// normal numbers
exp = exp - 127 + 15;
// overflow does not fit => INF
if (exp > 30) {
return sgn ? (ushort)0xFC00 : (ushort)0x7C00; // -INF : INF
}
// subnormal numbers
if (exp < -38) {
return sgn ? (ushort)0x8000 : (ushort)0x0000; // -0 or 0 ? just 0 ?
}
if (exp <= 0) // subnormal
{
man >>= (exp + 14);
// rounding
man++;
man >>= 1;
if (sgn)
return (ushort)(0x8000 | man);
return man;
}
// normal
// TODO rounding
exp <<= 10;
man++;
man >>= 1;
if (sgn)
return (ushort)(0x8000 | exp | man);
return (ushort)(exp | man);
}
//This function is faulty!!!!
ushort f32tof16(float f) {
//uint t = *(uint*)&f;
//uint t = (uint)BitConverter.SingleToInt32Bits(f);
uint t = SingleToInt32Bits(f);
// man bits = 10; but we keep 11 for rounding
ushort man = (ushort)((t & 0x007FFFFF) >> 12);
short exp = (short)((t & 0x7F800000) >> 23);
bool sgn = (t & 0x80000000) != 0;
// handle 0
if ((t & 0x7FFFFFFF) == 0) {
return sgn ? (ushort)0x8000 : (ushort)0x0000;
}
// denormalized float32 does not fit in float16
if (exp == 0x00) {
return sgn ? (ushort)0x8000 : (ushort)0x0000;
}
// handle infinity & NAN
if (exp == 0x00FF) {
if (man != 0)
return 0xFE00; // NAN
return sgn ? (ushort)0xFC00 : (ushort)0x7C00; // -INF : INF
}
// normal numbers
exp = (short)(exp - 127 + 15);
// overflow does not fit => INF
if (exp > 30) {
return sgn ? (ushort)0xFC00 : (ushort)0x7C00; // -INF : INF
}
// subnormal numbers
if (exp < -38) {
return sgn ? (ushort)0x8000 : (ushort)0x0000; // -0 or 0 ? just 0 ?
}
if (exp <= 0) // subnormal
{
man >>= (exp + 14);
// rounding
man++;
man >>= 1;
if (sgn)
return (ushort)(0x8000 | man);
return man;
}
// normal
// TODO rounding
exp <<= 10;
man++;
man >>= 1;
ushort uexp = (ushort)exp;
if (sgn)
return (ushort)(0x8000 | uexp | man);
return (ushort)(uexp | man);
}
// -- END OF FILE --
}
}

View File

@ -0,0 +1,501 @@
#if !UNITY_5_6_OR_NEWER
using System;
using System.Formats.Asn1;
using NUnit.Framework;
namespace LinearAlgebra.Test {
public class AngleTests {
[SetUp]
public void Setup() {
}
[Test]
public void Construct() {
// Degrees
float angle = 0.0f;
AngleFloat a = AngleFloat.Degrees(angle);
Assert.AreEqual(angle, a.inDegrees);
angle = -180.0f;
a = AngleFloat.Degrees(angle);
Assert.AreEqual(angle, a.inDegrees);
angle = 270.0f;
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);
Assert.AreEqual(angle, a.inRadians);
angle = (float)-Math.PI;
a = AngleFloat.Radians(angle);
Assert.AreEqual(angle, a.inRadians);
angle = (float)Math.PI * 1.5f;
a = AngleFloat.Radians(angle);
Assert.AreEqual(-Math.PI * 0.5f, a.inRadians, 1.0E-05F);
// Revolutions
angle = 0.0f;
a = AngleFloat.Revolutions(angle);
Assert.AreEqual(angle, a.inRevolutions);
angle = -0.5f;
a = AngleFloat.Revolutions(angle);
Assert.AreEqual(angle, a.inRevolutions);
angle = 0.75f;
a = AngleFloat.Revolutions(angle);
Assert.AreEqual(-0.25f, a.inRevolutions);
}
[Test]
public void Revolutions() {
AngleFloat a;
// Test zero
a = AngleFloat.Revolutions(0.0f);
Assert.AreEqual(0.0f, a.inRevolutions);
// Test positive values within range
a = AngleFloat.Revolutions(0.25f);
Assert.AreEqual(0.25f, a.inRevolutions);
a = AngleFloat.Revolutions(0.5f);
Assert.AreEqual(-0.5f, a.inRevolutions);
// Test negative values within range
a = AngleFloat.Revolutions(-0.25f);
Assert.AreEqual(-0.25f, a.inRevolutions);
a = AngleFloat.Revolutions(-0.5f);
Assert.AreEqual(-0.5f, a.inRevolutions);
// Test values outside range (positive)
a = AngleFloat.Revolutions(1.0f);
Assert.AreEqual(0.0f, a.inRevolutions);
a = AngleFloat.Revolutions(1.25f);
Assert.AreEqual(0.25f, a.inRevolutions);
a = AngleFloat.Revolutions(1.75f);
Assert.AreEqual(-0.25f, a.inRevolutions);
// Test values outside range (negative)
a = AngleFloat.Revolutions(-1.0f);
Assert.AreEqual(0.0f, a.inRevolutions);
a = AngleFloat.Revolutions(-1.25f);
Assert.AreEqual(-0.25f, a.inRevolutions);
a = AngleFloat.Revolutions(-1.75f);
Assert.AreEqual(0.25f, a.inRevolutions);
// Test infinity
a = AngleFloat.Revolutions(float.PositiveInfinity);
Assert.AreEqual(float.PositiveInfinity, a.inRevolutions);
a = AngleFloat.Revolutions(float.NegativeInfinity);
Assert.AreEqual(float.NegativeInfinity, a.inRevolutions);
}
[Test]
public void Equality() {
// Test equality operator
Assert.IsTrue(AngleFloat.Degrees(90) == AngleFloat.Degrees(90), "90 == 90");
Assert.IsFalse(AngleFloat.Degrees(90) == AngleFloat.Degrees(45), "90 == 45");
Assert.IsTrue(AngleFloat.Degrees(0) == AngleFloat.Degrees(0), "0 == 0");
Assert.IsTrue(AngleFloat.Degrees(-180) == AngleFloat.Degrees(-180), "-180 == -180");
// Test inequality operator
Assert.IsTrue(AngleFloat.Degrees(90) != AngleFloat.Degrees(45), "90 != 45");
Assert.IsFalse(AngleFloat.Degrees(90) != AngleFloat.Degrees(90), "90 != 90");
Assert.IsTrue(AngleFloat.Degrees(0) != AngleFloat.Degrees(1), "0 != 1");
// Test greater than operator
Assert.IsTrue(AngleFloat.Degrees(90) > AngleFloat.Degrees(45), "90 > 45");
Assert.IsFalse(AngleFloat.Degrees(45) > AngleFloat.Degrees(90), "45 > 90");
Assert.IsFalse(AngleFloat.Degrees(90) > AngleFloat.Degrees(90), "90 > 90");
// Test greater than or equal operator
Assert.IsTrue(AngleFloat.Degrees(90) >= AngleFloat.Degrees(45), "90 >= 45");
Assert.IsTrue(AngleFloat.Degrees(90) >= AngleFloat.Degrees(90), "90 >= 90");
Assert.IsFalse(AngleFloat.Degrees(45) >= AngleFloat.Degrees(90), "45 >= 90");
// Test less than operator
Assert.IsTrue(AngleFloat.Degrees(45) < AngleFloat.Degrees(90), "45 < 90");
Assert.IsFalse(AngleFloat.Degrees(90) < AngleFloat.Degrees(45), "90 < 45");
Assert.IsFalse(AngleFloat.Degrees(90) < AngleFloat.Degrees(90), "90 < 90");
// Test less than or equal operator
Assert.IsTrue(AngleFloat.Degrees(45) <= AngleFloat.Degrees(90), "45 <= 90");
Assert.IsTrue(AngleFloat.Degrees(90) <= AngleFloat.Degrees(90), "90 <= 90");
Assert.IsFalse(AngleFloat.Degrees(90) <= AngleFloat.Degrees(45), "90 <= 45");
}
// [Test]
// public void Normalize() {
// float r = 0;
// r = Angle.Normalize(90);
// Assert.AreEqual(r, 90, "Normalize 90");
// r = Angle.Normalize(-90);
// Assert.AreEqual(r, -90, "Normalize -90");
// r = Angle.Normalize(270);
// Assert.AreEqual(r, -90, "Normalize 270");
// r = Angle.Normalize(270 + 360);
// Assert.AreEqual(r, -90, "Normalize 270+360");
// r = Angle.Normalize(-270);
// Assert.AreEqual(r, 90, "Normalize -270");
// r = Angle.Normalize(-270 - 360);
// Assert.AreEqual(r, 90, "Normalize -270-360");
// r = Angle.Normalize(0);
// Assert.AreEqual(r, 0, "Normalize 0");
// r = Angle.Normalize(float.PositiveInfinity);
// Assert.AreEqual(r, float.PositiveInfinity, "Normalize INFINITY");
// r = Angle.Normalize(float.NegativeInfinity);
// Assert.AreEqual(r, float.NegativeInfinity, "Normalize INFINITY");
// }
[Test]
public void Clamp() {
float r = 0;
r = AngleFloat.Clamp(AngleFloat.Degrees(1), AngleFloat.Degrees(0), AngleFloat.Degrees(2));
Assert.AreEqual(1, r, "Clamp 1 0 2");
r = AngleFloat.Clamp(AngleFloat.Degrees(-1), AngleFloat.Degrees(0), AngleFloat.Degrees(2));
Assert.AreEqual(0, r, "Clamp -1 0 2");
r = AngleFloat.Clamp(AngleFloat.Degrees(3), AngleFloat.Degrees(0), AngleFloat.Degrees(2));
Assert.AreEqual(2, r, "Clamp 3 0 2");
r = AngleFloat.Clamp(AngleFloat.Degrees(1), AngleFloat.Degrees(0), AngleFloat.Degrees(0));
Assert.AreEqual(0, r, "Clamp 1 0 0");
r = AngleFloat.Clamp(AngleFloat.Degrees(0), AngleFloat.Degrees(0), AngleFloat.Degrees(0));
Assert.AreEqual(0, r, "Clamp 0 0 0");
r = AngleFloat.Clamp(AngleFloat.Degrees(0), AngleFloat.Degrees(1), AngleFloat.Degrees(-1));
Assert.AreEqual(1, r, "Clamp 0 1 -1");
r = AngleFloat.Clamp(AngleFloat.Degrees(1), AngleFloat.Degrees(0), AngleFloat.Degrees(float.PositiveInfinity));
Assert.AreEqual(1, r, "Clamp 1 0 INFINITY");
r = AngleFloat.Clamp(AngleFloat.Degrees(1), AngleFloat.Degrees(float.NegativeInfinity), AngleFloat.Degrees(1));
Assert.AreEqual(1, r, "Clamp 1 -INFINITY 1");
}
[Test]
public void Cos() {
// Test zero
Assert.AreEqual(1.0f, AngleFloat.Cos(AngleFloat.Degrees(0)), 1.0E-05F, "Cos(0°)");
// Test 90 degrees
Assert.AreEqual(0.0f, AngleFloat.Cos(AngleFloat.Degrees(90)), 1.0E-05F, "Cos(90°)");
// Test 180 degrees
Assert.AreEqual(-1.0f, AngleFloat.Cos(AngleFloat.Degrees(180)), 1.0E-05F, "Cos(180°)");
// Test 270 degrees
Assert.AreEqual(0.0f, AngleFloat.Cos(AngleFloat.Degrees(270)), 1.0E-05F, "Cos(270°)");
// Test 45 degrees
Assert.AreEqual(MathF.Sqrt(2) / 2, AngleFloat.Cos(AngleFloat.Degrees(45)), 1.0E-05F, "Cos(45°)");
// Test negative angle
Assert.AreEqual(1.0f, AngleFloat.Cos(AngleFloat.Degrees(-360)), 1.0E-05F, "Cos(-360°)");
// Test using radians
Assert.AreEqual(1.0f, AngleFloat.Cos(AngleFloat.Radians(0)), 1.0E-05F, "Cos(0 rad)");
Assert.AreEqual(0.0f, AngleFloat.Cos(AngleFloat.Radians((float)Math.PI / 2)), 1.0E-05F, "Cos(π/2)");
Assert.AreEqual(-1.0f, AngleFloat.Cos(AngleFloat.Radians((float)Math.PI)), 1.0E-05F, "Cos(π)");
}
[Test]
public void Sin() {
// Test zero
Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Degrees(0)), 1.0E-05F, "Sin(0°)");
// Test 90 degrees
Assert.AreEqual(1.0f, AngleFloat.Sin(AngleFloat.Degrees(90)), 1.0E-05F, "Sin(90°)");
// Test 180 degrees
Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Degrees(180)), 1.0E-05F, "Sin(180°)");
// Test 270 degrees
Assert.AreEqual(-1.0f, AngleFloat.Sin(AngleFloat.Degrees(270)), 1.0E-05F, "Sin(270°)");
// Test 45 degrees
Assert.AreEqual(MathF.Sqrt(2) / 2, AngleFloat.Sin(AngleFloat.Degrees(45)), 1.0E-05F, "Sin(45°)");
// Test negative angle
Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Degrees(-360)), 1.0E-05F, "Sin(-360°)");
// Test using radians
Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Radians(0)), 1.0E-05F, "Sin(0 rad)");
Assert.AreEqual(1.0f, AngleFloat.Sin(AngleFloat.Radians((float)Math.PI / 2)), 1.0E-05F, "Sin(π/2)");
Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Radians((float)Math.PI)), 1.0E-05F, "Sin(π)");
}
[Test]
public void Tan() {
// Test zero
Assert.AreEqual(0.0f, AngleFloat.Tan(AngleFloat.Degrees(0)), 1.0E-05F, "Tan(0°)");
// Test 45 degrees
Assert.AreEqual(1.0f, AngleFloat.Tan(AngleFloat.Degrees(45)), 1.0E-05F, "Tan(45°)");
// Test -45 degrees
Assert.AreEqual(-1.0f, AngleFloat.Tan(AngleFloat.Degrees(-45)), 1.0E-05F, "Tan(-45°)");
// Test using radians
Assert.AreEqual(0.0f, AngleFloat.Tan(AngleFloat.Radians(0)), 1.0E-05F, "Tan(0 rad)");
Assert.AreEqual(1.0f, AngleFloat.Tan(AngleFloat.Radians((float)Math.PI / 4)), 1.0E-05F, "Tan(π/4)");
}
[Test]
public void Acos() {
// Test 1 (0 degrees)
Assert.AreEqual(0.0f, AngleFloat.Acos(1.0f).inRadians, 1.0E-05F, "Acos(1)");
// Test 0 (90 degrees or π/2 radians)
Assert.AreEqual((float)Math.PI / 2, AngleFloat.Acos(0.0f).inRadians, 1.0E-05F, "Acos(0)");
// Test -1 (-180 degrees or π radians)
Assert.AreEqual((float)-Math.PI, AngleFloat.Acos(-1.0f).inRadians, 1.0E-05F, "Acos(-1)");
// Test 0.5 (60 degrees or π/3 radians)
Assert.AreEqual((float)Math.PI / 3, AngleFloat.Acos(0.5f).inRadians, 1.0E-05F, "Acos(0.5)");
// Test sqrt(2)/2 (45 degrees or π/4 radians)
Assert.AreEqual((float)Math.PI / 4, AngleFloat.Acos(MathF.Sqrt(2) / 2).inRadians, 1.0E-05F, "Acos(√2/2)");
}
[Test]
public void Asin() {
// Test 0 (0 degrees)
Assert.AreEqual(0.0f, AngleFloat.Asin(0.0f).inRadians, 1.0E-05F, "Asin(0)");
// Test 1 (90 degrees or π/2 radians)
Assert.AreEqual((float)Math.PI / 2, AngleFloat.Asin(1.0f).inRadians, 1.0E-05F, "Asin(1)");
// Test -1 (-90 degrees or -π/2 radians)
Assert.AreEqual(-(float)Math.PI / 2, AngleFloat.Asin(-1.0f).inRadians, 1.0E-05F, "Asin(-1)");
// Test 0.5 (30 degrees or π/6 radians)
Assert.AreEqual((float)Math.PI / 6, AngleFloat.Asin(0.5f).inRadians, 1.0E-05F, "Asin(0.5)");
// Test sqrt(2)/2 (45 degrees or π/4 radians)
Assert.AreEqual((float)Math.PI / 4, AngleFloat.Asin(MathF.Sqrt(2) / 2).inRadians, 1.0E-05F, "Asin(√2/2)");
}
[Test]
public void Atan() {
// Test zero
Assert.AreEqual(0.0f, AngleFloat.Atan(0.0f).inRadians, 1.0E-05F, "Atan(0)");
// Test 1 (45 degrees or π/4 radians)
Assert.AreEqual((float)Math.PI / 4, AngleFloat.Atan(1.0f).inRadians, 1.0E-05F, "Atan(1)");
// Test -1 (-45 degrees or -π/4 radians)
Assert.AreEqual(-(float)Math.PI / 4, AngleFloat.Atan(-1.0f).inRadians, 1.0E-05F, "Atan(-1)");
// Test sqrt(3) (60 degrees or π/3 radians)
Assert.AreEqual((float)Math.PI / 3, AngleFloat.Atan(MathF.Sqrt(3)).inRadians, 1.0E-05F, "Atan(√3)");
// Test 1/sqrt(3) (30 degrees or π/6 radians)
Assert.AreEqual((float)Math.PI / 6, AngleFloat.Atan(1.0f / MathF.Sqrt(3)).inRadians, 1.0E-05F, "Atan(1/√3)");
// Test positive infinity
Assert.AreEqual((float)Math.PI / 2, AngleFloat.Atan(float.PositiveInfinity).inRadians, 1.0E-05F, "Atan(+∞)");
// Test negative infinity
Assert.AreEqual(-(float)Math.PI / 2, AngleFloat.Atan(float.NegativeInfinity).inRadians, 1.0E-05F, "Atan(-∞)");
}
[Test]
public void Atan2() {
// Test basic quadrant I
Assert.AreEqual((float)Math.PI / 4, AngleFloat.Atan2(1.0f, 1.0f).inRadians, 1.0E-05F, "Atan2(1, 1)");
// Test quadrant II
Assert.AreEqual(3 * (float)Math.PI / 4, AngleFloat.Atan2(1.0f, -1.0f).inRadians, 1.0E-05F, "Atan2(1, -1)");
// Test quadrant III
Assert.AreEqual(-(float)Math.PI * 0.75f, AngleFloat.Atan2(-1.0f, -1.0f).inRadians, 1.0E-05F, "Atan2(-1, -1)");
// Test quadrant IV
Assert.AreEqual(-(float)Math.PI / 4, AngleFloat.Atan2(-1.0f, 1.0f).inRadians, 1.0E-05F, "Atan2(-1, 1)");
// Test positive x-axis
Assert.AreEqual(0.0f, AngleFloat.Atan2(0.0f, 1.0f).inRadians, 1.0E-05F, "Atan2(0, 1)");
// Test positive y-axis
Assert.AreEqual((float)Math.PI / 2, AngleFloat.Atan2(1.0f, 0.0f).inRadians, 1.0E-05F, "Atan2(1, 0)");
// Test negative y-axis
Assert.AreEqual(-(float)Math.PI / 2, AngleFloat.Atan2(-1.0f, 0.0f).inRadians, 1.0E-05F, "Atan2(-1, 0)");
// Test origin
Assert.AreEqual(0.0f, AngleFloat.Atan2(0.0f, 0.0f).inRadians, 1.0E-05F, "Atan2(0, 0)");
// Test with different magnitudes
Assert.AreEqual((float)Math.PI / 3, AngleFloat.Atan2(MathF.Sqrt(3), 1.0f).inRadians, 1.0E-05F, "Atan2(√3, 1)");
// Test negative x-axis
Assert.AreEqual((float)-Math.PI, AngleFloat.Atan2(0.0f, -1.0f).inRadians, 1.0E-05F, "Atan2(0, -1)");
}
[Test]
public void Multiplication() {
AngleFloat r = AngleFloat.zero;
// Angle * float
r = AngleFloat.Degrees(90) * 2;
Assert.AreEqual(-180, r.inDegrees, "Multiply 90 * 2");
r = AngleFloat.Degrees(45) * 0.5f;
Assert.AreEqual(22.5f, r.inDegrees, "Multiply 45 * 0.5");
r = AngleFloat.Degrees(90) * 0;
Assert.AreEqual(0, r.inDegrees, "Multiply 90 * 0");
r = AngleFloat.Degrees(-90) * 2;
Assert.AreEqual(-180, r.inDegrees, "Multiply -90 * 2");
r = AngleFloat.Degrees(270) * 2;
Assert.AreEqual(-180, r.inDegrees, "Multiply 270 * 2 (normalized)");
// float * Angle
r = 2 * AngleFloat.Degrees(90);
Assert.AreEqual(-180, r.inDegrees, "Multiply 2 * 90");
r = 0.5f * AngleFloat.Degrees(45);
Assert.AreEqual(22.5, r.inDegrees, "Multiply 0.5 * 45");
r = 0 * AngleFloat.Degrees(90);
Assert.AreEqual(0, r.inDegrees, "Multiply 0 * 90");
r = 2 * AngleFloat.Degrees(-90);
Assert.AreEqual(-180, r.inDegrees, "Multiply 2 * -90");
// Negative factor
r = AngleFloat.Degrees(90) * -1;
Assert.AreEqual(-90, r.inDegrees, "Multiply 90 * -1");
r = -1 * AngleFloat.Degrees(90);
Assert.AreEqual(-90, r.inDegrees, "Multiply -1 * 90");
}
[Test]
public void MoveTowards() {
AngleFloat r = AngleFloat.zero;
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 30);
Assert.AreEqual(30, r.inDegrees, "MoveTowards 0 90 30");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 90);
Assert.AreEqual(90, r.inDegrees, "MoveTowards 0 90 90");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 180);
Assert.AreEqual(90, r.inDegrees, "MoveTowards 0 90 180");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 270);
Assert.AreEqual(90, r.inDegrees, "MoveTowrads 0 90 270");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), -30);
Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 90 -30");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), -30);
Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 -90 -30");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), -90);
Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 -90 -90");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), -180);
Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 -90 -180");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), -270);
Assert.AreEqual(0, r.inDegrees, "MoveTowrads 0 -90 -270");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 0);
Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 90 0");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(0), 0);
Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 0 0");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(0), 30);
Assert.AreEqual(0, r.inDegrees, "MoveTowrads 0 0 30");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), float.PositiveInfinity);
Assert.AreEqual(90, r.inDegrees, "MoveTowards 0 90 INFINITY");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(float.PositiveInfinity), 30);
Assert.AreEqual(30, r.inDegrees, "MoveTowrads 0 INFINITY 30");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), float.NegativeInfinity);
Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 -90 -INFINITY");
r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(float.NegativeInfinity), -30);
Assert.AreEqual(0, r.inDegrees, "MoveTowrads 0 -INFINITY -30");
}
[Test]
public void Difference() {
float r = 0;
r = Angles.Difference(0, 90);
Assert.AreEqual(90, r, "Difference 0 90");
r = Angles.Difference(0, -90);
Assert.AreEqual(-90, r, "Difference 0 -90");
r = Angles.Difference(0, 270);
Assert.AreEqual(-90, r, "Difference 0 270");
r = Angles.Difference(0, -270);
Assert.AreEqual(90, r, "Difference 0 -270");
r = Angles.Difference(90, 0);
Assert.AreEqual(-90, r, "Difference 90 0");
r = Angles.Difference(-90, 0);
Assert.AreEqual(90, r, "Difference -90 0");
r = Angles.Difference(0, 0);
Assert.AreEqual(0, r, "Difference 0 0");
r = Angles.Difference(90, 90);
Assert.AreEqual(0, r, "Difference 90 90");
r = Angles.Difference(0, float.PositiveInfinity);
Assert.AreEqual(float.PositiveInfinity, r, "Difference 0 INFINITY");
r = Angles.Difference(0, float.NegativeInfinity);
Assert.AreEqual(float.NegativeInfinity, r, "Difference 0 -INFINITY");
r = Angles.Difference(float.NegativeInfinity, float.PositiveInfinity);
Assert.AreEqual(float.PositiveInfinity, r, "Difference -INFINITY INFINITY");
}
}
}
#endif

View File

@ -0,0 +1,226 @@
#if !UNITY_5_6_OR_NEWER
using System;
using NUnit.Framework;
namespace LinearAlgebra.Test {
public class DirectionTest {
[Test]
public void RadiansForward() {
Direction d = Direction.Radians(0, 0);
Assert.AreEqual(0, d.horizontal.inDegrees, 0.0001f);
Assert.AreEqual(0, d.vertical.inDegrees, 0.0001f);
}
[Test]
public void RadiansUp() {
Direction d = Direction.Radians(0, (float)Math.PI / 2);
Assert.AreEqual(0, d.horizontal.inDegrees, 0.0001f);
Assert.AreEqual(90, d.vertical.inDegrees, 0.0001f);
}
[Test]
public void RadiansDown() {
Direction d = Direction.Radians(0, -(float)Math.PI / 2);
Assert.AreEqual(0, d.horizontal.inDegrees, 0.0001f);
Assert.AreEqual(-90, d.vertical.inDegrees, 0.0001f);
}
[Test]
public void RadiansArbitrary() {
Direction d = Direction.Radians((float)Math.PI / 4, (float)Math.PI / 6);
Assert.AreEqual(45, d.horizontal.inDegrees, 0.0001f);
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);
Direction d2 = Direction.Degrees(60, 45);
Assert.AreEqual(d1.horizontal.inDegrees, d2.horizontal.inDegrees, 0.0001f);
Assert.AreEqual(d1.vertical.inDegrees, d2.vertical.inDegrees, 0.0001f);
}
[Test]
public void ToVector3Forward() {
Direction d = Direction.forward;
Vector3Float v = d.ToVector3();
Assert.AreEqual(0, v.horizontal, 0.0001f);
Assert.AreEqual(0, v.vertical, 0.0001f);
Assert.AreEqual(1, v.depth, 0.0001f);
}
[Test]
public void ToVector3Up() {
Direction d = Direction.up;
Vector3Float v = d.ToVector3();
Assert.AreEqual(0, v.horizontal, 0.0001f);
Assert.AreEqual(1, v.vertical, 0.0001f);
Assert.AreEqual(0, v.depth, 0.0001f);
}
[Test]
public void ToVector3Down() {
Direction d = Direction.down;
Vector3Float v = d.ToVector3();
Assert.AreEqual(0, v.horizontal, 0.0001f);
Assert.AreEqual(-1, v.vertical, 0.0001f);
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);
Direction d = Direction.FromVector3(v);
Assert.AreEqual(0, d.horizontal.inDegrees, 0.0001f);
Assert.AreEqual(0, d.vertical.inDegrees, 0.0001f);
}
[Test]
public void ToVector3AndBack() {
Direction d1 = Direction.Degrees(45, 30);
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 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);
Direction d2 = new(AngleFloat.Degrees(45), AngleFloat.Degrees(135));
bool r;
r = d1 == d2;
Assert.True(r);
Assert.AreEqual(d1, d2);
}
[Test]
public void NotEqualWithDifferentHorizontal() {
Direction d1 = Direction.Degrees(45, 30);
Direction d2 = Direction.Degrees(90, 30);
Assert.True(d1 != d2);
}
[Test]
public void NotEqualWithDifferentVertical() {
Direction d1 = Direction.Degrees(45, 30);
Direction d2 = Direction.Degrees(45, 60);
Assert.True(d1 != d2);
}
[Test]
public void NotEqualWithDifferentBoth() {
Direction d1 = Direction.Degrees(45, 30);
Direction d2 = Direction.Degrees(90, 60);
Assert.True(d1 != d2);
}
[Test]
public void NotEqualWithSameValues() {
Direction d1 = Direction.Degrees(45, 30);
Direction d2 = Direction.Degrees(45, 30);
Assert.False(d1 != d2);
}
[Test]
public void EqualsWithSameValues() {
Direction d1 = Direction.Degrees(45, 30);
Direction d2 = Direction.Degrees(45, 30);
Assert.True(d1.Equals(d2));
}
[Test]
public void EqualsWithDifferentHorizontal() {
Direction d1 = Direction.Degrees(45, 30);
Direction d2 = Direction.Degrees(90, 30);
Assert.False(d1.Equals(d2));
}
[Test]
public void EqualsWithDifferentVertical() {
Direction d1 = Direction.Degrees(45, 30);
Direction d2 = Direction.Degrees(45, 60);
Assert.False(d1.Equals(d2));
}
[Test]
public void EqualsWithDifferentBoth() {
Direction d1 = Direction.Degrees(45, 30);
Direction d2 = Direction.Degrees(90, 60);
Assert.False(d1.Equals(d2));
}
[Test]
public void EqualsWithNonDirectionObject() {
Direction d = Direction.Degrees(45, 30);
Assert.False(d.Equals("not a direction"));
}
[Test]
public void EqualsWithNull() {
Direction d = Direction.Degrees(45, 30);
Assert.False(d.Equals(null));
}
[Test]
public void EqualsWithZeros() {
Direction d1 = Direction.forward;
Direction d2 = Direction.Degrees(0, 0);
Assert.True(d1.Equals(d2));
}
[Test]
public void HashCode() {
Direction d1 = Direction.Degrees(45, 30);
Direction d2 = Direction.Degrees(45, 30);
Assert.AreEqual(d1.GetHashCode(), d2.GetHashCode());
d1 = Direction.Degrees(45, 30);
d2 = Direction.Degrees(90, 30);
Assert.AreNotEqual(d1.GetHashCode(), d2.GetHashCode());
d1 = Direction.Degrees(45, 30);
d2 = Direction.Degrees(45, 60);
Assert.AreNotEqual(d1.GetHashCode(), d2.GetHashCode());
Direction d = Direction.Degrees(45, 30);
int hash1 = d.GetHashCode();
int hash2 = d.GetHashCode();
Assert.AreEqual(hash1, hash2);
d1 = Direction.forward;
d2 = Direction.Degrees(0, 0);
Assert.AreEqual(d1.GetHashCode(), d2.GetHashCode());
}
};
}
#endif

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\LinearAlgebra.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,185 @@
#if !UNITY_5_6_OR_NEWER
using NUnit.Framework;
namespace LinearAlgebra.Test {
public class QuaternionTest {
[SetUp]
public void Setup() {
}
[Test]
public void Normalize() {
Quaternion q1 = new(0, 0, 0, 1);
Quaternion r = Quaternion.identity;
r = q1.normalized;
Assert.AreEqual(r, q1, "q.normalized 0 0 0 1");
r = Quaternion.Normalize(q1);
Assert.AreEqual(r, q1, "q.normalized 0 0 0 1");
}
[Test]
public void ToAngles() {
Quaternion q1 = new(0, 0, 0, 1);
Vector3Float v = Vector3Float.zero;
v = Quaternion.ToAngles(q1);
Assert.AreEqual(v, new Vector3Float(0, 0, 0), "ToAngles 0 0 0 1");
q1 = new(1, 0, 0, 0);
v = Quaternion.ToAngles(q1);
Assert.AreEqual(0, v.horizontal, "1 0 0 0 H");
Assert.AreEqual(180, v.vertical, "1 0 0 0 V");
Assert.AreEqual(180, v.depth, "1 0 0 0 D");
}
[Test]
public void Multiplication() {
Quaternion q1 = new(0, 0, 0, 1);
Quaternion q2 = new(1, 0, 0, 0);
Quaternion r;
r = q1 * q2;
Assert.AreEqual(r, new Quaternion(1, 0, 0, 0), "0 0 0 1 * 1 0 0 0 ");
}
[Test]
public void MultiplicationVector() {
Quaternion q1 = new(0, 0, 0, 1);
Vector3Float v1 = new(0, 1, 0);
Vector3Float r;
r = q1 * v1;
Assert.AreEqual(r, new Vector3Float(0, 1, 0), "0 0 0 1 * Vector 0 1 0");
q1 = new(1, 0, 0, 0);
r = q1 * v1;
Assert.AreEqual(r, new Vector3Float(0, -1, 0), "1 0 0 0 * Vector 0 1 0");
}
[Test]
public void Equality() {
Quaternion q1 = new(0, 0, 0, 1);
Quaternion q2 = new(1, 0, 0, 0);
Assert.AreNotEqual(q1, q2, "0 0 0 1 == 1 0 0 0");
q2 = new(0, 0, 0, 1);
Assert.AreEqual(q1, q2, "0 0 0 1 == 1 0 0 0");
}
[Test, Ignore("ToDo")]
public void Inverse() { }
[Test, Ignore("ToDo")]
public void LookRotation() { }
[Test, Ignore("ToDo")]
public void FromToRotation() { }
[Test, Ignore("ToDo")]
public void RotateTowards() { }
[Test, Ignore("ToDo")]
public void AngleAxis() { }
[Test, Ignore("ToDo")]
public void Angle() { }
[Test, Ignore("ToDo")]
public void Slerp() { }
[Test, Ignore("ToDo")]
public void SlerpUnclamped() { }
[Test]
public void Euler() {
Vector3Float v1 = new(0, 0, 0);
Quaternion q;
q = Quaternion.Euler(v1);
Assert.AreEqual(q, Quaternion.identity, "Euler Vector 0 0 0");
q = Quaternion.Euler(0, 0, 0);
Assert.AreEqual(q, Quaternion.identity, "Euler 0 0 0");
v1 = new(90, 90, -90);
q = Quaternion.Euler(v1);
Assert.AreEqual(q, new Quaternion(0, 0.707106709F, -0.707106709F, 0), "Euler Vector 90 90 -90");
q = Quaternion.Euler(90, 90, -90);
Assert.AreEqual(q, new Quaternion(0, 0.707106709F, -0.707106709F, 0), "Euler 90 90 -90");
}
[Test]
public void EulerToAngles() {
Vector3Float v;
Quaternion q;
Quaternion r;
//v = new(0, 0, 0);
q = Quaternion.Euler(0, 0 , 0);
v = Quaternion.ToAngles(q);
r = Quaternion.Euler(v);
Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f, "0 0 0");
q = Quaternion.Euler(-45, -30, -15);
v = Quaternion.ToAngles(q);
r = Quaternion.Euler(v);
Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f, "-45, -30, -15");
// Gimball lock
// q = Quaternion.Euler(90, 90, -90);
// v = Quaternion.ToAngles(q);
// r = Quaternion.Euler(v);
// Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f, "0 0 0");
}
[Test]
public void GetAngleAround() {
Vector3Float v1 = new(0, 1, 0);
Quaternion q1 = new(0, 0, 0, 1);
float f = Quaternion.GetAngleAround(v1, q1);
Assert.AreEqual(f, 0, "GetAngleAround 0 1 0 , 0 0 0 1");
q1 = new(0, 0.707106709F, -0.707106709F, 0);
f = Quaternion.GetAngleAround(v1, q1);
Assert.AreEqual(f, 180, "GetAngleAround 0 1 0 , 0 0.7 -0.7 0");
v1 = new(0, 0, 0);
f = Quaternion.GetAngleAround(v1, q1);
Assert.IsTrue(float.IsNaN(f), "GetAngleAround 0 0 0 , 0 0.7 -0.7 0");
}
[Test]
public void GetRotationAround() {
Vector3Float v1 = new(0, 1, 0);
Quaternion q1 = new(0, 0, 0, 1);
Quaternion q = Quaternion.GetRotationAround(v1, q1);
Assert.AreEqual(q, new Quaternion(0, 0, 0, 1), "GetRotationAround 0 1 0 , 0 0 0 1");
q1 = new(0, 0.707106709F, -0.707106709F, 0);
q = Quaternion.GetRotationAround(v1, q1);
Assert.AreEqual(q, new Quaternion(0, 1, 0, 0), "GetRotationAround 0 1 0 , 0 0.7 -0.7 0");
v1 = new(0, 0, 0);
q = Quaternion.GetRotationAround(v1, q1);
bool r = float.IsNaN(q.x) && float.IsNaN(q.y) && float.IsNaN(q.z) && float.IsNaN(q.w);
Assert.IsTrue(r, "GetRotationAround 0 0 0 , 0 0.7 -0.7 0");
}
[Test, Ignore("ToDo")]
public void GetSwingTwist() { }
[Test, Ignore("ToDo")]
public void Dot() { }
}
}
#endif

View File

@ -0,0 +1,271 @@
//#if !UNITY_5_6_OR_NEWER
using System;
using System.Collections.Generic;
using NUnit.Framework;
namespace LinearAlgebra.Test {
public class SphericalTest {
[SetUp]
public void Setup() {
}
[Test]
public void FromVector3() {
#if UNITY_5_6_OR_NEWER
UnityEngine.Vector3 v = new(0, 0, 1);
#else
Vector3Float v = new(0, 0, 1);
#endif
Spherical s = Spherical.FromVector3(v);
Assert.AreEqual(1.0f, s.distance, "s.distance 0 0 1");
Assert.AreEqual(0.0f, s.direction.horizontal.inDegrees, "s.hor 0 0 1");
Assert.AreEqual(0.0f, s.direction.vertical.inDegrees, 1.0E-05F, "s.vert 0 0 1");
v = new(0, 1, 0);
s = Spherical.FromVector3(v);
Assert.AreEqual(1.0f, s.distance, "s.distance 0 1 0");
Assert.AreEqual(0.0f, s.direction.horizontal.inDegrees, "s.hor 0 1 0");
Assert.AreEqual(90.0f, s.direction.vertical.inDegrees, "s.vert 0 1 0");
v = new(1, 0, 0);
s = Spherical.FromVector3(v);
Assert.AreEqual(1.0f, s.distance, "s.distance 1 0 0");
Assert.AreEqual(90.0f, s.direction.horizontal.inDegrees, "s.hor 1 0 0");
Assert.AreEqual(0.0f, s.direction.vertical.inDegrees, 1.0E-05F, "s.vert 1 0 0");
}
[Test]
public void Addition() {
Spherical v1 = Spherical.Degrees(1, 45, 0);
Spherical v2 = Spherical.zero;
Spherical r = Spherical.zero;
r = v1 + v2;
Assert.AreEqual(v1.distance, r.distance, 1.0E-05F, "Addition(0,0,0)");
r = v1;
r += v2;
Assert.AreEqual(v1.distance, r.distance, 1.0E-05F, "Addition(0,0,0)");
v2 = Spherical.Degrees(1, 0, 90);
r = v1 + v2;
Assert.AreEqual(Math.Sqrt(2), r.distance, 1.0E-05F, "Addition(1 0 90)");
Assert.AreEqual(45.0f, r.direction.horizontal.inDegrees, 1e-5f, "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(new List<Spherical> { 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(new List<Spherical> { 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(new List<Spherical> { 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);
}
[Test]
public void Average_CompareWithVector3() {
// 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
List<Spherical> dirs = new List<Spherical> {
new(1f, Direction.Radians(0f, elevation)),
new(2f, Direction.Radians(MathF.PI/2, elevation+1)),
new(3f, Direction.Radians(MathF.PI, elevation+2)),
new(4f, Direction.Radians(3*MathF.PI/2, elevation+3))
};
Spherical avg = Spherical.Average(dirs);
#if UNITY_5_3_OR_NEWER
UnityEngine.Vector3 r = UnityEngine.Vector3.zero;
#else
Vector3Float r = Vector3Float.zero;
#endif
foreach (Spherical dir in dirs) {
r += dir.ToVector3();
}
r = r / 4;
Spherical avg2 = Spherical.FromVector3(r);
Assert.AreEqual(avg, avg2);
}
}
}
//#endif

View File

@ -0,0 +1,131 @@
#if !UNITY_5_6_OR_NEWER
using NUnit.Framework;
namespace LinearAlgebra.Test {
[TestFixture]
public class SwingTwistTest {
[Test]
public void Degrees_CreatesSwingTwistWithDegreeAngles() {
SwingTwist st = SwingTwist.Degrees(45, 30, 15);
Assert.IsNotNull(st);
Assert.AreEqual(45, st.swing.horizontal.inDegrees, 0.01f);
Assert.AreEqual(30, st.swing.vertical.inDegrees, 0.01f);
Assert.AreEqual(15, st.twist.inDegrees, 0.01f);
}
[Test]
public void Radians_CreatesSwingTwistWithRadianAngles() {
float pi = (float)System.Math.PI;
SwingTwist st = SwingTwist.Radians(pi / 4, pi / 6, pi / 12);
Assert.IsNotNull(st);
Assert.AreEqual(45, st.swing.horizontal.inDegrees, 0.01f);
Assert.AreEqual(30, st.swing.vertical.inDegrees, 0.01f);
Assert.AreEqual(15, st.twist.inDegrees, 0.01f);
}
[Test]
public void Zero_CreatesZeroRotation() {
SwingTwist st = SwingTwist.zero;
Assert.AreEqual(0, st.swing.horizontal.inDegrees, 0.01f);
Assert.AreEqual(0, st.swing.vertical.inDegrees, 0.01f);
Assert.AreEqual(0, st.twist.inDegrees, 0.01f);
}
[Test]
public void QuaternionTest() {
Quaternion q;
SwingTwist s;
Quaternion r;
q = Quaternion.identity;
s = SwingTwist.FromQuaternion(q);
r = s.ToQuaternion();
Assert.AreEqual(q, r);
q = Quaternion.Euler(90, 0, 0);
s = SwingTwist.FromQuaternion(q);
Assert.AreEqual(0, s.swing.horizontal.inDegrees, 10e-2f);
Assert.AreEqual(90, s.swing.vertical.inDegrees, 10e-2f);
Assert.AreEqual(0, s.twist.inDegrees, 0.01f);
r = s.ToQuaternion();
Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f);
q = Quaternion.Euler(0, 90, 0);
s = SwingTwist.FromQuaternion(q);
Assert.AreEqual(90, s.swing.horizontal.inDegrees,10e-2f);
Assert.AreEqual(0, s.swing.vertical.inDegrees, 0.01f);
Assert.AreEqual(0, s.twist.inDegrees, 0.01f);
r = s.ToQuaternion();
Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f);
q = Quaternion.Euler(0, 0, 90);
s = SwingTwist.FromQuaternion(q);
Assert.AreEqual(0, s.swing.horizontal.inDegrees, 0.01f);
Assert.AreEqual(0, s.swing.vertical.inDegrees, 0.01f);
Assert.AreEqual(90, s.twist.inDegrees, 0.01f);
r = s.ToQuaternion();
Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f);
q = Quaternion.Euler(0, 180, 0);
s = SwingTwist.FromQuaternion(q);
Assert.AreEqual(-180, s.swing.horizontal.inDegrees, 0.01f);
Assert.AreEqual(0, s.swing.vertical.inDegrees, 0.01f);
Assert.AreEqual(0, s.twist.inDegrees, 0.01f);
r = s.ToQuaternion();
Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f);
q = Quaternion.Euler(0, 135, 0);
s = SwingTwist.FromQuaternion(q);
Assert.AreEqual(135, s.swing.horizontal.inDegrees, 0.01f);
Assert.AreEqual(0, s.swing.vertical.inDegrees, 0.01f);
Assert.AreEqual(0, s.twist.inDegrees, 0.01f);
r = s.ToQuaternion();
Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f);
q = Quaternion.Euler(60, 45, 30);
s = SwingTwist.FromQuaternion(q);
Assert.AreEqual(45, s.swing.horizontal.inDegrees, 0.01f);
Assert.AreEqual(60, s.swing.vertical.inDegrees, 0.01f);
Assert.AreEqual(30, s.twist.inDegrees, 0.01f);
// r = s.ToQuaternion();
// Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f);
// q = Quaternion.Euler(-45, -30, -15);
// s = SwingTwist.FromQuaternion(q);
// Assert.AreEqual(-30, s.swing.horizontal.inDegrees, 0.01f);
// Assert.AreEqual(-45, s.swing.vertical.inDegrees, 0.01f);
// Assert.AreEqual(-15, s.twist.inDegrees, 0.01f);
// r = s.ToQuaternion();
// Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f);
// q = Quaternion.Euler(180, 180, 180);
// s = SwingTwist.FromQuaternion(q);
// Assert.AreEqual(-180, s.swing.horizontal.inDegrees, 0.01f);
// Assert.AreEqual(-180, s.swing.vertical.inDegrees, 0.01f);
// Assert.AreEqual(-180, s.twist.inDegrees, 0.01f);
// r = s.ToQuaternion();
// Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f);
}
[Test]
public void ToAngleAxis_ConvertsToSpherical() {
SwingTwist st = SwingTwist.Degrees(45, 30, 15);
Spherical s = st.ToAngleAxis();
Assert.IsNotNull(s);
}
[Test]
public void FromAngleAxis_ConvertsFromSpherical() {
Spherical s = new(90, Direction.Degrees(45, 0));
SwingTwist st = SwingTwist.FromAngleAxis(s);
Assert.IsNotNull(st);
}
}
}
#endif

View File

@ -0,0 +1,364 @@
#if !UNITY_5_6_OR_NEWER
using NUnit.Framework;
namespace LinearAlgebra.Test {
using Vector2 = Vector2Float;
public class Vector2FloatTest {
[SetUp]
public void Setup() {
}
[Test]
public void FromPolar() {
}
[Test]
public void Equality() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
Assert.IsFalse(v1 == v2, "4 5 == 1 2");
Assert.IsTrue(v1 != v2, "4 5 != 1 2");
v2 = new(4, 5);
Assert.IsTrue(v1 == v2, "4 5 == 4 5");
Assert.IsFalse(v1 != v2, "4 5 != 4 5");
}
[Test]
public void Magnitude() {
Vector2 v = new(1, 2);
float m = 0;
m = v.magnitude;
Assert.AreEqual(m, 2.236068F, "v.magnitude 1 2");
m = Vector2.MagnitudeOf(v);
Assert.AreEqual(m, 2.236068F, "MagnitudeOf 1 2");
v = new(-1, -2);
m = v.magnitude;
Assert.AreEqual(m, 2.236068F, "v.magnitude -1 -2");
v = new(0, 0);
m = v.magnitude;
Assert.AreEqual(m, 0, "v.magnitude 0 0");
}
[Test]
public void SqrMagnitude() {
Vector2 v = new(1, 2);
float m = 0;
m = v.sqrMagnitude;
Assert.AreEqual(m, 5, "v.sqrMagnitude 1 2");
m = Vector2.SqrMagnitudeOf(v);
Assert.AreEqual(m, 5, "SqrMagnitudeOf 1 2");
v = new(-1, -2);
m = v.sqrMagnitude;
Assert.AreEqual(m, 5, "v.sqrMagnitude -1 -2");
v = new(0, 0);
m = v.sqrMagnitude;
Assert.AreEqual(m, 0, "v.sqrMagnitude 0 0");
}
[Test]
public void Distance() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
float f = 0;
f = Vector2.Distance(v1, v2);
Assert.AreEqual(f, 4.24264002f, 1.0E-05F, "Distance(4 5, 1 2)");
v2 = new(-1, -2);
f = Vector2.Distance(v1, v2);
Assert.AreEqual(f, 8.602325F, "Distance(4 5, 1 2)");
v2 = new(0, 0);
f = Vector2.Distance(v1, v2);
Assert.AreEqual(f, 6.403124F, 1.0E-05F, "Distance(4 5, 1 2)");
}
[Test]
public void Normalize() {
Vector2 v = new(0, 3);
Vector2Float r;
r = v.normalized;
Assert.AreEqual(0, r.horizontal, "normalized 0 3 H");
Assert.AreEqual(1, r.vertical, "normalized 0 3 V");
r = Vector2.Normalize(v);
Assert.AreEqual(0, r.horizontal, "Normalize 0 3 H");
Assert.AreEqual(1, r.vertical, "Normalize 0 3 V");
v = new(0, -3);
r = v.normalized;
Assert.AreEqual(0, r.horizontal, "normalized 0 -3 H");
Assert.AreEqual(-1, r.vertical, "normalized 0 -3 V");
v = new(0, 0);
r = v.normalized;
Assert.AreEqual(0, r.horizontal, "normalized 0 0 H");
Assert.AreEqual(0, r.vertical, "normalized 0 0 V");
}
[Test]
public void Negate() {
Vector2 v = new(4, 5);
Vector2 r;
r = -v;
Assert.AreEqual(-4, r.horizontal, "- 4 5 H");
Assert.AreEqual(-5, r.vertical, "- 4 5 V");
v = new(-4, -5);
r = -v;
Assert.AreEqual(4, r.horizontal, "- -4 -5 H");
Assert.AreEqual(5, r.vertical, "- -4 -5 V");
v = new(0, 0);
r = -v;
Assert.AreEqual(0, r.horizontal, "- 0 0 H");
Assert.AreEqual(0, r.vertical, "- 0 0 V");
}
[Test]
public void Subtract() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
Vector2 r = Vector2.zero;
r = v1 - v2;
Assert.IsTrue(r == new Vector2(3, 3), "4 5 - 1 2");
v2 = new(-1, -2);
r = v1 - v2;
Assert.IsTrue(r == new Vector2(5, 7), "4 5 - -1 -2");
v2 = new(4, 5);
r = v1 - v2;
Assert.IsTrue(r == new Vector2(0, 0), "4 5 - 4 5");
r = v1;
r -= v2;
Assert.AreEqual(r, new Vector2(0, 0), "4 5 - 4 5");
v2 = new(0, 0);
r = v1 - v2;
Assert.AreEqual(r, new Vector2(4, 5), "4 5 - 0 0");
r -= v2;
Assert.AreEqual(r, new Vector2(4, 5), "4 5 - 0 0");
}
[Test]
public void Addition() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
Vector2 r = Vector2.zero;
r = v1 + v2;
Assert.IsTrue(r == new Vector2(5, 7), "4 5 + 1 2");
v2 = new(-1, -2);
r = v1 + v2;
Assert.IsTrue(r == new Vector2(3, 3), "4 5 + -1 -2");
r = v1;
r += v2;
Assert.AreEqual(r, new Vector2(3, 3), "4 5 + -1 -2");
v2 = new(0, 0);
r = v1 + v2;
Assert.AreEqual(r, new Vector2(4, 5), "4 5 + 0 0");
r += v2;
Assert.AreEqual(r, new Vector2(4, 5), "4 5 + 0 0");
}
[Test]
public void Scale() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
Vector2 r;
r = Vector2.Scale(v1, v2);
Assert.AreEqual(4, r.horizontal, "Scale 4 5 , 1 2 H");
Assert.AreEqual(10, r.vertical, "Scale 4 5 , 1 2 V");
v2 = new(-1, -2);
r = Vector2.Scale(v1, v2);
Assert.AreEqual(-4, r.horizontal, "Scale 4 5 , -1 -2 H");
Assert.AreEqual(-10, r.vertical, "Scale 4 5 , -1 -2 V");
v2 = new(0, 0);
r = Vector2.Scale(v1, v2);
Assert.AreEqual(0, r.horizontal, "Scale 4 5 , 0 0 H");
Assert.AreEqual(0, r.vertical, "Scale 4 5 , 0 0 V");
}
[Test]
public void Multiply() {
Vector2 v1 = new(4, 5);
int f = 3;
Vector2 r;
r = v1 * f;
Assert.AreEqual(12, r.horizontal, "4 5 * 3 H");
Assert.AreEqual(15, r.vertical, "4 5 * 3 V");
r = f * v1;
Assert.AreEqual(12, r.horizontal, "3 * 4 5 H");
Assert.AreEqual(15, r.vertical, "3 * 4 5 V");
f = -3;
r = v1 * f;
Assert.AreEqual(-12, r.horizontal, "4 5 * -3 H");
Assert.AreEqual(-15, r.vertical, "4 5 * -3 V");
f = 0;
r = v1 * f;
Assert.AreEqual(0, r.horizontal, "4 5 * 0 H");
Assert.AreEqual(0, r.vertical, "4 5 * 0 V");
}
[Test]
public void Divide() {
Vector2 v1 = new(4, 5);
float f = 2;
Vector2 r;
r = v1 / f;
Assert.AreEqual(2, r.horizontal, "4 5 / 2 H");
Assert.AreEqual(2.5, r.vertical, "4 5 / 2 V");
f = -2;
r = v1 / f;
Assert.AreEqual(-2, r.horizontal, "4 5 / -2 H");
Assert.AreEqual(-2.5, r.vertical, "4 5 / -2 V");
f = 0;
r = v1 / f;
Assert.AreEqual(float.PositiveInfinity, r.horizontal, "4 5 / 0 H");
Assert.AreEqual(float.PositiveInfinity, r.vertical, "4 5 / 0 V");
}
[Test]
public void Dot() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
float f;
f = Vector2.Dot(v1, v2);
Assert.AreEqual(14, f, "Dot(4 5, 1 2)");
v2 = new(-1, -2);
f = Vector2.Dot(v1, v2);
Assert.AreEqual(-14, f, "Dot(4 5, -1 -2)");
v2 = new(0, 0);
f = Vector2.Dot(v1, v2);
Assert.AreEqual(0, f, "Dot(4 5, 0 0)");
}
[Test]
public void SignedAngle() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
float f;
f = Vector2.SignedAngle(v1, v2);
Assert.AreEqual(-12.094758f, f);
v2 = new(-1, -2);
f = Vector2.SignedAngle(v1, v2);
Assert.AreEqual(167.905228f, f);
v2 = new(0, 0);
f = Vector2.SignedAngle(v1, v2);
Assert.AreEqual(0, f);
v1 = new(0, 1);
v2 = new(1, 0);
f = Vector2.SignedAngle(v1, v2);
Assert.AreEqual(90, f);
v1 = new(0, 1);
v2 = new(0, -1);
f = Vector2.SignedAngle(v1, v2);
Assert.AreEqual(180, f);
}
[Test]
public void UnsignedAngle() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
float f;
f = Vector2.UnsignedAngle(v1, v2);
Assert.AreEqual(12.094758f, f);
v2 = new(-1, -2);
f = Vector2.UnsignedAngle(v1, v2);
Assert.AreEqual(167.905228f, f);
v2 = new(0, 0);
f = Vector2.UnsignedAngle(v1, v2);
Assert.AreEqual(0, f);
v1 = new(0, 1);
v2 = new(1, 0);
f = Vector2.UnsignedAngle(v1, v2);
Assert.AreEqual(90, f);
v1 = new(0, 1);
v2 = new(0, -1);
f = Vector2.UnsignedAngle(v1, v2);
Assert.AreEqual(180, f);
}
[Test]
public void Rotate() {
Vector2 v1 = new(1, 2);
Vector2 r;
r = Vector2.Rotate(v1, AngleFloat.Degrees(0));
Assert.AreEqual(0, Vector2.Distance(r, v1));
r = Vector2.Rotate(v1, AngleFloat.Degrees(180));
Assert.AreEqual(0, Vector2.Distance(r, new Vector2(-1, -2)), 1.0e-06);
r = Vector2.Rotate(v1, AngleFloat.Degrees(-90));
Assert.AreEqual(0, Vector2.Distance(r, new Vector2(2, -1)), 1.0e-06);
r = Vector2.Rotate(v1, AngleFloat.Degrees(270));
Assert.AreEqual(0, Vector2.Distance(r, new Vector2(2, -1)), 1.0e-06);
}
[Test]
public void Lerp() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
Vector2 r;
r = Vector2.Lerp(v1, v2, 0);
Assert.AreEqual(0, Vector2.Distance(r, v1), 0);
r = Vector2.Lerp(v1, v2, 1);
Assert.AreEqual(0, Vector2.Distance(r, v2), 0);
r = Vector2.Lerp(v1, v2, 0.5f);
Assert.AreEqual(0, Vector2.Distance(r, new Vector2(2.5f, 3.5f)), 0);
r = Vector2.Lerp(v1, v2, -1);
Assert.AreEqual(0, Vector2.Distance(r, new Vector2(7, 8)), 0);
r = Vector2.Lerp(v1, v2, 2);
Assert.AreEqual(0, Vector2.Distance(r, new Vector2(-2, -1)), 0);
}
}
}
#endif

View File

@ -0,0 +1,270 @@
#if !UNITY_5_6_OR_NEWER
using NUnit.Framework;
namespace LinearAlgebra.Test {
using Vector2 = Vector2Int;
public class Vector2IntTest {
[SetUp]
public void Setup() {
}
[Test]
public void FromPolar() {
}
[Test]
public void Equality() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
Assert.IsFalse(v1 == v2, "4 5 == 1 2");
Assert.IsTrue(v1 != v2, "4 5 != 1 2");
v2 = new(4, 5);
Assert.IsTrue(v1 == v2, "4 5 == 4 5");
Assert.IsFalse(v1 != v2, "4 5 != 4 5");
}
[Test]
public void Magnitude() {
Vector2 v = new(1, 2);
float m = 0;
m = v.magnitude;
Assert.AreEqual(m, 2.236068F, "v.magnitude 1 2");
m = Vector2.MagnitudeOf(v);
Assert.AreEqual(m, 2.236068F, "MagnitudeOf 1 2");
v = new(-1, -2);
m = v.magnitude;
Assert.AreEqual(m, 2.236068F, "v.magnitude -1 -2");
v = new(0, 0);
m = v.magnitude;
Assert.AreEqual(m, 0, "v.magnitude 0 0");
}
[Test]
public void SqrMagnitude() {
Vector2 v = new(1, 2);
float m = 0;
m = v.sqrMagnitude;
Assert.AreEqual(m, 5, "v.sqrMagnitude 1 2");
m = Vector2.SqrMagnitudeOf(v);
Assert.AreEqual(m, 5, "SqrMagnitudeOf 1 2");
v = new(-1, -2);
m = v.sqrMagnitude;
Assert.AreEqual(m, 5, "v.sqrMagnitude -1 -2");
v = new(0, 0);
m = v.sqrMagnitude;
Assert.AreEqual(m, 0, "v.sqrMagnitude 0 0");
}
[Test]
public void Distance() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
float f = 0;
f = Vector2.Distance(v1, v2);
Assert.AreEqual(f, 4.24264002f, 1.0E-05F, "Distance(4 5, 1 2)");
v2 = new(-1, -2);
f = Vector2.Distance(v1, v2);
Assert.AreEqual(f, 8.602325F, "Distance(4 5, 1 2)");
v2 = new(0, 0);
f = Vector2.Distance(v1, v2);
Assert.AreEqual(f, 6.403124F, 1.0E-05F, "Distance(4 5, 1 2)");
}
[Test]
public void Normalize() {
Vector2 v = new(0, 3);
Vector2Float r;
r = v.normalized;
Assert.AreEqual(0, r.horizontal, "normalized 0 3 H");
Assert.AreEqual(1, r.vertical, "normalized 0 3 V");
r = Vector2.Normalize(v);
Assert.AreEqual(0, r.horizontal, "Normalize 0 3 H");
Assert.AreEqual(1, r.vertical, "Normalize 0 3 V");
v = new(0, -3);
r = v.normalized;
Assert.AreEqual(0, r.horizontal, "normalized 0 -3 H");
Assert.AreEqual(-1, r.vertical, "normalized 0 -3 V");
v = new(0, 0);
r = v.normalized;
Assert.AreEqual(0, r.horizontal, "normalized 0 0 H");
Assert.AreEqual(0, r.vertical, "normalized 0 0 V");
}
[Test]
public void Negate() {
Vector2 v = new(4, 5);
Vector2 r;
r = -v;
Assert.AreEqual(-4, r.horizontal, "- 4 5 H");
Assert.AreEqual(-5, r.vertical, "- 4 5 V");
v = new(-4, -5);
r = -v;
Assert.AreEqual(4, r.horizontal, "- -4 -5 H");
Assert.AreEqual(5, r.vertical, "- -4 -5 V");
v = new(0, 0);
r = -v;
Assert.AreEqual(0, r.horizontal, "- 0 0 H");
Assert.AreEqual(0, r.vertical, "- 0 0 V");
}
[Test]
public void Subtract() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
Vector2 r = Vector2.zero;
r = v1 - v2;
Assert.IsTrue(r == new Vector2(3, 3), "4 5 - 1 2");
v2 = new(-1, -2);
r = v1 - v2;
Assert.IsTrue(r == new Vector2(5, 7), "4 5 - -1 -2");
v2 = new(4, 5);
r = v1 - v2;
Assert.IsTrue(r == new Vector2(0, 0), "4 5 - 4 5");
r = v1;
r -= v2;
Assert.AreEqual(r, new Vector2(0, 0), "4 5 - 4 5");
v2 = new(0, 0);
r = v1 - v2;
Assert.AreEqual(r, new Vector2(4, 5), "4 5 - 0 0");
r -= v2;
Assert.AreEqual(r, new Vector2(4, 5), "4 5 - 0 0");
}
[Test]
public void Addition() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
Vector2 r = Vector2.zero;
r = v1 + v2;
Assert.IsTrue(r == new Vector2(5, 7), "4 5 + 1 2");
v2 = new(-1, -2);
r = v1 + v2;
Assert.IsTrue(r == new Vector2(3, 3), "4 5 + -1 -2");
r = v1;
r += v2;
Assert.AreEqual(r, new Vector2(3, 3), "4 5 + -1 -2");
v2 = new(0, 0);
r = v1 + v2;
Assert.AreEqual(r, new Vector2(4, 5), "4 5 + 0 0");
r += v2;
Assert.AreEqual(r, new Vector2(4, 5), "4 5 + 0 0");
}
[Test]
public void Scale() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
Vector2 r;
r = Vector2.Scale(v1, v2);
Assert.AreEqual(4, r.horizontal, "Scale 4 5 , 1 2 H");
Assert.AreEqual(10, r.vertical, "Scale 4 5 , 1 2 V");
v2 = new(-1, -2);
r = Vector2.Scale(v1, v2);
Assert.AreEqual(-4, r.horizontal, "Scale 4 5 , -1 -2 H");
Assert.AreEqual(-10, r.vertical, "Scale 4 5 , -1 -2 V");
v2 = new(0, 0);
r = Vector2.Scale(v1, v2);
Assert.AreEqual(0, r.horizontal, "Scale 4 5 , 0 0 H");
Assert.AreEqual(0, r.vertical, "Scale 4 5 , 0 0 V");
}
[Test]
public void Multiply() {
Vector2 v1 = new(4, 5);
int f = 3;
Vector2 r;
r = v1 * f;
Assert.AreEqual(12, r.horizontal, "4 5 * 3 H");
Assert.AreEqual(15, r.vertical, "4 5 * 3 V");
r = f * v1;
Assert.AreEqual(12, r.horizontal, "3 * 4 5 H");
Assert.AreEqual(15, r.vertical, "3 * 4 5 V");
f = -3;
r = v1 * f;
Assert.AreEqual(-12, r.horizontal, "4 5 * -3 H");
Assert.AreEqual(-15, r.vertical, "4 5 * -3 V");
f = 0;
r = v1 * f;
Assert.AreEqual(0, r.horizontal, "4 5 * 0 H");
Assert.AreEqual(0, r.vertical, "4 5 * 0 V");
}
[Test]
public void Divide() {
Vector2 v1 = new(4, 5);
int f = 2;
Vector2 r;
r = v1 / f;
Assert.AreEqual(2, r.horizontal, "4 5 / 2 H");
Assert.AreEqual(2, r.vertical, "4 5 / 2 V");
f = -2;
r = v1 / f;
Assert.AreEqual(-2, r.horizontal, "4 5 / -2 H");
Assert.AreEqual(-2, r.vertical, "4 5 / -2 V");
Assert.Throws<System.DivideByZeroException>(() => {
f = 0;
r = v1 / f;
Assert.AreEqual(float.PositiveInfinity, r.horizontal, "4 5 / 0 H");
Assert.AreEqual(float.PositiveInfinity, r.vertical, "4 5 / 0 V");
});
}
[Test]
public void Dot() {
Vector2 v1 = new(4, 5);
Vector2 v2 = new(1, 2);
int f;
f = Vector2.Dot(v1, v2);
Assert.AreEqual(14, f, "Dot(4 5, 1 2)");
v2 = new(-1, -2);
f = Vector2.Dot(v1, v2);
Assert.AreEqual(-14, f, "Dot(4 5, -1 -2)");
v2 = new(0, 0);
f = Vector2.Dot(v1, v2);
Assert.AreEqual(0, f, "Dot(4 5, 0 0)");
}
}
}
#endif

View File

@ -0,0 +1,581 @@
#if !UNITY_5_6_OR_NEWER
using NUnit.Framework;
namespace LinearAlgebra.Test {
using Vector3 = Vector3Float;
public class Vector3FloatTest {
[Test]
public void FromSpherical() {
Vector3 v = new(0, 0, 1);
Spherical s = Spherical.FromVector3(v);
Vector3 r = Vector3.FromSpherical(s);
Assert.AreEqual(0, r.horizontal, "0 0 1");
Assert.AreEqual(0, r.vertical, 1.0e-06, "0 0 1");
Assert.AreEqual(1, r.depth, "0 0 1");
v = new(0, 1, 0);
s = Spherical.FromVector3(v);
r = Vector3.FromSpherical(s);
Assert.AreEqual(0, r.horizontal, "0 0 1");
Assert.AreEqual(1, r.vertical, "0 0 1");
Assert.AreEqual(0, r.depth, 1.0e-06, "0 0 1");
v = new(1, 0, 0);
s = Spherical.FromVector3(v);
r = Vector3.FromSpherical(s);
Assert.AreEqual(1, r.horizontal, "0 0 1");
Assert.AreEqual(0, r.vertical, 1.0e-06, "0 0 1");
Assert.AreEqual(0, r.depth, 1.0e-06, "0 0 1");
}
[Test]
public void Magnitude() {
Vector3 v = new(1, 2, 3);
float m = 0;
m = v.magnitude;
Assert.AreEqual(3.7416575f, m, "magnitude 1 2 3");
m = Vector3.MagnitudeOf(v);
Assert.AreEqual(3.7416575f, m, "MagnitudeOf 1 2 3");
v = new(-1, -2, -3);
m = v.magnitude;
Assert.AreEqual(3.7416575f, m, "magnitude -1 -2 -3");
v = new(0, 0, 0);
m = v.magnitude;
Assert.AreEqual(0, m, "magnitude 0 0 0");
// Infinity tests are still missing
}
[Test]
public void SqrMagnitude() {
Vector3 v = new(1, 2, 3);
float m = 0;
m = v.sqrMagnitude;
Assert.AreEqual(14, m, "sqrMagnitude 1 2 3");
m = Vector3.SqrMagnitudeOf(v);
Assert.AreEqual(14, m, "SqrMagnitudeOf 1 2 3");
v = new(-1, -2, -3);
m = v.sqrMagnitude;
Assert.AreEqual(14, m, "sqrMagnitude -1 -2 -3");
v = new(0, 0, 0);
m = v.sqrMagnitude;
Assert.AreEqual(0, m, "sqrMagnitude 0 0 0");
// Infinity tests are still missing
}
[Test]
public void Normalize() {
Vector3 v = new(0, 2, 0);
Vector3 r;
r = v.normalized;
Assert.AreEqual(new Vector3(0, 1, 0), r, "normalized 0 2 0");
r = Vector3.Normalize(v);
Assert.AreEqual(new Vector3(0, 1, 0), r, "Normalize 0 2 0");
v = new(0, -2, 0);
r = v.normalized;
Assert.AreEqual(new Vector3(0, -1, 0), r, "normalized 0 -2 0");
v = new(0, 0, 0);
r = v.normalized;
Assert.AreEqual(new Vector3(0, 0, 0), r, "normalized 0 0 0");
v = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
r = v.normalized;
Assert.IsTrue(float.IsNaN(r.horizontal), "normalized infinity infinity infinity");
Assert.IsTrue(float.IsNaN(r.vertical), "normalized infinity infinity infinity");
Assert.IsTrue(float.IsNaN(r.depth), "normalized infinity infinity infinity");
v = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
r = v.normalized;
Assert.IsTrue(float.IsNaN(r.horizontal), "normalized -infinity -infinity -infinity");
Assert.IsTrue(float.IsNaN(r.vertical), "normalized -infinity -infinity -infinity");
Assert.IsTrue(float.IsNaN(r.depth), "normalized -infinity -infinity -infinity");
}
[Test]
public void Negate() {
Vector3 v = new(4, 5, 6);
Vector3 r;
r = -v;
Assert.AreEqual(-4, r.horizontal, "- 4 5 6 H");
Assert.AreEqual(-5, r.vertical, "- 4 5 6 V");
Assert.AreEqual(-6, r.depth, "- 4 5 6 D");
v = new(-4, -5, -6);
r = -v;
Assert.AreEqual(4, r.horizontal, "- -4 -5 -6 H");
Assert.AreEqual(5, r.vertical, "- -4 -5 -6 V");
Assert.AreEqual(6, r.depth, "- -4 -5 -6 D");
v = new(0, 0, 0);
r = -v;
Assert.AreEqual(new Vector3(0, 0, 0), r, "- 0 0 0");
Assert.AreEqual(0, r.horizontal, "- 0 0 0 H");
Assert.AreEqual(0, r.vertical, "- 0 0 0 V");
Assert.AreEqual(0, r.depth, "- 0 0 0 D");
v = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
r = -v;
Assert.AreEqual(new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity), r, "- inifinty infinity infinity");
v = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
r = -v;
Assert.AreEqual(new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity), r, "- -inifinty -infinity -infinity");
}
[Test]
public void Subtract() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r = Vector3.zero;
r = v1 - v2;
Assert.IsTrue(r == new Vector3(3, 3, 3), "4 5 6 - 1 2 3");
v2 = new(-1, -2, -3);
r = v1 - v2;
Assert.IsTrue(r == new Vector3(5, 7, 9), "4 5 6 - -1 -2 -3");
v2 = new(4, 5, 6);
r = v1 - v2;
Assert.IsTrue(r == new Vector3(0, 0, 0), "4 5 6 - 4 5 6");
r = v1;
r -= v2;
Assert.AreEqual(r, new Vector3(0, 0, 0), "4 5 6 - 4 5 6");
v2 = new(0, 0, 0);
r = v1 - v2;
Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 - 0 0 0");
r -= v2;
Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 - 0 0 0");
// Infinity tests are still missing
}
[Test]
public void Addition() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r = Vector3.zero;
r = v1 + v2;
Assert.IsTrue(r == new Vector3(5, 7, 9), "4 5 6 + 1 2 3");
v2 = new(-1, -2, -3);
r = v1 + v2;
Assert.IsTrue(r == new Vector3(3, 3, 3), "4 5 6 + -1 -2 -3");
r = v1;
r += v2;
Assert.AreEqual(r, new Vector3(3, 3, 3), "4 5 6 + -1 -2 -3");
v2 = new(0, 0, 0);
r = v1 + v2;
Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 + 0 0 0");
r += v2;
Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 + 0 0 0");
// Infinity tests are still missing
}
[Test]
public void Scale() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r;
r = Vector3.Scale(v1, v2);
Assert.AreEqual(4, r.horizontal, "Scale 4 5 6 , 1 2 3 H");
Assert.AreEqual(10, r.vertical, "Scale 4 5 6 , 1 2 3 V");
Assert.AreEqual(18, r.depth, "Scale 4 5 6 , 1 2 3 D");
v2 = new(-1, -2, -3);
r = Vector3.Scale(v1, v2);
Assert.AreEqual(-4, r.horizontal, "Scale 4 5 6 , -1 -2 -3 H");
Assert.AreEqual(-10, r.vertical, "Scale 4 5 6 , -1 -2 -3 V");
Assert.AreEqual(-18, r.depth, "Scale 4 5 6 , -1 -2 -3 D");
v2 = new(0, 0, 0);
r = Vector3.Scale(v1, v2);
Assert.AreEqual(0, r.horizontal, "Scale 4 5 6 , 0 0 0 H");
Assert.AreEqual(0, r.vertical, "Scale 4 5 6 , 0 0 0 V");
Assert.AreEqual(0, r.depth, "Scale 4 5 6 , 0 0 0 D");
v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
r = Vector3.Scale(v1, v2);
Assert.AreEqual(float.PositiveInfinity, r.horizontal, "Scale 4 5 6 , inf inf inf H");
Assert.AreEqual(float.PositiveInfinity, r.vertical, "Scale 4 5 6 , inf inf inf V");
Assert.AreEqual(float.PositiveInfinity, r.depth, "Scale 4 5 6 , inf inf inf D");
v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
r = Vector3.Scale(v1, v2);
Assert.AreEqual(float.NegativeInfinity, r.horizontal, "Scale 4 5 6 , -inf -inf -inf H");
Assert.AreEqual(float.NegativeInfinity, r.vertical, "Scale 4 5 6 , -inf -inf -inf V");
Assert.AreEqual(float.NegativeInfinity, r.depth, "Scale 4 5 6 , -inf -inf -inf D");
}
[Test]
public void Multiply() {
Vector3 v1 = new(4, 5, 6);
float f = 3;
Vector3 r;
r = v1 * f;
Assert.AreEqual(12, r.horizontal, "4 5 6 * 3 H");
Assert.AreEqual(15, r.vertical, "4 5 6 * 3 V");
Assert.AreEqual(18, r.depth, "4 5 6 * 3 D");
f = -3;
r = v1 * f;
Assert.AreEqual(-12, r.horizontal, "4 5 6 * -3 H");
Assert.AreEqual(-15, r.vertical, "4 5 6 * -3 V");
Assert.AreEqual(-18, r.depth, "4 5 6 * -3 D");
f = 0;
r = v1 * f;
Assert.AreEqual(0, r.horizontal, "4 5 6 * 0 H");
Assert.AreEqual(0, r.vertical, "4 5 6 * 0 V");
Assert.AreEqual(0, r.depth, "4 5 6 * 0 D");
f = float.PositiveInfinity;
r = v1 * f;
Assert.AreEqual(float.PositiveInfinity, r.horizontal, "4 5 6 * inf H");
Assert.AreEqual(float.PositiveInfinity, r.vertical, "4 5 6 * inf V");
Assert.AreEqual(float.PositiveInfinity, r.depth, "4 5 6 * inf D");
f = float.NegativeInfinity;
r = v1 * f;
Assert.AreEqual(float.NegativeInfinity, r.horizontal, "4 5 6 * -inf H");
Assert.AreEqual(float.NegativeInfinity, r.vertical, "4 5 6 * -inf V");
Assert.AreEqual(float.NegativeInfinity, r.depth, "4 5 6 * -inf D");
}
[Test]
public void Divide() {
Vector3 v1 = new(4, 5, 6);
float f = 2;
Vector3 r;
r = v1 / f;
Assert.AreEqual(2, r.horizontal, "4 5 6 / 2 H");
Assert.AreEqual(2.5, r.vertical, "4 5 6 / 2 V");
Assert.AreEqual(3, r.depth, "4 5 6 / 2 D");
f = -2;
r = v1 / f;
Assert.AreEqual(-2, r.horizontal, "4 5 6 / -2 H");
Assert.AreEqual(-2.5, r.vertical, "4 5 6 / -2 V");
Assert.AreEqual(-3, r.depth, "4 5 6 / -2 D");
f = 0;
r = v1 / f;
Assert.AreEqual(float.PositiveInfinity, r.horizontal, "4 5 6 / 0 H");
Assert.AreEqual(float.PositiveInfinity, r.vertical, "4 5 6 / 0 V");
Assert.AreEqual(float.PositiveInfinity, r.depth, "4 5 6 / 0 D");
f = float.PositiveInfinity;
r = v1 / f;
Assert.AreEqual(0, r.horizontal, "4 5 6 / inf H");
Assert.AreEqual(0, r.vertical, "4 5 6 / inf V");
Assert.AreEqual(0, r.depth, "4 5 6 / inf D");
f = float.NegativeInfinity;
r = v1 / f;
Assert.AreEqual(0, r.horizontal, "4 5 6 / -inf H");
Assert.AreEqual(0, r.vertical, "4 5 6 / -inf V");
Assert.AreEqual(0, r.depth, "4 5 6 / -inf D");
}
[Test]
public void Dot() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
float f;
f = Vector3.Dot(v1, v2);
Assert.AreEqual(32, f, "Dot(4 5 6, 1 2 3)");
v2 = new(-1, -2, -3);
f = Vector3.Dot(v1, v2);
Assert.AreEqual(-32, f, "Dot(4 5 6, -1 -2 -3)");
v2 = new(0, 0, 0);
f = Vector3.Dot(v1, v2);
Assert.AreEqual(0, f, "Dot(4 5 6, 0 0 0)");
v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
f = Vector3.Dot(v1, v2);
Assert.AreEqual(float.PositiveInfinity, f, "Dot(4 5 6, inf inf inf)");
v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
f = Vector3.Dot(v1, v2);
Assert.AreEqual(float.NegativeInfinity, f, "Dot(4 5 6, -inf -inf -inf)");
}
[Test]
public void Equality() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
bool r;
r = v1 == v2;
Assert.IsFalse(r, "4 5 6 == 1 2 3");
v2 = new(4, 5, 6);
r = v1 == v2;
Assert.IsTrue(r, "4 5 6 == 4 5 6");
v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
r = v1 == v2;
Assert.IsFalse(r, "4 5 6 == inf inf inf");
v1 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
r = v1 == v2;
Assert.IsFalse(r, "-inf -inf -inf == inf inf inf");
}
[Test]
public void Distance() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
float f;
f = Vector3.Distance(v1, v2);
Assert.AreEqual(5.19615221F, f, "Distance(4 5 6, 1 2 3)");
v2 = new(-1, -2, -3);
f = Vector3.Distance(v1, v2);
Assert.AreEqual(12.4498997F, f, "Distance(4 5 6, -1 -2 -3)");
v2 = new(0, 0, 0);
f = Vector3.Distance(v1, v2);
Assert.AreEqual(v1.magnitude, f, "Distance(4 5 6, 0 0 0)");
v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
f = Vector3.Distance(v1, v2);
Assert.AreEqual(float.PositiveInfinity, f, "Distance(4 5 6, inf inf inf)");
v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
f = Vector3.Distance(v1, v2);
Assert.AreEqual(float.PositiveInfinity, f, "Distance(4 5 6, -inf -inf -inf)");
}
[Test]
public void Cross() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r;
r = Vector3.Cross(v1, v2);
Assert.AreEqual(3, r.horizontal, "Cross(4 5 6, 1 2 3) H");
Assert.AreEqual(-6, r.vertical, "Cross(4 5 6, 1 2 3) V");
Assert.AreEqual(3, r.depth, "Cross(4 5 6, 1 2 3) D");
v2 = new(-1, -2, -3);
r = Vector3.Cross(v1, v2);
Assert.AreEqual(-3, r.horizontal, "Cross(4 5 6, -1 -2 -3) H");
Assert.AreEqual(6, r.vertical, "Cross(4 5 6, -1 -2 -3) V");
Assert.AreEqual(-3, r.depth, "Cross(4 5 6, -1 -2 -3) D");
v2 = new(0, 0, 0);
r = Vector3.Cross(v1, v2);
Assert.AreEqual(0, r.horizontal, "Cross(4 5 6, 0 0 0) H");
Assert.AreEqual(0, r.vertical, "Cross(4 5 6, 0 0 0) V");
Assert.AreEqual(0, r.depth, "Cross(4 5 6, 0 0 0) D");
v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
r = Vector3.Cross(v1, v2);
Assert.IsTrue(float.IsNaN(r.horizontal), "Cross(4 5 6, inf inf inf) H");
Assert.IsTrue(float.IsNaN(r.vertical), "Cross(4 5 6, inf inf inf) V");
Assert.IsTrue(float.IsNaN(r.depth), "Cross(4 5 6, inf inf inf) D");
v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
r = Vector3.Cross(v1, v2);
Assert.IsTrue(float.IsNaN(r.horizontal), "Cross(4 5 6, -inf -inf -inf) H");
Assert.IsTrue(float.IsNaN(r.vertical), "Cross(4 5 6, -inf -inf -inf) V");
Assert.IsTrue(float.IsNaN(r.depth), "Cross(4 5 6, -inf -inf -inf) D");
}
[Test]
public void Project() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r;
r = Vector3.Project(v1, v2);
Assert.AreEqual(2.28571439F, r.horizontal, "Project(4 5 6, 1 2 3) H");
Assert.AreEqual(4.57142878F, r.vertical, "Project(4 5 6, 1 2 3) V");
Assert.AreEqual(6.85714293F, r.depth, "Project(4 5 6, 1 2 3) D");
v2 = new(-1, -2, -3);
r = Vector3.Project(v1, v2);
Assert.AreEqual(2.28571439F, r.horizontal, "Project(4 5 6, -1 -2 -3) H");
Assert.AreEqual(4.57142878F, r.vertical, "Project(4 5 6, -1 -2 -3) V");
Assert.AreEqual(6.85714293F, r.depth, "Project(4 5 6, -1 -2 -3) D");
v2 = new(0, 0, 0);
r = Vector3.Project(v1, v2);
Assert.AreEqual(0, r.horizontal, "Project(4 5 6, 0 0 0) H");
Assert.AreEqual(0, r.vertical, "Project(4 5 6, 0 0 0) V");
Assert.AreEqual(0, r.depth, "Project(4 5 6, 0 0 0) D");
r = Vector3.Project(v2, v1);
Assert.AreEqual(0, r.horizontal, "Project(0 0 0, 4 5 6) H");
Assert.AreEqual(0, r.vertical, "Project(0 0 0, 4 5 6) V");
Assert.AreEqual(0, r.depth, "Project(0 0 0, 4 5 6) D");
v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
r = Vector3.Project(v1, v2);
Assert.IsTrue(float.IsNaN(r.horizontal), "Project(4 5 6, inf inf inf) H");
Assert.IsTrue(float.IsNaN(r.vertical), "Project(4 5 6, inf inf inf) V");
Assert.IsTrue(float.IsNaN(r.depth), "Project(4 5 6, inf inf inf) D");
v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
r = Vector3.Project(v1, v2);
Assert.IsTrue(float.IsNaN(r.horizontal), "Project(4 5 6, -inf -inf -inf) H");
Assert.IsTrue(float.IsNaN(r.vertical), "Project(4 5 6, -inf -inf -inf) V");
Assert.IsTrue(float.IsNaN(r.depth), "Project(4 5 6, -inf -inf -inf) D");
}
[Test]
public void ProjectOnPlane() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r;
r = Vector3.ProjectOnPlane(v1, v2);
Assert.AreEqual(1.71428561F, r.horizontal, "ProjectOnPlane(4 5 6, 1 2 3) H");
Assert.AreEqual(0.428571224F, r.vertical, "ProjectOnPlane(4 5 6, 1 2 3) V");
Assert.AreEqual(-0.857142925F, r.depth, "ProjectOnPlane(4 5 6, 1 2 3) D");
v2 = new(-1, -2, -3);
r = Vector3.ProjectOnPlane(v1, v2);
Assert.AreEqual(1.71428561F, r.horizontal, "ProjectOnPlane(4 5 6, -1 -2 -3) H");
Assert.AreEqual(0.428571224F, r.vertical, "ProjectOnPlane(4 5 6, -1 -2 -3) V");
Assert.AreEqual(-0.857142925F, r.depth, "ProjectOnPlane(4 5 6, -1 -2 -3) D");
v2 = new(0, 0, 0);
r = Vector3.ProjectOnPlane(v1, v2);
Assert.AreEqual(4, r.horizontal, "ProjectOnPlane(4 5 6, 0 0 0) H");
Assert.AreEqual(5, r.vertical, "ProjectOnPlane(4 5 6, 0 0 0) V");
Assert.AreEqual(6, r.depth, "ProjectOnPlane(4 5 6, 0 0 0) D");
r = Vector3.ProjectOnPlane(v2, v1);
Assert.AreEqual(0, r.horizontal, "ProjectOnPlane(0 0 0, 4 5 6) H");
Assert.AreEqual(0, r.vertical, "ProjectOnPlane(0 0 0, 4 5 6) V");
Assert.AreEqual(0, r.depth, "ProjectOnPlane(0 0 0, 4 5 6) D");
v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
r = Vector3.ProjectOnPlane(v1, v2);
Assert.IsTrue(float.IsNaN(r.horizontal), "ProjectOnPlane(4 5 6, inf inf inf) H");
Assert.IsTrue(float.IsNaN(r.vertical), "ProjectOnPlane(4 5 6, inf inf inf) V");
Assert.IsTrue(float.IsNaN(r.depth), "ProjectOnPlane(4 5 6, inf inf inf) D");
v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
r = Vector3.ProjectOnPlane(v1, v2);
Assert.IsTrue(float.IsNaN(r.horizontal), "ProjectOnPlane(4 5 6, -inf -inf -inf) H");
Assert.IsTrue(float.IsNaN(r.vertical), "ProjectOnPlane(4 5 6, -inf -inf -inf) V");
Assert.IsTrue(float.IsNaN(r.depth), "ProjectOnPlane(4 5 6, -inf -inf -inf) D");
}
[Test]
public void UnsignedAngle() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
AngleFloat a;
a = Vector3.UnsignedAngle(v1, v2);
Assert.AreEqual(12.9331379F, a.inDegrees, "Angle(4 5 6, 1 2 3)");
v2 = new(-1, -2, -3);
a = Vector3.UnsignedAngle(v1, v2);
Assert.AreEqual(167.066849F, a.inDegrees, "Angle(4 5 6, -1 -2 -3)");
v2 = new(0, 0, 0);
a = Vector3.UnsignedAngle(v1, v2);
Assert.AreEqual(0, a.inDegrees, "Angle(4 5 6, 0 0 0)");
v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
a = Vector3.UnsignedAngle(v1, v2);
Assert.IsTrue(float.IsNaN(a.inDegrees), "Angle(4 5 6, inf inf inf)");
v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
a = Vector3.UnsignedAngle(v1, v2);
Assert.IsTrue(float.IsNaN(a.inDegrees), "Angle(4 5 6, inf inf inf)");
}
[Test]
public void SignedAngle() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 v3 = new(7, 8, -9);
AngleFloat a;
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(-12.9331379F, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, 7 8 -9)");
v2 = new(-1, -2, -3);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(167.066849F, a.inDegrees, "SignedAngle(4 5 6, -1 -2 -3, 7 8 -9)");
v2 = new(0, 0, 0);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(0, a.inDegrees, "SignedAngle(4 5 6, 0 0 0, 7 8 -9)");
v2 = new(1, 2, 3);
v3 = new(-7, -8, 9);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(12.9331379F, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, -7 -8 9)");
v3 = new(0, 0, 0);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(0, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, 0 0 0)");
v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.IsTrue(float.IsNaN(a.inDegrees), "SignedAngle(4 5 6, inf inf inf, 0 0 0)");
v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.IsTrue(float.IsNaN(a.inDegrees), "SignedAngle(4 5 6, -inf -inf -inf, 0 0 0)");
}
[Test]
public void Lerp() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r;
r = Vector3.Lerp(v1, v2, 0);
Assert.AreEqual(0, Vector3.Distance(r, v1), 0);
r = Vector3.Lerp(v1, v2, 1);
Assert.AreEqual(0, Vector3.Distance(r, v2), 0);
r = Vector3.Lerp(v1, v2, 0.5f);
Assert.AreEqual(0, Vector3.Distance(r, new Vector3(2.5f, 3.5f, 4.5f)), 0);
r = Vector3.Lerp(v1, v2, -1);
Assert.AreEqual(0, Vector3.Distance(r, new Vector3(7, 8, 9)), 0);
r = Vector3.Lerp(v1, v2, 2);
Assert.AreEqual(0, Vector3.Distance(r, new Vector3(-2, -1, 0)), 0);
}
}
}
#endif

View File

@ -0,0 +1,349 @@
#if !UNITY_5_6_OR_NEWER
using NUnit.Framework;
namespace LinearAlgebra.Test {
using Vector3 = Vector3Int;
public class Vector3IntTest {
[Test]
public void Equality() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Assert.IsFalse(v1 == v2, "4 5 6 == 1 2 3");
Assert.IsTrue(v1 != v2, "4 5 6 != 1 2 3");
v2 = new(4, 5, 6);
Assert.IsTrue(v1 == v2, "4 5 6 == 4 5 6");
Assert.IsFalse(v1 != v2, "4 5 6 != 4 5 6");
}
[Test]
public void Magnitude() {
Vector3 v = new(1, 2, 3);
float m = 0;
m = v.magnitude;
Assert.AreEqual(3.7416575f, m, "magnitude 1 2 3");
m = Vector3.MagnitudeOf(v);
Assert.AreEqual(3.7416575f, m, "MagnitudeOf 1 2 3");
v = new(-1, -2, -3);
m = v.magnitude;
Assert.AreEqual(3.7416575f, m, "magnitude -1 -2 -3");
v = new(0, 0, 0);
m = v.magnitude;
Assert.AreEqual(0, m, "magnitude 0 0 0");
// Infinity tests are still missing
}
[Test]
public void SqrMagnitude() {
Vector3 v = new(1, 2, 3);
float m = 0;
m = v.sqrMagnitude;
Assert.AreEqual(14, m, "sqrMagnitude 1 2 3");
m = Vector3.SqrMagnitudeOf(v);
Assert.AreEqual(14, m, "SqrMagnitudeOf 1 2 3");
v = new(-1, -2, -3);
m = v.sqrMagnitude;
Assert.AreEqual(14, m, "sqrMagnitude -1 -2 -3");
v = new(0, 0, 0);
m = v.sqrMagnitude;
Assert.AreEqual(0, m, "sqrMagnitude 0 0 0");
// Infinity tests are still missing
}
[Test]
public void Distance() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
float f;
f = Vector3.Distance(v1, v2);
Assert.AreEqual(5.19615221F, f, "Distance(4 5 6, 1 2 3)");
v2 = new(-1, -2, -3);
f = Vector3.Distance(v1, v2);
Assert.AreEqual(12.4498997F, f, "Distance(4 5 6, -1 -2 -3)");
v2 = new(0, 0, 0);
f = Vector3.Distance(v1, v2);
Assert.AreEqual(v1.magnitude, f, "Distance(4 5 6, 0 0 0)");
}
[Test]
public void Normalize() {
Vector3 v = new(0, 2, 0);
Vector3Float r;
r = v.normalized;
//Assert.AreEqual(new Vector3(0, 1, 0), r, "normalized 0 2 0");
Assert.AreEqual(0, r.horizontal, "normalized 0 2 0");
Assert.AreEqual(1, r.vertical, "normalized 0 2 0");
Assert.AreEqual(0, r.depth, "normalized 0 2 0");
r = Vector3.Normalize(v);
Assert.AreEqual(new Vector3Float(0, 1, 0), r, "Normalize 0 2 0");
v = new(0, -2, 0);
r = v.normalized;
Assert.AreEqual(new Vector3Float(0, -1, 0), r, "normalized 0 -2 0");
v = new(0, 0, 0);
r = v.normalized;
Assert.AreEqual(new Vector3Float(0, 0, 0), r, "normalized 0 0 0");
}
[Test]
public void Negate() {
Vector3 v = new(4, 5, 6);
Vector3 r;
r = -v;
Assert.AreEqual(-4, r.horizontal, "- 4 5 6 H");
Assert.AreEqual(-5, r.vertical, "- 4 5 6 V");
Assert.AreEqual(-6, r.depth, "- 4 5 6 D");
v = new(-4, -5, -6);
r = -v;
Assert.AreEqual(4, r.horizontal, "- -4 -5 -6 H");
Assert.AreEqual(5, r.vertical, "- -4 -5 -6 V");
Assert.AreEqual(6, r.depth, "- -4 -5 -6 D");
v = new(0, 0, 0);
r = -v;
Assert.AreEqual(new Vector3(0, 0, 0), r, "- 0 0 0");
Assert.AreEqual(0, r.horizontal, "- 0 0 0 H");
Assert.AreEqual(0, r.vertical, "- 0 0 0 V");
Assert.AreEqual(0, r.depth, "- 0 0 0 D");
}
[Test]
public void Subtract() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r = Vector3.zero;
r = v1 - v2;
Assert.IsTrue(r == new Vector3(3, 3, 3), "4 5 6 - 1 2 3");
v2 = new(-1, -2, -3);
r = v1 - v2;
Assert.IsTrue(r == new Vector3(5, 7, 9), "4 5 6 - -1 -2 -3");
v2 = new(4, 5, 6);
r = v1 - v2;
Assert.IsTrue(r == new Vector3(0, 0, 0), "4 5 6 - 4 5 6");
r = v1;
r -= v2;
Assert.AreEqual(r, new Vector3(0, 0, 0), "4 5 6 - 4 5 6");
v2 = new(0, 0, 0);
r = v1 - v2;
Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 - 0 0 0");
r -= v2;
Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 - 0 0 0");
// Infinity tests are still missing
}
[Test]
public void Addition() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r = Vector3.zero;
r = v1 + v2;
Assert.IsTrue(r == new Vector3(5, 7, 9), "4 5 6 + 1 2 3");
v2 = new(-1, -2, -3);
r = v1 + v2;
Assert.IsTrue(r == new Vector3(3, 3, 3), "4 5 6 + -1 -2 -3");
r = v1;
r += v2;
Assert.AreEqual(r, new Vector3(3, 3, 3), "4 5 6 + -1 -2 -3");
v2 = new(0, 0, 0);
r = v1 + v2;
Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 + 0 0 0");
r += v2;
Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 + 0 0 0");
// Infinity tests are still missing
}
[Test]
public void Scale() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r;
r = Vector3.Scale(v1, v2);
Assert.AreEqual(4, r.horizontal, "Scale 4 5 6 , 1 2 3 H");
Assert.AreEqual(10, r.vertical, "Scale 4 5 6 , 1 2 3 V");
Assert.AreEqual(18, r.depth, "Scale 4 5 6 , 1 2 3 D");
v2 = new(-1, -2, -3);
r = Vector3.Scale(v1, v2);
Assert.AreEqual(-4, r.horizontal, "Scale 4 5 6 , -1 -2 -3 H");
Assert.AreEqual(-10, r.vertical, "Scale 4 5 6 , -1 -2 -3 V");
Assert.AreEqual(-18, r.depth, "Scale 4 5 6 , -1 -2 -3 D");
v2 = new(0, 0, 0);
r = Vector3.Scale(v1, v2);
Assert.AreEqual(0, r.horizontal, "Scale 4 5 6 , 0 0 0 H");
Assert.AreEqual(0, r.vertical, "Scale 4 5 6 , 0 0 0 V");
Assert.AreEqual(0, r.depth, "Scale 4 5 6 , 0 0 0 D");
}
[Test]
public void Multiply() {
Vector3 v1 = new(4, 5, 6);
int f = 3;
Vector3 r;
r = v1 * f;
Assert.AreEqual(12, r.horizontal, "4 5 6 * 3 H");
Assert.AreEqual(15, r.vertical, "4 5 6 * 3 V");
Assert.AreEqual(18, r.depth, "4 5 6 * 3 D");
r = f * v1;
Assert.AreEqual(12, r.horizontal, "3 * 4 5 6 H");
Assert.AreEqual(15, r.vertical, "3 * 4 5 6 V");
Assert.AreEqual(18, r.depth, "3 * 4 5 6 D");
f = -3;
r = v1 * f;
Assert.AreEqual(-12, r.horizontal, "4 5 6 * -3 H");
Assert.AreEqual(-15, r.vertical, "4 5 6 * -3 V");
Assert.AreEqual(-18, r.depth, "4 5 6 * -3 D");
f = 0;
r = v1 * f;
Assert.AreEqual(0, r.horizontal, "4 5 6 * 0 H");
Assert.AreEqual(0, r.vertical, "4 5 6 * 0 V");
Assert.AreEqual(0, r.depth, "4 5 6 * 0 D");
}
[Test]
public void Divide() {
Vector3 v1 = new(4, 5, 6);
int f = 2;
Vector3 r;
r = v1 / f;
Assert.AreEqual(2, r.horizontal, "4 5 6 / 2 H");
Assert.AreEqual(2, r.vertical, "4 5 6 / 2 V");
Assert.AreEqual(3, r.depth, "4 5 6 / 2 D");
f = -2;
r = v1 / f;
Assert.AreEqual(-2, r.horizontal, "4 5 6 / -2 H");
Assert.AreEqual(-2, r.vertical, "4 5 6 / -2 V");
Assert.AreEqual(-3, r.depth, "4 5 6 / -2 D");
Assert.Throws<System.DivideByZeroException>(() => {
f = 0;
r = v1 / f;
});
}
[Test]
public void Dot() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
float f;
f = Vector3.Dot(v1, v2);
Assert.AreEqual(32, f, "Dot(4 5 6, 1 2 3)");
v2 = new(-1, -2, -3);
f = Vector3.Dot(v1, v2);
Assert.AreEqual(-32, f, "Dot(4 5 6, -1 -2 -3)");
v2 = new(0, 0, 0);
f = Vector3.Dot(v1, v2);
Assert.AreEqual(0, f, "Dot(4 5 6, 0 0 0)");
}
[Test]
public void Cross() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 r;
r = Vector3.Cross(v1, v2);
Assert.AreEqual(3, r.horizontal, "Cross(4 5 6, 1 2 3) H");
Assert.AreEqual(-6, r.vertical, "Cross(4 5 6, 1 2 3) V");
Assert.AreEqual(3, r.depth, "Cross(4 5 6, 1 2 3) D");
v2 = new(-1, -2, -3);
r = Vector3.Cross(v1, v2);
Assert.AreEqual(-3, r.horizontal, "Cross(4 5 6, -1 -2 -3) H");
Assert.AreEqual(6, r.vertical, "Cross(4 5 6, -1 -2 -3) V");
Assert.AreEqual(-3, r.depth, "Cross(4 5 6, -1 -2 -3) D");
v2 = new(0, 0, 0);
r = Vector3.Cross(v1, v2);
Assert.AreEqual(0, r.horizontal, "Cross(4 5 6, 0 0 0) H");
Assert.AreEqual(0, r.vertical, "Cross(4 5 6, 0 0 0) V");
Assert.AreEqual(0, r.depth, "Cross(4 5 6, 0 0 0) D");
}
[Test]
public void UnsignedAngle() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
AngleFloat a;
a = Vector3.UnsignedAngle(v1, v2);
Assert.AreEqual(12.9331379F, a.inDegrees, "Angle(4 5 6, 1 2 3)");
v2 = new(-1, -2, -3);
a = Vector3.UnsignedAngle(v1, v2);
Assert.AreEqual(167.066849F, a.inDegrees, "Angle(4 5 6, -1 -2 -3)");
v2 = new(0, 0, 0);
a = Vector3.UnsignedAngle(v1, v2);
Assert.AreEqual(0, a.inDegrees, "Angle(4 5 6, 0 0 0)");
}
[Test]
public void SignedAngle() {
Vector3 v1 = new(4, 5, 6);
Vector3 v2 = new(1, 2, 3);
Vector3 v3 = new(7, 8, -9);
AngleFloat a;
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(-12.9331379F, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, 7 8 -9)");
v2 = new(-1, -2, -3);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(167.066849F, a.inDegrees, "SignedAngle(4 5 6, -1 -2 -3, 7 8 -9)");
v2 = new(0, 0, 0);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(0, a.inDegrees, "SignedAngle(4 5 6, 0 0 0, 7 8 -9)");
v2 = new(1, 2, 3);
v3 = new(-7, -8, 9);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(12.9331379F, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, -7 -8 9)");
v3 = new(0, 0, 0);
a = Vector3.SignedAngle(v1, v2, v3);
Assert.AreEqual(0, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, 0 0 0)");
}
}
}
#endif

59
MemoryCell.cs Normal file
View File

@ -0,0 +1,59 @@
using System;
using Unity.Mathematics;
[Serializable]
public class MemoryCell : Neuron {
public MemoryCell(ClusterPrefab cluster, string name) : base(cluster, name) { }
public MemoryCell(Cluster parent, string name) : base(parent, name) { }
public bool staticMemory = false;
public override bool isSleeping {
get {
if (staticMemory)
return false;
return base.isSleeping;
}
}
public override Nucleus ShallowCloneTo(Cluster newParent) {
MemoryCell clone = new(newParent, this.name);
CloneFields(clone);
clone.staticMemory = this.staticMemory;
return clone;
}
#region State
private bool initialized = false;
private float3 _memorizedValue;
public override void UpdateStateIsolated() {
// A memorycell does not have an activation function
float3 result = Combinator();
if (initialized)
// Output the previous, memorized value
this.outputValue = this._memorizedValue;
else {
// The first time, the result is directly set in output
this.outputValue = result;
this.initialized = true;
}
// Store the result for the next time
this._memorizedValue = result;
}
public override void UpdateNuclei() {
if (staticMemory)
// Static memory does not get stale or go to sleep
return;
base.UpdateNuclei();
}
#endregion State
}

2
MemoryCell.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 29633aa3fe5cd9dcc8d886051f45d4d8

View File

@ -0,0 +1,12 @@
{
"folders": [
{
"path": "../.."
},
{
"name": "LinearAlgebra-csharp",
"path": "LinearAlgebra-csharp"
}
],
"settings": {}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 5099850b415a5749b9205723281be597
guid: cfec45da5945b94d684a763d86b0dcf8
DefaultImporter:
externalObjects: {}
userData:

31
NanoBrain.cs Normal file
View File

@ -0,0 +1,31 @@
using System;
using UnityEngine;
public class NanoBrain : MonoBehaviour {
public ClusterPrefab defaultBrain;
[NonSerialized]
private Cluster brainInstance;
public Cluster brain {
get {
if (brainInstance == null && defaultBrain != null) {
brainInstance = new Cluster(defaultBrain) {
name = defaultBrain.name + " (Instance)"
};
}
return brainInstance;
}
}
public static void UpdateWeight(Cluster brain, string name, float weight) {
Nucleus root = brain.defaultOutput;
foreach (Synapse synapse in root.synapses) {
if (synapse.neuron.name == name) {
if (synapse.weight != weight) {
synapse.weight = weight;
// Debug.Log($"Updated weight for {name}");
}
}
}
}
}

2
NanoBrain.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 92f34a5e4027a1dc39efd8ce63cf6aba

334
Neuron.cs Normal file
View File

@ -0,0 +1,334 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[Serializable]
public class Neuron : Nucleus {
public Neuron(Cluster parent, string name) {
this.parent = parent;
this.name = name;
this.parent?.clusterNuclei.Add(this);
}
public Neuron(ClusterPrefab prefab, string name) {
this.clusterPrefab = prefab;
this.name = name;
if (this.clusterPrefab != null)
this.clusterPrefab.nuclei.Add(this);
else
Debug.LogError("No prefab when adding neuron to prefab");
}
#region Serialization
public enum CombinatorType {
Sum,
Product,
Max
}
public CombinatorType combinator = CombinatorType.Sum;
public enum CurvePresets {
Linear,
Power,
Sqrt,
Reciprocal,
Custom
}
[SerializeField]
public CurvePresets _curvePreset;
public CurvePresets curvePreset {
get { return _curvePreset; }
set {
_curvePreset = value;
this.curve = GenerateCurve();
}
}
public AnimationCurve curve;
public float curveMax = 1.0f;
public AnimationCurve GenerateCurve() {
switch (this.curvePreset) {
case CurvePresets.Linear:
this.curveMax = 1;
return Presets.Linear(1);
case CurvePresets.Power:
this.curveMax = 1;
return Presets.Power(2.0f, 1);
case CurvePresets.Sqrt:
this.curveMax = 1;
return Presets.Power(0.5f, 1);
case CurvePresets.Reciprocal:
this.curveMax = 1 / 0.01f * 1;
return Presets.Reciprocal(1);
default:
this.curveMax = 1;
return this.curve;
}
}
public static class Presets {
private const int samples = 32;
public static AnimationCurve Linear(float weight) {
return AnimationCurve.Linear(0f, 0f, 1000f, weight * 1000);
}
public static AnimationCurve Power(float exponent, float weight) {
// build keyframes
Keyframe[] keys = new Keyframe[samples];
for (int i = 0; i < samples; i++) {
float t = i / (float)(samples - 1);
float v = Mathf.Pow(t, exponent) * weight;
keys[i] = new Keyframe(t, v);
}
AnimationCurve curve = new(keys);
// set tangent modes for each key to Auto (smooth). Use Linear if you prefer straight segments.
for (int i = 0; i < curve.length; i++) {
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Auto);
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Auto);
}
return curve;
}
public static AnimationCurve Reciprocal(float weight) {
int samples = 128;
float xMin = 0.001f;
float xMax = 1;
var keys = new Keyframe[samples];
for (int i = 0; i < samples; i++) {
float t = i / (float)(samples - 1);
float x = Mathf.Lerp(xMin, xMax, t);
float y = 1f / x * weight;
keys[i] = new Keyframe(x, y);
}
var curve = new AnimationCurve(keys);
for (int i = 0; i < curve.length; i++) {
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Linear);
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Linear);
}
return curve;
}
}
#endregion Serialization
protected float3 _outputValue;
public virtual float3 outputValue {
get { return _outputValue; }
set {
_outputValue = value;
if (this.isFiring)
WhenFiring?.Invoke();
}
}
public bool isFiring => length(_outputValue) > 0.5f;
public Action WhenFiring;
public virtual bool isSleeping => lengthsq(this.outputValue) == 0;
[NonSerialized]
public int stale = 1000;
public readonly int staleValueForSleep = 20;
// this clone the nucleus without the synapses and receivers
public override Nucleus ShallowCloneTo(Cluster newParent) {
Neuron clone = new(newParent, this.name);
CloneFields(clone);
return clone;
}
public override Nucleus Clone(ClusterPrefab prefab) {
Neuron clone = new(prefab, this.name);
CloneFields(clone);
foreach (Synapse synapse in this.synapses) {
Synapse clonedSynapse = clone.AddSynapse(synapse.neuron);
clonedSynapse.weight = synapse.weight;
}
foreach (Nucleus receiver in this.receivers) {
clone.AddReceiver(receiver);
}
return clone;
}
protected virtual void CloneFields(Neuron clone) {
clone.clusterPrefab = this.clusterPrefab;
clone.bias = this.bias;
clone.combinator = this.combinator;
clone.curve = this.curve;
clone.curvePreset = this.curvePreset;
clone.curveMax = this.curveMax;
}
public static void Delete(Nucleus nucleus) {
foreach (Synapse synapse in nucleus.synapses) {
if (synapse.neuron is Neuron synapse_nucleus) {
if (synapse_nucleus.receivers.Count > 1) {
// there is another nucleus feeding into this input nucleus
synapse_nucleus.receivers.RemoveAll(r => r == nucleus);
}
else {
// No other links, delete it.
Neuron.Delete(synapse_nucleus);
}
}
}
if (nucleus is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers) {
if (receiver != null && receiver.synapses != null)
receiver.synapses.RemoveAll(s => s.neuron == nucleus);
}
}
else if (nucleus is Cluster cluster) {
// remove all receivers for this cluster
foreach (Neuron output in cluster.outputs) {
foreach (Nucleus receiver in output.receivers) {
receiver.synapses.RemoveAll(s => s.neuron == output);
}
}
}
if (nucleus.clusterPrefab != null) {
nucleus.clusterPrefab.nuclei.RemoveAll(n => n == nucleus);
nucleus.clusterPrefab.RefreshOutputs();
nucleus.clusterPrefab.GarbageCollection();
}
}
public override void UpdateStateIsolated() {
float3 result = Combinator();
this.outputValue = Activator(result);
}
#region Combinator
protected Func<float3> Combinator => combinator switch {
CombinatorType.Sum => CombinatorSum,
CombinatorType.Product => CombinatorProduct,
CombinatorType.Max => CombinatorMax,
_ => CombinatorSum
};
public float3 CombinatorSum() {
float3 sum = this.bias;
foreach (Synapse synapse in this.synapses)
sum += synapse.weight * synapse.neuron.outputValue;
return sum;
}
public float3 CombinatorProduct() {
float3 product = this.bias;
foreach (Synapse synapse in this.synapses) {
product *= synapse.weight * synapse.neuron.outputValue;
}
return product;
}
public float3 CombinatorMax() {
float3 max = this.bias;
float maxLength = length(max);
//Applying the weight factors
foreach (Synapse synapse in this.synapses) {
float3 input = synapse.weight * synapse.neuron.outputValue;
float inputLength = length(input);
if (inputLength > maxLength) {
max = input;
maxLength = inputLength;
}
}
return max;
}
#endregion Combinator
#region Activator
public Func<float3, float3> Activator => this.curvePreset switch {
CurvePresets.Linear => ActivatorLinear,
CurvePresets.Sqrt => ActivatorSqrt,
CurvePresets.Power => ActivatorPower,
CurvePresets.Reciprocal => ActivatorReciprocal,
_ => ActivatorCustom
};
protected float3 ActivatorLinear(float3 input) {
return input;
}
protected float3 ActivatorSqrt(float3 input) {
float3 result = normalize(input) * System.MathF.Sqrt(length(input));
return result;
}
protected float3 ActivatorPower(float3 input) {
float3 result = normalize(input) * System.MathF.Pow(length(input), 2);
return result;
}
protected float3 ActivatorReciprocal(float3 input) {
float magnitude = length(input);
if (magnitude == 0)
return new float3(0, 0, 0);
float3 result = normalize(input) * (1 / magnitude);
return result;
}
protected float3 ActivatorCustom(float3 input) {
float activatedValue = this.curve.Evaluate(length(input));
float3 result = normalize(input) * activatedValue;
return result;
}
#endregion Activator
#region Receivers
[SerializeReference]
private List<Nucleus> _receivers = new();
public virtual List<Nucleus> receivers {
get { return _receivers; }
set { _receivers = value; }
}
public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) {
this._receivers.Add(receiverToAdd);
receiverToAdd.AddSynapse(this, weight);
}
public virtual void RemoveReceiver(Nucleus receiverToRemove) {
if (this is IReceptor receptor) {
foreach (Nucleus element in receptor.nucleiArray) {
if (element is Neuron neuron) {
neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove);
receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == neuron);
}
}
}
else {
this._receivers.RemoveAll(receiver => receiver == receiverToRemove);
receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this);
}
}
#endregion Receivers
public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
if (this.parent is ClusterReceptor clusterReceptor)
clusterReceptor.ProcessStimulus(this, inputValue, thingId, thingName);
else
ProcessStimulusDirect(inputValue, thingId, thingName);
}
public void ProcessStimulusDirect(Vector3 inputValue, int thingId = 0, string thingName = null) {
this.stale = 0;
this.bias = inputValue;
this.parent.UpdateFromNucleus(this);
}
}

2
Neuron.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 750748f3f0e7d472fbf88ab02987074c

1305
NewVelocity.asset Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
fileFormatVersion: 2
guid: 138594cdb62397137913b39c26d3de5a
guid: 61eea9f818639ec20b7a7bf4e86fff66
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

72
Nucleus.cs Normal file
View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public abstract class Nucleus {
public string name;
[SerializeReference]
public ClusterPrefab clusterPrefab;
[SerializeReference]
public Cluster parent;
public bool trace = false;
public abstract Nucleus ShallowCloneTo(Cluster parent);
public abstract Nucleus Clone(ClusterPrefab prefab);
public enum Type {
None,
Neuron,
MemoryCell,
Cluster,
Receptor,
ClusterReceptor,
}
#region Synapses
public Vector3 bias = Vector3.zero;
[SerializeField]
private List<Synapse> _synapses = new();
public List<Synapse> synapses => _synapses;
public Synapse AddSynapse(Neuron sendingNucleus, float weight = 1.0f) {
Synapse synapse = new(sendingNucleus, weight);
this.synapses.Add(synapse);
return synapse;
}
public Synapse GetSynapse(Nucleus sender) {
foreach (Synapse synapse in this.synapses)
if (synapse.neuron == sender)
return synapse;
return null;
}
public void RemoveSynapse(Nucleus sendingNucleus) {
this.synapses.RemoveAll(synapse => synapse.neuron == sendingNucleus);
}
#endregion Synapses
#region Update
public abstract void UpdateStateIsolated();
public virtual void UpdateNuclei() {
}
public virtual void SetBias(Vector3 inputValue) {
this.bias = inputValue;
this.parent.UpdateFromNucleus(this);
}
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = "") {
}
#endregion Update
}

2
Nucleus.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4310eea6ab77628b085387a226c1c386

Some files were not shown because too many files have changed in this diff Show More