commit 8e87e4ea77308b51c3691bdad96e7f9707952821 Author: Pascal Serrarens Date: Tue Apr 7 09:12:29 2026 +0200 Squashed 'NanoBrain/' content from commit b3423b9 git-subtree-dir: NanoBrain git-subtree-split: b3423b99a752cdabbc4e7c51565fb54425481feb diff --git a/Cluster.cs b/Cluster.cs new file mode 100644 index 0000000..996fb2c --- /dev/null +++ b/Cluster.cs @@ -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 TopologicalSort(List nodes) { + Dictionary 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 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 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 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 clusterNuclei = new(); + // the nuclei sorted using topological sorting + // to ensure that the cluster is computer in the right order + public List sortedNuclei; + //public Dictionary nucleiDict = new(); + + public List _inputs = null; + public virtual List 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> computeOrders = new(); + private void ComputeOrders() { + foreach (Nucleus input in this._inputs) + computeOrders[input] = TopologicalSort2(input); + } + + private List TopologicalSort2(Nucleus startNode) { + Dictionary inDegree = new(); + HashSet 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 queue = new Queue(); + queue.Enqueue(startNode); + visited.Add(startNode); + + while (queue.Count > 0) { + Nucleus current = queue.Dequeue(); + List 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 sortedOrder = new List(); + while (queue.Count > 0) { + Nucleus current = queue.Dequeue(); + sortedOrder.Add(current); // Process the node + + List 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 _outputs = null; + public List 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 CollectReceivers() { + List 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 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 + +} diff --git a/Cluster.cs.meta b/Cluster.cs.meta new file mode 100644 index 0000000..a10caff --- /dev/null +++ b/Cluster.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f13cdc4a175a9f379a00317ae68d8bea \ No newline at end of file diff --git a/ClusterPrefab.cs b/ClusterPrefab.cs new file mode 100644 index 0000000..760e8bb --- /dev/null +++ b/ClusterPrefab.cs @@ -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 nuclei = new(); + + + public virtual Nucleus output => this.nuclei[0] as Nucleus; + + public List _inputs = null; + public virtual List 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 _outputs = null; + public List 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(); + if (nuclei.Count == 0) + new Neuron(this, "Output"); // Every cluster should have at least 1 neuron + } + + public void GarbageCollection() { + HashSet 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 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 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 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; + } +} diff --git a/ClusterPrefab.cs.meta b/ClusterPrefab.cs.meta new file mode 100644 index 0000000..ee35e0b --- /dev/null +++ b/ClusterPrefab.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60a957541c24c57e78018c202ebb1d9b \ No newline at end of file diff --git a/ClusterReceptor.cs b/ClusterReceptor.cs new file mode 100644 index 0000000..ac65e7a --- /dev/null +++ b/ClusterReceptor.cs @@ -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 CollectReceivers() { + List 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 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()) { + 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 receiversToRemove = new(); + foreach (KeyValuePair 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]; + + } + } +} \ No newline at end of file diff --git a/ClusterReceptor.cs.meta b/ClusterReceptor.cs.meta new file mode 100644 index 0000000..027f164 --- /dev/null +++ b/ClusterReceptor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4f64f5d72a422a7c8bb9ace598432aad \ No newline at end of file diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..090b3ac --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3aedf57a50b6dfa46a59457c87b8ef9d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/BrainEditorWindow.cs b/Editor/BrainEditorWindow.cs new file mode 100644 index 0000000..11bba19 --- /dev/null +++ b/Editor/BrainEditorWindow.cs @@ -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 nodes = new(); + readonly List 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("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 GetIncomingEdges(DagNode node) { + List incoming = new(); + foreach (DagEdge e in edges) { + if (e.toId == node.id) + incoming.Add(e); + } + return incoming; + } + List GetOutgoingEdges(DagNode node) { + List 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()); + 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 layer = new(); + Queue 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; + } + +} diff --git a/Editor/BrainEditorWindow.cs.meta b/Editor/BrainEditorWindow.cs.meta new file mode 100644 index 0000000..5d8b61f --- /dev/null +++ b/Editor/BrainEditorWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f041740900808273ab006e7d276a78e9 diff --git a/Editor/BrainPickerWindow.cs b/Editor/BrainPickerWindow.cs new file mode 100644 index 0000000..503bd10 --- /dev/null +++ b/Editor/BrainPickerWindow.cs @@ -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 onPicked; + private string search = ""; + + public static void ShowPicker(Action onPicked, string title = "Select Cluster") { + var w = CreateInstance(); + 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(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(); + } +} diff --git a/Editor/BrainPickerWindow.cs.meta b/Editor/BrainPickerWindow.cs.meta new file mode 100644 index 0000000..b2de114 --- /dev/null +++ b/Editor/BrainPickerWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9197e2d322d23b5798ab4aef729815b0 \ No newline at end of file diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs new file mode 100644 index 0000000..5aeb9c9 --- /dev/null +++ b/Editor/ClusterInspector.cs @@ -0,0 +1,1100 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; + +using UnityEngine; +using UnityEngine.UIElements; +using Unity.Mathematics; +using static Unity.Mathematics.math; + +[CustomEditor(typeof(ClusterPrefab))] +public class ClusterInspector : Editor { + protected static VisualElement mainContainer; + protected static VisualElement inspectorContainer; + + protected bool breakOnWake = false; + + #region Start + + public override VisualElement CreateInspectorGUI() { + ClusterPrefab prefab = target as ClusterPrefab; + + if (prefab != null) + prefab.EnsureInitialization(); + + serializedObject.Update(); + + VisualElement root = new(); + CreateInspector(root, prefab, prefab.output, null); + + serializedObject.ApplyModifiedProperties(); + return root; + } + + public static GraphView CreateInspector(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + root.style.paddingLeft = 0; + root.style.paddingRight = 0; + root.style.paddingTop = 0; + root.style.paddingBottom = 0; + + root.styleSheets.Add(Resources.Load("GraphStyles")); + + // does the main container have added value? + // is just is like the root + mainContainer = new() { + style = { + height = 450, + flexDirection = FlexDirection.Row + } + }; + GraphView graph = new(cluster); + graph.style.flexGrow = 1; + + inspectorContainer = new VisualElement { + name = "inspector", + style = { + width = 300, + flexGrow = 0 + } + }; + + mainContainer.Add(graph); + mainContainer.Add(inspectorContainer); + root.Add(mainContainer); + + graph.SetGraph(gameObject, output, inspectorContainer); + + return graph; + } + + + public class GraphView : VisualElement { + readonly ClusterPrefab prefab; + SerializedObject serializedBrain; + Nucleus currentNucleus; + GameObject gameObject; + private List layers = new(); + private readonly Dictionary neuroidPositions = new(); + private bool expandArray = false; + + ClusterPrefab prefabAsset; + readonly PopupField outputsField; + + public GraphView(ClusterPrefab prefab) { + this.prefab = prefab; + + name = "content"; + style.flexGrow = 1; + + IMGUIContainer graphContainer = new(OnIMGUI); + graphContainer.style.position = Position.Absolute; + graphContainer.style.left = 0; graphContainer.style.top = 0; + graphContainer.style.right = 0; graphContainer.style.bottom = 0; + graphContainer.pickingMode = PickingMode.Position; + graphContainer.focusable = true; + Add(graphContainer); + + VisualElement outputContainer = new() { + style = { + flexDirection = FlexDirection.Row, + alignItems = Align.Center, + } + }; + + List names = this.prefab.outputs.Select(output => output.name).ToList(); + if (names.Count > 0 && names.First() != null) { + outputsField = new(names, names.First()) { + style = { flexGrow = 1 } + }; + outputsField.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); + outputContainer.Add(outputsField); + } + + Button addButton = new(() => OnAddClusterOutput()) { + text = "Add" + }; + outputContainer.Add(addButton); + + Add(outputContainer); + + // Subscribe when added to panel (editor UI ready) + RegisterCallback(evt => Subscribe()); + RegisterCallback(evt => Unsubscribe()); + } + + void OnOutputChanged(string outputName) { + if (this.currentNucleus.parent != null) + // Get nucleus in the parent instance + this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); + else + // Get nucleus in the prefab + this.currentNucleus = this.prefab.GetNucleus(outputName); + } + + void OnAddClusterOutput() { + Nucleus newOutput = new Neuron(this.prefab, "New Output"); + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsField.value = newOutput.name; + + this.currentNucleus = newOutput; + } + + bool subscribed = false; + void Subscribe() { + if (subscribed) return; + SceneView.duringSceneGui += OnSceneGUI; + subscribed = true; + SceneView.RepaintAll(); + } + + void Unsubscribe() { + if (!subscribed) return; + SceneView.duringSceneGui -= OnSceneGUI; + subscribed = false; + } + + public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer) { + this.gameObject = gameObject; + //this.cluster = brain; + if (Application.isPlaying == false) + this.serializedBrain = new SerializedObject(this.prefab); + this.currentNucleus = nucleus; + Rebuild(inspectorContainer); + } + + void Rebuild(VisualElement inspectorContainer) { + BuildLayers(); + + if (this.currentNucleus == null) { + inspectorContainer.Clear(); + return; + } + + string path = AssetDatabase.GetAssetPath(this.prefab); // or known path + this.prefabAsset = AssetDatabase.LoadAssetAtPath(path); + if (this.prefabAsset == null) { + // create in memory save if it doesn't exist + this.prefabAsset = CreateInstance(); + //Debug.LogError("Cluster Prefab is not found on disk"); + } + DrawInspector(inspectorContainer); + } + + private void BuildLayers() { + // A temporary list to track what's been added to layers + this.layers = new(); + int layerIx = 0; + + Nucleus selectedNucleus = this.currentNucleus; + if (selectedNucleus == null) + return; + NeuroidLayer currentLayer = new() { ix = layerIx }; + + if (selectedNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { + foreach (Nucleus receiver in selectedNeuron.receivers) { + Nucleus outputNeuroid = receiver; + if (outputNeuroid != null) { + AddToLayer(currentLayer, outputNeuroid); + // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); + } + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + layerIx++; + currentLayer = new() { ix = layerIx }; + } + + AddToLayer(currentLayer, selectedNucleus); + this.layers.Add(currentLayer); + // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); + + layerIx++; + currentLayer = new() { ix = layerIx }; + + if (selectedNucleus.synapses != null) { + foreach (Synapse synapse in selectedNucleus.synapses) { + Nucleus input = synapse.neuron; + AddToLayer(currentLayer, input); + // Debug.Log($"layer {layerIx} nucleus {input.name}"); + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + } + } + + private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { + if (nucleus == null) + return; + layer.neuroids.Add(nucleus); + //nucleus.layerIx = layer.ix; + // Store its position + Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); + neuroidPositions[nucleus] = neuroidPosition; + + } + + + public void OnIMGUI() { + if (currentNucleus == null) + return; + + if (Application.isPlaying == false) + serializedBrain.Update(); + + Handles.BeginGUI(); + DrawGraph(); + Handles.EndGUI(); + + } + + private void DrawGraph() { + float size = 20; + Vector3 position = new(150, 210, 0); + + DrawReceivers(this.currentNucleus, position, size); + DrawSynapses(this.currentNucleus, position, size); + + // Draw selected Nucleus + if (expandArray) { + if (this.currentNucleus is IReceptor receptor1) { + float maxValue = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + if (nucleus is Neuron neuron) { + float value = length(neuron.outputValue); + if (value > maxValue) + maxValue = value; + } + } + + float spacing = 400f / receptor1.nucleiArray.Count(); + float margin = 10 + spacing / 2; + float xMin = 150 - size; + float xMax = 150 + size; + float yMin = 10 + margin - size / 2; + float yMax = 400 - margin + size; + Vector3[] verts = new Vector3[4] { + new(xMin, yMin, 0), + new(xMax, yMin, 0), + new(xMax, yMax, 0), + new(xMin, yMax, 0) + }; + Handles.color = Color.black; + Handles.DrawAAConvexPolygon(verts); + int row = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + Vector3 pos = new(150, margin + row * spacing, 0.0f); + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); + DrawNucleus(nucleus, pos, maxValue, size); + row++; + } + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + Vector3 labelPos = new(150, yMax + size + 5, 0); + string receptorName = receptor1.GetName(); + int colonPos = receptorName.IndexOf(":"); + if (colonPos > 0) { + string baseName = receptorName[..colonPos]; + Handles.Label(labelPos, baseName, style); + } + else + Handles.Label(labelPos, receptorName, style); + } + else { + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + float maxValue = 1; + if (this.currentNucleus is Neuron neuron) + maxValue = length(neuron.outputValue); + else if (this.currentNucleus is Cluster cluster) + maxValue = length(cluster.defaultOutput.outputValue); + + DrawNucleus(this.currentNucleus, position, maxValue, 20); + + } + } + else { + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + float maxValue = 1; + if (this.currentNucleus is Neuron neuron) + maxValue = length(neuron.outputValue); + else if (this.currentNucleus is Cluster cluster) + maxValue = length(cluster.defaultOutput.outputValue); + DrawNucleus(this.currentNucleus, position, maxValue, 20); + } + } + + private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { + List receivers; + if (nucleus is Neuron neuron) + receivers = neuron.receivers; + else if (nucleus is Cluster cluster) + receivers = cluster.CollectReceivers(); + else + return; + + int nodeCount = receivers.Count(); //neuron != null ? neuron.receivers.Count() : 1; + + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + foreach (Nucleus receiver in receivers) { + if (receiver is Neuron neuroid) { + float value = length(neuroid.outputValue); + if (value > maxValue) + maxValue = value; + } + } + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / nodeCount; + float margin = 10 + spacing / 2; + + int row = 0; + List drawnArrays = new(); + foreach (Nucleus receiver in receivers) { + if (receiver is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + + Nucleus receiverNucleus = receiver; + if (receiverNucleus == null) + continue; + + Vector3 pos = new(50, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + + DrawNucleus(receiverNucleus, pos, maxValue, size); + row++; + } + } + + private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + int nodeCount = nucleus.synapses.Count; + + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + int neuronCount = 0; + List drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron == null) + continue; + + if (synapse.neuron is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + if (synapse.neuron is Neuron synapseNeuron) { + float value = length(synapseNeuron.outputValue) * synapse.weight; + // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); + if (value > maxValue) + maxValue = value; + } + neuronCount++; + } + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / neuronCount; + float margin = 10 + spacing / 2; + + int row = 0; + drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron is null) + continue; + + if (synapse.neuron is Receptor neuron) { + if (drawnArrays.Contains(neuron.nucleiArray)) + continue; + drawnArrays.Add(neuron.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + Vector3 pos = new(250, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + Color color = Color.black; + if (Application.isPlaying) { + if (maxValue == 0 || !float.IsFinite(maxValue)) + maxValue = 1; + float brightness = 0; + if (synapse.neuron is Neuron synapseNeuron) + brightness = length(synapseNeuron.outputValue * synapse.weight) / maxValue; + color = new Color(brightness, brightness, brightness, 1f); + } + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { + // the synapse nucleus is part of a subcluster + DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); + } + // else if (synapse.nucleus.cluster != null && synapse.nucleus.cluster != this.currentNucleus.cluster) { + // DrawNucleus(synapse.nucleus.parent, pos, maxValue, size, color); + // } + else { + DrawNucleus(synapse.neuron, pos, maxValue, size, color); + } + row++; + } + } + + private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { + Color color; + if (Application.isPlaying) { + float brightness = 0; + if (nucleus is Neuron neuron) + brightness = length(neuron.outputValue) / maxValue; + color = new Color(brightness, brightness, brightness, 1f); + } + else + color = Color.black; + DrawNucleus(nucleus, position, maxValue, size, color); + } + + private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size, Color color) { + if (nucleus is MemoryCell) { + Handles.color = Color.white; + Handles.DrawWireDisc(position + Vector3.right * 10, Vector3.forward, size); + } + + Handles.color = color; + Handles.DrawSolidDisc(position, Vector3.forward, size); + + Handles.color = Color.white; + // Position the label in front of the disc + Vector3 labelPosition = position + (Vector3.forward * 0.1f); + + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.MiddleCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + + if (nucleus is IReceptor receptor1) { + if (expandArray) { + // Put array indices above elements + style.alignment = TextAnchor.LowerCenter; + Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc + int colonPos1 = nucleus.name.IndexOf(":"); + if (colonPos1 > 0) { + string extName = nucleus.name[(colonPos1 + 2)..]; + Handles.Label(labelPos1, extName, style); + } + } + else { + // draw the array size label + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); + style.normal.textColor = Color.white; + } + } + + if (expandArray == false || nucleus is not IReceptor) { + // put name below nucleus + Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron + style.alignment = TextAnchor.UpperCenter; + + int colonPos = nucleus.name.IndexOf(":"); + if (colonPos > 0 && colonPos < nucleus.name.Length - 2) { + // if it is an array, we should not show the :0 of the first element + string baseName = nucleus.name[..colonPos]; + Handles.Label(labelPos, baseName, style); + } + else + Handles.Label(labelPos, nucleus.name, style); + + } + + // Draw Cluster ring + if (nucleus is Cluster) { + Handles.color = Color.white; + Handles.DrawWireDisc(position, Vector3.forward, size + 5); + } + + // Tooltip + Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2); + int id = GUIUtility.GetControlID(FocusType.Passive); + Event e = Event.current; + EventType et = e.GetTypeForControl(id); + if (e != null && neuronRect.Contains(e.mousePosition)) { + // Process Hover + HandleMouseHover(nucleus, neuronRect); + // Process click + if (e.type == EventType.MouseDown && e.button == 0) { + // Consume the event so the scene doesn't also handle it + e.Use(); + HandleClicked(nucleus); + } + } + } + + private void HandleMouseHover(Nucleus nucleus, Rect rect) { + GUIContent tooltip; + if (nucleus is Neuron neuron) { + tooltip = new( + $"{nucleus.name}" + + $"\nValue: {length(neuron.outputValue)}"); + } + else + tooltip = new($"{nucleus.name}"); + + Vector2 mousePosition = Event.current.mousePosition; + + // Display tooltip with some offset + Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip); + Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y); + + GUI.Box(tooltipRect, tooltip); + } + + private void HandleClicked(Nucleus nucleus) { + if (nucleus == this.currentNucleus) { + if (nucleus is Receptor || nucleus is ClusterReceptor) + expandArray = !expandArray; + else + expandArray = false; + } + // else if (nucleus is ReceptorInstance receptor) { + // this.currentNucleus = receptor.receptor; + // expandArray = false; + // BuildLayers(); + // } + else { + this.currentNucleus = nucleus; + expandArray = false; + BuildLayers(); + } + } + + private VisualElement inspectorIMGUIContainer; + private bool showSynapses = true; + private bool showActivation = true; + protected bool breakOnWake = false; + protected bool trace = false; + void DrawInspector(VisualElement inspectorContainer) { + if (inspectorContainer == null) + return; + + inspectorContainer.Clear(); + if (this.currentNucleus == null) + return; + + // create a SerializedObject wrapper so Unity inspector controls work (and Undo) + SerializedObject so = new(prefabAsset); + this.inspectorIMGUIContainer = new IMGUIContainer(() => InspectorHandler(so)); + + inspectorContainer.Add(inspectorIMGUIContainer); + } + + void InspectorHandler(SerializedObject serializedObject) { + bool anythingChanged = false; + + if (serializedObject == null || serializedObject.targetObject == null) + return; + + if (this.currentNucleus == null) + return; + + serializedObject.Update(); + + GUIStyle headerStyle = new(EditorStyles.boldLabel) { + alignment = TextAnchor.MiddleLeft, + margin = new RectOffset(10, 0, 4, 4) + }; + GUIStyle boldTextFieldStyle = new(EditorStyles.textField) { + fontStyle = FontStyle.Bold + }; + + GUILayout.Label(this.currentNucleus.GetType().ToString(), headerStyle); + string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); + if (newName != this.currentNucleus.name) { + this.currentNucleus.name = newName; + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + anythingChanged = true; + } + + if (Application.isPlaying) { + if (currentNucleus is Neuron currentNeuron1) { + GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); + EditorGUILayout.FloatField(nameLabel, length(currentNeuron1.outputValue)); + } + else + EditorGUILayout.LabelField(" "); + } + else + EditorGUILayout.LabelField(" "); + + if (this.currentNucleus is MemoryCell memory) { + memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); + } + + if (this.currentNucleus is IReceptor receptor1) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.IntField("Array size", receptor1.nucleiArray.Count()); + if (GUILayout.Button("Add")) { + Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); + receptor1.AddReceptorElement(this.prefab); + anythingChanged = true; + } + if (GUILayout.Button("Del")) { + Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); + receptor1.RemoveReceptorElement(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); + } + + // Synapses + + if (this.currentNucleus is not Receptor && this.currentNucleus is not ClusterReceptor) { + showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); + if (showSynapses) { + if (this.currentNucleus is Neuron neuron2) { + Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); + anythingChanged |= newCombinator != neuron2.combinator; + neuron2.combinator = newCombinator; + } + + EditorGUIUtility.wideMode = true; + EditorGUIUtility.labelWidth = 100; + Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); + anythingChanged |= newBias != this.currentNucleus.bias; + this.currentNucleus.bias = newBias; + + Nucleus[] array = null; + int elementIx = -1; + if (this.currentNucleus.synapses.Count > 0) { + Synapse[] synapses = this.currentNucleus.synapses.ToArray(); + foreach (Synapse synapse in synapses) { + if (synapse.neuron == null) + continue; + + if (array != null) { + if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) { + int thisElementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + if (thisElementIx == elementIx) + continue; + else + elementIx = thisElementIx; + } + // if (array.Contains(synapse.nucleus)) + // continue; + else if (array.Contains(synapse.neuron.parent)) + continue; + } + else { + if (synapse.neuron.parent is IReceptor iReceptor) { + array = iReceptor.nucleiArray; + if (iReceptor is Cluster iCluster) + elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + } + // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) + // array = receptor2.nucleiArray; + } + + EditorGUILayout.Space(); + + if (Application.isPlaying) { + if (synapse.neuron is Neuron synapseNeuron) { + Vector3 value = synapseNeuron.outputValue * synapse.weight; + GUIContent synapseValueLabel = new(synapse.neuron.name, synapseNeuron.outputValue.ToString()); + EditorGUILayout.FloatField(synapseValueLabel, length(synapseNeuron.outputValue)); + } + } + else { + EditorGUILayout.BeginHorizontal(); + + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus) { + // If it is a cluster + GUIStyle labelStyle = new(GUI.skin.label); + float labelWidth = 200; + if (synapse.neuron.clusterPrefab != null) { + labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.baseName}.")).x; + GUILayout.Label($"{synapse.neuron.parent.baseName}", GUILayout.Width(labelWidth)); + } + string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); + int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); + int newIndex = EditorGUILayout.Popup(selectedIndex, options); + if (newIndex != selectedIndex && synapse.neuron.parent.clusterNuclei[newIndex] is Neuron newNeuron) + ChangeSynapse(synapse, newNeuron); + } + else + GUILayout.Label(synapse.neuron.name); + + bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); + if (disconnecting && synapse.neuron is Neuron synapseNeuron) { + synapseNeuron.RemoveReceiver(this.currentNucleus); + this.prefab.GarbageCollection(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); + + } + + EditorGUI.indentLevel++; + float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); + if (newWeight != synapse.weight) { + if (synapse.neuron.parent is IReceptor receptor) { + Nucleus[] receptorArray = receptor.nucleiArray; + foreach (Synapse s in this.currentNucleus.synapses) { + if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) + s.weight = newWeight; + } + } + else + synapse.weight = newWeight; + anythingChanged = true; + } + EditorGUI.indentLevel--; + } + } + + EditorGUILayout.Space(); + anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus); + anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); + } + EditorGUILayout.EndFoldoutHeaderGroup(); + } + + // Activation + + if (this.currentNucleus is not Cluster) { + EditorGUILayout.Space(); + showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation"); + if (showActivation) { + if (this.currentNucleus is Neuron neuron) { + if (this.currentNucleus is not MemoryCell) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150)); + if (neuron.curveMax > 0) + EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax)); + else + EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax)); + Neuron.CurvePresets newPreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); + anythingChanged |= newPreset != neuron.curvePreset; + neuron.curvePreset = newPreset; + EditorGUILayout.EndHorizontal(); + } + if (neuron is Receptor receptor2) { + if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) + receptor2.array = new NucleusArray(neuron); + } + } + + EditorGUILayout.Space(); + } + EditorGUILayout.EndFoldoutHeaderGroup(); + } + + if (GUILayout.Button("Delete this neuron")) + DeleteNucleus(this.currentNucleus); + + if (this.currentNucleus is Cluster subCluster) { + if (GUILayout.Button("Edit Cluster")) + EditCluster(subCluster); + } + + EditorGUILayout.Space(); + breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); + if (breakOnWake && this.currentNucleus is Neuron currentNeuron) { + if (currentNeuron.isSleeping == false) + Debug.Break(); + } + trace = EditorGUILayout.Toggle("Trace", trace); + this.currentNucleus.trace = trace; + + serializedObject.ApplyModifiedProperties(); + if (anythingChanged) { + EditorUtility.SetDirty(prefabAsset); + AssetDatabase.SaveAssets(); + } + } + + void OnSceneGUI(SceneView sceneView) { + if (this.gameObject != null) { + if (this.currentNucleus is IReceptor receptor) { + foreach (Nucleus nucleus in receptor.nucleiArray) { + if (nucleus is Neuron neuron) { + Vector3 worldVector = this.gameObject.transform.TransformVector(neuron.outputValue); + Handles.color = Color.yellow; + Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); + } + } + } + else { + if (this.currentNucleus is Neuron currentNeuron) { + Vector3 worldVector = this.gameObject.transform.TransformVector(currentNeuron.outputValue); + Handles.color = Color.yellow; + Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); + } + } + } + } + + #region Synapses + + protected virtual void AddInput(Nucleus.Type selectedType, Nucleus nucleus) { + switch (selectedType) { + case Nucleus.Type.Neuron: + AddNeuronInput(nucleus); + break; + case Nucleus.Type.MemoryCell: + AddMemoryCellInput(nucleus); + break; + // case Nucleus.Type.Selector: + // AddSelectorInput(nucleus); + // break; + case Nucleus.Type.Cluster: + AddClusterInput(nucleus); + break; + // case Nucleus.Type.Pulsar: + // AddPulsarInput(nucleus); + // break; + case Nucleus.Type.Receptor: + AddReceptorInput(nucleus); + break; + // case Nucleus.Type.ReceptorArray: + // AddReceptorArrayInput(nucleus); + // break; + case Nucleus.Type.ClusterReceptor: + AddClusterReceptorInput(nucleus); + break; + default: + break; + } + } + + protected virtual void AddNeuronInput(Nucleus nucleus) { + Neuron newNeuroid = new(this.prefab, "New neuron"); + newNeuroid.AddReceiver(nucleus); + this.currentNucleus = newNeuroid; + BuildLayers(); + } + + // protected void AddSelectorInput(Nucleus nucleus) { + // Selector newSelector = new(this.prefab, "New Selector"); + // newSelector.AddReceiver(nucleus); + // this.currentNucleus = newSelector; + // BuildLayers(); + // } + + // protected void AddPulsarInput(Nucleus nucleus) { + // Pulsar newPulsar = new(this.prefab, "New Pulsar"); + // newPulsar.AddReceiver(nucleus); + // this.currentNucleus = newPulsar; + // BuildLayers(); + // } + + protected virtual void AddMemoryCellInput(Nucleus nucleus) { + MemoryCell newMemory = new(this.prefab, "New memory cell"); + newMemory.AddReceiver(nucleus); + this.currentNucleus = newMemory; + BuildLayers(); + } + + protected virtual void AddClusterInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster"); + } + private void OnClusterPicked(Nucleus nucleus, ClusterPrefab prefab) { + Cluster subclusterInstance = new(prefab, this.prefab); + subclusterInstance.defaultOutput.AddReceiver(nucleus); + } + + protected virtual void AddReceptorInput(Nucleus nucleus) { + Receptor newReceptor = new(this.prefab, "New Receptor"); + newReceptor.AddReceiver(nucleus); + this.currentNucleus = newReceptor; + BuildLayers(); + } + + protected virtual void AddClusterReceptorInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); + } + private void OnClusterReceptorPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { + ClusterReceptor clusterInstance = new(selectedPrefab, this.prefab, "New " + selectedPrefab.name); + clusterInstance.defaultOutput.AddReceiver(nucleus); + this.currentNucleus = clusterInstance; + BuildLayers(); + } + + private void EditCluster(Cluster subCluster) { + // May be used with storedPrefab... + Selection.activeObject = subCluster.prefab; + EditorGUIUtility.PingObject(subCluster.prefab); + var editor = Editor.CreateEditor(subCluster.prefab); + } + + int selectedConnectNucleus = -1; + // Connect to another nucleus in the same cluster + protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { + if (cluster == null) + return false; + + IEnumerable synapseNuclei = this.currentNucleus.synapses + .Where(synapse => synapse.neuron != null) + .Select(synapse => synapse.neuron); + + IEnumerable nuclei = cluster.nuclei + .Except(synapseNuclei); + IEnumerable nucleiNames = nuclei + .Select(n => { + int idx = n.name.IndexOf(':'); + return idx < 0 ? n.name : n.name[..idx]; + }) + .Distinct(); + + string[] names = nucleiNames.ToArray(); + EditorGUILayout.BeginHorizontal(); + selectedConnectNucleus = EditorGUILayout.Popup(selectedConnectNucleus, names); + bool connecting = GUILayout.Button("Connect", GUILayout.Width(80)); + EditorGUILayout.EndHorizontal(); + if (connecting) { + Nucleus nucleus = nuclei.ElementAt(selectedConnectNucleus); + if (nucleus is IReceptor receptor) + receptor.AddArrayReceiver(this.currentNucleus); + else if (nucleus is Neuron neuron) + neuron.AddReceiver(this.currentNucleus); + else if (nucleus is Cluster subCluster) + subCluster.defaultOutput.AddReceiver(this.currentNucleus); + + } + return connecting; + } + + protected virtual void DeleteNucleus(Nucleus nucleus) { + if (nucleus == null) + return; + + if (nucleus is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) { + if (receiver != null) { + this.currentNucleus = receiver; + break; + } + } + } + this.prefab.nuclei.Remove(nucleus); + + if (outputsField.value == nucleus.name) { + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsField.index = 0; + } + + Neuron.Delete(nucleus); + + this.currentNucleus = this.prefab.output; + BuildLayers(); + } + + Nucleus.Type selectedType = Nucleus.Type.None; + protected virtual bool AddSynapse(ClusterPrefab cluster, Nucleus nucleus) { + if (cluster == null) + return false; + + EditorGUILayout.BeginHorizontal(); + selectedType = (Nucleus.Type)EditorGUILayout.EnumPopup(selectedType); + bool connecting = GUILayout.Button("Add", GUILayout.Width(80)); + EditorGUILayout.EndHorizontal(); + + if (connecting) { + AddInput(selectedType, this.currentNucleus); + } + return connecting; + // if (selectedType == Nucleus.Type.None) + // return false; + + // AddInput(selectedType, this.currentNucleus); + // return true; + } + + protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { + Neuron synapseNeuron = synapse.neuron as Neuron; + if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) { + if (synapse.neuron.parent is ClusterReceptor receptor) { + // the new nucleus is part of a (cluster) receptor, + // so we have to change all synapses to this nucleus array elements + int oldNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, synapse.neuron); + int newNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, newNucleus); + foreach (Nucleus element in receptor.nucleiArray) { + if (element is not ClusterReceptor clusterReceptor) + continue; + // Get the same neuron as the synapse.nucleus in a different element + // of the ClusterReceptor array + Nucleus oldElementNucleus = clusterReceptor.clusterNuclei[oldNucleusIx]; + if (oldElementNucleus is not Neuron oldElementNeuron) + continue; + // Get the same neuron as newNucleus in a different element + // of the ClusterReceptor array + Nucleus newElementNucleus = clusterReceptor.clusterNuclei[newNucleusIx]; + if (newElementNucleus is not Neuron newElementNeuron) + continue; + + oldElementNeuron.RemoveReceiver(this.currentNucleus); + newElementNeuron.AddReceiver(this.currentNucleus); + // Now find the synapse which pointed to the old Neuron + // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron); + // synapseForUpdate.nucleus = newElementNeuron; + } + } + else { + // it is a neuron in a subcluster + synapseNeuron.RemoveReceiver(this.currentNucleus); + newNucleus.AddReceiver(this.currentNucleus); + } + } + else { + synapseNeuron.RemoveReceiver(this.currentNucleus); + newNucleus.AddReceiver(this.currentNucleus); + } + } + + protected virtual void DisconnectNucleus(Neuron nucleus) { + if (this.currentNucleus.clusterPrefab == null) + return; + string[] names = this.currentNucleus.synapses.Select(synapse => synapse.neuron.name).ToArray(); + int selectedIndex = -1; + selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names); + if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.clusterPrefab.nuclei.Count) { + Synapse synapse = this.currentNucleus.synapses[selectedIndex]; + Neuron synapseNeuron = synapse.neuron as Neuron; + synapseNeuron.RemoveReceiver(this.currentNucleus); + } + } + + #endregion Synapses + } + + #endregion Start + +} + +public class NeuroidLayer { + public int ix = 0; + public List neuroids = new(); +} diff --git a/Editor/ClusterInspector.cs.meta b/Editor/ClusterInspector.cs.meta new file mode 100644 index 0000000..a1a18f5 --- /dev/null +++ b/Editor/ClusterInspector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1fc1fb7db9f7ad54a87d31313e7f457d \ No newline at end of file diff --git a/Editor/DAGWindow.cs b/Editor/DAGWindow.cs new file mode 100644 index 0000000..aaf5aa3 --- /dev/null +++ b/Editor/DAGWindow.cs @@ -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 nodes = new List(); + List edges = new List(); + + 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("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 GetCircleOutlinePoints(Vector2 center, float radius, int segments) + { + var pts = new List(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 GetBezierPoints(Vector2 p0, Vector2 p1, Vector2 p2, int seg) + { + var pts = new List(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()); + 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 layer = new Dictionary(); + Queue q = new Queue(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; + } +} diff --git a/Editor/DAGWindow.cs.meta b/Editor/DAGWindow.cs.meta new file mode 100644 index 0000000..ea0ee9e --- /dev/null +++ b/Editor/DAGWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 95393aed582b8b30d965400672aec4d8 \ No newline at end of file diff --git a/Editor/NanoBrain_Editor.cs b/Editor/NanoBrain_Editor.cs new file mode 100644 index 0000000..164e1db --- /dev/null +++ b/Editor/NanoBrain_Editor.cs @@ -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; + } + +} \ No newline at end of file diff --git a/Editor/NanoBrain_Editor.cs.meta b/Editor/NanoBrain_Editor.cs.meta new file mode 100644 index 0000000..eaf830b --- /dev/null +++ b/Editor/NanoBrain_Editor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f05072314d39990639a2dbf99f322664 \ No newline at end of file diff --git a/Editor/Resources.meta b/Editor/Resources.meta new file mode 100644 index 0000000..e9c19e4 --- /dev/null +++ b/Editor/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7b61a93fc9332d2adae74fe4abe92d53 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Resources/GraphStyles.uss b/Editor/Resources/GraphStyles.uss new file mode 100644 index 0000000..79bafe8 --- /dev/null +++ b/Editor/Resources/GraphStyles.uss @@ -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; + } diff --git a/Editor/Resources/GraphStyles.uss.meta b/Editor/Resources/GraphStyles.uss.meta new file mode 100644 index 0000000..2546c45 --- /dev/null +++ b/Editor/Resources/GraphStyles.uss.meta @@ -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 diff --git a/IReceptor.cs b/IReceptor.cs new file mode 100644 index 0000000..b56a360 --- /dev/null +++ b/IReceptor.cs @@ -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); + } + + } +} \ No newline at end of file diff --git a/IReceptor.cs.meta b/IReceptor.cs.meta new file mode 100644 index 0000000..0c0ee6f --- /dev/null +++ b/IReceptor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 73f052292ad16bb53a3c07aa1694c705 \ No newline at end of file diff --git a/Icons.meta b/Icons.meta new file mode 100644 index 0000000..4b8dfb3 --- /dev/null +++ b/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 885c5a70637820322b07e023ce18fdd5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Icons/NeuraalNetwerkIcoonSchets1.png b/Icons/NeuraalNetwerkIcoonSchets1.png new file mode 100644 index 0000000..3a314ee Binary files /dev/null and b/Icons/NeuraalNetwerkIcoonSchets1.png differ diff --git a/Icons/NeuraalNetwerkIcoonSchets1.png.meta b/Icons/NeuraalNetwerkIcoonSchets1.png.meta new file mode 100644 index 0000000..1ea36b8 --- /dev/null +++ b/Icons/NeuraalNetwerkIcoonSchets1.png.meta @@ -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: diff --git a/Icons/NeuraalNetwerkIcoonSchets2.png b/Icons/NeuraalNetwerkIcoonSchets2.png new file mode 100644 index 0000000..35853d6 Binary files /dev/null and b/Icons/NeuraalNetwerkIcoonSchets2.png differ diff --git a/Icons/NeuraalNetwerkIcoonSchets2.png.meta b/Icons/NeuraalNetwerkIcoonSchets2.png.meta new file mode 100644 index 0000000..524e4c8 --- /dev/null +++ b/Icons/NeuraalNetwerkIcoonSchets2.png.meta @@ -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: diff --git a/Icons/NeuraalNetwerkIcoonSchets3.png b/Icons/NeuraalNetwerkIcoonSchets3.png new file mode 100644 index 0000000..f2faf0c Binary files /dev/null and b/Icons/NeuraalNetwerkIcoonSchets3.png differ diff --git a/Icons/NeuraalNetwerkIcoonSchets3.png.meta b/Icons/NeuraalNetwerkIcoonSchets3.png.meta new file mode 100644 index 0000000..e7b0a3c --- /dev/null +++ b/Icons/NeuraalNetwerkIcoonSchets3.png.meta @@ -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: diff --git a/Identity.asset b/Identity.asset new file mode 100644 index 0000000..2471b04 --- /dev/null +++ b/Identity.asset @@ -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: [] diff --git a/Identity.asset.meta b/Identity.asset.meta new file mode 100644 index 0000000..b2382a6 --- /dev/null +++ b/Identity.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5f4d2ea0d0115b3549f8e9aa5e669163 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/LinearAlgebra.meta b/LinearAlgebra.meta new file mode 100644 index 0000000..c54c1af --- /dev/null +++ b/LinearAlgebra.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d98555a675e8e5e879de17db950b55fe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LinearAlgebra/.editorconfig b/LinearAlgebra/.editorconfig new file mode 100644 index 0000000..1ec7f97 --- /dev/null +++ b/LinearAlgebra/.editorconfig @@ -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 \ No newline at end of file diff --git a/LinearAlgebra/.gitea/workflows/unit_tests.yaml b/LinearAlgebra/.gitea/workflows/unit_tests.yaml new file mode 100644 index 0000000..e98b26e --- /dev/null +++ b/LinearAlgebra/.gitea/workflows/unit_tests.yaml @@ -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 diff --git a/LinearAlgebra/.gitignore b/LinearAlgebra/.gitignore new file mode 100644 index 0000000..b32a30c --- /dev/null +++ b/LinearAlgebra/.gitignore @@ -0,0 +1,5 @@ +DoxyGen/DoxyWarnLogfile.txt +.vscode/settings.json +**bin +**obj +**.meta diff --git a/LinearAlgebra/LinearAlgebra-csharp.sln b/LinearAlgebra/LinearAlgebra-csharp.sln new file mode 100644 index 0000000..4b13b2b --- /dev/null +++ b/LinearAlgebra/LinearAlgebra-csharp.sln @@ -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 diff --git a/LinearAlgebra/src/Angle.cs b/LinearAlgebra/src/Angle.cs new file mode 100644 index 0000000..7d2fd6b --- /dev/null +++ b/LinearAlgebra/src/Angle.cs @@ -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); + + /// + /// Get the sign of the angle + /// + /// The angle + /// -1 when the angle is negative, 1 when it is positive and 0 in all other cases + public static int Sign(AngleFloat a) { + if (a.value < 0) + return -1; + if (a.value > 0) + return 1; + return 0; + } + + /// + /// Returns the magnitude of the angle + /// + /// The angle + /// The positive magnitude of the angle + /// Negative values are negated to get a positive result + public static AngleFloat Abs(AngleFloat a) { + if (Sign(a) < 0) + return -a; + else + return a; + } + + /// + /// Tests the equality of two angles + /// + /// + /// + /// True when the angles are equal, false otherwise + /// The equality is determine within the limits of precision of a float + public static bool operator ==(AngleFloat a1, AngleFloat a2) { + return a1.value == a2.value; + } + + /// + /// Tests the inequality of two angles + /// + /// + /// + /// True when the angles are not equal, false otherwise + /// The equality is determine within the limits of precision of a float + 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(); + } + + + /// + /// Tests if the first angle is greater than the second + /// + /// + /// + /// True when a1 is greater than a2, False otherwise + public static bool operator >(AngleFloat a1, AngleFloat a2) { + return a1.value > a2.value; + } + + /// + /// Tests if the first angle is greater than or equal to the second + /// + /// + /// + /// True when a1 is greater than or equal to a2, False otherwise + public static bool operator >=(AngleFloat a1, AngleFloat a2) { + return a1.value >= a2.value; + } + + /// + /// Tests if the first angle is less than the second + /// + /// + /// + /// True when a1 is less than a2, False otherwise + public static bool operator <(AngleFloat a1, AngleFloat a2) { + return a1.value < a2.value; + } + + /// + /// Tests if the first angle is less than or equal to the second + /// + /// + /// + /// True when a1 is less than or equal to a2, False otherwise + public static bool operator <=(AngleFloat a1, AngleFloat a2) { + return a1.value <= a2.value; + } + + /// + /// Negate the angle + /// + /// The angle + /// The negated angle + /// 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; + } + + /// + /// Subtract two angles + /// + /// Angle 1 + /// Angle 2 + /// The result of the subtraction + public static AngleFloat operator -(AngleFloat a1, AngleFloat a2) { + AngleFloat r = new(a1.value - a2.value); + return r; + } + /// + /// Add two angles + /// + /// Angle 1 + /// Angle 2 + /// The result of the addition + public static AngleFloat operator +(AngleFloat a1, AngleFloat a2) { + AngleFloat r = new(a1.value + a2.value); + return r; + } + + /// + /// Multiplies the angle + /// + /// The angle to multiply + /// The factor by which the angle is multiplied + /// The multiplied angle + 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); + } + + /// + /// Clamp the angle between the given min and max values + /// + /// The angle to clamp + /// The minimum angle + /// The maximum angle + /// The clamped angle + /// 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)); + } + + /// + /// Rotate from one angle to the other with a maximum degrees + /// + /// Starting angle + /// Target angle + /// Maximum angle to rotate + /// The resulting angle + /// 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; + } + } + + + /// + /// %Angle utilities + /// + 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; + + + /// + /// Determine the angle difference, result is a normalized angle + /// + /// First first angle + /// The second angle + /// the angle between the two angles + /// Angle values should be degrees + public static float Difference(float a, float b) { + float r = Normalize(b - a); + return r; + } + + /// + /// Normalize an angle to the range -180 < angle <= 180 + /// + /// The angle to normalize + /// The normalized angle in interval (-180..180] + /// 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; + } + + /// + /// Map interval of angles between vectors [0..Pi] to interval [0..1] + /// + /// The first vector + /// The second vector + /// The resulting factor in interval [0..1] + /// 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; + // } + + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Decomposition.cs b/LinearAlgebra/src/Decomposition.cs new file mode 100644 index 0000000..ddaf434 --- /dev/null +++ b/LinearAlgebra/src/Decomposition.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/Direction.cs b/LinearAlgebra/src/Direction.cs new file mode 100644 index 0000000..fd25b98 --- /dev/null +++ b/LinearAlgebra/src/Direction.cs @@ -0,0 +1,261 @@ +using System; +#if UNITY_5_3_OR_NEWER +using Vector3Float = UnityEngine.Vector3; +#endif + +namespace LinearAlgebra +{ + + /// + /// A direction in 3D space + /// + /// 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; + + /// + /// Create a new direction + /// + /// The horizontal angle + /// The vertical angle + /// The direction will be normalized automatically + /// to ensure the angles are within the allowed ranges + public Direction(AngleFloat horizontal, AngleFloat vertical) + { + this.horizontal = horizontal; + this.vertical = vertical; + this.Normalize(); + } + + /// + /// Create a direction using angle values in degrees + /// + /// The horizontal angle in degrees + /// The vertical angle in degrees + /// The direction + /// The direction will be normalized automatically + /// to ensure the angles are within the allowed ranges + public static Direction Degrees(float horizontal, float vertical) + { + Direction d = new() + { + horizontal = AngleFloat.Degrees(horizontal), + vertical = AngleFloat.Degrees(vertical) + }; + d.Normalize(); + return d; + } + /// + /// Create a direction using angle values in radians + /// + /// The horizontal angle in radians + /// The vertical angle in radians + /// The direction + 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})"; + } + + /// + /// A forward direction with zero for both angles + /// + public readonly static Direction forward = Degrees(0, 0); + /// + /// A backward direction with horizontal angle -180 and zero vertical + /// angle + /// + public readonly static Direction backward = Degrees(-180, 0); + /// + /// A upward direction with zero horizontal angle and vertical angle 90 + /// + public readonly static Direction up = Degrees(0, 90); + /// + /// A downward direction with zero horizontal angle and vertical angle + /// -90 + /// + public readonly static Direction down = Degrees(0, -90); + /// + /// A left-pointing direction with horizontal angle -90 and zero + /// vertical angle + /// + public readonly static Direction left = Degrees(-90, 0); + /// + /// A right-pointing direction with horizontal angle 90 and zero + /// vertical angle + /// + 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 + /// + /// Convert the direction into a carthesian vector + /// + /// The carthesian vector corresponding to this direction. + 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); + } + + /// + /// Convert a carthesian vector into a direction + /// + /// The carthesian vector + /// The direction + /// Information about the length of the carthesian vector is not + /// included in this transformation + 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 + /// + /// Convert the direction into a carthesian vector + /// + /// The carthesian vector corresponding to this direction. + 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); + } + + /// + /// Convert a carthesian vector into a direction + /// + /// The carthesian vector + /// The direction + /// Information about the length of the carthesian vector is not + /// included in this transformation + 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); + } + + /// + /// Tests the equality of two directions + /// + /// + /// + /// True when the direction angles are equal, false otherwise. + public static bool operator ==(Direction d1, Direction d2) + { + bool horizontalEq = d1.horizontal == d2.horizontal; + bool verticalEq = d1.vertical == d2.vertical; + return horizontalEq && verticalEq; + } + + /// + /// Tests the inequality of two directions + /// + /// + /// + /// True when the direction angles are not equal, false otherwise. + 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; + } + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Float.cs b/LinearAlgebra/src/Float.cs new file mode 100644 index 0000000..2217b84 --- /dev/null +++ b/LinearAlgebra/src/Float.cs @@ -0,0 +1,41 @@ +namespace LinearAlgebra { + + /// + /// Float number utilities + /// + public class Float { + /// + /// The precision of float numbers + /// + public const float epsilon = 1E-05f; + /// + /// The square of the float number precision + /// + public const float sqrEpsilon = 1e-10f; + + /// + /// Clamp the value between the given minimum and maximum values + /// + /// The value to clamp + /// The minimum value + /// The maximum value + /// The clamped value + public static float Clamp(float f, float min, float max) { + if (f < min) + return min; + if (f > max) + return max; + return f; + } + + /// + /// Clamp the value between to the interval [0..1] + /// + /// The value to clamp + /// The clamped value + public static float Clamp01(float f) { + return Clamp(f, 0, 1); + } + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/LinearAlgebra.csproj b/LinearAlgebra/src/LinearAlgebra.csproj new file mode 100644 index 0000000..d2d5a85 --- /dev/null +++ b/LinearAlgebra/src/LinearAlgebra.csproj @@ -0,0 +1,14 @@ + + + + false + false + net8.0 + + + + + + + + diff --git a/LinearAlgebra/src/Matrix.cs b/LinearAlgebra/src/Matrix.cs new file mode 100644 index 0000000..37c6c24 --- /dev/null +++ b/LinearAlgebra/src/Matrix.cs @@ -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]; + } + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Quat32.cs b/LinearAlgebra/src/Quat32.cs new file mode 100644 index 0000000..19ee9bc --- /dev/null +++ b/LinearAlgebra/src/Quat32.cs @@ -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); + } + } + + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/Quaternion.cs b/LinearAlgebra/src/Quaternion.cs new file mode 100644 index 0000000..7936843 --- /dev/null +++ b/LinearAlgebra/src/Quaternion.cs @@ -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; + + /// + /// create a new quaternion with the given values + /// + /// x component + /// y component + /// z component + /// w component + public Quaternion(float x, float y, float z, float w) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + /// + /// An identity quaternion + /// + 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; + + /// + /// Convert to unit quaternion + /// + /// 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; + } + } + + /// + /// Convert to unity quaternion + /// + /// The quaternion to convert + /// A unit quaternion + /// This will preserve the orientation, + /// but ensures that it is a unit quaternion. + public static Quaternion Normalize(Quaternion q) { + return q.normalized; + } + + /// + /// Convert to euler angles + /// + /// The quaternion to convert + /// A vector containing euler angles + /// 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); + // } + } + + /// + /// Create a rotation from euler angles + /// + /// The angle around the right axis + /// The angle around the upward axis + /// The angle around the forward axis + /// The resulting quaternion + /// 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)); + } + /// + /// Create a rotation from a vector containing euler angles + /// + /// Vector with the euler angles + /// The resulting quaternion + /// 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; + } + + /// + /// Multiply two quaternions + /// + /// + /// + /// The resulting rotation + 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); + } + + /// + /// Rotate a vector using this quaterion + /// + /// The rotation + /// The vector to rotate + /// The rotated vector + 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; + } + + /// + /// The inverse of quaterion + /// + /// The quaternion for which the inverse is + /// needed The inverted quaternion + 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); + } + + /// + /// A rotation which looks in the given direction + /// + /// The look direction + /// The up direction + /// The look rotation + 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); + } + + /// + /// Creates a quaternion with the given forward direction with up = + /// Vector3::up + /// + /// The look direction + /// The rotation for this direction + /// 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); + } + + /// + /// Calculat the rotation from on vector to another + /// + /// The from direction + /// The to direction + /// The rotation from the first to the second vector + 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; + + } + + /// + /// Rotate form one orientation to anther with a maximum amount of degrees + /// + /// The from rotation + /// The destination rotation + /// The maximum amount of degrees to + /// rotate The possibly limited rotation + 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); + + } + + /// + /// Convert an angle/axis representation to a quaternion + /// + /// The angle + /// The axis + /// The resulting quaternion + 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; + } + /// + /// Convert this quaternion to angle/axis representation + /// + /// A pointer to the angle for the result + /// A pointer to the axis for the result + 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; + } + } + + /// + /// Get the angle between two orientations + /// + /// The first orientation + /// The second orientation + /// The smallest angle in degrees between the two + /// orientations + 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); + } + + /// + /// Sherical lerp between two rotations + /// + /// The first rotation + /// The second rotation + /// The factor between 0 and 1. + /// The resulting rotation + /// 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); + } + + /// + /// Unclamped sherical lerp between two rotations + /// + /// The first rotation + /// The second rotation + /// The factor + /// The resulting rotation + /// 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; + } + + /// + /// Convert this quaternion to angle/axis representation + /// + /// A pointer to the angle for the result + /// A pointer to the axis for the result + 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); + } + } + + /// + /// Returns the angle of around the give axis for a rotation + /// + /// The axis around which the angle should be + /// computed The source rotation + /// The signed angle around the axis + 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; + } + + /// + /// Returns the rotation limited around the given axis + /// + /// The axis which which the rotation should be + /// limited The source rotation + /// The rotation around the given axis + 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; + + } + + /// + /// Swing-twist decomposition of a rotation + /// + /// The base direction for the decomposition + /// The source rotation + /// A pointer to the quaternion for the swing + /// result A pointer to the quaternion for the + /// twist result + static void GetSwingTwist(Vector3Float axis, Quaternion q, + out Quaternion swing, out Quaternion twist) { + twist = GetRotationAround(axis, q); + swing = q * Inverse(twist); + } + + /// + /// Calculate the dot product of two quaternions + /// + /// The first rotation + /// The second rotation + /// + 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 + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Spherical.cs b/LinearAlgebra/src/Spherical.cs new file mode 100644 index 0000000..318839d --- /dev/null +++ b/LinearAlgebra/src/Spherical.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; + +#if UNITY_5_3_OR_NEWER +using Vector3 = UnityEngine.Vector3; +#endif + +namespace LinearAlgebra { + /// + /// A spherical vector + /// + /// This is a struct such that it is a value type and cannot be null + public struct Spherical { + /// + /// Create a spherical vector + /// + /// The distance in meters + /// The direction of the vector + public Spherical(float distance, Direction direction) { + if (distance > 0) { + this.distance = distance; + this.direction = direction; + } + else { + this.distance = -distance; + this.direction = -direction; + } + } + + /// + /// Create spherical vector. All given angles are in degrees + /// + /// The distance in meters + /// The horizontal angle in degrees + /// The vertical angle in degrees + /// + 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; + } + + /// + /// The distance in meters + /// + /// @remark The distance should never be negative + public float distance; + /// + /// The direction of the vector + /// + public Direction direction; + + /// + /// A spherical vector with zero degree angles and distance + /// + public readonly static Spherical zero = new(0, Direction.forward); + /// + /// A normalized forward-oriented vector + /// + 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 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 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); + } + + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/SwingTwist.cs b/LinearAlgebra/src/SwingTwist.cs new file mode 100644 index 0000000..df6e048 --- /dev/null +++ b/LinearAlgebra/src/SwingTwist.cs @@ -0,0 +1,136 @@ +// #if !UNITY_5_3_OR_NEWER +// using UnityEngine; +// #endif + +namespace LinearAlgebra { + + /// + /// An orientation using swing and twist angles + /// + /// The swing rotation + /// The twist rotation + public struct SwingTwist { + public Direction swing; + public AngleFloat twist; + + public SwingTwist(Direction swing, AngleFloat twist) { + this.swing = swing; + this.twist = twist; + } + + /// + /// Create a swing/twist rotation using angles in degrees + /// + /// The swing angle in the horizontal plane in degrees + /// The swing angle in the vertical plan in degrees + /// The twist angle in degrees + /// The swing/twist rotation + 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; + } + + /// + /// Create a swing/twist rotation using angles in degrees + /// + /// The swing angle in the horizontal plane in degrees + /// The swing angle in the vertical plan in degrees + /// The twist angle in degrees + /// The swing/twist rotation + 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 + /// + /// A zero angle rotation + /// + 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); + } + + /// + /// Convert a quaternion in a swing/twist rotation + /// + /// The quaternion to convert + /// The swing/twist rotation + 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 + /// + /// A zero angle rotation + /// + 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); + } + + /// + /// Convert a quaternion in a swing/twist rotation + /// + /// The quaternion to convert + /// The swing/twist rotation + 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 + + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Vector2Float.cs b/LinearAlgebra/src/Vector2Float.cs new file mode 100644 index 0000000..ac1867c --- /dev/null +++ b/LinearAlgebra/src/Vector2Float.cs @@ -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; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector2Int zero = new(0, 0); + /// + /// A vector with values (1, 1) + /// + public static readonly Vector2Int one = new(1, 1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Int up = new(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Int down = new(0, -1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Int forward = new(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Int back = new(0, -1); + /// + /// A vector3 with values (-1, 0) + /// + public static readonly Vector2Int left = new(-1, 0); + /// + /// A vector with values (1, 0) + /// + public static readonly Vector2Int right = new(1, 0); + + /// + /// Tests if the vector has equal values as the given vector + /// + /// The vector to compare to + /// true if the vector values are equal + public readonly bool Equals(Vector2Int v) => this.horizontal == v.horizontal && vertical == v.vertical; + + /// + /// Tests if the vector is equal to the given object + /// + /// The object to compare to + /// false when the object is not a Vector2 or does not have equal values + public override readonly bool Equals(object obj) { + if (obj is not Vector2Int v) + return false; + + return (this.horizontal == v.horizontal && this.vertical == v.vertical); + } + + /// + /// Tests if the two vectors have equal values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have equal values + /// 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); + } + /// + /// Tests if two vectors have different values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have different values + /// 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; + } + } + */ + + /// + /// 2-dimensional vectors + /// + public struct Vector2Float { + + /// + /// The right axis of the vector + /// + public float horizontal; // left/right + /// + /// The upward/forward axis of the vector + /// + public float vertical; // forward/backward + // directions are to be inline with Vector3 as much as possible... + + /// + /// Create a new 2-dimensional vector + /// + /// x axis value + /// y axis value + public Vector2Float(float x, float y) { + this.horizontal = x; + this.vertical = y; + } + + /// + /// Convert a Vector2Int into a Vector2Float + /// + /// The Vector2Int + public Vector2Float(Vector2Int v) { + this.horizontal = v.horizontal; + this.vertical = v.vertical; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector2Float zero = new Vector2Float(0, 0); + /// + /// A vector with values (1, 1) + /// + public static readonly Vector2Float one = new Vector2Float(1, 1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Float up = new Vector2Float(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Float down = new Vector2Float(0, -1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Float forward = new Vector2Float(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Float back = new Vector2Float(0, -1); + /// + /// A vector3 with values (-1, 0) + /// + public static readonly Vector2Float left = new Vector2Float(-1, 0); + /// + /// A vector with values (1, 0) + /// + public static readonly Vector2Float right = new Vector2Float(1, 0); + + /// + /// The squared length of this vector + /// + /// The squared length + /// 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; + } + + /// + /// The length of this vector + /// + /// The length of this vector + public readonly float magnitude => MathF.Sqrt(horizontal * horizontal + vertical * vertical); + public static float MagnitudeOf(Vector2Float v) { + return v.magnitude; + } + + /// + /// Convert the vector to a length of a 1 + /// + /// The vector with length 1 + 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; + } + + /// + /// Add two vectors + /// + /// The first vector + /// The second vector + /// The result of adding the two vectors + public static Vector2Float operator +(Vector2Float v1, Vector2Float v2) { + Vector2Float v = new Vector2Float(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical); + return v; + } + + /// + /// Subtract two vectors + /// + /// The first vector + /// The second vector + /// The result of adding the two vectors + public static Vector2Float operator -(Vector2Float v1, Vector2Float v2) { + Vector2Float v = new Vector2Float(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical); + return v; + } + + /// + /// Negate the vector + /// + /// The vector to negate + /// The negated vector + /// 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; + } + + /// + /// Scale a vector uniformly down + /// + /// The vector to scale + /// The scaling factor + /// The scaled vector + /// 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; + } + + + /// + /// Scale a vector uniformly up + /// + /// The vector to scale + /// The scaling factor + /// The scaled vector + /// 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; + } + + /// + /// Scale a vector uniformly up + /// + /// The scaling factor + /// The vector to scale + /// The scaled vector + /// 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); + } + + /// + /// Tests if the vector has equal values as the given vector + /// + /// The vector to compare to + /// true if the vector values are equal + //public readonly bool Equals(Vector2Float v1) => horizontal == v1.horizontal && vertical == v1.vertical; + + /// + /// Tests if the two vectors have equal values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have equal values + /// 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); + } + + /// + /// Tests if two vectors have different values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have different values + /// 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); + } + + /// + /// Tests if the vector is equal to the given object + /// + /// The object to compare to + /// false when the object is not a Vector2 or does not have equal values + public override readonly bool Equals(object obj) { + if (obj is not Vector2Float v) + return false; + + return (horizontal == v.horizontal && vertical == v.vertical); + } + + /// + /// Get an hash code for the vector + /// + /// The hash code + public override readonly int GetHashCode() { + return HashCode.Combine(horizontal, vertical); + } + + /// + /// Get the distance between two vectors + /// + /// The first vector + /// The second vector + /// The distance between the two vectors + 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; + } + + /// + /// The dot product of two vectors + /// + /// The first vector + /// The second vector + /// The dot product of the two vectors + public static float Dot(Vector2Float v1, Vector2Float v2) { + return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical; + } + + /// + /// Calculate the signed angle between two vectors. + /// + /// The starting vector + /// The ending vector + /// The axis to rotate around + /// The signed angle in degrees + 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)); + } + + /// + /// Rotates the vector with the given angle + /// + /// The vector to rotate + /// The angle in degrees + /// + 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; + } + + /// + /// Lerp between two vectors + /// + /// The from vector + /// The to vector + /// The interpolation distance [0..1] + /// The lerped vector + /// 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; + } + + /// + /// Map interval of angles between vectors [0..Pi] to interval [0..1] + /// + /// The first vector + /// The second vector + /// The resulting factor in interval [0..1] + /// Vectors a and b must be normalized + public static float ToFactor(Vector2Float v1, Vector2Float v2) { + return (1 - Vector2Float.Dot(v1, v2)) / 2; + } + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/Vector2Int.cs b/LinearAlgebra/src/Vector2Int.cs new file mode 100644 index 0000000..ed68e8b --- /dev/null +++ b/LinearAlgebra/src/Vector2Int.cs @@ -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; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector2Int zero = new(0, 0); + /// + /// A vector with values (1, 1) + /// + public static readonly Vector2Int one = new(1, 1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Int up = new(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Int down = new(0, -1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Int forward = new(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Int back = new(0, -1); + /// + /// A vector3 with values (-1, 0) + /// + public static readonly Vector2Int left = new(-1, 0); + /// + /// A vector with values (1, 0) + /// + public static readonly Vector2Int right = new(1, 0); + + /* + /// + /// Get an hash code for the vector + /// + /// The hash code + public override int GetHashCode() { + return (this.horizontal, this.vertical).GetHashCode(); + } + + /// + /// Tests if the vector has equal values as the given vector + /// + /// The vector to compare to + /// true if the vector values are equal + public readonly bool Equals(Vector2Int v) => this.horizontal == v.horizontal && vertical == v.vertical; + + */ + + /// + /// Tests if the two vectors have equal values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have equal values + /// 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); + } + /// + /// Tests if two vectors have different values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have different values + /// 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); + } + + /// + /// Tests if the vector is equal to the given object + /// + /// The object to compare to + /// false when the object is not a Vector2 or does not have equal values + public override readonly bool Equals(object obj) { + if (obj is not Vector2Int v) + return false; + + return (this.horizontal == v.horizontal && this.vertical == v.vertical); + } + + /// + /// Get an hash code for the vector + /// + /// The hash code + 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; + } + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/Vector3Float.cs b/LinearAlgebra/src/Vector3Float.cs new file mode 100644 index 0000000..bcf8626 --- /dev/null +++ b/LinearAlgebra/src/Vector3Float.cs @@ -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; + } + + /// + /// A vector with zero for all axis + /// + 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); + } + + /// + /// Convert the vector to a length of a 1 + /// + /// The vector with length 1 + 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; + } + + } + } + */ + + /// + /// 3-dimensional vectors + /// + /// This uses the right-handed coordinate system. + public struct Vector3Float { + + /// + /// The right axis of the vector + /// + public float horizontal; //> left/right + /// + /// The upward axis of the vector + /// + public float vertical; //> up/down + /// + /// The forward axis of the vector + /// + public float depth; //> forward/backward + + /// + /// Create a new 3-dimensional vector + /// + /// x axis value + /// y axis value + /// z axis value + 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})"; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector3Float zero = new Vector3Float(0, 0, 0); + /// + /// A vector with one for all axis + /// + public static readonly Vector3Float one = new Vector3Float(1, 1, 1); + /// + /// A Vector3Float with values (-1, 0, 0) + /// + public static readonly Vector3Float left = new Vector3Float(-1, 0, 0); + /// + /// A vector with values (1, 0, 0) + /// + public static readonly Vector3Float right = new Vector3Float(1, 0, 0); + /// + /// A vector with values (0, -1, 0) + /// + public static readonly Vector3Float down = new Vector3Float(0, -1, 0); + /// + /// A vector with values (0, 1, 0) + /// + public static readonly Vector3Float up = new Vector3Float(0, 1, 0); + /// + /// A vector with values (0, 0, -1) + /// + public static readonly Vector3Float back = new Vector3Float(0, -1, 0); + /// + /// A vector with values (0, 0, 1) + /// + 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); + /// + /// The vector length + /// + /// The vector for which you need the length + /// The vector length + 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); + + /// + /// The squared vector length + /// + /// The vector for which you need the squared length + /// The squared vector length + /// 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 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; + } + + /// + /// Negate te vector such that it points in the opposite direction + /// + /// + /// The negated vector + public static Vector3Float operator -(Vector3Float v1) { + Vector3Float v = new(-v1.horizontal, -v1.vertical, -v1.depth); + return v; + } + + /// + /// Subtract two vectors + /// + /// + /// + /// The result of the subtraction + 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; + } + + /// + /// Add two vectors + /// + /// + /// + /// The result of the addition + 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 \ No newline at end of file diff --git a/LinearAlgebra/src/Vector3Int.cs b/LinearAlgebra/src/Vector3Int.cs new file mode 100644 index 0000000..18edf40 --- /dev/null +++ b/LinearAlgebra/src/Vector3Int.cs @@ -0,0 +1,273 @@ +//#if !UNITY_5_3_OR_NEWER +using System; + +namespace LinearAlgebra { + + /// + /// 3-dimensional vectors + /// + /// This uses the right-handed coordinate system. + /// + /// Create a new 3-dimensional vector + /// + /// x axis value + /// y axis value + /// z axis value + public struct Vector3Int { + + /// + /// The right axis of the vector + /// + public int horizontal; //> left/right + /// + /// The upward axis of the vector + /// + public int vertical; //> up/down + /// + /// The forward axis of the vector + /// + public int depth; //> forward/backward + + public Vector3Int(int horizontal, int vertical, int depth) { + this.horizontal = horizontal; + this.vertical = vertical; + this.depth = depth; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector3Int zero = new(0, 0, 0); + /// + /// A vector with one for all axis + /// + public static readonly Vector3Int one = new(1, 1, 1); + /// + /// A Vector3Int with values (-1, 0, 0) + /// + public static readonly Vector3Int left = new(-1, 0, 0); + /// + /// A vector with values (1, 0, 0) + /// + public static readonly Vector3Int right = new(1, 0, 0); + /// + /// A vector with values (0, -1, 0) + /// + public static readonly Vector3Int down = new(0, -1, 0); + /// + /// A vector with values (0, 1, 0) + /// + public static readonly Vector3Int up = new(0, 1, 0); + /// + /// A vector with values (0, 0, -1) + /// + public static readonly Vector3Int back = new(0, -1, 0); + /// + /// A vector with values (0, 0, 1) + /// + 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); + /// + /// The vector length + /// + /// The vector for which you need the length + /// The vector length + 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); + + /// + /// The squared vector length + /// + /// The vector for which you need the squared length + /// The squared vector length + /// 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 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; + } + + /// + /// Negate te vector such that it points in the opposite direction + /// + /// + /// The negated vector + public static Vector3Int operator -(Vector3Int v1) { + Vector3Int v = new(-v1.horizontal, -v1.vertical, -v1.depth); + return v; + } + + /// + /// Subtract two vectors + /// + /// + /// + /// The result of the subtraction + 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; + } + + /// + /// Add two vectors + /// + /// + /// + /// The result of the addition + 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 \ No newline at end of file diff --git a/LinearAlgebra/src/float16.cs b/LinearAlgebra/src/float16.cs new file mode 100644 index 0000000..4b58cdd --- /dev/null +++ b/LinearAlgebra/src/float16.cs @@ -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 -- + } + +} \ No newline at end of file diff --git a/LinearAlgebra/test/AngleTest.cs b/LinearAlgebra/test/AngleTest.cs new file mode 100644 index 0000000..8362d82 --- /dev/null +++ b/LinearAlgebra/test/AngleTest.cs @@ -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 diff --git a/LinearAlgebra/test/DirectionTest.cs b/LinearAlgebra/test/DirectionTest.cs new file mode 100644 index 0000000..0eb9882 --- /dev/null +++ b/LinearAlgebra/test/DirectionTest.cs @@ -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 + diff --git a/LinearAlgebra/test/LinearAlgebra_Test.csproj b/LinearAlgebra/test/LinearAlgebra_Test.csproj new file mode 100644 index 0000000..5b48e60 --- /dev/null +++ b/LinearAlgebra/test/LinearAlgebra_Test.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + false + true + + + + + + + + + + + + + diff --git a/LinearAlgebra/test/QuaternionTest.cs b/LinearAlgebra/test/QuaternionTest.cs new file mode 100644 index 0000000..9dd5a96 --- /dev/null +++ b/LinearAlgebra/test/QuaternionTest.cs @@ -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 \ No newline at end of file diff --git a/LinearAlgebra/test/SphericalTest.cs b/LinearAlgebra/test/SphericalTest.cs new file mode 100644 index 0000000..b28b9d9 --- /dev/null +++ b/LinearAlgebra/test/SphericalTest.cs @@ -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 { 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 { 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 { 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 { 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 { + 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 { + 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 dirs = new List { + 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 \ No newline at end of file diff --git a/LinearAlgebra/test/SwingTwistTest.cs b/LinearAlgebra/test/SwingTwistTest.cs new file mode 100644 index 0000000..5f05a96 --- /dev/null +++ b/LinearAlgebra/test/SwingTwistTest.cs @@ -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 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector2FloatTest.cs b/LinearAlgebra/test/Vector2FloatTest.cs new file mode 100644 index 0000000..867765a --- /dev/null +++ b/LinearAlgebra/test/Vector2FloatTest.cs @@ -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 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector2IntTest.cs b/LinearAlgebra/test/Vector2IntTest.cs new file mode 100644 index 0000000..3647ca0 --- /dev/null +++ b/LinearAlgebra/test/Vector2IntTest.cs @@ -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(() => { + 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 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector3FloatTest.cs b/LinearAlgebra/test/Vector3FloatTest.cs new file mode 100644 index 0000000..fd3c2dc --- /dev/null +++ b/LinearAlgebra/test/Vector3FloatTest.cs @@ -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 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector3IntTest.cs b/LinearAlgebra/test/Vector3IntTest.cs new file mode 100644 index 0000000..b718178 --- /dev/null +++ b/LinearAlgebra/test/Vector3IntTest.cs @@ -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(() => { + 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 \ No newline at end of file diff --git a/MemoryCell.cs b/MemoryCell.cs new file mode 100644 index 0000000..7b7b8e5 --- /dev/null +++ b/MemoryCell.cs @@ -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 +} diff --git a/MemoryCell.cs.meta b/MemoryCell.cs.meta new file mode 100644 index 0000000..ef74aba --- /dev/null +++ b/MemoryCell.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 29633aa3fe5cd9dcc8d886051f45d4d8 \ No newline at end of file diff --git a/NanoBrain-Unity.code-workspace b/NanoBrain-Unity.code-workspace new file mode 100644 index 0000000..5194438 --- /dev/null +++ b/NanoBrain-Unity.code-workspace @@ -0,0 +1,12 @@ +{ + "folders": [ + { + "path": "../.." + }, + { + "name": "LinearAlgebra-csharp", + "path": "LinearAlgebra-csharp" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/NanoBrain-Unity.code-workspace.meta b/NanoBrain-Unity.code-workspace.meta new file mode 100644 index 0000000..65bb132 --- /dev/null +++ b/NanoBrain-Unity.code-workspace.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cfec45da5945b94d684a763d86b0dcf8 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NanoBrain.cs b/NanoBrain.cs new file mode 100644 index 0000000..5a7525e --- /dev/null +++ b/NanoBrain.cs @@ -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}"); + } + } + } + } +} \ No newline at end of file diff --git a/NanoBrain.cs.meta b/NanoBrain.cs.meta new file mode 100644 index 0000000..1666c60 --- /dev/null +++ b/NanoBrain.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 92f34a5e4027a1dc39efd8ce63cf6aba \ No newline at end of file diff --git a/Neuron.cs b/Neuron.cs new file mode 100644 index 0000000..05982de --- /dev/null +++ b/Neuron.cs @@ -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 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 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 _receivers = new(); + public virtual List 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); + } +} \ No newline at end of file diff --git a/Neuron.cs.meta b/Neuron.cs.meta new file mode 100644 index 0000000..e520090 --- /dev/null +++ b/Neuron.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 750748f3f0e7d472fbf88ab02987074c \ No newline at end of file diff --git a/NewVelocity.asset b/NewVelocity.asset new file mode 100644 index 0000000..87c56b4 --- /dev/null +++ b/NewVelocity.asset @@ -0,0 +1,1305 @@ +%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: NewVelocity + m_EditorClassIdentifier: Assembly-CSharp::ClusterPrefab + nuclei: + - rid: 2262690579536937007 + - rid: 2262690579536937008 + - rid: 2262690579536937009 + - rid: 2262690579536937010 + references: + version: 2 + RefIds: + - rid: -2 + type: {class: , ns: , asm: } + - rid: 2262690579536937007 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Proximity + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: + - nucleus: + rid: 2262690579536937008 + weight: 1 + combinator: 0 + _curvePreset: 3 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0.001 + value: 999.99994 + inSlope: 0 + outSlope: -112788.63 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.008866142 + value: 112.788635 + inSlope: -112788.63 + outSlope: -6740.78 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.016732283 + value: 59.76471 + inSlope: -6740.78 + outSlope: -2429.6155 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.024598425 + value: 40.653008 + inSlope: -2429.6155 + outSlope: -1252.2269 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.032464568 + value: 30.802813 + inSlope: -1252.2269 + outSlope: -763.7558 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.040330708 + value: 24.795002 + inSlope: -763.7558 + outSlope: -514.45264 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.04819685 + value: 20.748245 + inSlope: -514.45264 + outSlope: -370.0882 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.056062993 + value: 17.837078 + inSlope: -370.0882 + outSlope: -279.01324 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.06392913 + value: 15.642321 + inSlope: -279.01324 + outSlope: -217.87398 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.07179528 + value: 13.928493 + inSlope: -217.87398 + outSlope: -174.8461 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.079661414 + value: 12.553129 + inSlope: -174.8461 + outSlope: -143.41913 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.087527566 + value: 11.424973 + inSlope: -143.41913 + outSlope: -119.76661 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.0953937 + value: 10.482872 + inSlope: -119.76661 + outSlope: -101.519356 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.10325985 + value: 9.684306 + inSlope: -101.519356 + outSlope: -87.14706 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.11112598 + value: 8.9987955 + inSlope: -87.14706 + outSlope: -75.62513 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.11899213 + value: 8.403917 + inSlope: -75.62513 + outSlope: -66.24654 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.12685826 + value: 7.882813 + inSlope: -66.24654 + outSlope: -58.510654 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.13472441 + value: 7.4225597 + inSlope: -58.510654 + outSlope: -52.055042 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.14259055 + value: 7.0130873 + inSlope: -52.055042 + outSlope: -46.612007 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.1504567 + value: 6.6464305 + inSlope: -46.612007 + outSlope: -41.98024 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.15832284 + value: 6.316208 + inSlope: -41.98024 + outSlope: -38.006134 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.16618897 + value: 6.0172467 + inSlope: -38.006134 + outSlope: -34.570965 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.17405513 + value: 5.745306 + inSlope: -34.570965 + outSlope: -31.581244 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.18192126 + value: 5.496884 + inSlope: -31.581244 + outSlope: -28.963417 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.1897874 + value: 5.2690535 + inSlope: -28.963417 + outSlope: -26.658009 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.19765353 + value: 5.059358 + inSlope: -26.658009 + outSlope: -24.617418 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.20551969 + value: 4.8657136 + inSlope: -24.617418 + outSlope: -22.802412 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.21338584 + value: 4.6863465 + inSlope: -22.802412 + outSlope: -21.181019 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.22125196 + value: 4.519734 + inSlope: -21.181019 + outSlope: -19.72667 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.22911811 + value: 4.364561 + inSlope: -19.72667 + outSlope: -18.417059 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.23698425 + value: 4.21969 + inSlope: -18.417059 + outSlope: -17.233776 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.2448504 + value: 4.0841265 + inSlope: -17.233776 + outSlope: -16.160883 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.25271654 + value: 3.9570026 + inSlope: -16.160883 + outSlope: -15.185221 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.2605827 + value: 3.8375535 + inSlope: -15.185221 + outSlope: -14.295299 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.2684488 + value: 3.725105 + inSlope: -14.295299 + outSlope: -13.481375 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.27631494 + value: 3.6190586 + inSlope: -13.481375 + outSlope: -12.735047 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.28418112 + value: 3.5188825 + inSlope: -12.735047 + outSlope: -12.04901 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.29204726 + value: 3.4241033 + inSlope: -12.04901 + outSlope: -11.416967 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.2999134 + value: 3.3342957 + inSlope: -11.416967 + outSlope: -10.8334 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.30777952 + value: 3.249079 + inSlope: -10.8334 + outSlope: -10.293426 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.31564566 + value: 3.1681094 + inSlope: -10.293426 + outSlope: -9.792865 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.3235118 + value: 3.0910773 + inSlope: -9.792865 + outSlope: -9.327949 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.33137795 + value: 3.0177023 + inSlope: -9.327949 + outSlope: -8.895375 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.33924407 + value: 2.9477303 + inSlope: -8.895375 + outSlope: -8.492224 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.34711024 + value: 2.880929 + inSlope: -8.492224 + outSlope: -8.115812 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.3549764 + value: 2.8170888 + inSlope: -8.115812 + outSlope: -7.76395 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.36284253 + value: 2.7560165 + inSlope: -7.76395 + outSlope: -7.434456 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.37070867 + value: 2.697536 + inSlope: -7.434456 + outSlope: -7.1255083 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.3785748 + value: 2.641486 + inSlope: -7.1255083 + outSlope: -6.8354197 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.38644093 + value: 2.5877175 + inSlope: -6.8354197 + outSlope: -6.562695 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.39430708 + value: 2.5360944 + inSlope: -6.562695 + outSlope: -6.305974 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.40217322 + value: 2.4864907 + inSlope: -6.305974 + outSlope: -6.064021 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4100394 + value: 2.43879 + inSlope: -6.064021 + outSlope: -5.835745 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4179055 + value: 2.3928854 + inSlope: -5.835745 + outSlope: -5.6201315 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.42577165 + value: 2.3486767 + inSlope: -5.6201315 + outSlope: -5.4162097 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4336378 + value: 2.306072 + inSlope: -5.4162097 + outSlope: -5.223229 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.44150394 + value: 2.2649853 + inSlope: -5.223229 + outSlope: -5.040342 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4493701 + value: 2.2253373 + inSlope: -5.040342 + outSlope: -4.8669295 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4572362 + value: 2.1870534 + inSlope: -4.8669295 + outSlope: -4.7023005 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.46510234 + value: 2.1500645 + inSlope: -4.7023005 + outSlope: -4.5458865 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.47296852 + value: 2.1143057 + inSlope: -4.5458865 + outSlope: -4.3971753 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.48083466 + value: 2.079717 + inSlope: -4.3971753 + outSlope: -4.2555995 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4887008 + value: 2.0462418 + inSlope: -4.2555995 + outSlope: -4.1207685 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.49656692 + value: 2.0138273 + inSlope: -4.1207685 + outSlope: -3.9922712 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5044331 + value: 1.9824234 + inSlope: -3.9922712 + outSlope: -3.8696532 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5122992 + value: 1.9519844 + inSlope: -3.8696532 + outSlope: -3.7526293 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5201653 + value: 1.9224657 + inSlope: -3.7526293 + outSlope: -3.6408176 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.52803147 + value: 1.8938265 + inSlope: -3.6408176 + outSlope: -3.5339315 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5358976 + value: 1.8660281 + inSlope: -3.5339315 + outSlope: -3.4316826 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.54376376 + value: 1.839034 + inSlope: -3.4316826 + outSlope: -3.3338284 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5516299 + value: 1.8128096 + inSlope: -3.3338284 + outSlope: -3.240066 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.55949605 + value: 1.7873228 + inSlope: -3.240066 + outSlope: -3.1502352 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.56736225 + value: 1.7625424 + inSlope: -3.1502352 + outSlope: -3.0640743 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5752284 + value: 1.7384399 + inSlope: -3.0640743 + outSlope: -2.9814053 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.58309454 + value: 1.7149878 + inSlope: -2.9814053 + outSlope: -2.9020314 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5909606 + value: 1.6921601 + inSlope: -2.9020314 + outSlope: -2.8257964 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.59882677 + value: 1.669932 + inSlope: -2.8257964 + outSlope: -2.7525082 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6066929 + value: 1.6482804 + inSlope: -2.7525082 + outSlope: -2.6820538 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.61455905 + value: 1.627183 + inSlope: -2.6820538 + outSlope: -2.6142666 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6224252 + value: 1.6066188 + inSlope: -2.6142666 + outSlope: -2.5490105 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.63029134 + value: 1.5865679 + inSlope: -2.5490105 + outSlope: -2.4861636 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6381575 + value: 1.5670114 + inSlope: -2.4861636 + outSlope: -2.4256358 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.64602363 + value: 1.547931 + inSlope: -2.4256358 + outSlope: -2.3672597 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6538898 + value: 1.5293097 + inSlope: -2.3672597 + outSlope: -2.3109925 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.66175586 + value: 1.5111313 + inSlope: -2.3109925 + outSlope: -2.2566907 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.669622 + value: 1.4933798 + inSlope: -2.2566907 + outSlope: -2.2042859 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.67748815 + value: 1.4760406 + inSlope: -2.2042859 + outSlope: -2.1536992 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6853543 + value: 1.4590993 + inSlope: -2.1536992 + outSlope: -2.1048093 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6932205 + value: 1.4425424 + inSlope: -2.1048093 + outSlope: -2.0575728 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.70108664 + value: 1.4263573 + inSlope: -2.0575728 + outSlope: -2.011927 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7089528 + value: 1.4105312 + inSlope: -2.011927 + outSlope: -1.9677659 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7168189 + value: 1.3950524 + inSlope: -1.9677659 + outSlope: -1.9250447 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7246851 + value: 1.3799098 + inSlope: -1.9250447 + outSlope: -1.8837026 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7325512 + value: 1.3650923 + inSlope: -1.8837026 + outSlope: -1.8436778 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7404173 + value: 1.3505898 + inSlope: -1.8436778 + outSlope: -1.8049132 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.74828345 + value: 1.336392 + inSlope: -1.8049132 + outSlope: -1.7673749 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7561496 + value: 1.3224896 + inSlope: -1.7673749 + outSlope: -1.7309732 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.76401573 + value: 1.3088735 + inSlope: -1.7309732 + outSlope: -1.695693 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7718819 + value: 1.295535 + inSlope: -1.695693 + outSlope: -1.6614736 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.779748 + value: 1.2824656 + inSlope: -1.6614736 + outSlope: -1.6282848 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.78761417 + value: 1.2696573 + inSlope: -1.6282848 + outSlope: -1.5960962 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7954803 + value: 1.2571021 + inSlope: -1.5960962 + outSlope: -1.564832 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.80334646 + value: 1.2447929 + inSlope: -1.564832 + outSlope: -1.5344887 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.81121254 + value: 1.2327225 + inSlope: -1.5344887 + outSlope: -1.5050048 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.81907874 + value: 1.2208838 + inSlope: -1.5050048 + outSlope: -1.4763738 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8269449 + value: 1.2092705 + inSlope: -1.4763738 + outSlope: -1.4485649 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.83481103 + value: 1.1978759 + inSlope: -1.4485649 + outSlope: -1.4215137 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8426772 + value: 1.186694 + inSlope: -1.4215137 + outSlope: -1.3952202 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8505433 + value: 1.175719 + inSlope: -1.3952202 + outSlope: -1.369639 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.85840946 + value: 1.1649452 + inSlope: -1.369639 + outSlope: -1.3447852 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8662756 + value: 1.154367 + inSlope: -1.3447852 + outSlope: -1.320568 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.87414175 + value: 1.1439792 + inSlope: -1.320568 + outSlope: -1.2970176 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8820079 + value: 1.1337767 + inSlope: -1.2970176 + outSlope: -1.2740829 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.889874 + value: 1.1237546 + inSlope: -1.2740829 + outSlope: -1.2517655 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8977401 + value: 1.113908 + inSlope: -1.2517655 + outSlope: -1.2300034 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.90560627 + value: 1.1042327 + inSlope: -1.2300034 + outSlope: -1.2088321 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9134724 + value: 1.0947238 + inSlope: -1.2088321 + outSlope: -1.1881914 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.92133856 + value: 1.0853773 + inSlope: -1.1881914 + outSlope: -1.1680659 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9292047 + value: 1.0761892 + inSlope: -1.1680659 + outSlope: -1.1484709 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.93707085 + value: 1.0671551 + inSlope: -1.1484709 + outSlope: -1.1293371 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.94493705 + value: 1.0582715 + inSlope: -1.1293371 + outSlope: -1.1106901 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9528032 + value: 1.0495347 + inSlope: -1.1106901 + outSlope: -1.0925045 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.96066934 + value: 1.0409409 + inSlope: -1.0925045 + outSlope: -1.0747513 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9685354 + value: 1.0324868 + inSlope: -1.0747513 + outSlope: -1.0574516 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.97640157 + value: 1.0241687 + inSlope: -1.0574516 + outSlope: -1.0405389 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9842677 + value: 1.0159837 + inSlope: -1.0405389 + outSlope: -1.0240355 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.99213386 + value: 1.0079285 + inSlope: -1.0240355 + outSlope: -1.0079259 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1 + value: 1 + inSlope: -1.0079259 + outSlope: 0 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 100 + _receivers: [] + - rid: 2262690579536937008 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Position + 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: + - rid: 2262690579536937007 + - rid: 2262690579536937009 + - rid: 2262690579536937009 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Velocity + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: + - nucleus: + rid: 2262690579536937008 + weight: 1 + - nucleus: + rid: 2262690579536937010 + weight: 1 + 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: [] + - rid: 2262690579536937010 + type: {class: MemoryCell, ns: , asm: Assembly-CSharp} + data: + name: New memory cell + 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: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 1 + _receivers: + - rid: 2262690579536937009 + staticMemory: 0 diff --git a/NewVelocity.asset.meta b/NewVelocity.asset.meta new file mode 100644 index 0000000..5718f8b --- /dev/null +++ b/NewVelocity.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 61eea9f818639ec20b7a7bf4e86fff66 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Nucleus.cs b/Nucleus.cs new file mode 100644 index 0000000..2b1f5da --- /dev/null +++ b/Nucleus.cs @@ -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 _synapses = new(); + public List 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 + +} \ No newline at end of file diff --git a/Nucleus.cs.meta b/Nucleus.cs.meta new file mode 100644 index 0000000..08b3cf8 --- /dev/null +++ b/Nucleus.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4310eea6ab77628b085387a226c1c386 \ No newline at end of file diff --git a/NucleusArray.cs b/NucleusArray.cs new file mode 100644 index 0000000..9f8a172 --- /dev/null +++ b/NucleusArray.cs @@ -0,0 +1,157 @@ +using System.Linq; +using System.Collections.Generic; +using UnityEngine; +using Unity.Mathematics; +using static Unity.Mathematics.math; + +[System.Serializable] +public class NucleusArray { + [SerializeReference] + private Nucleus[] _nuclei; + public Nucleus[] nuclei { + get { + return _nuclei; + } + set { + _nuclei = value; + } + } + + public NucleusArray(Nucleus nucleus) { + this._nuclei = new Nucleus[1]; + this._nuclei[0] = nucleus; + } + public NucleusArray(ClusterPrefab cluster) { + this._nuclei = new Nucleus[0]; + } + public NucleusArray(int size, string name) { + this._nuclei = new Nucleus[size]; + } + + + public void AddNucleus(ClusterPrefab prefab) { + if (this._nuclei.Length == 0) { + Debug.LogError("Empty perceptoid array, cannot add"); + return; + } + int newLength = this._nuclei.Length + 1; + Nucleus[] newArray = new Nucleus[newLength]; + + for (int i = 0; i < this._nuclei.Length; i++) + newArray[i] = this._nuclei[i]; + if (this._nuclei[0] is Nucleus nucleus) { + newArray[newLength - 1] = nucleus.Clone(prefab); + newArray[newLength - 1].name += $": {newLength - 1}"; + } + + this._nuclei = newArray; + } + + public void RemoveNucleus() { + int newLength = this._nuclei.Length - 1; + if (newLength == 0) { + Debug.LogWarning("Perceptoid array cannot be empty"); + return; + } + Nucleus[] newPerceptei = new Nucleus[newLength]; + for (int i = 0; i < newLength; i++) + newPerceptei[i] = this._nuclei[i]; + // Delete the last perception + if (this._nuclei[newLength] is Nucleus nucleus) + Neuron.Delete(nucleus); //this._nuclei[newLength]); + + this._nuclei = newPerceptei; + } + + public Dictionary thingReceivers = new(); + + + private Nucleus FindReceiver(int thingId, float3 inputValue) { + // No existing nucleus for this thing + float inputMagnitude = length(inputValue); + Neuron selectedReceiver = null; + float selectedMagnitude = 0; + foreach (Nucleus nucleusReceiver in this._nuclei) { + if (nucleusReceiver is not Neuron receiver) + continue; + if (thingReceivers.ContainsValue(receiver) == false) { + // We found an unusued receiver + thingReceivers.Add(thingId, receiver); + return receiver; + } + else if (receiver.isSleeping) { + // A sleeping receiver is not active and can therefore always be used + thingReceivers.Add(thingId, receiver); + 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.outputValue); + } + // Look for the receiver with the lowest magnitude + else { + float magnitude = length(receiver.outputValue); + + if (magnitude < inputMagnitude && length(receiver.outputValue) < selectedMagnitude) { + selectedReceiver = receiver; + selectedMagnitude = length(selectedReceiver.outputValue); + } + } + } + if (selectedReceiver != null) { + // 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; + } + + public virtual void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) { + CleanupReceivers(); + + if (this._nuclei[0] is Neuron neuron) + inputValue = neuron.Activator(inputValue); + + if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) { + // No existing nucleus for this thing + selectedReceiver = FindReceiver(thingId, inputValue); + } + 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; + } + + if (selectedReceiver is Neuron selectedNucleus) + selectedNucleus.ProcessStimulusDirect(inputValue); + } + + private void CleanupReceivers() { + // Remove a thing-receiver connection when the nucleus is inactive + List receiversToRemove = new(); + foreach (KeyValuePair item in thingReceivers) { + if (item.Value != null && item.Value is Neuron neuron && neuron.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]; + + } + } +} \ No newline at end of file diff --git a/NucleusArray.cs.meta b/NucleusArray.cs.meta new file mode 100644 index 0000000..61e26b7 --- /dev/null +++ b/NucleusArray.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f8cac60bd79854595a8571c042f77998 \ No newline at end of file diff --git a/Receptor.cs b/Receptor.cs new file mode 100644 index 0000000..a102835 --- /dev/null +++ b/Receptor.cs @@ -0,0 +1,78 @@ +using UnityEngine; +using Unity.Mathematics; +using static Unity.Mathematics.math; + +[System.Serializable] +public class Receptor : Neuron, IReceptor { + public Receptor(Cluster parent, string name) : base(parent, name) { + this.array = new NucleusArray(this); + if (this.name.IndexOf(":") < 0) + this.name += ": 0"; + } + public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) { + this.array = new NucleusArray(this); + } + + public string GetName() { + return this.name; + } + + public override Nucleus ShallowCloneTo(Cluster parent) { + Receptor clone = new(parent, name) { + + }; + CloneFields(clone); + return clone; + } + public override Nucleus Clone(ClusterPrefab prefab) { + Receptor clone = new(prefab, name) { + array = this._array + }; + CloneFields(clone); + // Adding receivers will also add synapses to the receivers + foreach (Nucleus receiver in this.receivers.ToArray()) + clone.AddReceiver(receiver); + + return clone; + } + + [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 virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { + IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); + } + + public override void UpdateStateIsolated() { + this.outputValue = this.bias; + } + + public override void UpdateNuclei() { + this.stale++; + if (this.stale > staleValueForSleep && lengthsq(this.bias) > 0) { + this.bias = new float3(0, 0, 0); + this.parent.UpdateFromNucleus(this); + } + } + + public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { + this._array ??= new NucleusArray(this.parent); + this._array.ProcessStimulus(thingId, inputValue, thingName); + } +} \ No newline at end of file diff --git a/Receptor.cs.meta b/Receptor.cs.meta new file mode 100644 index 0000000..56793ae --- /dev/null +++ b/Receptor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cfb9734aebc3ab85aacf87d26fb92e55 \ No newline at end of file diff --git a/Scene.meta b/Scene.meta new file mode 100644 index 0000000..d71b5e5 --- /dev/null +++ b/Scene.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bfd7dadd61c0891d8a94db0196e61a8a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scene/TestScene Boid.unity b/Scene/TestScene Boid.unity new file mode 100644 index 0000000..401756e --- /dev/null +++ b/Scene/TestScene Boid.unity @@ -0,0 +1,487 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1001 &551770709 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.x + value: 0.71 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093763, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_Name + value: Boid2 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} +--- !u!1 &968074744 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 968074747} + - component: {fileID: 968074746} + - component: {fileID: 968074745} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &968074745 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + m_Enabled: 1 +--- !u!20 &968074746 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &968074747 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1342149740 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1342149742} + - component: {fileID: 1342149741} + m_Layer: 0 + m_Name: SwamControl + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1342149741 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342149740} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0464906885ae3494f8fd0314719fb2db, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::SwarmControl + speed: 0.5 + inertia: 0.1 + alignmentForce: 0 + cohesionForce: 1 + separationForce: 1 + avoidanceForce: 5 + separationDistance: 0.5 + perceptionDistance: 1 + spaceSize: {x: 10, y: 10, z: 10} + boundaryWidth: {x: 1, y: 1, z: 1} +--- !u!4 &1342149742 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342149740} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -1.00377, y: -1.02283, z: 0.72231} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2011285159 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2011285161} + - component: {fileID: 2011285160} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &2011285160 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011285159} + m_Enabled: 1 + serializedVersion: 12 + m_Type: 1 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize2D: {x: 0.5, y: 0.5} + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 +--- !u!4 &2011285161 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011285159} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1001 &4573752827112804207 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093763, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_Name + value: Boid1 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 968074747} + - {fileID: 2011285161} + - {fileID: 4573752827112804207} + - {fileID: 551770709} + - {fileID: 1342149742} diff --git a/Scene/TestScene Boid.unity.meta b/Scene/TestScene Boid.unity.meta new file mode 100644 index 0000000..81fe061 --- /dev/null +++ b/Scene/TestScene Boid.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4f343147e37db9eeda3e98058c553c92 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scene/TestScene Experiment.unity b/Scene/TestScene Experiment.unity new file mode 100644 index 0000000..ac54ba4 --- /dev/null +++ b/Scene/TestScene Experiment.unity @@ -0,0 +1,365 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &388118692 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 388118694} + - component: {fileID: 388118693} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &388118693 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 388118692} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9051408e82b511584998506096af4bf0, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::SelectorBrain + defaultBrain: {fileID: 11400000, guid: d5b3a22d9bb7d13aeb3174077125967b, type: 2} + input1: {x: 0, y: 0, z: 1} + input2: {x: 0, y: -2, z: 0} + output: {x: 0, y: 0, z: 0} +--- !u!4 &388118694 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 388118692} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -2.01476, y: -0, z: 0.65362} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &968074744 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 968074747} + - component: {fileID: 968074746} + - component: {fileID: 968074745} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &968074745 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + m_Enabled: 1 +--- !u!20 &968074746 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &968074747 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2011285159 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2011285161} + - component: {fileID: 2011285160} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &2011285160 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011285159} + m_Enabled: 1 + serializedVersion: 12 + m_Type: 1 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize2D: {x: 0.5, y: 0.5} + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 +--- !u!4 &2011285161 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011285159} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 968074747} + - {fileID: 2011285161} + - {fileID: 388118694} diff --git a/Scene/TestScene Experiment.unity.meta b/Scene/TestScene Experiment.unity.meta new file mode 100644 index 0000000..676153c --- /dev/null +++ b/Scene/TestScene Experiment.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1070383882ed0f5379a3b34e8ccb1f75 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts.meta b/Scripts.meta new file mode 100644 index 0000000..6083b0e --- /dev/null +++ b/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 363b69b84de0e4b729794c10e7c40ab5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Experimental.meta b/Scripts/Experimental.meta new file mode 100644 index 0000000..7c7ad14 --- /dev/null +++ b/Scripts/Experimental.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2c1e3956a0b70ae6b8d09fb467b73621 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/NeuraalNetwerkIcoonSchets1.png b/Scripts/NeuraalNetwerkIcoonSchets1.png new file mode 100644 index 0000000..82980ef Binary files /dev/null and b/Scripts/NeuraalNetwerkIcoonSchets1.png differ diff --git a/Scripts/NeuraalNetwerkIcoonSchets1.png.meta b/Scripts/NeuraalNetwerkIcoonSchets1.png.meta new file mode 100644 index 0000000..f31713f --- /dev/null +++ b/Scripts/NeuraalNetwerkIcoonSchets1.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: cad48149d984d2eddae5808eb1517cb5 +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: diff --git a/Scripts/NeuraalNetwerkIcoonSchets2.png b/Scripts/NeuraalNetwerkIcoonSchets2.png new file mode 100644 index 0000000..35853d6 Binary files /dev/null and b/Scripts/NeuraalNetwerkIcoonSchets2.png differ diff --git a/Scripts/NeuraalNetwerkIcoonSchets2.png.meta b/Scripts/NeuraalNetwerkIcoonSchets2.png.meta new file mode 100644 index 0000000..9abd599 --- /dev/null +++ b/Scripts/NeuraalNetwerkIcoonSchets2.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 2e644ed036e8939bf94586314a4f4607 +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: diff --git a/Synapse.cs b/Synapse.cs new file mode 100644 index 0000000..424b7e6 --- /dev/null +++ b/Synapse.cs @@ -0,0 +1,15 @@ +using System; +using UnityEngine; + +[Serializable] +public class Synapse { + [SerializeReference] + public Neuron neuron; + + public float weight; + + public Synapse(Neuron nucleus, float weight = 1.0f) { + this.neuron = nucleus; + this.weight = weight; + } +} \ No newline at end of file diff --git a/Synapse.cs.meta b/Synapse.cs.meta new file mode 100644 index 0000000..e62612c --- /dev/null +++ b/Synapse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 334a58eafccd60cbdb32f719e9e861c6 \ No newline at end of file diff --git a/Velocity.asset b/Velocity.asset new file mode 100644 index 0000000..0001385 --- /dev/null +++ b/Velocity.asset @@ -0,0 +1,128 @@ +%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: Velocity + m_EditorClassIdentifier: Assembly-CSharp::ClusterPrefab + nuclei: + - rid: 2262690551513219315 + - rid: 2262690551513219316 + - rid: 2262690551513219317 + references: + version: 2 + RefIds: + - rid: -2 + type: {class: , ns: , asm: } + - rid: 2262690551513219315 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Velocity + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: + - nucleus: + rid: 2262690551513219316 + weight: 1 + - nucleus: + rid: 2262690551513219317 + weight: 1 + 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: [] + - rid: 2262690551513219316 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Position + 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: + - rid: 2262690551513219315 + - rid: 2262690551513219317 + type: {class: MemoryCell, ns: , asm: Assembly-CSharp} + data: + name: New memory cell + 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: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 1 + _receivers: + - rid: 2262690551513219315 + staticMemory: 0 diff --git a/Velocity.asset.meta b/Velocity.asset.meta new file mode 100644 index 0000000..38684df --- /dev/null +++ b/Velocity.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c61aecac62c26de4aaefb2612bcc9a5d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: