diff --git a/Assets/Editor/EmmyTest.cs b/Assets/Editor/EmmyTest.cs
index 0628ffaf..d6464bbc 100644
--- a/Assets/Editor/EmmyTest.cs
+++ b/Assets/Editor/EmmyTest.cs
@@ -13,14 +13,8 @@ public class EmmyTest : Editor
         [MenuItem("ArcCreate/TestEmmy/Scenecontrol")]
         public static void GenerateTestScenecontrolEmmy()
         {
-            Assembly scAssembly = Assembly.GetAssembly(typeof(ScenecontrolService));
-            var emmy = LuaRunner.GetCommonEmmySharp();
-            emmy.AppendAssembly(scAssembly);
-            emmy.AppendFunction(typeof(ScenecontrolLuaEnvironment).GetMethod("AddScenecontrol"));
-            emmy.AppendFunction(typeof(ScenecontrolLuaEnvironment).GetMethod("Notify"));
-            emmy.AppendFunction(typeof(ScenecontrolLuaEnvironment).GetMethod("NotifyWarn"));
-            emmy.AppendFunction(typeof(ScenecontrolLuaEnvironment).GetMethod("NotifyError"));
-            emmy.Build(Path.GetDirectoryName(Application.dataPath));
+            ScenecontrolLuaEnvironment env = new ScenecontrolLuaEnvironment();
+            env.GenerateEmmyLua(Path.Combine(Application.dataPath, ".."));
         }
     }
 }
\ No newline at end of file
diff --git a/Assets/Editor/ScenecontrolChecks.cs b/Assets/Editor/ScenecontrolChecks.cs
new file mode 100644
index 00000000..669f5ea8
--- /dev/null
+++ b/Assets/Editor/ScenecontrolChecks.cs
@@ -0,0 +1,43 @@
+using System;
+using System.IO;
+using System.Linq;
+using ArcCreate.Gameplay.Scenecontrol;
+using ArcCreate.Utility.Animation;
+using DG.DOTweenEditor;
+using DG.Tweening;
+using UnityEditor;
+using UnityEngine;
+
+namespace ArcCreate.EditorScripts
+{
+    public static class ScenecontrolChecks
+    {
+        public const string IOFolder = "Assets/Scripts/Gameplay/Scenecontrol/IO";
+        public const string DeserializationFile = IOFolder + "/ScenecontrolDeserialization.cs";
+        public const string SerializationFile = IOFolder + "/ScenecontrolSerialization.cs";
+
+        [InitializeOnLoadMethod]
+        public static void CheckScenecontrol()
+        {
+            var allChannelTypes = TypeCache.GetTypesDerivedFrom<IChannel>();
+
+            var deser = File.ReadAllText(DeserializationFile);
+            var ser = File.ReadAllText(SerializationFile);
+
+            foreach (var chanType in allChannelTypes.Where(t => !Attribute.IsDefined(t, typeof(SerializationExemptAttribute))))
+            {
+                var chan = chanType.Name;
+
+                if (!deser.Contains(chan))
+                {
+                    Debug.LogError($"Channel type {chanType} is missing deserialization code! Please add it!");
+                }
+
+                if (!ser.Contains(chan))
+                {
+                    Debug.LogError($"Channel type {chanType} is missing serialization code! Please add it!");
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Editor/ScenecontrolChecks.cs.meta b/Assets/Editor/ScenecontrolChecks.cs.meta
new file mode 100644
index 00000000..1021b1fd
--- /dev/null
+++ b/Assets/Editor/ScenecontrolChecks.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3e8ac28742af86646807db7afb3bdcf1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Materials/Compose/Waveform.mat b/Assets/Materials/Compose/Waveform.mat
index 297eb470..f1ffd1cf 100644
--- a/Assets/Materials/Compose/Waveform.mat
+++ b/Assets/Materials/Compose/Waveform.mat
@@ -72,7 +72,7 @@ Material:
     - _SmoothnessTextureChannel: 0
     - _SpecularHighlights: 1
     - _SrcBlend: 1
-    - _ToSample: 10478160
+    - _ToSample: 14773500
     - _UVSec: 0
     - _ZWrite: 1
     m_Colors:
diff --git a/Assets/Prefabs/Gameplay/ScenecontrolNoteIndividual.prefab b/Assets/Prefabs/Gameplay/ScenecontrolNoteIndividual.prefab
new file mode 100644
index 00000000..3110e8eb
--- /dev/null
+++ b/Assets/Prefabs/Gameplay/ScenecontrolNoteIndividual.prefab
@@ -0,0 +1,46 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &9148057743627579955
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6791676609819765838}
+  - component: {fileID: 5835389651473541310}
+  m_Layer: 0
+  m_Name: ScenecontrolNoteIndividual
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6791676609819765838
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 9148057743627579955}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children: []
+  m_Father: {fileID: 0}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &5835389651473541310
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 9148057743627579955}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 992745bc6c6c41d4f9dbd4512489c76e, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  isPersistent: 1
diff --git a/Assets/Prefabs/Gameplay/ScenecontrolNoteIndividual.prefab.meta b/Assets/Prefabs/Gameplay/ScenecontrolNoteIndividual.prefab.meta
new file mode 100644
index 00000000..d2f9217d
--- /dev/null
+++ b/Assets/Prefabs/Gameplay/ScenecontrolNoteIndividual.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 5f66e696345e59a4191b57a5ecf86d21
+PrefabImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scenes/Gameplay.unity b/Assets/Scenes/Gameplay.unity
index 851fef6b..972716c3 100644
--- a/Assets/Scenes/Gameplay.unity
+++ b/Assets/Scenes/Gameplay.unity
@@ -38,7 +38,7 @@ RenderSettings:
   m_ReflectionIntensity: 1
   m_CustomReflection: {fileID: 0}
   m_Sun: {fileID: 0}
-  m_IndirectSpecularColor: {r: 0.37311956, g: 0.3807402, b: 0.35872734, a: 1}
+  m_IndirectSpecularColor: {r: 0.37311918, g: 0.3807398, b: 0.35872716, a: 1}
   m_UseRadianceAmbientProbe: 0
 --- !u!157 &3
 LightmapSettings:
@@ -589,10 +589,11 @@ MonoBehaviour:
   m_OnCullStateChanged:
     m_PersistentCalls:
       m_Calls: []
-  m_text: Title
+  m_text: 
   m_isRightToLeft: 0
-  m_fontAsset: {fileID: 11400000, guid: c7ac2a5a49fae4b4bb4d928c18726d16, type: 2}
-  m_sharedMaterial: {fileID: 2100000, guid: 7d94f1e4755948de58fab75d76ecc932, type: 2}
+  m_fontAsset: {fileID: 11400000, guid: fc246767bf759519bb5f459275194444, type: 2}
+  m_sharedMaterial: {fileID: 3714936077332224653, guid: fc246767bf759519bb5f459275194444,
+    type: 2}
   m_fontSharedMaterials: []
   m_fontMaterial: {fileID: 0}
   m_fontMaterials: []
@@ -616,22 +617,22 @@ MonoBehaviour:
   m_faceColor:
     serializedVersion: 2
     rgba: 4294967295
-  m_fontSize: 29
-  m_fontSizeBase: 29
+  m_fontSize: 36
+  m_fontSizeBase: 36
   m_fontWeight: 400
-  m_enableAutoSizing: 1
-  m_fontSizeMin: 20
-  m_fontSizeMax: 29
+  m_enableAutoSizing: 0
+  m_fontSizeMin: 18
+  m_fontSizeMax: 72
   m_fontStyle: 0
   m_HorizontalAlignment: 1
-  m_VerticalAlignment: 512
+  m_VerticalAlignment: 256
   m_textAlignment: 65535
   m_characterSpacing: 0
   m_wordSpacing: 0
   m_lineSpacing: 0
   m_lineSpacingMax: 0
   m_paragraphSpacing: 0
-  m_charWidthMaxAdj: 30
+  m_charWidthMaxAdj: 0
   m_enableWordWrapping: 1
   m_wordWrappingRatios: 0.4
   m_overflowMode: 0
@@ -2012,7 +2013,7 @@ RectTransform:
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0, y: 1}
   m_AnchorMax: {x: 0, y: 1}
-  m_AnchoredPosition: {x: 25, y: -200.00002}
+  m_AnchoredPosition: {x: 25, y: -200}
   m_SizeDelta: {x: 300, y: 50}
   m_Pivot: {x: 0, y: 1}
 --- !u!114 &377893499
@@ -8281,7 +8282,7 @@ RectTransform:
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0, y: 1}
   m_AnchorMax: {x: 0, y: 1}
-  m_AnchoredPosition: {x: 25, y: -200.00002}
+  m_AnchoredPosition: {x: 25, y: -200}
   m_SizeDelta: {x: 300, y: 50}
   m_Pivot: {x: 0, y: 1}
 --- !u!114 &1409023644
@@ -9401,6 +9402,8 @@ MonoBehaviour:
     type: 3}
   GroupPrefab: {fileID: 4044602181011751714, guid: 3a19ff92a7a5f7b529ca21daf779e77a,
     type: 3}
+  IndividualPrefab: {fileID: 9148057743627579955, guid: 5f66e696345e59a4191b57a5ecf86d21,
+    type: 3}
   DefaultMaterial: {fileID: 2100000, guid: 0077ff23d523e3c18aae9ce29959c184, type: 2}
   ColorBurnMaterial: {fileID: 2100000, guid: 40f43e7d31b274b3aaf597edb06654e0, type: 2}
   ColorDodgeMaterial: {fileID: 2100000, guid: 8be0bcbba0ce3e760adfbfe8df96f2ef, type: 2}
@@ -11539,10 +11542,11 @@ MonoBehaviour:
   m_OnCullStateChanged:
     m_PersistentCalls:
       m_Calls: []
-  m_text: Composer
+  m_text: 
   m_isRightToLeft: 0
-  m_fontAsset: {fileID: 11400000, guid: c7ac2a5a49fae4b4bb4d928c18726d16, type: 2}
-  m_sharedMaterial: {fileID: 2100000, guid: 7d94f1e4755948de58fab75d76ecc932, type: 2}
+  m_fontAsset: {fileID: 11400000, guid: fc246767bf759519bb5f459275194444, type: 2}
+  m_sharedMaterial: {fileID: 3714936077332224653, guid: fc246767bf759519bb5f459275194444,
+    type: 2}
   m_fontSharedMaterials: []
   m_fontMaterial: {fileID: 0}
   m_fontMaterials: []
@@ -11566,22 +11570,22 @@ MonoBehaviour:
   m_faceColor:
     serializedVersion: 2
     rgba: 4294967295
-  m_fontSize: 23
-  m_fontSizeBase: 23
+  m_fontSize: 36
+  m_fontSizeBase: 36
   m_fontWeight: 400
-  m_enableAutoSizing: 1
+  m_enableAutoSizing: 0
   m_fontSizeMin: 18
-  m_fontSizeMax: 23
+  m_fontSizeMax: 72
   m_fontStyle: 0
   m_HorizontalAlignment: 1
-  m_VerticalAlignment: 512
+  m_VerticalAlignment: 256
   m_textAlignment: 65535
   m_characterSpacing: 0
   m_wordSpacing: 0
   m_lineSpacing: 0
   m_lineSpacingMax: 0
   m_paragraphSpacing: 0
-  m_charWidthMaxAdj: 50
+  m_charWidthMaxAdj: 0
   m_enableWordWrapping: 1
   m_wordWrappingRatios: 0.4
   m_overflowMode: 0
@@ -12406,10 +12410,11 @@ MonoBehaviour:
   m_OnCullStateChanged:
     m_PersistentCalls:
       m_Calls: []
-  m_text: Future ?
+  m_text: 
   m_isRightToLeft: 0
-  m_fontAsset: {fileID: 11400000, guid: b7b1b4ddbd331de42ac5353299efdd2d, type: 2}
-  m_sharedMaterial: {fileID: 2100000, guid: ea4b41747e7d6cd059f99e4c986f0c8a, type: 2}
+  m_fontAsset: {fileID: 11400000, guid: fc246767bf759519bb5f459275194444, type: 2}
+  m_sharedMaterial: {fileID: 3714936077332224653, guid: fc246767bf759519bb5f459275194444,
+    type: 2}
   m_fontSharedMaterials: []
   m_fontMaterial: {fileID: 0}
   m_fontMaterials: []
@@ -12433,14 +12438,14 @@ MonoBehaviour:
   m_faceColor:
     serializedVersion: 2
     rgba: 4294967295
-  m_fontSize: 25
-  m_fontSizeBase: 25
+  m_fontSize: 36
+  m_fontSizeBase: 36
   m_fontWeight: 400
-  m_enableAutoSizing: 1
-  m_fontSizeMin: 15
-  m_fontSizeMax: 25
+  m_enableAutoSizing: 0
+  m_fontSizeMin: 18
+  m_fontSizeMax: 72
   m_fontStyle: 0
-  m_HorizontalAlignment: 2
+  m_HorizontalAlignment: 1
   m_VerticalAlignment: 256
   m_textAlignment: 65535
   m_characterSpacing: 0
@@ -12448,7 +12453,7 @@ MonoBehaviour:
   m_lineSpacing: 0
   m_lineSpacingMax: 0
   m_paragraphSpacing: 0
-  m_charWidthMaxAdj: 50
+  m_charWidthMaxAdj: 0
   m_enableWordWrapping: 1
   m_wordWrappingRatios: 0.4
   m_overflowMode: 0
diff --git a/Assets/Scripts/Compose/Editing/NoteCreation.cs b/Assets/Scripts/Compose/Editing/NoteCreation.cs
index 9408b763..17ff4e82 100644
--- a/Assets/Scripts/Compose/Editing/NoteCreation.cs
+++ b/Assets/Scripts/Compose/Editing/NoteCreation.cs
@@ -344,7 +344,7 @@ private void Update()
             float z = cursorPosition.z;
 
             GroupProperties groupProperties = Services.Gameplay.Chart.GetTimingGroup(tg).GroupProperties;
-            Vector3 pos = (groupProperties.FallDirection * z) + new Vector3(ArcFormula.LaneToWorldX(cursorLane), 0, 0);
+            Vector3 pos = (groupProperties.GetFallDirection() * z) + new Vector3(ArcFormula.LaneToWorldX(cursorLane), 0, 0);
             Quaternion rot = groupProperties.RotationIndividual;
             Vector3 scl = groupProperties.ScaleIndividual;
 
@@ -363,11 +363,11 @@ private void Update()
                     previewHold.localScale = scl;
                     break;
                 case CreateNoteMode.Arc:
-                    pos = (groupProperties.FallDirection * z) + new Vector3(cursorPosition.x, 1, 0);
+                    pos = (groupProperties.GetFallDirection() * z) + new Vector3(cursorPosition.x, 1, 0);
                     previewArc.localPosition = pos;
                     break;
                 case CreateNoteMode.Trace:
-                    pos = (groupProperties.FallDirection * z) + new Vector3(cursorPosition.x, 1, 0);
+                    pos = (groupProperties.GetFallDirection() * z) + new Vector3(cursorPosition.x, 1, 0);
                     previewTrace.localPosition = pos;
                     break;
                 case CreateNoteMode.ArcTap:
@@ -414,7 +414,7 @@ private void Update()
                         previewArcTap.gameObject.SetActive(true);
                         float worldX = parentArc.WorldXAt(cursorTiming);
                         float worldY = parentArc.WorldYAt(cursorTiming);
-                        pos = (groupProperties.FallDirection * z) + new Vector3(worldX, worldY, 0);
+                        pos = (groupProperties.GetFallDirection() * z) + new Vector3(worldX, worldY, 0);
                         previewArcTap.localPosition = pos;
                         previewArcTap.localRotation = rot;
                         previewArcTap.localScale = scl;
diff --git a/Assets/Scripts/Compose/EventsEditor/Scenecontrol/CommandTypes/LuaScenecontrol.cs b/Assets/Scripts/Compose/EventsEditor/Scenecontrol/CommandTypes/LuaScenecontrol.cs
index 8d0b4a96..7a8e15a3 100644
--- a/Assets/Scripts/Compose/EventsEditor/Scenecontrol/CommandTypes/LuaScenecontrol.cs
+++ b/Assets/Scripts/Compose/EventsEditor/Scenecontrol/CommandTypes/LuaScenecontrol.cs
@@ -1,8 +1,10 @@
+using EmmySharp;
 using MoonSharp.Interpreter;
 
 namespace ArcCreate.Compose.EventsEditor
 {
     [MoonSharpUserData]
+    [EmmyAlias("ScenecontrolArgs")]
     internal class LuaScenecontrol
     {
         public int Timing { get; set; }
diff --git a/Assets/Scripts/Compose/EventsEditor/Scenecontrol/ScenecontrolLuaEnvironment.cs b/Assets/Scripts/Compose/EventsEditor/Scenecontrol/ScenecontrolLuaEnvironment.cs
index 842b0408..ab24df38 100644
--- a/Assets/Scripts/Compose/EventsEditor/Scenecontrol/ScenecontrolLuaEnvironment.cs
+++ b/Assets/Scripts/Compose/EventsEditor/Scenecontrol/ScenecontrolLuaEnvironment.cs
@@ -7,6 +7,7 @@
 using ArcCreate.Gameplay.Data;
 using ArcCreate.Gameplay.Scenecontrol;
 using ArcCreate.Utility.Lua;
+using EmmySharp;
 using MoonSharp.Interpreter;
 using UnityEngine;
 
@@ -47,6 +48,7 @@ public void TestReimport()
         public void SetupScript(Script script)
         {
             script.Globals["Channel"] = new ValueChannelBuilder();
+            script.Globals["NoteData"] = new NoteChannelBuilder();
             script.Globals["StringChannel"] = new StringChannelBuilder();
             script.Globals["TextChannel"] = new TextChannelBuilder();
             script.Globals["Trigger"] = new TriggerBuilder();
@@ -65,6 +67,11 @@ public void SetupScript(Script script)
                 return new ConstantChannel((float)dyn.Number);
             });
 
+            Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Boolean, typeof(BooleanChannel), dyn =>
+            {
+                return new BooleanConstantChannel(dyn.CastToBool());
+            });
+
             Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.String, typeof(StringChannel), dyn =>
             {
                 return StringChannelBuilder.Constant(dyn.String);
@@ -84,19 +91,26 @@ public void Rebuild()
             ExecuteEvents();
         }
 
-        public void GenerateEmmyLua()
+        public void GenerateEmmyLua(string file = null)
         {
             Assembly scAssembly = Assembly.GetAssembly(typeof(ScenecontrolService));
             LuaRunner.GetCommonEmmySharp()
+                .AppendAlias("ScenecontrolArgs", EmmyType.Table(
+                    ("timing", EmmyType.Integer),
+                    ("timingGroup", EmmyType.Integer),
+                    ("args", EmmyType.Array(EmmyType.Option(EmmyType.Integer, EmmyType.String)))))
                 .AppendAssembly(scAssembly)
-                .AppendFunction(typeof(ScenecontrolLuaEnvironment).GetMethod("AddScenecontrol"))
-                .AppendFunction(typeof(ScenecontrolLuaEnvironment).GetMethod("Notify"))
-                .AppendFunction(typeof(ScenecontrolLuaEnvironment).GetMethod("NotifyWarn"))
-                .AppendFunction(typeof(ScenecontrolLuaEnvironment).GetMethod("NotifyError"))
-                .Build(Path.GetDirectoryName(Services.Project.CurrentProject.Path));
+                .AppendFunction<ScenecontrolLuaEnvironment>("AddScenecontrol")
+                .AppendFunction<ScenecontrolLuaEnvironment>("Notify")
+                .AppendFunction<ScenecontrolLuaEnvironment>("NotifyWarn")
+                .AppendFunction<ScenecontrolLuaEnvironment>("NotifyError")
+                .Build(file ?? Path.GetDirectoryName(Services.Project.CurrentProject.Path));
         }
 
-        public void AddScenecontrol(string name, DynValue argNames, DynValue scDef)
+        public void AddScenecontrol(
+            string name,
+            [EmmyType(typeof(string[]))] DynValue argNames,
+            [EmmyType("fun(args: ScenecontrolArgs)")] DynValue scDef)
         {
             if (scenecontrolTypes.ContainsKey(name))
             {
diff --git a/Assets/Scripts/Gameplay/Chart/ChartService.cs b/Assets/Scripts/Gameplay/Chart/ChartService.cs
index 5bbdcc4d..5bcf4c3c 100644
--- a/Assets/Scripts/Gameplay/Chart/ChartService.cs
+++ b/Assets/Scripts/Gameplay/Chart/ChartService.cs
@@ -442,12 +442,12 @@ public void UpdateChartJudgement(int currentTiming)
             }
         }
 
-        public void UpdateChartRender(int currentTiming)
+        public void UpdateRenderingNotes(int currentTiming)
         {
             for (int i = 0; i < timingGroups.Count; i++)
             {
                 TimingGroup tg = timingGroups[i];
-                tg.UpdateGroupRender(currentTiming);
+                tg.UpdateRenderingNotes(currentTiming);
 
                 if (i == 0)
                 {
@@ -456,6 +456,14 @@ public void UpdateChartRender(int currentTiming)
             }
         }
 
+        public void Render(int currentTiming)
+        {
+            for (int i = 0; i < timingGroups.Count; i++)
+            {
+                timingGroups[i].Render(currentTiming);
+            }
+        }
+
         public void NotifyEdit()
         {
             gameplayData.NotifyChartEdit();
diff --git a/Assets/Scripts/Gameplay/Chart/IChartService.cs b/Assets/Scripts/Gameplay/Chart/IChartService.cs
index e81354ce..3d9a1d2f 100644
--- a/Assets/Scripts/Gameplay/Chart/IChartService.cs
+++ b/Assets/Scripts/Gameplay/Chart/IChartService.cs
@@ -72,11 +72,17 @@ IEnumerable<T> FindEventsWithinRange<T>(int from, int to)
         /// <param name="currentTiming">The current audio timing.</param>
         void UpdateChartJudgement(int currentTiming);
 
+        /// <summary>
+        /// Update the lists of which notes are rendering this frame.
+        /// </summary>
+        /// <param name="currentTiming">The current audio timing.</param>
+        void UpdateRenderingNotes(int currentTiming);
+
         /// <summary>
         /// Update rendering of all notes in the chart.
         /// </summary>
         /// <param name="currentTiming">The current audio timing.</param>
-        void UpdateChartRender(int currentTiming);
+        void Render(int currentTiming);
 
         void NotifyEdit();
     }
diff --git a/Assets/Scripts/Gameplay/Chart/NoteGroup/ArcNoteGroup.cs b/Assets/Scripts/Gameplay/Chart/NoteGroup/ArcNoteGroup.cs
index 4b777efa..700b6bd8 100644
--- a/Assets/Scripts/Gameplay/Chart/NoteGroup/ArcNoteGroup.cs
+++ b/Assets/Scripts/Gameplay/Chart/NoteGroup/ArcNoteGroup.cs
@@ -6,9 +6,9 @@ namespace ArcCreate.Gameplay.Chart
 {
     public class ArcNoteGroup : LongNoteGroup<Arc>, IComparer<Arc>
     {
-        public override void UpdateRender(int timing, double floorPosition, GroupProperties groupProperties)
+        public override void UpdateRenderingNotes(int timing, double floorPosition, GroupProperties groupProperties)
         {
-            LastRenderingNotes.Clear();
+            RenderingNotes.Clear();
             if (Notes.Count == 0 || !groupProperties.Visible)
             {
                 return;
@@ -32,19 +32,13 @@ public override void UpdateRender(int timing, double floorPosition, GroupPropert
             while (notesInRange.MoveNext())
             {
                 var note = notesInRange.Current;
-                LastRenderingNotes.Add(note);
+                RenderingNotes.Add(note);
                 note.RecalculateDepth(camera, nearClipPlane, farClipPlane, floorPosition);
             }
 
-            if (LastRenderingNotes.Count < 100)
+            if (RenderingNotes.Count < 100)
             {
-                LastRenderingNotes.Sort(this);
-            }
-
-            for (int i = LastRenderingNotes.Count - 1; i >= 0; i--)
-            {
-                Arc note = LastRenderingNotes[i];
-                note.UpdateRender(timing, floorPosition, groupProperties);
+                RenderingNotes.Sort(this);
             }
         }
 
diff --git a/Assets/Scripts/Gameplay/Chart/NoteGroup/LongNoteGroup.cs b/Assets/Scripts/Gameplay/Chart/NoteGroup/LongNoteGroup.cs
index 3283e08b..37a88b49 100644
--- a/Assets/Scripts/Gameplay/Chart/NoteGroup/LongNoteGroup.cs
+++ b/Assets/Scripts/Gameplay/Chart/NoteGroup/LongNoteGroup.cs
@@ -14,13 +14,13 @@ public abstract class LongNoteGroup<Note> : NoteGroup<Note>
     {
         private readonly RangeTree<Note> timingTree = new RangeTree<Note>();
         private readonly RangeTree<Note> floorPositionTree = new RangeTree<Note>();
-        private readonly List<Note> lastRenderingNotes = new List<Note>();
+        private readonly List<Note> renderingNotes = new List<Note>(TimingGroup.RenderingNotesPreallocCount);
 
         protected RangeTree<Note> TimingTree => timingTree;
 
         protected RangeTree<Note> FloorPositionTree => floorPositionTree;
 
-        protected List<Note> LastRenderingNotes => lastRenderingNotes;
+        protected List<Note> RenderingNotes => renderingNotes;
 
         public override void UpdateJudgement(int timing, double floorPosition, GroupProperties groupProperties)
         {
@@ -42,9 +42,9 @@ public override void UpdateJudgement(int timing, double floorPosition, GroupProp
             }
         }
 
-        public override void UpdateRender(int timing, double floorPosition, GroupProperties groupProperties)
+        public override void UpdateRenderingNotes(int timing, double floorPosition, GroupProperties groupProperties)
         {
-            lastRenderingNotes.Clear();
+            renderingNotes.Clear();
             if (Notes.Count == 0 || !groupProperties.Visible)
             {
                 return;
@@ -64,8 +64,15 @@ public override void UpdateRender(int timing, double floorPosition, GroupPropert
             while (notesInRange.MoveNext())
             {
                 var note = notesInRange.Current;
-                lastRenderingNotes.Add(note);
-                note.UpdateRender(timing, floorPosition, groupProperties);
+                renderingNotes.Add(note);
+            }
+        }
+
+        public override void Render(int timing, double floorPosition, GroupProperties groupProperties)
+        {
+            for (int i = 0; i < renderingNotes.Count; i++)
+            {
+                renderingNotes[i].UpdateRender(timing, floorPosition, groupProperties);
             }
         }
 
@@ -168,6 +175,6 @@ public override IEnumerable<Note> FindEventsWithinRange(int from, int to)
             }
         }
 
-        public override IEnumerable<Note> GetRenderingNotes() => lastRenderingNotes;
+        public override IReadOnlyList<Note> GetRenderingNotes() => renderingNotes;
     }
 }
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Chart/NoteGroup/NoteGroup.cs b/Assets/Scripts/Gameplay/Chart/NoteGroup/NoteGroup.cs
index a4e3b858..3be34751 100644
--- a/Assets/Scripts/Gameplay/Chart/NoteGroup/NoteGroup.cs
+++ b/Assets/Scripts/Gameplay/Chart/NoteGroup/NoteGroup.cs
@@ -153,7 +153,15 @@ public void Update(IEnumerable<Note> notes)
         /// <param name="timing">The timing value.</param>
         /// <param name="floorPosition">Floor position value corresponding to the timing value.</param>
         /// <param name="groupProperties">The group properties of the notes' timing group.</param>
-        public abstract void UpdateRender(int timing, double floorPosition, GroupProperties groupProperties);
+        public abstract void UpdateRenderingNotes(int timing, double floorPosition, GroupProperties groupProperties);
+
+        /// <summary>
+        /// Actually render the notes for this frame.
+        /// </summary>
+        /// <param name="timing">The timing value.</param>
+        /// <param name="floorPosition">Floor position value corresponding to the timing value.</param>
+        /// <param name="groupProperties">The group properties of the notes' timing group.</param>
+        public abstract void Render(int timing, double floorPosition, GroupProperties groupProperties);
 
         /// <summary>
         /// Called every time there's a change to the note list.
@@ -185,7 +193,7 @@ public void Update(IEnumerable<Note> notes)
         /// Find all rendering notes.
         /// </summary>
         /// <returns>List of rendering notes.</returns>
-        public abstract IEnumerable<Note> GetRenderingNotes();
+        public abstract IReadOnlyList<Note> GetRenderingNotes();
 
         /// <summary>
         /// Called after notes are loaded into the group.
diff --git a/Assets/Scripts/Gameplay/Chart/NoteGroup/ShortNoteGroup.cs b/Assets/Scripts/Gameplay/Chart/NoteGroup/ShortNoteGroup.cs
index d539c3da..3f0e6bb1 100644
--- a/Assets/Scripts/Gameplay/Chart/NoteGroup/ShortNoteGroup.cs
+++ b/Assets/Scripts/Gameplay/Chart/NoteGroup/ShortNoteGroup.cs
@@ -14,7 +14,7 @@ public abstract class ShortNoteGroup<Note> : NoteGroup<Note>
     {
         private CachedBisect<Note, int> timingSearch;
         private CachedBisect<Note, double> floorPositionSearch;
-        private readonly List<Note> lastRenderingNotes = new List<Note>();
+        private readonly List<Note> renderingNotes = new List<Note>(TimingGroup.RenderingNotesPreallocCount);
 
         public override int ComboAt(int timing)
         {
@@ -44,9 +44,9 @@ public override void UpdateJudgement(int timing, double floorPosition, GroupProp
             }
         }
 
-        public override void UpdateRender(int timing, double floorPosition, GroupProperties groupProperties)
+        public override void UpdateRenderingNotes(int timing, double floorPosition, GroupProperties groupProperties)
         {
-            lastRenderingNotes.Clear();
+            renderingNotes.Clear();
             if (Notes.Count == 0 || !groupProperties.Visible)
             {
                 return;
@@ -71,12 +71,19 @@ public override void UpdateRender(int timing, double floorPosition, GroupPropert
                     break;
                 }
 
-                note.UpdateRender(timing, floorPosition, groupProperties);
-                lastRenderingNotes.Add(note);
+                renderingNotes.Add(note);
                 renderIndex++;
             }
         }
 
+        public override void Render(int timing, double floorPosition, GroupProperties groupProperties)
+        {
+            for (int i = 0; i < renderingNotes.Count; i++)
+            {
+                renderingNotes[i].UpdateRender(timing, floorPosition, groupProperties);
+            }
+        }
+
         public override void RebuildList()
         {
             timingSearch = new CachedBisect<Note, int>(Notes, note => note.Timing);
@@ -123,6 +130,6 @@ public override IEnumerable<Note> FindEventsWithinRange(int from, int to)
             }
         }
 
-        public override IEnumerable<Note> GetRenderingNotes() => lastRenderingNotes;
+        public override IReadOnlyList<Note> GetRenderingNotes() => renderingNotes;
     }
 }
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Chart/TimingGroup.Dispatch.cs b/Assets/Scripts/Gameplay/Chart/TimingGroup.Dispatch.cs
index 52aa721a..995e4f6f 100644
--- a/Assets/Scripts/Gameplay/Chart/TimingGroup.Dispatch.cs
+++ b/Assets/Scripts/Gameplay/Chart/TimingGroup.Dispatch.cs
@@ -2,6 +2,7 @@
 using System.Linq;
 using ArcCreate.ChartFormat;
 using ArcCreate.Gameplay.Data;
+using ArcCreate.Utility.Extension;
 
 namespace ArcCreate.Gameplay.Chart
 {
@@ -10,12 +11,16 @@ namespace ArcCreate.Gameplay.Chart
     /// </summary>
     public partial class TimingGroup
     {
+        public const int RenderingNotesPreallocCount = 40;
+
         private TapNoteGroup taps;
         private HoldNoteGroup holds;
         private ArcNoteGroup arcs;
         private ArcTapNoteGroup arcTaps;
         private GroupProperties groupProperties;
 
+        private readonly List<Note> renderingNotes = new List<Note>(RenderingNotesPreallocCount * 4);
+
         public TimingGroup(int tg)
         {
             GroupNumber = tg;
@@ -29,6 +34,49 @@ public TimingGroup(int tg)
 
         public bool IsVisible { get; set; } = true;
 
+        /// <summary>
+        /// Enable note-individual parameter overrides for this timing group.
+        /// </summary>
+        public void EnableIndividualOverrides()
+        {
+            GroupProperties.IndividualOverrides.Enable(this);
+        }
+
+        /// <summary>
+        /// Disable note-individual parameter overrides for this timing group.
+        /// </summary>
+        public void DisableIndividualOverrides()
+        {
+            GroupProperties.IndividualOverrides.Disable();
+        }
+
+        /// <summary>
+        /// Get a temporary enumerable of every note in this timing group.
+        /// </summary>
+        /// <returns>The temporary enumerable.</returns>
+        public IEnumerable<Note> GetAllNotes()
+        {
+            foreach (var tap in taps.Notes)
+            {
+                yield return tap;
+            }
+
+            foreach (var hold in holds.Notes)
+            {
+                yield return hold;
+            }
+
+            foreach (var arctap in arcTaps.Notes)
+            {
+                yield return arctap;
+            }
+
+            foreach (var arc in arcs.Notes)
+            {
+                yield return arc;
+            }
+        }
+
         /// <summary>
         /// Load a timing group data representation into this instance.
         /// </summary>
@@ -67,6 +115,7 @@ public void Load()
             arcTaps = new ArcTapNoteGroup();
 
             groupProperties = new GroupProperties();
+
             timings = new List<TimingEvent>
             {
                 new TimingEvent
@@ -107,7 +156,7 @@ public void UpdateGroupJudgement(int timing)
             arcTaps.UpdateJudgement(timing, floorPosition, groupProperties);
         }
 
-        public void UpdateGroupRender(int timing)
+        public void UpdateRenderingNotes(int timing)
         {
             if (!IsVisible)
             {
@@ -115,10 +164,30 @@ public void UpdateGroupRender(int timing)
             }
 
             double floorPosition = GetFloorPosition(timing);
-            taps.UpdateRender(timing, floorPosition, groupProperties);
-            holds.UpdateRender(timing, floorPosition, groupProperties);
-            arcs.UpdateRender(timing, floorPosition, groupProperties);
-            arcTaps.UpdateRender(timing, floorPosition, groupProperties);
+            taps.UpdateRenderingNotes(timing, floorPosition, groupProperties);
+            holds.UpdateRenderingNotes(timing, floorPosition, groupProperties);
+            arcs.UpdateRenderingNotes(timing, floorPosition, groupProperties);
+            arcTaps.UpdateRenderingNotes(timing, floorPosition, groupProperties);
+
+            renderingNotes.Clear();
+            renderingNotes.FastAddRange(taps.GetRenderingNotes());
+            renderingNotes.FastAddRange(holds.GetRenderingNotes());
+            renderingNotes.FastAddRange(arcs.GetRenderingNotes());
+            renderingNotes.FastAddRange(arcTaps.GetRenderingNotes());
+        }
+
+        public void Render(int timing)
+        {
+            if (!IsVisible)
+            {
+                return;
+            }
+
+            double floorPosition = GetFloorPosition(timing);
+            taps.Render(timing, floorPosition, groupProperties);
+            holds.Render(timing, floorPosition, groupProperties);
+            arcs.Render(timing, floorPosition, groupProperties);
+            arcTaps.Render(timing, floorPosition, groupProperties);
         }
 
         /// <summary>
@@ -323,27 +392,7 @@ public IEnumerable<T> FindEventsWithinRange<T>(int from, int to)
         /// </summary>
         /// <returns>List of rendering notes.</returns>
         public IEnumerable<Note> GetRenderingNotes()
-        {
-            foreach (var note in taps.GetRenderingNotes())
-            {
-                yield return note;
-            }
-
-            foreach (var note in holds.GetRenderingNotes())
-            {
-                yield return note;
-            }
-
-            foreach (var note in arcTaps.GetRenderingNotes())
-            {
-                yield return note;
-            }
-
-            foreach (var note in arcs.GetRenderingNotes())
-            {
-                yield return note;
-            }
-        }
+            => renderingNotes;
 
         /// <summary>
         /// Clear notes from this timing gruop and destroy all notes.
diff --git a/Assets/Scripts/Gameplay/Data/Events/Arc.Rendering.cs b/Assets/Scripts/Gameplay/Data/Events/Arc.Rendering.cs
index 70f9617c..ed9bd6c1 100644
--- a/Assets/Scripts/Gameplay/Data/Events/Arc.Rendering.cs
+++ b/Assets/Scripts/Gameplay/Data/Events/Arc.Rendering.cs
@@ -2,6 +2,7 @@
 using ArcCreate.Gameplay.Judgement;
 using ArcCreate.Gameplay.Render;
 using ArcCreate.Gameplay.Utility;
+using ArcCreate.Utility;
 using UnityEngine;
 
 namespace ArcCreate.Gameplay.Data
@@ -137,7 +138,7 @@ public override void GetColliderPosition(int timing, out Vector3 pos, out Vector
             double fp = TimingGroupInstance.GetFloorPosition(timing);
             float z = ZPos(fp);
             Vector3 basePos = new Vector3(ArcFormula.ArcXToWorld(XStart), ArcFormula.ArcYToWorld(YStart), 0);
-            pos = (TimingGroupInstance.GroupProperties.FallDirection * z) + basePos;
+            pos = (TimingGroupInstance.GroupProperties.GetFallDirection(this) * z) + basePos;
             scl = TimingGroupInstance.GroupProperties.ScaleIndividual;
         }
 
@@ -146,10 +147,12 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
             float z = ZPos(currentFloorPosition);
 
             Vector3 basePos = new Vector3(ArcFormula.ArcXToWorld(XStart), ArcFormula.ArcYToWorld(YStart), 0);
-            Vector3 pos = (groupProperties.FallDirection * z) + basePos;
-            Quaternion rot = groupProperties.RotationIndividual;
-            Vector3 scl = groupProperties.ScaleIndividual;
-            Matrix4x4 matrix = groupProperties.GroupMatrix * Matrix4x4.TRS(pos, rot, scl);
+            TRS noteTransformation =
+                TRS.TranslateOnly((groupProperties.GetFallDirection(this) * z) + basePos)
+                + groupProperties.GetNoteTransform(this);
+            TRS transformation = noteTransformation * groupProperties.GroupTransform;
+
+            Matrix4x4 matrix = transformation.Matrix;
 
             float alpha = 1;
             float redArcValue = Services.Skin.GetRedArcValue(Color);
@@ -183,7 +186,7 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
                 alpha = EndTiming - Timing <= 1 ? Values.MaxArcAlpha / 2 : Values.MaxArcAlpha;
             }
 
-            Color color = groupProperties.Color;
+            Color color = groupProperties.GetColor(this);
             color.a *= Mathf.Min(alpha, arcGroupAlpha);
 
             int clipToTiming;
@@ -204,7 +207,7 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
                 currentFloorPosition,
                 clipToTiming,
                 clipToFloorPosition,
-                groupProperties.FallDirection,
+                groupProperties.GetFallDirection(this),
                 z,
                 groupProperties.NoClip);
 
@@ -222,7 +225,7 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
                     continue;
                 }
 
-                var (bodyMatrix, shadowMatrix) = segment.GetMatrices(currentFloorPosition, groupProperties.FallDirection, z, pos.y);
+                var (bodyMatrix, shadowMatrix) = segment.GetMatrices(currentFloorPosition, groupProperties.GetFallDirection(this), z, transformation.Translation.y, noteTransformation.Scale);
                 if (IsTrace)
                 {
                     Services.Render.DrawTraceSegment(matrix * bodyMatrix, color, IsSelected);
@@ -243,8 +246,8 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
 
             if (!groupProperties.NoHeightIndicator && (clipToTiming <= Timing || groupProperties.NoClip) && ShouldDrawHeightIndicator)
             {
-                Matrix4x4 heightIndicatorMatrix = Matrix4x4.Scale(new Vector3(1, pos.y - (Values.TraceMeshOffset / 2), 1));
-                Services.Render.DrawHeightIndicator(matrix * heightIndicatorMatrix, heightIndicatorColor * groupProperties.Color);
+                Matrix4x4 heightIndicatorMatrix = Matrix4x4.Scale(new Vector3(1, transformation.Translation.y - (Values.TraceMeshOffset / 2), 1));
+                Services.Render.DrawHeightIndicator(matrix * heightIndicatorMatrix, heightIndicatorColor * color);
             }
 
             if (!groupProperties.NoHead && (clipToTiming <= Timing || groupProperties.NoClip) && IsFirstArcOfGroup)
@@ -261,7 +264,7 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
 
             if (!groupProperties.NoArcCap && shouldDrawArcCap)
             {
-                Services.Render.DrawArcCap(arcCap, matrix * arcCapMatrix, arcCapColor * groupProperties.Color, isControllerMode);
+                Services.Render.DrawArcCap(arcCap, matrix * arcCapMatrix, arcCapColor * color, isControllerMode);
             }
 
             if (currentTiming <= longParticleUntil && currentTiming >= Timing && currentTiming <= EndTiming)
@@ -269,7 +272,7 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
                 Services.Particle.PlayArcParticle(
                     Color,
                     firstArcOfBranch ?? this,
-                    new Vector3(WorldXAt(currentTiming), WorldYAt(currentTiming), 0));
+                    new Vector2(ArcXAt(currentTiming), ArcYAt(currentTiming)));
             }
         }
 
diff --git a/Assets/Scripts/Gameplay/Data/Events/ArcSegmentData.cs b/Assets/Scripts/Gameplay/Data/Events/ArcSegmentData.cs
index 7eaabe36..1c509580 100644
--- a/Assets/Scripts/Gameplay/Data/Events/ArcSegmentData.cs
+++ b/Assets/Scripts/Gameplay/Data/Events/ArcSegmentData.cs
@@ -25,7 +25,7 @@ public float CalculateZPos(double currentFloorPosition)
         public float CalculateEndZPos(double currentFloorPosition)
             => ArcFormula.FloorPositionToZ(EndFloorPosition - currentFloorPosition);
 
-        public (Matrix4x4 body, Matrix4x4 shadow) GetMatrices(double floorPosition, Vector3 fallDirection, float baseZ, float baseY)
+        public (Matrix4x4 body, Matrix4x4 shadow) GetMatrices(double floorPosition, Vector3 fallDirection, float baseZ, float baseY, Vector3 noteScale)
         {
             float startZ = ArcFormula.FloorPositionToZ(FloorPosition - floorPosition);
             float endZ = ArcFormula.FloorPositionToZ(EndFloorPosition - floorPosition);
@@ -34,16 +34,22 @@ public float CalculateEndZPos(double currentFloorPosition)
             startPos = ((endPos - startPos) * From) + startPos;
             Vector3 dir = endPos - startPos;
 
+            startPos.x /= noteScale.x;
+            startPos.y /= noteScale.y;
+
+            endPos.x /= noteScale.x;
+            endPos.y /= noteScale.y;
+
             Matrix4x4 bodyMatrix = new Matrix4x4(
                 new Vector4(1, 0, 0, 0),
                 new Vector4(0, 1, 0, 0),
-                new Vector4(dir.x, dir.y, dir.z, 0),
+                new Vector4(dir.x / noteScale.x, dir.y / noteScale.y, dir.z / noteScale.z, 0),
                 new Vector4(startPos.x, startPos.y, startPos.z, 1));
 
             Matrix4x4 shadowMatrix = new Matrix4x4(
                 new Vector4(1, 0, 0, 0),
                 new Vector4(0, 1, 0, 0),
-                new Vector4(dir.x, 0, dir.z, 0),
+                new Vector4(dir.x / noteScale.x, 0, dir.z / noteScale.z, 0),
                 new Vector4(startPos.x, -baseY, startPos.z, 1));
 
             return (bodyMatrix, shadowMatrix);
diff --git a/Assets/Scripts/Gameplay/Data/Events/ArcTap.cs b/Assets/Scripts/Gameplay/Data/Events/ArcTap.cs
index c28d9cb0..aef27679 100644
--- a/Assets/Scripts/Gameplay/Data/Events/ArcTap.cs
+++ b/Assets/Scripts/Gameplay/Data/Events/ArcTap.cs
@@ -1,6 +1,7 @@
 using System.Collections.Generic;
 using ArcCreate.Gameplay.Judgement;
 using ArcCreate.Gameplay.Render;
+using ArcCreate.Utility;
 using UnityEngine;
 
 namespace ArcCreate.Gameplay.Data
@@ -68,7 +69,9 @@ public override void GetColliderPosition(int timing, out Vector3 pos, out Vector
             double fp = TimingGroupInstance.GetFloorPosition(timing);
             float z = ZPos(fp);
             Vector3 basePos = new Vector3(WorldX, WorldY, 0);
-            pos = (TimingGroupInstance.GroupProperties.FallDirection * z) + basePos;
+            pos = (TimingGroupInstance.GroupProperties.GetFallDirection(this) * z) + basePos;
+
+            // TODO: this code (and other segments like it) fail to account for per-note data
             scl = TimingGroupInstance.GroupProperties.ScaleIndividual;
         }
 
@@ -100,14 +103,16 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
             }
 
             float z = ZPos(currentFloorPosition);
-            Vector3 pos = (groupProperties.FallDirection * z) + new Vector3(WorldX, WorldY, 0);
-            Quaternion rot = groupProperties.RotationIndividual;
-            Vector3 scl = groupProperties.ScaleIndividual;
-            Matrix4x4 matrix = groupProperties.GroupMatrix * Matrix4x4.TRS(pos, rot, scl);
-            Matrix4x4 shadowMatrix = matrix * Matrix4x4.Translate(new Vector3(0, -pos.y, 0));
+            TRS noteTransform =
+                TRS.TranslateOnly((groupProperties.GetFallDirection(this) * z) + new Vector3(WorldX, WorldY, 0))
+                + groupProperties.GetNoteTransform(this);
+            TRS transform = noteTransform * groupProperties.GroupTransform;
+
+            Matrix4x4 matrix = transform.Matrix;
+            Matrix4x4 shadowMatrix = matrix * Matrix4x4.Translate(new Vector3(0, -transform.Translation.y, 0));
 
             float alpha = ArcFormula.CalculateFadeOutAlpha(z);
-            Color color = groupProperties.Color;
+            Color color = groupProperties.GetColor(this);
             color.a *= alpha;
 
             Services.Render.DrawArcTap(isSfx, texture, matrix, color, IsSelected);
diff --git a/Assets/Scripts/Gameplay/Data/Events/Hold.cs b/Assets/Scripts/Gameplay/Data/Events/Hold.cs
index 12e38d59..376c7b96 100644
--- a/Assets/Scripts/Gameplay/Data/Events/Hold.cs
+++ b/Assets/Scripts/Gameplay/Data/Events/Hold.cs
@@ -100,7 +100,7 @@ public override void GetColliderPosition(int timing, out Vector3 pos, out Vector
             float z = ZPos(fp);
             float endZ = EndZPos(fp);
             Vector3 basePos = new Vector3(ArcFormula.LaneToWorldX(Lane), 0, 0);
-            pos = (TimingGroupInstance.GroupProperties.FallDirection * z) + basePos;
+            pos = (TimingGroupInstance.GroupProperties.GetFallDirection(this) * z) + basePos;
             scl = TimingGroupInstance.GroupProperties.ScaleIndividual;
             scl.z *= z - endZ;
         }
@@ -136,12 +136,13 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
 
             float z = ZPos(currentFloorPosition);
             float endZ = EndZPos(currentFloorPosition);
-            Vector3 pos = (groupProperties.FallDirection * z) + new Vector3(ArcFormula.LaneToWorldX(Lane), 0, 0);
-            Quaternion rot = groupProperties.RotationIndividual;
-            Vector3 scl = groupProperties.ScaleIndividual;
-            Matrix4x4 matrix = groupProperties.GroupMatrix
-                             * Matrix4x4.TRS(pos, rot, scl)
-                             * MatrixUtility.Shear(groupProperties.FallDirection * (z - endZ));
+
+            TRS noteTransform =
+                TRS.TranslateOnly((groupProperties.GetFallDirection(this) * z) + new Vector3(ArcFormula.LaneToWorldX(Lane), 0, 0))
+                + groupProperties.GetNoteTransform(this);
+            TRS transform = noteTransform * groupProperties.GroupTransform;
+
+            Matrix4x4 matrix = transform * MatrixUtility.Shear(groupProperties.GetFallDirection(this) * (z - endZ));
 
             float alpha = 1;
             if (highlight)
@@ -170,7 +171,7 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
             }
 
             alpha *= Values.MaxHoldAlpha;
-            Color color = groupProperties.Color;
+            Color color = groupProperties.GetColor(this);
             color.a *= alpha;
 
             float from = 0;
diff --git a/Assets/Scripts/Gameplay/Data/Events/Tap.cs b/Assets/Scripts/Gameplay/Data/Events/Tap.cs
index 450e1909..245d602c 100644
--- a/Assets/Scripts/Gameplay/Data/Events/Tap.cs
+++ b/Assets/Scripts/Gameplay/Data/Events/Tap.cs
@@ -1,6 +1,7 @@
 using System.Collections.Generic;
 using ArcCreate.Gameplay.Judgement;
 using ArcCreate.Gameplay.Render;
+using ArcCreate.Utility;
 using UnityEngine;
 
 namespace ArcCreate.Gameplay.Data
@@ -63,7 +64,7 @@ public override void GetColliderPosition(int timing, out Vector3 pos, out Vector
         {
             float z = ZPos(TimingGroupInstance.GetFloorPosition(timing));
             Vector3 basePos = new Vector3(ArcFormula.LaneToWorldX(Lane), 0, 0);
-            pos = (TimingGroupInstance.GroupProperties.FallDirection * z) + basePos;
+            pos = (TimingGroupInstance.GroupProperties.GetFallDirection(this) * z) + basePos;
             scl = TimingGroupInstance.GroupProperties.ScaleIndividual;
             scl.z *= ArcFormula.CalculateTapSizeScalar(z);
         }
@@ -91,14 +92,20 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
 
             float z = ZPos(currentFloorPosition);
             Vector3 basePos = new Vector3(ArcFormula.LaneToWorldX(Lane), 0, 0);
-            Vector3 pos = (groupProperties.FallDirection * z) + basePos;
-            Quaternion rot = groupProperties.RotationIndividual;
-            Vector3 scl = groupProperties.ScaleIndividual;
-            scl.z *= ArcFormula.CalculateTapSizeScalar(z);
-            Matrix4x4 matrix = groupProperties.GroupMatrix * Matrix4x4.TRS(pos, rot, scl);
+
+            TRS noteTransform =
+                TRS.TranslateOnly((groupProperties.GetFallDirection(this) * z) + basePos)
+                + groupProperties.GetNoteTransform(this);
+            noteTransform.Scale = new Vector3(
+                noteTransform.Scale.x,
+                noteTransform.Scale.y,
+                noteTransform.Scale.z * ArcFormula.CalculateTapSizeScalar(z));
+
+            TRS transform = noteTransform * groupProperties.GroupTransform;
+            Matrix4x4 matrix = transform.Matrix;
 
             float alpha = ArcFormula.CalculateFadeOutAlpha(z);
-            Color color = groupProperties.Color;
+            Color color = groupProperties.GetColor(this);
             Color connectionColor = connectionLineColor;
             color.a *= alpha;
             connectionColor.a *= alpha;
@@ -110,6 +117,9 @@ public void UpdateRender(int currentTiming, double currentFloorPosition, GroupPr
                 Vector3 arctapPos = new Vector3(arctap.WorldX, arctap.WorldY, 0);
                 Vector3 direction = arctapPos - basePos;
 
+                Color thisConnectionColor = connectionColor;
+                thisConnectionColor.a *= Mathf.Min(color.a, groupProperties.GetColor(arctap).a);
+
                 Matrix4x4 lineMatrix = matrix * Matrix4x4.TRS(
                     pos: Vector3.zero,
                     q: Quaternion.LookRotation(direction, Vector3.up),
diff --git a/Assets/Scripts/Gameplay/Data/GroupProperties.cs b/Assets/Scripts/Gameplay/Data/GroupProperties.cs
index d4ed7fff..939857a8 100644
--- a/Assets/Scripts/Gameplay/Data/GroupProperties.cs
+++ b/Assets/Scripts/Gameplay/Data/GroupProperties.cs
@@ -1,5 +1,8 @@
+using System;
+using System.Runtime.CompilerServices;
 using ArcCreate.ChartFormat;
 using ArcCreate.Gameplay.Skin;
+using ArcCreate.Utility;
 using UnityEngine;
 
 namespace ArcCreate.Gameplay.Data
@@ -80,22 +83,43 @@ public GroupProperties(RawTimingGroup raw)
 
         public float SCAngleY { get; set; } = 0;
 
-        public Matrix4x4 GroupMatrix { get; set; } = Matrix4x4.identity;
+        public TRS GroupTransform { get; set; } = TRS.identity;
 
         public bool Visible { get; set; } = true;
 
-        public Vector3 FallDirection
+        public NoteIndividualProperties IndividualOverrides { get; set; } = new NoteIndividualProperties();
+
+        public Vector3 GetFallDirection(Note note = null)
         {
-            get
-            {
-                float angleXf = 90.0f - AngleX - SCAngleX;
-                float angleYf = AngleY + SCAngleY;
+            float angleXf = 90.0f - GetAngleX(note);
+            float angleYf = GetAngleY(note);
 
-                float x = Mathf.Sin(angleXf * Mathf.Deg2Rad) * Mathf.Sin(angleYf * Mathf.Deg2Rad);
-                float y = -Mathf.Cos(angleXf * Mathf.Deg2Rad);
-                float z = Mathf.Sin(angleXf * Mathf.Deg2Rad) * Mathf.Cos(angleYf * Mathf.Deg2Rad);
-                return new Vector3(x, y, z);
-            }
+            float x = Mathf.Sin(angleXf * Mathf.Deg2Rad) * Mathf.Sin(angleYf * Mathf.Deg2Rad);
+            float y = -Mathf.Cos(angleXf * Mathf.Deg2Rad);
+            float z = Mathf.Sin(angleXf * Mathf.Deg2Rad) * Mathf.Cos(angleYf * Mathf.Deg2Rad);
+
+            return new Vector3(x, y, z);
+        }
+
+        public Color GetColor(Note note)
+        {
+            return Color * GetNIProperty(note, Color.white, ni => ni.UseColor, n => n.Color);
+        }
+
+        public TRS GetNoteTransform(Note note)
+        {
+            return GetNIProperty(note, TRS.identity, ni => ni.UsePosition, n => n.Transform)
+                + new TRS(default, RotationIndividual, ScaleIndividual);
+        }
+
+        public float GetAngleX(Note note)
+        {
+            return AngleX + SCAngleX + GetNIProperty(note, 0, ni => ni.UseAngle, n => n.Angles.x);
+        }
+
+        public float GetAngleY(Note note)
+        {
+            return AngleY + SCAngleY + GetNIProperty(note, 0, ni => ni.UseAngle, n => n.Angles.y);
         }
 
         public RawTimingGroup ToRaw()
@@ -117,5 +141,30 @@ public RawTimingGroup ToRaw()
                 ArcResolution = ArcResolution,
             };
         }
+
+        /// <summary>
+        /// Get a property which may be overriden on a per-note basis.
+        /// </summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private T GetNIProperty<T>(Note note, T defaultValue, Predicate<NoteIndividualProperties> nienabled, Func<NoteProperties, T> nivalue)
+        {
+            if (note is null)
+            {
+                return defaultValue;
+            }
+
+            if (!IndividualOverrides.IsEnabled)
+            {
+                return defaultValue;
+            }
+
+            if (!nienabled(IndividualOverrides))
+            {
+                return defaultValue;
+            }
+
+            var ni = IndividualOverrides.PropertiesFor(note);
+            return nivalue(ni);
+        }
     }
 }
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Data/NoteIndividualProperties.cs b/Assets/Scripts/Gameplay/Data/NoteIndividualProperties.cs
new file mode 100644
index 00000000..94ceb8ff
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Data/NoteIndividualProperties.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using ArcCreate.ChartFormat;
+using ArcCreate.Gameplay.Chart;
+using ArcCreate.Gameplay.Skin;
+using ArcCreate.Utility;
+using UnityEngine;
+
+namespace ArcCreate.Gameplay.Data
+{
+    public class NoteIndividualProperties
+    {
+        private Dictionary<Note, NoteProperties> properties;
+
+        public bool IsEnabled => properties != null;
+
+        public bool UseColor { get; set; } = false;
+
+        public bool UsePosition { get; set; } = false;
+
+        public bool UseAngle { get; set; } = false;
+
+        public void Enable(TimingGroup timinggroup)
+        {
+            properties = new Dictionary<Note, NoteProperties>();
+
+            foreach (var note in timinggroup.GetAllNotes())
+            {
+                properties[note] = new NoteProperties();
+            }
+        }
+
+        public void Disable()
+        {
+            properties = null;
+        }
+
+        /// <summary>
+        /// Get the properties for a given note, which can be modified
+        /// for use later on. Returns `null` if <see cref="IsEnabled"/>
+        /// returns false.
+        /// </summary>
+        /// <returns>The properties which are associated with a note.</returns>
+        /// <param name="note">The note to find the properties of.</param>
+        public NoteProperties PropertiesFor(Note note)
+        {
+            if (!IsEnabled)
+            {
+                return null;
+            }
+
+            if (!properties.TryGetValue(note, out var prop))
+            {
+                prop = new NoteProperties();
+                properties[note] = prop;
+            }
+
+            return prop;
+        }
+
+        public void SetAllColors(Color color)
+        {
+            foreach (var props in properties.Values)
+            {
+                props.Color = color;
+            }
+        }
+
+        public void SetAllTransforms(TRS transform)
+        {
+            foreach (var props in properties.Values)
+            {
+                props.Transform = transform;
+            }
+        }
+
+        public void SetAllAngles(Vector2 xy)
+        {
+            foreach (var props in properties.Values)
+            {
+                props.Angles = xy;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Data/NoteIndividualProperties.cs.meta b/Assets/Scripts/Gameplay/Data/NoteIndividualProperties.cs.meta
new file mode 100644
index 00000000..7faa30a4
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Data/NoteIndividualProperties.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7391146f92de24a4ca453d8ff2c7d429
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Data/NoteProperties.cs b/Assets/Scripts/Gameplay/Data/NoteProperties.cs
new file mode 100644
index 00000000..1fdc41be
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Data/NoteProperties.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using ArcCreate.ChartFormat;
+using ArcCreate.Gameplay.Skin;
+using ArcCreate.Utility;
+using UnityEngine;
+
+namespace ArcCreate.Gameplay.Data
+{
+    public class NoteProperties
+    {
+        public Color Color { get; set; } = Color.white;
+
+        public TRS Transform { get; set; } = TRS.identity;
+
+        public Vector2 Angles { get; set; } = Vector2.zero;
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Data/NoteProperties.cs.meta b/Assets/Scripts/Gameplay/Data/NoteProperties.cs.meta
new file mode 100644
index 00000000..1b81e14c
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Data/NoteProperties.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7e06d0efdb3319c44a45e2ee98cea2d9
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/GameplayManager.cs b/Assets/Scripts/Gameplay/GameplayManager.cs
index 1787a844..7e7d4972 100644
--- a/Assets/Scripts/Gameplay/GameplayManager.cs
+++ b/Assets/Scripts/Gameplay/GameplayManager.cs
@@ -143,10 +143,11 @@ private void Update()
 
             Services.Chart.UpdateChartJudgement(currentTiming);
             Services.Judgement.ProcessInput(currentTiming);
-            Services.Chart.UpdateChartRender(currentTiming);
+            Services.Chart.UpdateRenderingNotes(currentTiming);
             Services.Score.UpdateDisplay(currentTiming);
             Services.Camera.UpdateCamera(currentTiming);
             Services.Scenecontrol.UpdateScenecontrol(currentTiming);
+            Services.Chart.Render(currentTiming);
             Services.Render.UpdateRenderers();
             gameplayData.NotifyUpdate(currentTiming);
         }
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels.meta
new file mode 100644
index 00000000..ac3238cf
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1942290d07b96024390ce8857ea25bb0
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/AndChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/AndChannel.cs
new file mode 100644
index 00000000..3c77a2c5
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/AndChannel.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class AndChannel : BooleanChannel
+    {
+        private BooleanChannel a;
+        private BooleanChannel b;
+
+        public AndChannel()
+        {
+        }
+
+        public AndChannel(BooleanChannel a, BooleanChannel b)
+        {
+            this.a = a;
+            this.b = b;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            a = deserialization.GetUnitFromId<BooleanChannel>(properties[0]);
+            b = deserialization.GetUnitFromId<BooleanChannel>(properties[1]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>()
+            {
+                serialization.AddUnitAndGetId(a),
+                serialization.AddUnitAndGetId(b),
+            };
+        }
+
+        public override bool ValueAt(int timing)
+            => a.ValueAt(timing) && b.ValueAt(timing);
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/AndChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/AndChannel.cs.meta
new file mode 100644
index 00000000..62fe2590
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/AndChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 152c57fa2acbe5748b9ecf3bcabc9a54
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanChannel.cs
new file mode 100644
index 00000000..fcc4e263
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanChannel.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using EmmySharp;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [SerializationExempt]
+    [MoonSharpUserData]
+    [EmmyDoc("Channel defining a boolean value at any given input timing value")]
+    public abstract class BooleanChannel : ISerializableUnit, IChannel
+    {
+        public abstract bool ValueAt(int timing);
+
+        [MoonSharpHidden]
+        public abstract List<object> SerializeProperties(ScenecontrolSerialization serialization);
+
+        [MoonSharpHidden]
+        public abstract void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization);
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanChannel.cs.meta
new file mode 100644
index 00000000..1e27bfaf
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4948eb5efc7db1e47b87f00c4014ea1d
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanConstantChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanConstantChannel.cs
new file mode 100644
index 00000000..d62a702a
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanConstantChannel.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class BooleanConstantChannel : BooleanChannel
+    {
+        private bool value;
+
+        public BooleanConstantChannel()
+        {
+        }
+
+        public BooleanConstantChannel(bool value)
+        {
+            this.value = value;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            value = (bool)properties[0];
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>()
+            {
+                value,
+            };
+        }
+
+        public override bool ValueAt(int timing)
+            => value;
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanConstantChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanConstantChannel.cs.meta
new file mode 100644
index 00000000..e7476bd2
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/BooleanConstantChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7d65d7a18ebe25a4c93bef5968bebade
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonType.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonType.cs
new file mode 100644
index 00000000..447912c3
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonType.cs
@@ -0,0 +1,12 @@
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    public enum ComparisonType
+    {
+        False,
+        Equals,
+        GreaterThan,
+        LessThan,
+        GreaterEqual,
+        LessEqual,
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonType.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonType.cs.meta
new file mode 100644
index 00000000..438d76f3
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonType.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bc238786d9110204995a2697293e0e43
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonUtilities.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonUtilities.cs
new file mode 100644
index 00000000..7e840666
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonUtilities.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    public static class ComparisonUtilities
+    {
+        public static bool Compare<T, C>(this ComparisonType type, C comparer, T a, T b)
+            where C : IComparer<T>
+        {
+            var c = comparer.Compare(a, b);
+
+            switch (type)
+            {
+                case ComparisonType.Equals: return c == 0;
+                case ComparisonType.GreaterThan: return c > 0;
+                case ComparisonType.LessThan: return c < 0;
+                case ComparisonType.GreaterEqual: return c >= 0;
+                case ComparisonType.LessEqual: return c <= 0;
+
+                default: return false;
+            }
+        }
+
+        public static bool Compare<T>(this ComparisonType type, T a, T b)
+            where T : IComparable<T>
+        {
+            var c = a.CompareTo(b);
+
+            switch (type)
+            {
+                case ComparisonType.Equals: return c == 0;
+                case ComparisonType.GreaterThan: return c > 0;
+                case ComparisonType.LessThan: return c < 0;
+                case ComparisonType.GreaterEqual: return c >= 0;
+                case ComparisonType.LessEqual: return c <= 0;
+
+                default: return false;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonUtilities.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonUtilities.cs.meta
new file mode 100644
index 00000000..dc5c92ac
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/ComparisonUtilities.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: aecc1a1f90a3c90408f927c158e5d6b0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NotChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NotChannel.cs
new file mode 100644
index 00000000..ce8d4a65
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NotChannel.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NotChannel : BooleanChannel
+    {
+        private BooleanChannel a;
+
+        public NotChannel()
+        {
+        }
+
+        public NotChannel(BooleanChannel a)
+        {
+            this.a = a;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            a = deserialization.GetUnitFromId<BooleanChannel>(properties[0]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>()
+            {
+                serialization.AddUnitAndGetId(a),
+            };
+        }
+
+        public override bool ValueAt(int timing)
+            => !a.ValueAt(timing);
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NotChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NotChannel.cs.meta
new file mode 100644
index 00000000..7443e6e3
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NotChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0c9e2de5727024a4a9eede52daaf8301
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NumericalComparisonChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NumericalComparisonChannel.cs
new file mode 100644
index 00000000..dc035b90
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NumericalComparisonChannel.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NumericalComparisonChannel : BooleanChannel
+    {
+        private ValueChannel a;
+        private ValueChannel b;
+        private ComparisonType comparison;
+
+        public NumericalComparisonChannel()
+        {
+        }
+
+        public NumericalComparisonChannel(ValueChannel a, ValueChannel b, ComparisonType c)
+        {
+            this.a = a;
+            this.b = b;
+            comparison = c;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            a = deserialization.GetUnitFromId<ValueChannel>(properties[0]);
+            b = deserialization.GetUnitFromId<ValueChannel>(properties[1]);
+            comparison = (ComparisonType)properties[2];
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>()
+            {
+                serialization.AddUnitAndGetId(a),
+                serialization.AddUnitAndGetId(b),
+                comparison,
+            };
+        }
+
+        public override bool ValueAt(int timing)
+            => comparison.Compare(a.ValueAt(timing), b.ValueAt(timing));
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NumericalComparisonChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NumericalComparisonChannel.cs.meta
new file mode 100644
index 00000000..ea40e4e1
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/NumericalComparisonChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 70a78767ec27e224b970a8fd25f0652b
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/OrChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/OrChannel.cs
new file mode 100644
index 00000000..0b3a3a09
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/OrChannel.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class OrChannel : BooleanChannel
+    {
+        private BooleanChannel a;
+        private BooleanChannel b;
+
+        public OrChannel()
+        {
+        }
+
+        public OrChannel(BooleanChannel a, BooleanChannel b)
+        {
+            this.a = a;
+            this.b = b;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            a = deserialization.GetUnitFromId<BooleanChannel>(properties[0]);
+            b = deserialization.GetUnitFromId<BooleanChannel>(properties[1]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>()
+            {
+                serialization.AddUnitAndGetId(a),
+                serialization.AddUnitAndGetId(b),
+            };
+        }
+
+        public override bool ValueAt(int timing)
+            => a.ValueAt(timing) || b.ValueAt(timing);
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/OrChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/OrChannel.cs.meta
new file mode 100644
index 00000000..b32e5603
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/OrChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 642be99af590eea4fa960a543aa885a5
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/StringComparisonChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/StringComparisonChannel.cs
new file mode 100644
index 00000000..a6415073
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/StringComparisonChannel.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class StringComparisonChannel : BooleanChannel
+    {
+        private StringChannel a;
+        private StringChannel b;
+        private ComparisonType comparison;
+
+        public StringComparisonChannel()
+        {
+        }
+
+        public StringComparisonChannel(StringChannel a, StringChannel b, ComparisonType c)
+        {
+            this.a = a;
+            this.b = b;
+            comparison = c;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            a = deserialization.GetUnitFromId<StringChannel>(properties[0]);
+            b = deserialization.GetUnitFromId<StringChannel>(properties[1]);
+            comparison = (ComparisonType)properties[2];
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>()
+            {
+                serialization.AddUnitAndGetId(a),
+                serialization.AddUnitAndGetId(b),
+                comparison,
+            };
+        }
+
+        public override bool ValueAt(int timing)
+            => comparison.Compare(a.ValueAt(timing), b.ValueAt(timing));
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/StringComparisonChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/StringComparisonChannel.cs.meta
new file mode 100644
index 00000000..5d358755
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/BooleanChannels/StringComparisonChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 579d484d1f5ea2f44be28baefdb4ca90
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/IChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/IChannel.cs
new file mode 100644
index 00000000..164d4fa7
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/IChannel.cs
@@ -0,0 +1,6 @@
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    public interface IChannel
+    {
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/IChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/IChannel.cs.meta
new file mode 100644
index 00000000..345a1ce0
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/IChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3d56ebf1f51b0c14db5b7ebc9757b4ba
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/AbsChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/AbsChannel.cs
new file mode 100644
index 00000000..7737b61a
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/AbsChannel.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+using UnityEngine;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class AbsChannel : ValueChannel
+    {
+        private ValueChannel target;
+
+        public AbsChannel()
+        {
+        }
+
+        public AbsChannel(ValueChannel channel)
+        {
+            target = channel;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            target = deserialization.GetUnitFromId<ValueChannel>(properties[0]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>
+            {
+                serialization.AddUnitAndGetId(target),
+            };
+        }
+
+        public override float ValueAt(int timing)
+        {
+            return Mathf.Abs(target.ValueAt(timing));
+        }
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield return target;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/AbsChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/AbsChannel.cs.meta
new file mode 100644
index 00000000..8801a04c
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/AbsChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 665e09baef6b5be4f9d731f590f02805
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ChainChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ChainChannel.cs
new file mode 100644
index 00000000..b01c6747
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ChainChannel.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class ChainChannel : ValueChannel
+    {
+        private ValueChannel outer;
+        private ValueChannel inner;
+
+        public ChainChannel()
+        {
+        }
+
+        public ChainChannel(ValueChannel a, ValueChannel b)
+        {
+            this.outer = a;
+            this.inner = b;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            outer = deserialization.GetUnitFromId<ValueChannel>(properties[0]);
+            inner = deserialization.GetUnitFromId<ValueChannel>(properties[1]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>
+            {
+                serialization.AddUnitAndGetId(outer),
+                serialization.AddUnitAndGetId(inner),
+            };
+        }
+
+        public override float ValueAt(int timing)
+            => outer.ValueAt((int)inner.ValueAt(timing));
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield return outer;
+            yield return inner;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ChainChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ChainChannel.cs.meta
new file mode 100644
index 00000000..94fb4de6
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ChainChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4c37e31490ab3fb498b1fae95e37a059
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ClampChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ClampChannel.cs
index de573de6..5e9370ff 100644
--- a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ClampChannel.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ClampChannel.cs
@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using ArcCreate.Gameplay.Data;
 using MoonSharp.Interpreter;
 using UnityEngine;
 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/IfElseChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/IfElseChannel.cs
new file mode 100644
index 00000000..d055a07e
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/IfElseChannel.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class IfElseChannel : ValueChannel
+    {
+        private BooleanChannel cond;
+        private ValueChannel onTrue;
+        private ValueChannel onFalse;
+
+        public IfElseChannel()
+        {
+        }
+
+        public IfElseChannel(BooleanChannel cond, ValueChannel onTrue, ValueChannel onFalse)
+        {
+            this.cond = cond;
+            this.onTrue = onTrue;
+            this.onFalse = onFalse;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            cond = deserialization.GetUnitFromId<BooleanChannel>(properties[0]);
+            onTrue = deserialization.GetUnitFromId<ValueChannel>(properties[1]);
+            onFalse = deserialization.GetUnitFromId<ValueChannel>(properties[2]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>
+            {
+                serialization.AddUnitAndGetId(cond),
+                serialization.AddUnitAndGetId(onTrue),
+                serialization.AddUnitAndGetId(onFalse),
+            };
+        }
+
+        public override float ValueAt(int timing)
+        {
+            return cond.ValueAt(timing) ? onTrue.ValueAt(timing) : onFalse.ValueAt(timing);
+        }
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield return onTrue;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/IfElseChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/IfElseChannel.cs.meta
new file mode 100644
index 00000000..5469a553
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/IfElseChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a93b1483d5c33344f8959f59d41c6235
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ModuloChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ModuloChannel.cs
new file mode 100644
index 00000000..51ae935e
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ModuloChannel.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class ModuloChannel : ValueChannel
+    {
+        private ValueChannel a;
+        private ValueChannel b;
+
+        public ModuloChannel()
+        {
+        }
+
+        public ModuloChannel(ValueChannel a, ValueChannel b)
+        {
+            this.a = a;
+            this.b = b;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            a = deserialization.GetUnitFromId<ValueChannel>(properties[0]);
+            b = deserialization.GetUnitFromId<ValueChannel>(properties[1]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>
+            {
+                serialization.AddUnitAndGetId(a),
+                serialization.AddUnitAndGetId(b),
+            };
+        }
+
+        public override float ValueAt(int timing)
+            => a.ValueAt(timing) % b.ValueAt(timing);
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield return a;
+            yield return b;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ModuloChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ModuloChannel.cs.meta
new file mode 100644
index 00000000..06bd10e0
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/ModuloChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 351a1ba733a922c46bacb73ffd80e407
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureCosChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureCosChannel.cs
new file mode 100644
index 00000000..d467c131
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureCosChannel.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+using UnityEngine;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class PureCosChannel : ValueChannel
+    {
+        private ValueChannel input;
+
+        public PureCosChannel()
+        {
+        }
+
+        public PureCosChannel(ValueChannel input)
+        {
+            this.input = input;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            input = deserialization.GetUnitFromId<ValueChannel>(properties[0]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>()
+            {
+                serialization.AddUnitAndGetId(input),
+            };
+        }
+
+        public override float ValueAt(int timing)
+        {
+            return Mathf.Cos(input.ValueAt(timing));
+        }
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield return input;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureCosChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureCosChannel.cs.meta
new file mode 100644
index 00000000..db571ec2
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureCosChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e729a79b4fd03c54981b3db13691bae1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureSineChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureSineChannel.cs
new file mode 100644
index 00000000..519cd227
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureSineChannel.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+using UnityEngine;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class PureSineChannel : ValueChannel
+    {
+        private ValueChannel input;
+
+        public PureSineChannel()
+        {
+        }
+
+        public PureSineChannel(ValueChannel input)
+        {
+            this.input = input;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            input = deserialization.GetUnitFromId<ValueChannel>(properties[0]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>()
+            {
+                serialization.AddUnitAndGetId(input),
+            };
+        }
+
+        public override float ValueAt(int timing)
+        {
+            return Mathf.Sin(input.ValueAt(timing));
+        }
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield return input;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureSineChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureSineChannel.cs.meta
new file mode 100644
index 00000000..bca78b7a
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/PureSineChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2731d73fd6bb78946bfb7a0ee49551cc
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeScaleChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeScaleChannel.cs
new file mode 100644
index 00000000..f683d0ec
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeScaleChannel.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class TimeScaleChannel : ValueChannel
+    {
+        private ValueChannel value;
+        private ValueChannel scale;
+
+        public TimeScaleChannel()
+        {
+        }
+
+        public TimeScaleChannel(ValueChannel a, ValueChannel b)
+        {
+            this.value = a;
+            this.scale = b;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            value = deserialization.GetUnitFromId<ValueChannel>(properties[0]);
+            scale = deserialization.GetUnitFromId<ValueChannel>(properties[1]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>
+            {
+                serialization.AddUnitAndGetId(value),
+                serialization.AddUnitAndGetId(scale),
+            };
+        }
+
+        public override float ValueAt(int timing)
+            => value.ValueAt(timing * (int)scale.ValueAt(timing));
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield return value;
+            yield return scale;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeScaleChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeScaleChannel.cs.meta
new file mode 100644
index 00000000..12f78352
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeScaleChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c7fcc6f4fb7ce3a4b9dc99bae6c28ab6
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeShiftChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeShiftChannel.cs
new file mode 100644
index 00000000..136a2270
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeShiftChannel.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class TimeShiftChannel : ValueChannel
+    {
+        private ValueChannel value;
+        private ValueChannel shift;
+
+        public TimeShiftChannel()
+        {
+        }
+
+        public TimeShiftChannel(ValueChannel a, ValueChannel b)
+        {
+            this.value = a;
+            this.shift = b;
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+            value = deserialization.GetUnitFromId<ValueChannel>(properties[0]);
+            shift = deserialization.GetUnitFromId<ValueChannel>(properties[1]);
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>
+            {
+                serialization.AddUnitAndGetId(value),
+                serialization.AddUnitAndGetId(shift),
+            };
+        }
+
+        public override float ValueAt(int timing)
+            => value.ValueAt(timing + (int)shift.ValueAt(timing));
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield return value;
+            yield return shift;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeShiftChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeShiftChannel.cs.meta
new file mode 100644
index 00000000..9fd8c3a1
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimeShiftChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ac08633a05fd5934a990c262b614596e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimingChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimingChannel.cs
new file mode 100644
index 00000000..a6f59ab2
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimingChannel.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using ArcCreate.Gameplay.Data;
+using MoonSharp.Interpreter;
+using UnityEngine;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class TimingChannel : ValueChannel
+    {
+        public TimingChannel()
+        {
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>();
+        }
+
+        public override float ValueAt(int timing)
+            => timing;
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimingChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimingChannel.cs.meta
new file mode 100644
index 00000000..21c705fd
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/MathChannels/TimingChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 33b7c5ff69f6b794d9029c480780909b
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels.meta
new file mode 100644
index 00000000..ba0f698d
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9d49474509046a04f9c8671cd98d44ad
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteChannelBuilder.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteChannelBuilder.cs
new file mode 100644
index 00000000..df595ee3
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteChannelBuilder.cs
@@ -0,0 +1,48 @@
+using EmmySharp;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    [EmmyAlias("NoteData")]
+    [EmmyDoc("Class for getting data about notes")]
+    [EmmySingleton]
+    public class NoteChannelBuilder
+    {
+        [EmmyDoc("Channel which returns the timing of a given node.")]
+        public static NoteTimingChannel Timing()
+            => new NoteTimingChannel();
+
+        [EmmyDoc("Channel which returns the floor-position of a given note.")]
+        public static NoteFloorPositionChannel FloorPos()
+            => new NoteFloorPositionChannel();
+
+        [EmmyDoc("Channel which returns the x-position of a given note at its start time.")]
+        public static NoteXPositionChannel X()
+            => new NoteXPositionChannel();
+
+        [EmmyDoc("Channel which returns the y-position of a given note at its start time.")]
+        public static NoteYPositionChannel Y()
+            => new NoteYPositionChannel();
+
+        [EmmyDoc("Channel which returns the z-position of a given note at its start time.")]
+        public static NoteZPositionChannel Z()
+            => new NoteZPositionChannel();
+
+        [EmmyDoc("Channel which returns the signed time until a given note is started.")]
+        public static SumChannel Delta()
+            => Timing() - ValueChannelBuilder.Timing();
+
+        [EmmyDoc("Channel which returns the signed time until a given note is started.")]
+        public static NoteIDChannel ID()
+            => new NoteIDChannel();
+
+        [EmmyDoc("Channel which returns if a note is an arc")]
+        public static NoteIsArcChannel IsArc()
+            => new NoteIsArcChannel();
+
+        [EmmyDoc("Channel which returns the type of a note")]
+        public static NoteTypeChannel Type()
+            => new NoteTypeChannel();
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteChannelBuilder.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteChannelBuilder.cs.meta
new file mode 100644
index 00000000..86b4ff16
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteChannelBuilder.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8ccd9c80026b5de459675f4e1ef2219e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteFloorPositionChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteFloorPositionChannel.cs
new file mode 100644
index 00000000..f22086a7
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteFloorPositionChannel.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NoteFloorPositionChannel : ValueChannel
+    {
+        public NoteFloorPositionChannel()
+        {
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>();
+        }
+
+        public override float ValueAt(int timing)
+            => (float?)NoteIndividualController.CurrentNote?.FloorPosition ?? 0;
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteFloorPositionChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteFloorPositionChannel.cs.meta
new file mode 100644
index 00000000..17c05dd5
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteFloorPositionChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3c9224b62a7f3cf45bc65cccbb9fcefd
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIDChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIDChannel.cs
new file mode 100644
index 00000000..79b23efd
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIDChannel.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NoteIDChannel : ValueChannel
+    {
+        private static ObjectIDGenerator idGenerator = new ObjectIDGenerator();
+
+        public NoteIDChannel()
+        {
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>();
+        }
+
+        public override float ValueAt(int timing)
+        {
+            if (NoteIndividualController.CurrentNote == null)
+            {
+                return 0;
+            }
+
+            return idGenerator.GetId(NoteIndividualController.CurrentNote, out _);
+        }
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIDChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIDChannel.cs.meta
new file mode 100644
index 00000000..3c6bc407
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIDChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d7ff9f0726d94744e8dd77e5753052bb
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIsArcChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIsArcChannel.cs
new file mode 100644
index 00000000..92c1fd59
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIsArcChannel.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using ArcCreate.Gameplay.Data;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NoteIsArcChannel : BooleanChannel
+    {
+        public NoteIsArcChannel()
+        {
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>();
+        }
+
+        public override bool ValueAt(int timing)
+            => NoteIndividualController.CurrentNote is Arc;
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIsArcChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIsArcChannel.cs.meta
new file mode 100644
index 00000000..88885561
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteIsArcChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 22d2f60edca1866469174237dcc7bd5f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTimingChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTimingChannel.cs
new file mode 100644
index 00000000..9f9db2cf
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTimingChannel.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NoteTimingChannel : ValueChannel
+    {
+        public NoteTimingChannel()
+        {
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>();
+        }
+
+        public override float ValueAt(int timing)
+            => NoteIndividualController.CurrentNote?.Timing ?? 0;
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTimingChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTimingChannel.cs.meta
new file mode 100644
index 00000000..73202dcc
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTimingChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a738b871116a4c746945dae0ac16ede2
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTypeChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTypeChannel.cs
new file mode 100644
index 00000000..8cb0c0b1
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTypeChannel.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using ArcCreate.Gameplay.Data;
+using EmmySharp;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NoteTypeChannel : StringChannel
+    {
+        public NoteTypeChannel()
+        {
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>();
+        }
+
+        [return: EmmyType(@"""tap"" | ""hold"" | ""arctap"" | ""arc""")]
+        public override string ValueAt(int timing)
+        {
+            switch (NoteIndividualController.CurrentNote)
+            {
+                case Tap t:
+                    return "tap";
+                case Hold h:
+                    return "hold";
+                case ArcTap at:
+                    return "arctap";
+                case Arc a:
+                    return "arc";
+            }
+
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTypeChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTypeChannel.cs.meta
new file mode 100644
index 00000000..038681d6
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteTypeChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: af45b2b15a3e40a409c1adc35ef8dee7
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteXPositionChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteXPositionChannel.cs
new file mode 100644
index 00000000..60d2718c
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteXPositionChannel.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NoteXPositionChannel : ValueChannel
+    {
+        public NoteXPositionChannel()
+        {
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>();
+        }
+
+        public override float ValueAt(int timing)
+        {
+            if (NoteIndividualController.CurrentNote == null)
+            {
+                return 0;
+            }
+
+            return ArcFormula.UnmodifiedWorldPosition(NoteIndividualController.CurrentNote).x;
+        }
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteXPositionChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteXPositionChannel.cs.meta
new file mode 100644
index 00000000..4b508cae
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteXPositionChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b65a4c1967bbb52429e94b2b89122cd9
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteYPositionChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteYPositionChannel.cs
new file mode 100644
index 00000000..4bcb3887
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteYPositionChannel.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NoteYPositionChannel : ValueChannel
+    {
+        public NoteYPositionChannel()
+        {
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>();
+        }
+
+        public override float ValueAt(int timing)
+        {
+            if (NoteIndividualController.CurrentNote == null)
+            {
+                return 0;
+            }
+
+            return ArcFormula.UnmodifiedWorldPosition(NoteIndividualController.CurrentNote).y;
+        }
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteYPositionChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteYPositionChannel.cs.meta
new file mode 100644
index 00000000..b34da2d5
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteYPositionChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 089e404ac27ab89428bfc08614d41806
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteZPositionChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteZPositionChannel.cs
new file mode 100644
index 00000000..5cbb0cd9
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteZPositionChannel.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using MoonSharp.Interpreter;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NoteZPositionChannel : ValueChannel
+    {
+        public NoteZPositionChannel()
+        {
+        }
+
+        public override void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
+        {
+        }
+
+        public override List<object> SerializeProperties(ScenecontrolSerialization serialization)
+        {
+            return new List<object>();
+        }
+
+        public override float ValueAt(int timing)
+        {
+            if (NoteIndividualController.CurrentNote == null)
+            {
+                return 0;
+            }
+
+            return NoteIndividualController.CurrentNote.ZPos(
+                NoteIndividualController.CurrentNote.TimingGroupInstance.GetFloorPosition(timing));
+        }
+
+        protected override IEnumerable<ValueChannel> GetChildrenChannels()
+        {
+            yield break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteZPositionChannel.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteZPositionChannel.cs.meta
new file mode 100644
index 00000000..5f8a44ba
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/NoteChannels/NoteZPositionChannel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b587686ef898b8e41a00ab388027d20f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/StringChannels/StringChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/StringChannels/StringChannel.cs
index 310e0c11..98110b82 100644
--- a/Assets/Scripts/Gameplay/Scenecontrol/Channels/StringChannels/StringChannel.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/StringChannels/StringChannel.cs
@@ -6,7 +6,7 @@ namespace ArcCreate.Gameplay.Scenecontrol
 {
     [MoonSharpUserData]
     [EmmyDoc("Channel defining a string value at any given input timing value")]
-    public abstract class StringChannel : ISerializableUnit
+    public abstract class StringChannel : ISerializableUnit, IChannel
     {
         public abstract string ValueAt(int timing);
 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/TextChannels/TextChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/TextChannels/TextChannel.cs
index e9632420..ee2570dc 100644
--- a/Assets/Scripts/Gameplay/Scenecontrol/Channels/TextChannels/TextChannel.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/TextChannels/TextChannel.cs
@@ -6,7 +6,7 @@ namespace ArcCreate.Gameplay.Scenecontrol
 {
     [MoonSharpUserData]
     [EmmyDoc("Channel defining text at any given input value. Used for controlling TextController's text content")]
-    public abstract class TextChannel : ISerializableUnit
+    public abstract class TextChannel : ISerializableUnit, IChannel
     {
         public abstract int MaxLength { get; }
 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/ValueChannel.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/ValueChannel.cs
index b9710edd..fe6c8203 100644
--- a/Assets/Scripts/Gameplay/Scenecontrol/Channels/ValueChannel.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/ValueChannel.cs
@@ -4,9 +4,10 @@
 
 namespace ArcCreate.Gameplay.Scenecontrol
 {
+    [SerializationExempt]
     [MoonSharpUserData]
     [EmmyDoc("A generic channel that defines a value for a given input timing value")]
-    public abstract class ValueChannel : ISerializableUnit
+    public abstract class ValueChannel : ISerializableUnit, IChannel
     {
         [MoonSharpHidden]
         public static ValueChannel ConstantZeroChannel { get; } = new ConstantChannel(0);
@@ -45,6 +46,12 @@ public abstract class ValueChannel : ISerializableUnit
 
         public static ProductChannel operator /(ValueChannel a, ValueChannel b) => a * new InverseChannel(b);
 
+        public static ModuloChannel operator %(ValueChannel a, ValueChannel b) => new ModuloChannel(a, b);
+
+        public static ModuloChannel operator %(float a, ValueChannel b) => new ModuloChannel(new ConstantChannel(a), b);
+
+        public static ModuloChannel operator %(ValueChannel a, float b) => new ModuloChannel(a, new ConstantChannel(b));
+
         [EmmyDoc("Gets the value of this channel at the provided timing point")]
         public abstract float ValueAt(int timing);
 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Channels/ValueChannelBuilder.cs b/Assets/Scripts/Gameplay/Scenecontrol/Channels/ValueChannelBuilder.cs
index d1f2a6c0..61e934c0 100644
--- a/Assets/Scripts/Gameplay/Scenecontrol/Channels/ValueChannelBuilder.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Channels/ValueChannelBuilder.cs
@@ -86,5 +86,73 @@ public static SawChannel Saw(
         [EmmyDoc("Create a periodic sine channel")]
         public static SineChannel Sine(ValueChannel period, ValueChannel min, ValueChannel max, ValueChannel offset)
             => new SineChannel(period, min, max, offset);
+
+        [EmmyDoc("Create a pure sine channel")]
+        public static PureSineChannel Sine(ValueChannel input)
+            => new PureSineChannel(input);
+
+        [EmmyDoc("Create a pure cosine channel")]
+        public static PureCosChannel Cos(ValueChannel input)
+            => new PureCosChannel(input);
+
+        [EmmyDoc("Create an absolute value channel")]
+        public static AbsChannel Abs(ValueChannel input)
+            => new AbsChannel(input);
+
+        [EmmyDoc("Shift the timing input of a channel by another channel's output")]
+        public static TimeShiftChannel TimeShift(ValueChannel value, ValueChannel shift)
+            => new TimeShiftChannel(value, shift);
+
+        [EmmyDoc("Scale the timing input of a channel by another channel's output")]
+        public static TimeScaleChannel TimeScale(ValueChannel value, ValueChannel scale)
+            => new TimeScaleChannel(value, scale);
+
+        [EmmyDoc("Chain two channels together by sampling the outer channel with the result of the inner")]
+        public static ChainChannel Chain(ValueChannel outer, ValueChannel inner)
+            => new ChainChannel(outer, inner);
+
+        [EmmyDoc("Create a channel which returns the current timing")]
+        public static TimingChannel Timing()
+            => new TimingChannel();
+
+        [EmmyDoc("Inverts a boolean channel")]
+        public static NotChannel Not(BooleanChannel input)
+            => new NotChannel(input);
+
+        [EmmyDoc("Ands two boolean channel")]
+        public static AndChannel And(BooleanChannel a, BooleanChannel b)
+            => new AndChannel(a, b);
+
+        [EmmyDoc("Ors two boolean channel")]
+        public static OrChannel Or(BooleanChannel a, BooleanChannel b)
+            => new OrChannel(a, b);
+
+        [EmmyDoc("Checks if two channels are equal")]
+        public static BooleanChannel Equal(ValueChannel a, ValueChannel b)
+            => new NumericalComparisonChannel(a, b, ComparisonType.Equals);
+
+        [EmmyDoc("Checks if two string channels are equal")]
+        public static BooleanChannel Equal(StringChannel a, StringChannel b)
+            => new StringComparisonChannel(a, b, ComparisonType.Equals);
+
+        [EmmyDoc("Switches between one channel to another based on a condition")]
+        public static ValueChannel IfElse(BooleanChannel condition, ValueChannel onTrue, ValueChannel onFalse)
+            => new IfElseChannel(condition, onTrue, onFalse);
+
+        [EmmyDoc("Checks if one channel is greater than another")]
+        public static BooleanChannel GreaterThan(ValueChannel a, ValueChannel b)
+            => new NumericalComparisonChannel(a, b, ComparisonType.GreaterThan);
+
+        [EmmyDoc("Checks if one channel is greater than or equal to another")]
+        public static BooleanChannel GreaterEqual(ValueChannel a, ValueChannel b)
+            => new NumericalComparisonChannel(a, b, ComparisonType.GreaterEqual);
+
+        [EmmyDoc("Checks if one channel is greater than another")]
+        public static BooleanChannel LessThan(ValueChannel a, ValueChannel b)
+            => new NumericalComparisonChannel(a, b, ComparisonType.LessThan);
+
+        [EmmyDoc("Checks if one channel is greater than or equal to another")]
+        public static BooleanChannel LessEqual(ValueChannel a, ValueChannel b)
+            => new NumericalComparisonChannel(a, b, ComparisonType.LessEqual);
     }
 }
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Controller.cs b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Controller.cs
index ad5fd5cf..838aab9b 100755
--- a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Controller.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Controller.cs
@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using ArcCreate.Gameplay.Data;
 using ArcCreate.Utility.Lua;
 using MoonSharp.Interpreter;
 using UnityEngine;
@@ -129,10 +130,14 @@ public void CopyAllChannelsFrom(Controller controller)
                 txt.EnableTextModule = txt2.EnableTextModule;
             }
 
+            if (this is IAngleController a && controller is IAngleController a2)
+            {
+                a.AngleX = a2.AngleX;
+                a.AngleY = a2.AngleY;
+            }
+
             if (this is INoteGroupController tg && controller is INoteGroupController tg2)
             {
-                tg.AngleX = tg2.AngleX;
-                tg.AngleY = tg2.AngleY;
                 tg.RotationIndividualX = tg2.RotationIndividualX;
                 tg.RotationIndividualY = tg2.RotationIndividualY;
                 tg.RotationIndividualZ = tg2.RotationIndividualZ;
@@ -192,63 +197,90 @@ public virtual void UpdateController(int timing)
                 return;
             }
 
-            if (this is IPositionController pos && pos.EnablePositionModule)
+            // Potentially individual updates
+            void UpdateMaybeIndividual()
             {
-                Vector3 translation = pos.DefaultTranslation;
-                Vector3 rotation = pos.DefaultRotation.eulerAngles;
-                Vector3 scale = pos.DefaultScale;
+                if (this is IPositionController pos && pos.EnablePositionModule)
+                {
+                    Vector3 translation = pos.DefaultTranslation;
+                    Vector3 rotation = pos.DefaultRotation.eulerAngles;
+                    Vector3 scale = pos.DefaultScale;
 
-                translation.x = pos.TranslationX.ValueAt(timing);
-                translation.y = pos.TranslationY.ValueAt(timing);
-                translation.z = pos.TranslationZ.ValueAt(timing);
+                    translation.x = pos.TranslationX.ValueAt(timing);
+                    translation.y = pos.TranslationY.ValueAt(timing);
+                    translation.z = pos.TranslationZ.ValueAt(timing);
 
-                rotation.x = pos.RotationX.ValueAt(timing);
-                rotation.y = pos.RotationY.ValueAt(timing);
-                rotation.z = pos.RotationZ.ValueAt(timing);
+                    rotation.x = pos.RotationX.ValueAt(timing);
+                    rotation.y = pos.RotationY.ValueAt(timing);
+                    rotation.z = pos.RotationZ.ValueAt(timing);
 
-                scale.x = pos.ScaleX.ValueAt(timing);
-                scale.y = pos.ScaleY.ValueAt(timing);
-                scale.z = pos.ScaleZ.ValueAt(timing);
+                    scale.x = pos.ScaleX.ValueAt(timing);
+                    scale.y = pos.ScaleY.ValueAt(timing);
+                    scale.z = pos.ScaleZ.ValueAt(timing);
 
-                pos.UpdatePosition(translation, Quaternion.Euler(rotation), scale);
-            }
+                    pos.UpdatePosition(translation, Quaternion.Euler(rotation), scale);
+                }
 
-            if (this is IColorController col && col.EnableColorModule)
-            {
-                RGBA color = new RGBA(col.DefaultColor);
-                HSVA modify = new HSVA(0, 0, 0, 1)
+                if (this is IColorController col && col.EnableColorModule)
+                {
+                    RGBA color = new RGBA(col.DefaultColor);
+                    HSVA modify = new HSVA(0, 0, 0, 1)
+                    {
+                        H = col.ColorH.ValueAt(timing),
+                        S = col.ColorS.ValueAt(timing),
+                        V = col.ColorV.ValueAt(timing),
+                    };
+
+                    color.R = col.ColorR.ValueAt(timing);
+                    color.G = col.ColorG.ValueAt(timing);
+                    color.B = col.ColorB.ValueAt(timing);
+                    color.A = col.ColorA.ValueAt(timing);
+
+                    HSVA hsva = Convert.RGBAToHSVA(color);
+                    hsva.H = (hsva.H + modify.H) % 360;
+                    hsva.S = Mathf.Clamp(hsva.S + modify.S, 0, 1);
+                    hsva.V = Mathf.Clamp(hsva.V + modify.V, 0, 1);
+
+                    col.UpdateColor(Convert.HSVAToRGBA(hsva).ToColor());
+                }
+
+                if (this is ILayerController lyr && lyr.EnableLayerModule)
                 {
-                    H = col.ColorH.ValueAt(timing),
-                    S = col.ColorS.ValueAt(timing),
-                    V = col.ColorV.ValueAt(timing),
-                };
+                    string layer = lyr.DefaultLayer;
+                    int sort = lyr.DefaultSort;
+                    float alpha = lyr.DefaultAlpha;
 
-                color.R = col.ColorR.ValueAt(timing);
-                color.G = col.ColorG.ValueAt(timing);
-                color.B = col.ColorB.ValueAt(timing);
-                color.A = col.ColorA.ValueAt(timing);
+                    layer = lyr.Layer.ValueAt(timing);
+                    sort = (int)lyr.Sort.ValueAt(timing);
+                    alpha = lyr.Alpha.ValueAt(timing) / 255f;
 
-                HSVA hsva = Convert.RGBAToHSVA(color);
-                hsva.H = (hsva.H + modify.H) % 360;
-                hsva.S = Mathf.Clamp(hsva.S + modify.S, 0, 1);
-                hsva.V = Mathf.Clamp(hsva.V + modify.V, 0, 1);
+                    lyr.UpdateLayer(layer, sort, alpha);
+                }
 
-                col.UpdateColor(Convert.HSVAToRGBA(hsva).ToColor());
+                if (this is IAngleController a && a.EnableAngleModule)
+                {
+                    float x = a.AngleX.ValueAt(timing);
+                    float y = a.AngleY.ValueAt(timing);
+
+                    a.UpdateAngle(x, y);
+                }
             }
 
-            if (this is ILayerController lyr && lyr.EnableLayerModule)
+            if (this is INoteIndividualController ni)
             {
-                string layer = lyr.DefaultLayer;
-                int sort = lyr.DefaultSort;
-                float alpha = lyr.DefaultAlpha;
-
-                layer = lyr.Layer.ValueAt(timing);
-                sort = (int)lyr.Sort.ValueAt(timing);
-                alpha = lyr.Alpha.ValueAt(timing) / 255f;
-
-                lyr.UpdateLayer(layer, sort, alpha);
+                foreach (var note in Services.Chart.GetTimingGroup(ni.GroupNumber).GetRenderingNotes())
+                {
+                    NoteIndividualController.CurrentNote = note;
+                    UpdateMaybeIndividual();
+                    NoteIndividualController.CurrentNote = null;
+                }
+            }
+            else
+            {
+                UpdateMaybeIndividual();
             }
 
+            // Definitely non-individual updates
             if (this is ITextController txt && txt.EnableTextModule)
             {
                 float lineSpacing = txt.DefaultLineSpacing;
@@ -279,10 +311,7 @@ public virtual void UpdateController(int timing)
                 scale.y = tg.ScaleIndividualY.ValueAt(timing);
                 scale.z = tg.ScaleIndividualZ.ValueAt(timing);
 
-                angle.x = tg.AngleX.ValueAt(timing);
-                angle.y = tg.AngleY.ValueAt(timing);
-
-                tg.UpdateNoteGroup(Quaternion.Euler(rotation), scale, angle);
+                tg.UpdateNoteGroup(Quaternion.Euler(rotation), scale);
             }
 
             if (this is ICameraController cam && cam.EnableCameraModule)
@@ -354,6 +383,7 @@ public virtual void Reset()
         {
             Active = new ConstantChannel(DefaultActive ? 1 : 0);
             SetActive(DefaultActive);
+
             if (this is IPositionController pos)
             {
                 pos.UpdatePosition(pos.DefaultTranslation, pos.DefaultRotation, pos.DefaultScale);
@@ -405,11 +435,16 @@ public virtual void Reset()
                 txt.EnableTextModule = false;
             }
 
+            if (this is IAngleController a)
+            {
+                a.UpdateAngle(0, 0);
+                a.AngleX = new ConstantChannel(0);
+                a.AngleY = new ConstantChannel(0);
+            }
+
             if (this is INoteGroupController tg)
             {
-                tg.UpdateNoteGroup(Quaternion.identity, Vector3.one, Vector2.zero);
-                tg.AngleX = new ConstantChannel(0);
-                tg.AngleY = new ConstantChannel(0);
+                tg.UpdateNoteGroup(Quaternion.identity, Vector3.one);
                 tg.RotationIndividualX = new ConstantChannel(0);
                 tg.RotationIndividualY = new ConstantChannel(0);
                 tg.RotationIndividualZ = new ConstantChannel(0);
@@ -465,6 +500,7 @@ public virtual void Reset()
             }
         }
 
+        [MoonSharpHidden]
         public List<object> SerializeProperties(ScenecontrolSerialization serialization)
         {
             List<object> result = new List<object>
@@ -516,11 +552,16 @@ public List<object> SerializeProperties(ScenecontrolSerialization serialization)
                 result.Add(txt.CustomFont);
             }
 
+            if (this is IAngleController a)
+            {
+                result.Add(a.EnableAngleModule);
+                result.Add(serialization.AddUnitAndGetId(a.AngleX));
+                result.Add(serialization.AddUnitAndGetId(a.AngleY));
+            }
+
             if (this is INoteGroupController tg)
             {
                 result.Add(tg.EnableNoteGroupModule);
-                result.Add(serialization.AddUnitAndGetId(tg.AngleX));
-                result.Add(serialization.AddUnitAndGetId(tg.AngleY));
                 result.Add(serialization.AddUnitAndGetId(tg.RotationIndividualX));
                 result.Add(serialization.AddUnitAndGetId(tg.RotationIndividualY));
                 result.Add(serialization.AddUnitAndGetId(tg.RotationIndividualZ));
@@ -573,6 +614,7 @@ public List<object> SerializeProperties(ScenecontrolSerialization serialization)
             return result;
         }
 
+        [MoonSharpHidden]
         public void DeserializeProperties(List<object> properties, ScenecontrolDeserialization deserialization)
         {
             customParent = deserialization.GetUnitFromId<Controller>(properties[0]);
@@ -632,11 +674,17 @@ public void DeserializeProperties(List<object> properties, ScenecontrolDeseriali
                 txt.EnableTextModule = enable;
             }
 
+            if (this is IAngleController a)
+            {
+                bool enable = (bool)properties[offset++];
+                a.AngleX = deserialization.GetUnitFromId<ValueChannel>(properties[offset++]);
+                a.AngleY = deserialization.GetUnitFromId<ValueChannel>(properties[offset++]);
+                a.EnableAngleModule = enable;
+            }
+
             if (this is INoteGroupController tg)
             {
                 bool enable = (bool)properties[offset++];
-                tg.AngleX = deserialization.GetUnitFromId<ValueChannel>(properties[offset++]);
-                tg.AngleY = deserialization.GetUnitFromId<ValueChannel>(properties[offset++]);
                 tg.RotationIndividualX = deserialization.GetUnitFromId<ValueChannel>(properties[offset++]);
                 tg.RotationIndividualY = deserialization.GetUnitFromId<ValueChannel>(properties[offset++]);
                 tg.RotationIndividualZ = deserialization.GetUnitFromId<ValueChannel>(properties[offset++]);
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Interfaces.cs b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Interfaces.cs
index 6055c3d0..13724c31 100755
--- a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Interfaces.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Interfaces.cs
@@ -10,6 +10,11 @@ public interface IController
         bool DefaultActive { get; }
     }
 
+    public interface INoteIndividualController : IController
+    {
+        int GroupNumber { get; }
+    }
+
     public interface IPositionController : IController
     {
         bool EnablePositionModule { get; set; }
@@ -110,14 +115,21 @@ public interface ITextController : IController
         void ApplyCustomFont(string font);
     }
 
-    public interface INoteGroupController : IController
+    public interface IAngleController : IController 
     {
-        bool EnableNoteGroupModule { get; set; }
-
+        bool EnableAngleModule { get; set; }
+        
         ValueChannel AngleX { get; set; }
 
         ValueChannel AngleY { get; set; }
 
+        void UpdateAngle(float x, float y);
+    }
+
+    public interface INoteGroupController : IController
+    {
+        bool EnableNoteGroupModule { get; set; }
+
         ValueChannel RotationIndividualX { get; set; }
 
         ValueChannel RotationIndividualY { get; set; }
@@ -130,7 +142,7 @@ public interface INoteGroupController : IController
 
         ValueChannel ScaleIndividualZ { get; set; }
 
-        void UpdateNoteGroup(Quaternion rotation, Vector3 scale, Vector2 angle);
+        void UpdateNoteGroup(Quaternion rotation, Vector3 scale);
     }
 
     public interface ICameraController : IController
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteGroupController.cs b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteGroupController.cs
index 9f2e8f4b..e5987ce1 100755
--- a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteGroupController.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteGroupController.cs
@@ -1,4 +1,5 @@
 using ArcCreate.Gameplay.Chart;
+using ArcCreate.Utility;
 using EmmySharp;
 using MoonSharp.Interpreter;
 using UnityEngine;
@@ -7,7 +8,7 @@ namespace ArcCreate.Gameplay.Scenecontrol
 {
     [MoonSharpUserData]
     [EmmyDoc("Controller for a timing group")]
-    public class NoteGroupController : Controller, IPositionController, INoteGroupController, IColorController
+    public class NoteGroupController : Controller, IPositionController, INoteGroupController, IColorController, IAngleController
     {
         private ValueChannel translationX;
         private ValueChannel translationY;
@@ -132,7 +133,7 @@ public ValueChannel AngleX
             set
             {
                 angleX = value;
-                EnableNoteGroupModule = true;
+                EnableAngleModule = true;
             }
         }
 
@@ -142,7 +143,7 @@ public ValueChannel AngleY
             set
             {
                 angleY = value;
-                EnableNoteGroupModule = true;
+                EnableAngleModule = true;
             }
         }
 
@@ -290,6 +291,8 @@ public ValueChannel ColorA
 
         public bool EnableColorModule { get; set; }
 
+        public bool EnableAngleModule { get; set; }
+
         [MoonSharpHidden]
         public void UpdateColor(Color color)
         {
@@ -297,21 +300,26 @@ public void UpdateColor(Color color)
         }
 
         [MoonSharpHidden]
-        public void UpdateNoteGroup(Quaternion rotation, Vector3 scale, Vector2 angle)
+        public void UpdateNoteGroup(Quaternion rotation, Vector3 scale)
         {
-            TimingGroup.GroupProperties.SCAngleX = angle.x;
-            TimingGroup.GroupProperties.SCAngleY = angle.y;
             TimingGroup.GroupProperties.RotationIndividual = rotation;
             TimingGroup.GroupProperties.ScaleIndividual = scale;
         }
 
+        [MoonSharpHidden]
+        public void UpdateAngle(float x, float y)
+        {
+            TimingGroup.GroupProperties.SCAngleX = x;
+            TimingGroup.GroupProperties.SCAngleY = y;
+        }
+
         [MoonSharpHidden]
         public void UpdatePosition(Vector3 translation, Quaternion rotation, Vector3 scale)
         {
             transform.localPosition = translation;
             transform.localRotation = rotation;
             transform.localScale = scale;
-            TimingGroup.GroupProperties.GroupMatrix = Matrix4x4.TRS(translation, rotation, scale);
+            TimingGroup.GroupProperties.GroupTransform = new TRS(translation, rotation, scale);
         }
 
         protected override void SetActive(bool active)
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteIndividualController.cs b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteIndividualController.cs
new file mode 100644
index 00000000..3c47197b
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteIndividualController.cs
@@ -0,0 +1,289 @@
+using ArcCreate.Gameplay.Chart;
+using ArcCreate.Gameplay.Data;
+using ArcCreate.Utility;
+using EmmySharp;
+using MoonSharp.Interpreter;
+using UnityEngine;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [MoonSharpUserData]
+    public class NoteIndividualController : Controller, INoteIndividualController, IColorController, IPositionController, IAngleController
+    {
+        // Position
+        private ValueChannel translationX;
+        private ValueChannel translationY;
+        private ValueChannel translationZ;
+        private ValueChannel rotationX;
+        private ValueChannel rotationY;
+        private ValueChannel rotationZ;
+        private ValueChannel scaleX;
+        private ValueChannel scaleY;
+        private ValueChannel scaleZ;
+
+        // Angle
+        private ValueChannel angleX;
+        private ValueChannel angleY;
+
+        // Color
+        private ValueChannel colorR;
+        private ValueChannel colorG;
+        private ValueChannel colorB;
+        private ValueChannel colorH;
+        private ValueChannel colorV;
+        private ValueChannel colorA;
+        private ValueChannel colorS;
+
+        public static Note CurrentNote { get; set; }
+
+        public int GroupNumber => TimingGroup.GroupNumber;
+
+        [MoonSharpHidden] public TimingGroup TimingGroup { get; set; }
+
+        public ValueChannel ColorR
+        {
+            get => colorR;
+            set
+            {
+                colorR = value;
+                EnableColorModule = true;
+            }
+        }
+
+        public ValueChannel ColorG
+        {
+            get => colorG;
+            set
+            {
+                colorG = value;
+                EnableColorModule = true;
+            }
+        }
+
+        public ValueChannel ColorB
+        {
+            get => colorB;
+            set
+            {
+                colorB = value;
+                EnableColorModule = true;
+            }
+        }
+
+        public ValueChannel ColorH
+        {
+            get => colorH;
+            set
+            {
+                colorH = value;
+                EnableColorModule = true;
+            }
+        }
+
+        public ValueChannel ColorS
+        {
+            get => colorS;
+            set
+            {
+                colorS = value;
+                EnableColorModule = true;
+            }
+        }
+
+        public ValueChannel ColorV
+        {
+            get => colorV;
+            set
+            {
+                colorV = value;
+                EnableColorModule = true;
+            }
+        }
+
+        public ValueChannel ColorA
+        {
+            get => colorA;
+            set
+            {
+                colorA = value;
+                EnableColorModule = true;
+            }
+        }
+
+        public ValueChannel TranslationX
+        {
+            get => translationX;
+            set
+            {
+                translationX = value;
+                EnablePositionModule = true;
+            }
+        }
+
+        public ValueChannel TranslationY
+        {
+            get => translationY;
+            set
+            {
+                translationY = value;
+                EnablePositionModule = true;
+            }
+        }
+
+        public ValueChannel TranslationZ
+        {
+            get => translationZ;
+            set
+            {
+                translationZ = value;
+                EnablePositionModule = true;
+            }
+        }
+
+        public ValueChannel RotationX
+        {
+            get => rotationX;
+            set
+            {
+                rotationX = value;
+                EnablePositionModule = true;
+            }
+        }
+
+        public ValueChannel RotationY
+        {
+            get => rotationY;
+            set
+            {
+                rotationY = value;
+                EnablePositionModule = true;
+            }
+        }
+
+        public ValueChannel RotationZ
+        {
+            get => rotationZ;
+            set
+            {
+                rotationZ = value;
+                EnablePositionModule = true;
+            }
+        }
+
+        public ValueChannel ScaleX
+        {
+            get => scaleX;
+            set
+            {
+                scaleX = value;
+                EnablePositionModule = true;
+            }
+        }
+
+        public ValueChannel ScaleY
+        {
+            get => scaleY;
+            set
+            {
+                scaleY = value;
+                EnablePositionModule = true;
+            }
+        }
+
+        public ValueChannel ScaleZ
+        {
+            get => scaleZ;
+            set
+            {
+                scaleZ = value;
+                EnablePositionModule = true;
+            }
+        }
+
+        public ValueChannel AngleX
+        {
+            get => angleX;
+            set
+            {
+                angleX = value;
+                EnableAngleModule = true;
+            }
+        }
+
+        public ValueChannel AngleY
+        {
+            get => angleY;
+            set
+            {
+                angleY = value;
+                EnableAngleModule = true;
+            }
+        }
+
+        public bool EnableColorModule { get; set; }
+
+        public bool EnablePositionModule { get; set; }
+
+        public bool EnableAngleModule { get; set; }
+
+        public Color DefaultColor => Color.white;
+
+        public Vector3 DefaultTranslation => Vector3.zero;
+
+        public Quaternion DefaultRotation => Quaternion.identity;
+
+        public Vector3 DefaultScale => Vector3.one;
+
+        [MoonSharpHidden]
+        private NoteProperties CurrentProperties
+        {
+            get
+            {
+                return TimingGroup.GroupProperties.IndividualOverrides.PropertiesFor(CurrentNote);
+            }
+        }
+
+        [MoonSharpHidden]
+        public void UpdateColor(Color color)
+        {
+            if (CurrentNote is null)
+            {
+                TimingGroup.GroupProperties.IndividualOverrides.SetAllColors(color);
+                return;
+            }
+
+            TimingGroup.GroupProperties.IndividualOverrides.UseColor = true;
+            CurrentProperties.Color = color;
+        }
+
+        [MoonSharpHidden]
+        public void UpdatePosition(Vector3 translation, Quaternion rotation, Vector3 scale)
+        {
+            TRS trs = new TRS(translation, rotation, scale);
+
+            if (CurrentNote is null)
+            {
+                TimingGroup.GroupProperties.IndividualOverrides.SetAllTransforms(trs);
+                return;
+            }
+
+            TimingGroup.GroupProperties.IndividualOverrides.UsePosition = true;
+            CurrentProperties.Transform = trs;
+        }
+
+        [MoonSharpHidden]
+        public void UpdateAngle(float x, float y)
+        {
+            Vector2 angles = new Vector2(x, y);
+
+            if (CurrentNote is null)
+            {
+                TimingGroup.GroupProperties.IndividualOverrides.SetAllAngles(angles);
+                return;
+            }
+
+            TimingGroup.GroupProperties.IndividualOverrides.UseAngle = true;
+            CurrentProperties.Angles = angles;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteIndividualController.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteIndividualController.cs.meta
new file mode 100644
index 00000000..e2ba7723
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Internal/NoteIndividualController.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 992745bc6c6c41d4f9dbd4512489c76e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Scene.cs b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Scene.cs
index 8fac469f..c324b506 100755
--- a/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Scene.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/Controllers/Scene.cs
@@ -260,6 +260,7 @@ public CanvasController CameraCanvas
         [MoonSharpHidden] public GameObject SpritePrefab;
         [MoonSharpHidden] public GameObject TextPrefab;
         [MoonSharpHidden] public GameObject GroupPrefab;
+        [MoonSharpHidden] public GameObject IndividualPrefab;
 
         [Header("Materials")]
         [MoonSharpHidden] public Material DefaultMaterial;
@@ -295,6 +296,7 @@ public CanvasController CameraCanvas
 
         private readonly Dictionary<SpriteDefinition, Sprite> spriteCache = new Dictionary<SpriteDefinition, Sprite>();
         private readonly Dictionary<int, NoteGroupController> noteGroups = new Dictionary<int, NoteGroupController>();
+        private readonly Dictionary<int, NoteIndividualController> noteIndividualGroups = new Dictionary<int, NoteIndividualController>();
         private readonly List<UniTask<Sprite>> spriteTasks = new List<UniTask<Sprite>>();
         private CancellationTokenSource cts = new CancellationTokenSource();
 
@@ -322,6 +324,7 @@ public void ClearCache()
 
             spriteCache.Clear();
             noteGroups.Clear();
+            noteIndividualGroups.Clear();
         }
 
         [MoonSharpHidden]
@@ -516,6 +519,33 @@ public NoteGroupController GetNoteGroup(int tg)
             }
         }
 
+        [EmmyDoc("Creates a note individual controller for a timing group")]
+        public NoteIndividualController GetNoteIndividual(int tg)
+        {
+            if (noteIndividualGroups.TryGetValue(tg, out var cached))
+            {
+                return cached;
+            }
+
+            try
+            {
+                var group = Services.Chart.GetTimingGroup(tg);
+                group.EnableIndividualOverrides();
+                NoteIndividualController c = Instantiate(IndividualPrefab, transform).GetComponent<NoteIndividualController>();
+                c.TimingGroup = group;
+                c.SerializedType = $"ni.{group.GroupNumber}";
+                c.Start();
+                Services.Scenecontrol.AddReferencedController(c);
+                noteIndividualGroups.Add(group.GroupNumber, c);
+                return c;
+            }
+            catch (Exception e)
+            {
+                Debug.LogException(e);
+                return null;
+            }
+        }
+
         [MoonSharpHidden]
         public Controller CreateFromTypeName(string type)
         {
@@ -706,6 +736,8 @@ private Controller GetBaseControlerFromTypeName(string def, string arg)
                     return CameraCanvas;
                 case "tg":
                     return GetNoteGroup(int.Parse(arg));
+                case "ni":
+                    return GetNoteIndividual(int.Parse(arg));
                 default:
                     return null;
             }
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/IO/ScenecontrolDeserialization.cs b/Assets/Scripts/Gameplay/Scenecontrol/IO/ScenecontrolDeserialization.cs
index bca4d0c6..db14b750 100644
--- a/Assets/Scripts/Gameplay/Scenecontrol/IO/ScenecontrolDeserialization.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/IO/ScenecontrolDeserialization.cs
@@ -92,6 +92,42 @@ public ISerializableUnit GetUnitFromType(string type)
                     return new SineChannel();
                 case "channel.sum":
                     return new SumChannel();
+                case "channel.timing":
+                    return new TimingChannel();
+                case "channel.puresine":
+                    return new PureSineChannel();
+                case "channel.purecos":
+                    return new PureCosChannel();
+                case "channel.modulo":
+                    return new ModuloChannel();
+                case "channel.abs":
+                    return new AbsChannel();
+                case "channel.time.shift":
+                    return new TimeShiftChannel();
+                case "channel.time.scale":
+                    return new TimeScaleChannel();
+                case "channel.chain":
+                    return new ChainChannel();
+                case "channel.ifelse":
+                    return new IfElseChannel();
+
+                // Note channels
+                case "channel.note.timing":
+                    return new NoteTimingChannel();
+                case "channel.note.floorpos":
+                    return new NoteFloorPositionChannel();
+                case "channel.note.x":
+                    return new NoteXPositionChannel();
+                case "channel.note.y":
+                    return new NoteYPositionChannel();
+                case "channel.note.z":
+                    return new NoteZPositionChannel();
+                case "channel.note.id":
+                    return new NoteIDChannel();
+                case "channel.note.isarc":
+                    return new NoteIsArcChannel();
+                case "channel.note.type":
+                    return new NoteTypeChannel();
 
                 // Triggers channels
                 case "channel.trigger.accumulate":
@@ -115,6 +151,20 @@ public ISerializableUnit GetUnitFromType(string type)
                 case "channel.text.value":
                     return new ValueToTextChannel();
 
+                // Boolean channels
+                case "channel.bool.constant":
+                    return new BooleanConstantChannel();
+                case "channel.bool.not":
+                    return new NotChannel();
+                case "channel.bool.and":
+                    return new AndChannel();
+                case "channel.bool.or":
+                    return new OrChannel();
+                case "channel.bool.comp.num":
+                    return new NumericalComparisonChannel();
+                case "channel.bool.comp.str":
+                    return new StringComparisonChannel();
+
                 // Contexts
                 case "channel.context.droprate":
                     return new DropRateChannel();
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/IO/ScenecontrolSerialization.cs b/Assets/Scripts/Gameplay/Scenecontrol/IO/ScenecontrolSerialization.cs
index 48e71c15..2cbd3d2e 100644
--- a/Assets/Scripts/Gameplay/Scenecontrol/IO/ScenecontrolSerialization.cs
+++ b/Assets/Scripts/Gameplay/Scenecontrol/IO/ScenecontrolSerialization.cs
@@ -79,6 +79,42 @@ public string GetTypeFromUnit(ISerializableUnit unit)
                     return "channel.sine";
                 case SumChannel sum:
                     return "channel.sum";
+                case TimingChannel timing:
+                    return "channel.timing";
+                case AbsChannel abs:
+                    return "channel.abs";
+                case TimeScaleChannel ts:
+                    return "channel.time.scale";
+                case TimeShiftChannel tsh:
+                    return "channel.time.shift";
+                case PureSineChannel ps:
+                    return "channel.time.chain";
+                case ChainChannel chain:
+                    return "channel.puresine";
+                case PureCosChannel pc:
+                    return "channel.purecos";
+                case ModuloChannel pc:
+                    return "channel.modulo";
+                case IfElseChannel ie:
+                    return "channel.ifelse";
+
+                // Note channels
+                case NoteTimingChannel nTiming:
+                    return "channel.note.timing";
+                case NoteFloorPositionChannel nFP:
+                    return "channel.note.floorpos";
+                case NoteXPositionChannel nx:
+                    return "channel.note.x";
+                case NoteYPositionChannel ny:
+                    return "channel.note.y";
+                case NoteZPositionChannel nz:
+                    return "channel.note.z";
+                case NoteIDChannel id:
+                    return "channel.note.id";
+                case NoteIsArcChannel arc:
+                    return "channel.note.isarc";
+                case NoteTypeChannel type:
+                    return "channel.note.type";
 
                 // Trigger channels
                 case AccumulatingTriggerChannel accum:
@@ -102,6 +138,20 @@ public string GetTypeFromUnit(ISerializableUnit unit)
                 case ValueToTextChannel valuetext:
                     return "channel.text.value";
 
+                // Boolean channels
+                case BooleanConstantChannel bconst:
+                    return "channel.bool.constant";
+                case NotChannel not:
+                    return "channel.bool.not";
+                case AndChannel and:
+                    return "channel.bool.and";
+                case OrChannel or:
+                    return "channel.bool.or";
+                case NumericalComparisonChannel ncomp:
+                    return "channel.bool.comp.num";
+                case StringComparisonChannel scomp:
+                    return "channel.bool.comp.str";
+
                 // Contexts
                 case DropRateChannel droprate:
                     return "channel.context.droprate";
@@ -132,6 +182,7 @@ public string GetTypeFromUnit(ISerializableUnit unit)
                 case ObserveTrigger tobserve:
                     return "trigger.observe";
                 default:
+
                     if (unit is ISceneController controller)
                     {
                         string name = controller?.SerializedType;
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/IO/SerializationExemptAttribute.cs b/Assets/Scripts/Gameplay/Scenecontrol/IO/SerializationExemptAttribute.cs
new file mode 100644
index 00000000..da81657e
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/IO/SerializationExemptAttribute.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace ArcCreate.Gameplay.Scenecontrol
+{
+    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+    public sealed class SerializationExemptAttribute : Attribute
+    {
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Scenecontrol/IO/SerializationExemptAttribute.cs.meta b/Assets/Scripts/Gameplay/Scenecontrol/IO/SerializationExemptAttribute.cs.meta
new file mode 100644
index 00000000..a2eed710
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Scenecontrol/IO/SerializationExemptAttribute.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8cb8e355ac4baa444b2dde6528695bea
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Gameplay/Utility/ArcFormula.cs b/Assets/Scripts/Gameplay/Utility/ArcFormula.cs
index c369e7d8..5faa7e4e 100644
--- a/Assets/Scripts/Gameplay/Utility/ArcFormula.cs
+++ b/Assets/Scripts/Gameplay/Utility/ArcFormula.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using ArcCreate.Gameplay.Data;
 using UnityEngine;
 
@@ -208,5 +209,24 @@ public static float CalculateArcSegmentLength(int duration, float arcResolution)
             float length = Values.ArcSegmentLength / arcResolution;
             return duration < 1000 ? length : length * 2;
         }
+
+        public static Vector2 UnmodifiedWorldPosition(Note note)
+        {
+            switch (note)
+            {
+                case Tap t:
+                    return new Vector2(LaneToWorldX(t.Lane), 0);
+                case Hold h:
+                    return new Vector2(LaneToWorldX(h.Lane), 0);
+
+                case ArcTap at:
+                    return new Vector2(at.WorldX, at.WorldY);
+                case Arc a:
+                    return new Vector2(a.WorldXAt(a.Timing), a.WorldYAt(a.Timing));
+
+                default:
+                    throw new InvalidOperationException($"Unknown note type {note.GetType()}");
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/Utility/Values.cs b/Assets/Scripts/Gameplay/Utility/Values.cs
index 86df3601..926cb62a 100644
--- a/Assets/Scripts/Gameplay/Utility/Values.cs
+++ b/Assets/Scripts/Gameplay/Utility/Values.cs
@@ -1,7 +1,9 @@
+using MoonSharp.Interpreter;
 using UnityEngine;
 
 namespace ArcCreate.Gameplay
 {
+    [MoonSharpUserData]
     public static class Values
     {
         // Playfield
@@ -56,17 +58,17 @@ public static class Values
         public const int BeatlineThickness = 20;
 
         // Judgement
-        public const int ScoreModifyDelay = 500;
-        public const int ArcLockDuration = 500;
-        public const int ArcGraceDuration = 600;
-        public const int ArcRedFlashCycle = 500;
-        public const float ComboLostFlashDuration = 0.1f;
+        [MoonSharpHidden] public const int ScoreModifyDelay = 500;
+        [MoonSharpHidden] public const int ArcLockDuration = 500;
+        [MoonSharpHidden] public const int ArcGraceDuration = 600;
+        [MoonSharpHidden] public const int ArcRedFlashCycle = 500;
+        [MoonSharpHidden] public const float ComboLostFlashDuration = 0.1f;
         public const float ArcHitboxX = 1.9f;
         public const float ArcHitboxY = 2.5f;
         public const float ArcTapHitboxX = 3.02f;
         public const float ArcTapHitboxYDown = 3.1f;
         public const float ArcTapHitboxYUp = 2.5f;
-        public const float MinLongNoteTimeIncrement = 0.1f;
+        [MoonSharpHidden] public const float MinLongNoteTimeIncrement = 0.1f;
 
         // Camera
         public const float CameraY = 9f;
@@ -80,50 +82,50 @@ public static class Values
         public const float CameraArcPosScalar = 0.05f;
 
         // Strings
-        public const string EarlyText = "EARLY";
-        public const string LateText = "LATE";
-        public const string BeatlinePoolName = "beatline";
-        public const string TapParticlePoolName = "tapparticle";
-        public const string ArcParticlePoolName = "arcparticle";
-        public const string HoldParticlePoolName = "holdparticle";
+        [MoonSharpHidden] public const string EarlyText = "EARLY";
+        [MoonSharpHidden] public const string LateText = "LATE";
+        [MoonSharpHidden] public const string BeatlinePoolName = "beatline";
+        [MoonSharpHidden] public const string TapParticlePoolName = "tapparticle";
+        [MoonSharpHidden] public const string ArcParticlePoolName = "arcparticle";
+        [MoonSharpHidden] public const string HoldParticlePoolName = "holdparticle";
 
         // I sure hope no charter will make use of lane -2147483648
-        public const int InvalidLane = int.MinValue;
+        [MoonSharpHidden] public const int InvalidLane = int.MinValue;
 
-        public const int DelayBeforeAudioStart = 2000;
+        [MoonSharpHidden] public const int DelayBeforeAudioStart = 2000;
 
-        public const int DelayBeforeAudioResume = 200;
+        [MoonSharpHidden] public const int DelayBeforeAudioResume = 200;
 
-        public static int ChartAudioOffset { get; internal set; } = 0;
+        [MoonSharpHidden] public static int ChartAudioOffset { get; internal set; } = 0;
 
-        public static float BaseBpm { get; set; } = 100;
+        [MoonSharpHidden] public static float BaseBpm { get; set; } = 100;
 
-        public static float TimingPointDensity { get; set; } = 1;
+        [MoonSharpHidden] public static float TimingPointDensity { get; set; } = 1;
 
-        public static Color[] DefaultDifficultyColors { get; set; } = new Color[] { };
+        [MoonSharpHidden] public static Color[] DefaultDifficultyColors { get; set; } = new Color[] { };
 
-        public static float LaneFrom { get; set; } = 1;
+        [MoonSharpHidden] public static float LaneFrom { get; set; } = 1;
 
-        public static float LaneTo { get; set; } = 4;
+        [MoonSharpHidden] public static float LaneTo { get; set; } = 4;
 
-        public static bool EnableColliderGeneration { get; set; } = false;
+        [MoonSharpHidden] public static bool EnableColliderGeneration { get; set; } = false;
 
-        public static bool EnableArcRebuildSegment { get; set; } = true;
+        [MoonSharpHidden] public static bool EnableArcRebuildSegment { get; set; } = true;
 
-        public static Vector2 LaneScreenHitboxBase { get; set; } = Vector2.one;
+        [MoonSharpHidden] public static Vector2 LaneScreenHitboxBase { get; set; } = Vector2.one;
 
-        public static Vector2 ScreenSizeBase { get; set; } = Vector2.one;
+        [MoonSharpHidden] public static Vector2 ScreenSizeBase { get; set; } = Vector2.one;
 
-        public static Vector2 ScreenSize { get; set; } = Vector2.one;
+        [MoonSharpHidden] public static Vector2 ScreenSize { get; set; } = Vector2.one;
 
-        public static float LaneScreenHitboxHorizontal => LaneScreenHitboxBase.x * ScreenSize.x / ScreenSizeBase.x;
+        [MoonSharpHidden] public static float LaneScreenHitboxHorizontal => LaneScreenHitboxBase.x * ScreenSize.x / ScreenSizeBase.x;
 
-        public static float LaneScreenHitboxVertical => LaneScreenHitboxBase.y * ScreenSize.y / ScreenSizeBase.y;
+        [MoonSharpHidden] public static float LaneScreenHitboxVertical => LaneScreenHitboxBase.y * ScreenSize.y / ScreenSizeBase.y;
 
-        public static bool EnablePauseMenu { get; internal set; } = true;
+        [MoonSharpHidden] public static bool EnablePauseMenu { get; internal set; } = true;
 
-        public static bool ShouldNotifyOnAudioEnd { get; internal set; } = false;
+        [MoonSharpHidden] public static bool ShouldNotifyOnAudioEnd { get; internal set; } = false;
 
-        public static int RetryCount { get; internal set; } = 0;
+        [MoonSharpHidden] public static int RetryCount { get; internal set; } = 0;
     }
 }
\ No newline at end of file
diff --git a/Assets/Scripts/Utility/EmmySharp/EmmyHelpers.cs b/Assets/Scripts/Utility/EmmySharp/EmmyHelpers.cs
index 5f682784..2c65fa73 100644
--- a/Assets/Scripts/Utility/EmmySharp/EmmyHelpers.cs
+++ b/Assets/Scripts/Utility/EmmySharp/EmmyHelpers.cs
@@ -4,8 +4,10 @@
 /// </summary>
 
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
+using MoonSharp.Interpreter;
 
 namespace EmmySharp
 {
@@ -26,7 +28,7 @@ public static string ToCamelCase(this string self)
             return self;
         }
 
-        public static T GetAttrOr<T>(this ICustomAttributeProvider attrs)
+        public static T GetAttrOrNull<T>(this ICustomAttributeProvider attrs)
             where T : Attribute
         {
             if (attrs == null)
@@ -45,12 +47,195 @@ public static T GetAttrOr<T>(this ICustomAttributeProvider attrs)
         }
 
         public static string EmmyDoc(this ICustomAttributeProvider p)
-            => p.GetAttrOr<EmmyDocAttribute>()?.Documentation;
+            => p.GetAttrOrNull<EmmyDocAttribute>()?.Documentation;
 
-        public static string[] EmmyChoice(this ICustomAttributeProvider p)
-            => p.GetAttrOr<EmmyChoiceAttribute>()?.Values;
+        public static EmmyType GetEmmyType(this ICustomAttributeProvider p, Type baseType, IReadOnlyDictionary<string, EmmyType> aliases)
+        {
+            EmmyType GetTypeNoMod()
+            {
+                var typeAttr = p.GetAttrOrNull<EmmyTypeAttribute>();
+
+                if (typeAttr is null)
+                {
+                    var choicesAttr = p.GetAttrOrNull<EmmyChoiceAttribute>();
+
+                    if (choicesAttr is null)
+                    {
+                        return EmmyType.From(baseType);
+                    }
+                    else
+                    {
+                        return EmmyType.Option(choicesAttr.Values);
+                    }
+                }
+                else
+                {
+                    if (typeAttr.Raw is null)
+                    {
+                        return EmmyType.From(typeAttr.Type);
+                    }
+
+                    return EmmyType.Raw(typeAttr.Raw);
+                }
+            }
+
+            var nullable = p.GetAttrOrNull<EmmyNullableAttribute>() != null;
+            var ret = GetTypeNoMod();
+
+            if (nullable)
+            {
+                ret = EmmyType.Nullable(ret);
+            }
+
+            return ret;
+        }
+
+        public static string EmmyName(this ICustomAttributeProvider p, string nameSimple, bool camelCase = false)
+        {
+            if (camelCase)
+            {
+                nameSimple = nameSimple.ToCamelCase();
+            }
+
+            var name = p.EmmyAlias() ?? nameSimple;
+
+            return RenameAvoidKeyword(name);
+        }
+
+        public static bool Visible(this ICustomAttributeProvider p)
+        {
+            var isVisible = true;
+
+            if (p is Type)
+            {
+                isVisible = isVisible
+                    && p.GetAttrOrNull<MoonSharpUserDataAttribute>() != null;
+            }
+            else if (p is MethodInfo method)
+            {
+                var baseMethod = method.GetBaseDefinition();
+
+                isVisible = isVisible
+                    && (method == baseMethod || baseMethod.Visible());
+            }
+            else
+            {
+                List<Type> types = new List<Type>();
+
+                if (p is FieldInfo field)
+                {
+                    types.Add(field.FieldType);
+                }
+                else if (p is PropertyInfo prop)
+                {
+                    types.Add(prop.PropertyType);
+                }
+                else if (p is MethodInfo method2)
+                {
+                    types.Add(method2.ReturnType);
+                    types.AddRange(method2.GetParameters().Select(param => param.ParameterType));
+                }
+                else if (p is ParameterInfo param)
+                {
+                    types.Add(param.ParameterType);
+                }
+
+                foreach (var t in types)
+                {
+                    isVisible = isVisible
+                        && EmmyType.From(t) != null;
+                }
+            }
+
+            isVisible = isVisible
+                && p.GetAttrOrNull<MoonSharpHiddenAttribute>() == null
+                && p.GetAttrOrNull<MoonSharpHideMemberAttribute>() == null;
+
+            return isVisible;
+        }
+
+        public static string EmmyName<T>(this T p, bool camelCase = false)
+            where T : MemberInfo, ICustomAttributeProvider
+            => p.EmmyName(p.Name, camelCase);
 
         public static string EmmyAlias(this ICustomAttributeProvider p)
-            => p.GetAttrOr<EmmyAliasAttribute>()?.Alias;
+            => p.GetAttrOrNull<EmmyAliasAttribute>()?.Alias
+            ?? p.GetAttrOrNull<MoonSharpUserDataMetamethodAttribute>()?.Name;
+
+        private static string RenameAvoidKeyword(string name)
+        {
+            switch (name)
+            {
+                case "and":
+                case "break":
+                case "do":
+                case "else":
+                case "elseif":
+                case "end":
+                case "false":
+                case "for":
+                case "function":
+                case "goto":
+                case "if":
+                case "in":
+                case "local":
+                case "nil":
+                case "not":
+                case "or":
+                case "repeat":
+                case "return":
+                case "then":
+                case "true":
+                case "until":
+                case "while":
+                    return "_" + name;
+                default:
+                    return name;
+            }
+        }
+
+        private static readonly ISet<string> LuaOperators = new HashSet<string>
+        {
+            "__eq",
+            "__lt",
+            "__le",
+
+            "__unm",
+            "__add",
+            "__sub",
+            "__div",
+            "__mul",
+            "__mod",
+            "__pow",
+            "__concat",
+
+            "__tostring",
+        };
+
+        private static readonly IReadOnlyDictionary<string, string> OperatorMappings = new Dictionary<string, string>
+        {
+            { "op_Equality", "__eq" },
+            { "op_LessThan", "__lt" },
+            { "op_LessThanOrEqual", "__le" },
+            { "op_UnaryNegation", "__unm" },
+            { "op_Addition", "__add" },
+            { "op_Subtraction", "__sub" },
+            { "op_Multiply", "__mul" },
+            { "op_Division", "__div" },
+            { "op_Modulus", "__mod" },
+        };
+
+        public static string GetLuaOperator(string csOp)
+        {
+            if (!OperatorMappings.TryGetValue(csOp, out var luaOp))
+            {
+                return null;
+            }
+
+            return luaOp;
+        }
+
+        public static bool IsLuaOperator(string luaOp)
+            => LuaOperators.Contains(luaOp);
     }
 }
\ No newline at end of file
diff --git a/Assets/Scripts/Utility/EmmySharp/EmmyNullableAttribute.cs b/Assets/Scripts/Utility/EmmySharp/EmmyNullableAttribute.cs
new file mode 100644
index 00000000..0ddf3c3f
--- /dev/null
+++ b/Assets/Scripts/Utility/EmmySharp/EmmyNullableAttribute.cs
@@ -0,0 +1,14 @@
+/// <summary>
+/// EmmySharp, created by Dylan Rafael (floofer++) for use by 0thElement.
+/// The below classes help to generate EmmyLua workspace files from MoonSharp classes.
+/// </summary>
+
+using System;
+
+namespace EmmySharp
+{
+    [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.Property | AttributeTargets.Field)]
+    public class EmmyNullableAttribute : Attribute
+    {
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Utility/EmmySharp/EmmyNullableAttribute.cs.meta b/Assets/Scripts/Utility/EmmySharp/EmmyNullableAttribute.cs.meta
new file mode 100644
index 00000000..567005a7
--- /dev/null
+++ b/Assets/Scripts/Utility/EmmySharp/EmmyNullableAttribute.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 074e8b4547b99f74a9d06773daccdc43
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Utility/EmmySharp/EmmySharpBuilder.cs b/Assets/Scripts/Utility/EmmySharp/EmmySharpBuilder.cs
index e9c1eb10..e6d27c5f 100644
--- a/Assets/Scripts/Utility/EmmySharp/EmmySharpBuilder.cs
+++ b/Assets/Scripts/Utility/EmmySharp/EmmySharpBuilder.cs
@@ -10,12 +10,444 @@
 using System.Reflection;
 using System.Text;
 using MoonSharp.Interpreter;
+using UnityEngine;
 
 namespace EmmySharp
 {
+    public abstract class EmmyType
+    {
+        public virtual IEnumerable<EmmyType> Constituents
+        {
+            get
+            {
+                yield return this;
+            }
+        }
+
+        public static EmmyType From<T>()
+            => From(typeof(T));
+
+        public static EmmyType Void
+            => Raw("nil");
+
+        public static EmmyType Integer
+            => Raw("integer");
+
+        public static EmmyType Float
+            => Raw("number");
+
+        public static EmmyType Bool
+            => Raw("boolean");
+
+        public static EmmyType String
+            => Raw("string");
+
+        public static EmmyType Any
+            => Raw("any");
+
+        public static EmmyType From(Type ty)
+        {
+            if (ty == typeof(void))
+            {
+                return Void;
+            }
+            else if (ty == typeof(short) || ty == typeof(int) || ty == typeof(long)
+            || ty == typeof(ushort) || ty == typeof(uint) || ty == typeof(ulong))
+            {
+                return Integer;
+            }
+            else if (ty == typeof(float) || ty == typeof(double) || ty == typeof(decimal))
+            {
+                return Float;
+            }
+            else if (ty == typeof(bool))
+            {
+                return Bool;
+            }
+            else if (ty == typeof(string))
+            {
+                return String;
+            }
+            else if (ty == typeof(object) || ty == typeof(DynValue))
+            {
+                return Any;
+            }
+            else if (ty.IsArray)
+            {
+                var inner = From(ty.GetElementType());
+                return inner == null ? null : Array(inner);
+            }
+            else if (ty.IsGenericType && ty.GetGenericTypeDefinition() == typeof(Nullable<>))
+            {
+                var inner = From(ty.GetGenericArguments()[0]);
+                return inner == null ? null : Nullable(inner);
+            }
+            else if (ty.IsGenericType && ty.GetGenericTypeDefinition() == typeof(List<>))
+            {
+                var inner = From(ty.GetGenericArguments()[0]);
+                return inner == null ? null : Array(inner);
+            }
+            else if (ty.IsGenericType && ty.GetGenericTypeDefinition() == typeof(Dictionary<,>))
+            {
+                var key = From(ty.GetGenericArguments()[0]);
+                var value = From(ty.GetGenericArguments()[1]);
+
+                if (key == null || value == null)
+                {
+                    return null;
+                }
+
+                return Table(key, value);
+            }
+            else if (typeof(Delegate).IsAssignableFrom(ty))
+            {
+                var invoke = ty.GetMethod("Invoke");
+                var iparams = invoke.GetParameters();
+
+                if (iparams.Any(p => p.ParameterType == ty))
+                {
+                    throw new InvalidOperationException("Cannot create a function type which is self-referential!");
+                }
+
+                var ret = From(invoke.ReturnType);
+                var par = iparams.Select(p => new EmmyValue { Type = From(p.ParameterType), Name = p.Name })
+                    .ToArray();
+
+                if (ret == null || par.Any(p => p.Type == null))
+                {
+                    return null;
+                }
+
+                return Function(ret, par);
+            }
+            else if (ty.Visible())
+            {
+                return new WrapperType { Type = ty };
+            }
+
+            return null;
+        }
+
+        public static EmmyType Array(EmmyType inner)
+        {
+            return new ArrayType { Inner = inner };
+        }
+
+        public static EmmyType Nullable(EmmyType inner)
+        {
+            return new NullableType { Inner = inner };
+        }
+
+        public static EmmyType Table(EmmyType key, EmmyType value)
+        {
+            return new TableType { Key = key, Value = value };
+        }
+
+        public static EmmyType Table(params (string name, EmmyType type)[] fields)
+        {
+            return new TableLiteralType { Fields = fields.ToDictionary(p => p.name, p => p.type) };
+        }
+
+        public static EmmyType Alias(string name, EmmyType inner)
+        {
+            return new AliasType { Name = name, Inner = inner };
+        }
+
+        public static EmmyType Option(params EmmyType[] types)
+        {
+            return new OptionType { Options = types };
+        }
+
+        public static EmmyType Option(params string[] literals)
+        {
+            return Option(literals.Select(Literal).ToArray());
+        }
+
+        public static EmmyType Function(EmmyType ret, params EmmyValue[] parameters)
+        {
+            return new FunctionType { Return = ret, Parameters = parameters };
+        }
+
+        public static EmmyType Function(params EmmyValue[] parameters)
+        {
+            return new FunctionType { Return = null, Parameters = parameters };
+        }
+
+        public static EmmyType Literal(string s)
+        {
+            return new LiteralStringType { Value = s };
+        }
+
+        public static EmmyType Raw(string s)
+        {
+            return new RawType { Text = s };
+        }
+
+        public virtual string GetDefinition()
+            => string.Empty;
+
+        public abstract override string ToString();
+
+        private class WrapperType : EmmyType
+        {
+            public Type Type { get; set; }
+
+            public override string ToString()
+            {
+                return Type.Name;
+            }
+        }
+
+        private class AliasType : EmmyType
+        {
+            public string Name { get; set; }
+
+            public EmmyType Inner { get; set; }
+
+            public override IEnumerable<EmmyType> Constituents
+            {
+                get
+                {
+                    yield return this;
+                    yield return Inner;
+                }
+            }
+
+            public override string GetDefinition()
+            {
+                return "---@alias " + Name + " " + Inner.ToString();
+            }
+
+            public override string ToString()
+            {
+                return Name;
+            }
+        }
+
+        private class NullableType : EmmyType
+        {
+            public EmmyType Inner { get; set; }
+
+            public override IEnumerable<EmmyType> Constituents
+            {
+                get
+                {
+                    yield return Inner;
+                }
+            }
+
+            public override string ToString()
+            {
+                return Inner.ToString() + "?";
+            }
+        }
+
+        private class ArrayType : EmmyType
+        {
+            public EmmyType Inner { get; set; }
+
+            public override IEnumerable<EmmyType> Constituents
+            {
+                get
+                {
+                    yield return Inner;
+                }
+            }
+
+            public override string ToString()
+            {
+                return Inner.ToString() + "[]";
+            }
+        }
+
+        private class OptionType : EmmyType
+        {
+            public IReadOnlyList<EmmyType> Options { get; set; }
+
+            public override IEnumerable<EmmyType> Constituents
+            {
+                get
+                {
+                    foreach (var item in Options)
+                    {
+                        yield return item;
+                    }
+                }
+            }
+
+            public override string ToString()
+            {
+                return "(" + string.Join(" | ", Options) + ")";
+            }
+        }
+
+        private class LiteralStringType : EmmyType
+        {
+            public string Value { get; set; }
+
+            public override string ToString()
+            {
+                return $"\"{Value}\"";
+            }
+        }
+
+        private class RawType : EmmyType
+        {
+            public string Text { get; set; }
+
+            public override string ToString()
+            {
+                return Text;
+            }
+        }
+
+        private class TableLiteralType : EmmyType
+        {
+            public override IEnumerable<EmmyType> Constituents
+            {
+                get
+                {
+                    foreach (var f in Fields.Values)
+                    {
+                        yield return f;
+                    }
+                }
+            }
+
+            public IReadOnlyDictionary<string, EmmyType> Fields { get; set; }
+
+            public override string ToString()
+            {
+                return $"{{{string.Join(", ", Fields.Select(kvp => $"{kvp.Key}: {kvp.Value}"))}}}";
+            }
+        }
+
+        private class TableType : EmmyType
+        {
+            public override IEnumerable<EmmyType> Constituents
+            {
+                get
+                {
+                    yield return Key;
+                    yield return Value;
+                }
+            }
+
+            public EmmyType Key { get; set; }
+
+            public EmmyType Value { get; set; }
+
+            public override string ToString()
+            {
+                return $"table<{Key}, {Value}>";
+            }
+        }
+
+        private class FunctionType : EmmyType
+        {
+            public override IEnumerable<EmmyType> Constituents
+            {
+                get
+                {
+                    foreach (var v in Parameters)
+                    {
+                        yield return v.Type;
+                    }
+
+                    yield return Return;
+                }
+            }
+
+            public IReadOnlyList<EmmyValue> Parameters { get; set; }
+
+            public EmmyType Return { get; set; }
+
+            public override string ToString()
+            {
+                var ret = "fun(" + string.Join(", ", Parameters.Select(p => p.Name + ": " + p.Type.ToString())) + ")";
+                if (Return != null)
+                {
+                    ret += " : " + Return.ToString();
+                }
+
+                return ret;
+            }
+        }
+    }
+
+    public class EmmyValue
+    {
+        public string Name { get; set; }
+
+        public string Doc { get; set; }
+
+        public virtual EmmyType Type { get; set; }
+
+        public bool ParamHasDefaultValue { get; set; }
+    }
+
+    public class EmmyFunction : EmmyValue
+    {
+        public IReadOnlyList<EmmyValue> Parameters { get; set; }
+
+        public EmmyType Return { get; set; }
+
+        public override EmmyType Type
+        {
+            get => EmmyType.Function(Return, Parameters.ToArray());
+            set => throw new InvalidOperationException();
+        }
+    }
+
+    public class EmmyClass
+    {
+        public string Doc { get; set; }
+
+        public string Name { get; set; }
+
+        public bool IsStatic { get; set; }
+
+        public bool IsSingleton { get; set; }
+
+        public EmmyClass Base { get; set; }
+
+        public IReadOnlyList<EmmyValue> Members { get; set; }
+
+        public IReadOnlyList<EmmyValue> Statics { get; set; }
+    }
+
     public class EmmySharpBuilder
     {
-        private readonly StringBuilder builder = new StringBuilder();
+        private readonly StringBuilder outputBuilder = new StringBuilder();
+
+        private readonly HashSet<EmmyType> constituentTypes = new HashSet<EmmyType>();
+        private readonly Dictionary<Type, EmmyClass> classes = new Dictionary<Type, EmmyClass>();
+        private readonly List<EmmyValue> values = new List<EmmyValue>();
+
+        private readonly Dictionary<string, EmmyType> aliases = new Dictionary<string, EmmyType>();
+
+        private void Add(EmmyValue value)
+        {
+            values.Add(value);
+
+            foreach (var t in value.Type.Constituents)
+            {
+                constituentTypes.Add(t);
+            }
+        }
+
+        private void Add(Type source, EmmyClass classs)
+        {
+            classes[source] = classs;
+
+            foreach (var v in classs.Members.Concat(classs.Statics))
+            {
+                foreach (var t in v.Type.Constituents)
+                {
+                    constituentTypes.Add(t);
+                }
+            }
+        }
 
         /// <summary>
         /// Get an emmy sharp builder which contains type information about the
@@ -36,14 +468,39 @@ public static EmmySharpBuilder ForThisAssembly()
         /// </summary>
         /// <returns>The content string.</returns>
         public override string ToString()
-            => builder.ToString();
+        {
+            outputBuilder.Clear();
+            outputBuilder.AppendLine("---@meta").AppendLine();
+
+            foreach (var consType in constituentTypes)
+            {
+                var definition = consType.GetDefinition();
+
+                if (!string.IsNullOrEmpty(definition))
+                {
+                    outputBuilder.AppendLine(definition).AppendLine();
+                }
+            }
+
+            foreach (var freeValue in values)
+            {
+                RenderValue(freeValue);
+            }
+
+            foreach (var cls in classes.Values)
+            {
+                RenderClass(cls);
+            }
+
+            return outputBuilder.ToString();
+        }
 
         /// <summary>
         /// Append documentation. If null is provided, do nothing.
         /// </summary>
         /// <param name="doc">The document to append.</param>
         /// <returns>The builder instance.</returns>
-        public EmmySharpBuilder AppendDoc(string doc)
+        private EmmySharpBuilder RenderDoc(string doc)
         {
             if (doc is null)
             {
@@ -52,7 +509,7 @@ public EmmySharpBuilder AppendDoc(string doc)
 
             foreach (var line in doc.Split(Environment.NewLine.ToCharArray()))
             {
-                builder.AppendLine("---" + line);
+                outputBuilder.AppendLine("---" + line);
             }
 
             return this;
@@ -62,22 +519,27 @@ public EmmySharpBuilder AppendDoc(string doc)
         /// Append a static value specification to this builder.
         /// </summary>
         /// <param name="value">The value to append.</param>
-        /// <param name="baseTy">The base type.</param>
+        /// <param name="baseV">The base value.</param>
         /// <returns>The builder instance.</returns>
-        public EmmySharpBuilder AppendStaticValue(EmmySharpValue value, Type baseTy = null)
+        private EmmySharpBuilder RenderValue(EmmyValue value, string baseV = null)
         {
-            AppendDoc(value.Doc);
-            builder.Append("---@type ");
-            AppendTypeName(value.Type, value.Options);
-            builder.AppendLine();
+            if (value is EmmyFunction f)
+            {
+                return RenderFunction(f, baseV);
+            }
 
-            if (baseTy != null)
+            RenderDoc(value.Doc);
+            outputBuilder.Append("---@type ");
+            outputBuilder.Append(value.Type);
+            outputBuilder.AppendLine();
+
+            if (baseV != null)
             {
-                builder.Append(baseTy.Name + ".");
+                outputBuilder.Append(baseV + ".");
             }
 
-            builder.AppendLine(value.Name.ToCamelCase() + " = nil");
-            builder.AppendLine();
+            outputBuilder.AppendLine(value.Name + " = nil");
+            outputBuilder.AppendLine();
 
             return this;
         }
@@ -87,12 +549,13 @@ public EmmySharpBuilder AppendStaticValue(EmmySharpValue value, Type baseTy = nu
         /// </summary>
         /// <param name="field">The field to append.</param>
         /// <returns>The builder instance.</returns>
-        public EmmySharpBuilder AppendField(EmmySharpValue field)
+        private EmmySharpBuilder RenderField(EmmyValue field)
         {
-            AppendDoc(field.Doc);
-            builder.Append("---@field public " + field.Name.ToCamelCase() + " ");
-            AppendTypeName(field.Type, field.Options);
-            builder.AppendLine();
+            outputBuilder.Append("---@field public " + field.Name + " ");
+            outputBuilder.Append(field.Type);
+            outputBuilder.Append(' ');
+            outputBuilder.Append(field.Doc?.Replace(Environment.NewLine, "\n"));
+            outputBuilder.AppendLine();
 
             return this;
         }
@@ -100,118 +563,125 @@ public EmmySharpBuilder AppendField(EmmySharpValue field)
         /// <summary>
         /// Append a class definition (static or otherwise) to this builder.
         /// </summary>
-        /// <param name="type">The class type to append.</param>
-        /// <param name="values">List of values.</param>
         /// <returns>The builder instance.</returns>
-        public EmmySharpBuilder AppendClassDefinition(Type type, IEnumerable<EmmySharpValue> values)
+        private EmmySharpBuilder RenderClass(EmmyClass cls)
         {
-            AppendDoc(type.EmmyDoc());
-            string alias = type.EmmyAlias();
-            builder
-                .AppendLine($"{alias ?? type.Name} = {{}}")
+            RenderDoc(cls.Doc);
+            outputBuilder
+                .AppendLine($"{cls.Name} = {{}}")
                 .AppendLine();
 
-            var staticValues = values.Where(f => f.IsStatic).ToArray();
-
-            foreach (var staticVal in staticValues)
+            foreach (var staticVal in cls.IsSingleton ? cls.Statics.Concat(cls.Members) : cls.Statics)
             {
-                AppendStaticValue(staticVal, type);
+                RenderValue(staticVal, cls.Name);
             }
 
-            bool singleton = type.IsDefined(typeof(EmmySingletonAttribute));
-
             // Ignore instance values and table for static classes
-            if (!type.IsAbstract || !type.IsSealed)
+            if (!cls.IsStatic && !cls.IsSingleton)
             {
-                AppendDoc(type.EmmyDoc());
-                builder.Append($"---@class {alias ?? type.Name}");
+                RenderDoc(cls.Doc);
+                outputBuilder.Append($"---@class {cls.Name}");
 
-                if (type.BaseType != typeof(object) && Attribute.IsDefined(type.BaseType, typeof(MoonSharpUserDataAttribute)))
+                if (cls.Base != null)
                 {
-                    string baseAlias = type.BaseType.EmmyAlias();
-                    builder.Append($" : {baseAlias ?? type.BaseType.Name}");
+                    outputBuilder.Append($" : {cls.Base.Name}");
                 }
 
-                builder.AppendLine();
+                outputBuilder.AppendLine();
 
-                foreach (var field in values.Where(f => !f.IsStatic))
+                foreach (var field in cls.Members)
                 {
-                    AppendField(field);
+                    if (!(field is EmmyFunction))
+                    {
+                        RenderField(field);
+                    }
                 }
 
-                if (!singleton)
+                outputBuilder.AppendLine($"local {cls.Name}__inst = {{}}");
+                outputBuilder.AppendLine();
+
+                foreach (var field in cls.Members)
                 {
-                    builder.AppendLine($"{alias ?? type.Name}__inst = {{}}");
-                    builder.AppendLine();
+                    if (field is EmmyFunction f)
+                    {
+                        RenderFunction(f, $"{cls.Name}__inst");
+                    }
                 }
             }
-            else if (staticValues.Length != 0)
+
+            return this;
+        }
+
+        private EmmyFunction LoadMethod(MethodInfo met)
+        {
+            return new EmmyFunction
             {
-                builder.AppendLine();
-            }
+                Doc = met.EmmyDoc(),
+                Name = met.EmmyName(camelCase: true),
+                Parameters = met.GetParameters()
+                    .Select(p => new EmmyValue { Doc = p.EmmyDoc(), Name = p.EmmyName(p.Name), Type = p.GetEmmyType(p.ParameterType, aliases), ParamHasDefaultValue = p.HasDefaultValue })
+                    .ToArray(),
+                Return = met.ReturnParameter.GetEmmyType(met.ReturnType, aliases),
+            };
+        }
+
+        public EmmySharpBuilder AppendFunction(MethodInfo met)
+        {
+            Add(LoadMethod(met));
 
             return this;
         }
 
+        public EmmySharpBuilder AppendFunction<T>(string name)
+            => AppendFunction(typeof(T).GetMethod(name));
+
         /// <summary>
         /// Append a function with the given documentation. If this function belongs to a type,
         /// pass it as the member type.
         /// </summary>
-        /// <param name="method">The method to append.</param>
-        /// <param name="memberType">The member type.</param>
         /// <returns>The builder instance.</returns>
-        public EmmySharpBuilder AppendFunction(MethodInfo method, Type memberType = null)
+        private EmmySharpBuilder RenderFunction(EmmyFunction function, string baseV = "")
         {
-            AppendDoc(method.EmmyDoc());
-
-            var parameters = method.GetParameters();
+            RenderDoc(function.Doc);
 
-            foreach (var p in parameters)
+            foreach (var p in function.Parameters)
             {
-                builder.Append($"---@param {p.Name} ");
-                AppendTypeName(p.ParameterType, p.EmmyChoice());
-                builder.AppendLine();
+                outputBuilder.Append($"---@param {p.Name}{(p.ParamHasDefaultValue ? "?" : "")} ");
+                outputBuilder.Append(p.Type);
+                outputBuilder.Append(' ');
+                outputBuilder.Append(p.Doc);
+                outputBuilder.AppendLine();
             }
 
-            if (method.ReturnType != typeof(void))
+            if (function.Return != null)
             {
-                builder.Append($"---@return ");
-                AppendTypeName(method.ReturnType, method.ReturnTypeCustomAttributes.EmmyChoice());
-                builder.AppendLine();
+                outputBuilder.Append($"---@return ");
+                outputBuilder.Append(function.Return);
+                outputBuilder.AppendLine();
             }
 
-            builder.Append("function ");
+            outputBuilder.Append("function ");
 
-            if (memberType != null)
+            if (!string.IsNullOrEmpty(baseV))
             {
-                string memberTypeAlias = memberType.EmmyAlias();
-                builder.Append(memberTypeAlias ?? memberType.Name);
-
-                bool singleton = memberType.IsDefined(typeof(EmmySingletonAttribute));
-                if (!method.IsStatic && !singleton)
-                {
-                    builder.Append("__inst");
-                }
-
-                builder.Append('.');
+                outputBuilder.Append(baseV);
+                outputBuilder.Append('.');
             }
 
-            string alias = method.EmmyAlias();
-            builder.Append(alias ?? method.Name.ToCamelCase()).Append('(');
+            outputBuilder.Append(function.Name).Append('(');
 
-            for (var i = 0; i < parameters.Length; i++)
+            for (var i = 0; i < function.Parameters.Count; i++)
             {
-                var p = parameters[i];
-                string paramName = RenameAvoidKeyword(p.Name);
+                var p = function.Parameters[i];
 
-                builder.Append(paramName);
-                if (i != parameters.Length - 1)
+                outputBuilder.Append(p.Name);
+                if (i != function.Parameters.Count - 1)
                 {
-                    builder.Append(", ");
+                    outputBuilder.Append(", ");
                 }
             }
 
-            builder
+            outputBuilder
                 .AppendLine(") end")
                 .AppendLine();
 
@@ -219,147 +689,90 @@ public EmmySharpBuilder AppendFunction(MethodInfo method, Type memberType = null
         }
 
         /// <summary>
-        /// Appends the type name of the given type.
-        /// This function may 'error out', in which case an erroneous (but still legal)
-        /// type will be appended and a warning printed to the console's standard error.
+        /// Append the given type as if it were exposed by MoonSharp.
         /// </summary>
-        /// <param name="ty">Type to append.</param>
-        /// <param name="options">List of options.</param>
+        /// <param name="type">The type to append.</param>
         /// <returns>The builder instance.</returns>
-        public EmmySharpBuilder AppendTypeName(Type ty, string[] options = null)
+        public EmmySharpBuilder AppendType(Type type)
         {
-            if (ty == typeof(short) || ty == typeof(int) || ty == typeof(long)
-            || ty == typeof(ushort) || ty == typeof(uint) || ty == typeof(ulong))
+            EmmyClass BuildClass(Type ty)
             {
-                builder.Append("integer");
-            }
-            else if (ty == typeof(float) || ty == typeof(double) || ty == typeof(decimal))
-            {
-                builder.Append("number");
-            }
-            else if (ty == typeof(bool))
-            {
-                builder.Append("boolean");
-            }
-            else if (ty == typeof(string))
-            {
-                if (options != null)
+                if (ty.IsConstructedGenericType)
                 {
-                    builder
-                        .Append('(')
-                        .Append(string.Join(" | ", options.Select(t => $"'{t}'")))
-                        .Append(')');
+                    ty = ty.GetGenericTypeDefinition();
                 }
-                else
-                {
-                    builder.Append("string");
-                }
-            }
-            else if (ty == typeof(object))
-            {
-                builder.Append("any");
-            }
-            else if (ty.IsArray)
-            {
-                AppendTypeName(ty.GetElementType(), options);
-                builder.Append("[]");
-            }
-            else if (ty.IsGenericType && ty.GetGenericTypeDefinition() == typeof(List<>))
-            {
-                AppendTypeName(ty.GetGenericArguments()[0], options);
-                builder.Append("[]");
-            }
-            else if (ty.IsGenericType && ty.GetGenericTypeDefinition() == typeof(Dictionary<,>))
-            {
-                builder.Append("table<");
-                AppendTypeName(ty.GetGenericArguments()[0], options);
-                builder.Append(", ");
-                AppendTypeName(ty.GetGenericArguments()[1], options);
-                builder.Append('>');
-            }
-            else if (Attribute.IsDefined(ty, typeof(MoonSharpUserDataAttribute)))
-            {
-                builder.Append(ty.Name);
-            }
-            else if (typeof(Delegate).IsAssignableFrom(ty))
-            {
-                var invoke = ty.GetMethod("Invoke");
-                var iparams = invoke.GetParameters();
 
-                builder.Append("fun(");
-                for (var i = 0; i < iparams.Length; i++)
+                var cls = new EmmyClass { Name = ty.EmmyName() };
+
+                if (ty.BaseType != null && ty.BaseType.Visible())
                 {
-                    var p = iparams[i];
+                    var baseTy = ty.BaseType;
 
-                    if (p.ParameterType == ty)
+                    if (baseTy.IsConstructedGenericType)
                     {
-                        Console.Error.WriteLine($"[ERROR]: Cannot generate emmylua for type {ty.FullName} since it is a recursive delegate type, falling back to 'fun(...):any'");
-                        builder.Append(") : any");
-                        return this;
+                        // TODO: support real generics
+                        baseTy = baseTy.GetGenericTypeDefinition();
                     }
 
-                    builder.Append(p.Name + ":");
-                    AppendTypeName(p.ParameterType, p.EmmyChoice());
-
-                    if (i != iparams.Length - 1)
+                    if (!classes.TryGetValue(ty.BaseType, out var baseCls))
                     {
-                        builder.Append(", ");
+                        baseCls = BuildClass(ty.BaseType);
                     }
+
+                    cls.Base = baseCls;
                 }
 
-                builder.Append(')');
+                var members = new List<EmmyValue>();
+                var statics = new List<EmmyValue>();
 
-                if (invoke.ReturnType != typeof(void))
+                cls.Members = members;
+                cls.Statics = statics;
+
+                foreach (var val in ty.GetFields()
+                    .Where(t => t.IsPublic)
+                    .Where(t => t.DeclaringType == ty)
+                    .Where(EmmyHelpers.Visible))
                 {
-                    builder.Append(" : ");
-                    AppendTypeName(invoke.ReturnType, options);
+                    (val.IsStatic ? statics : members).Add(new EmmyValue
+                    {
+                        Doc = val.EmmyDoc(),
+                        Name = val.EmmyName(camelCase: true),
+                        Type = val.GetEmmyType(val.FieldType, aliases),
+                    });
                 }
-            }
-            else
-            {
-                Console.Error.WriteLine($"[ERROR]: Cannot generate emmylua for type {ty.FullName}, falling back to type 'any'");
-                builder.Append("any");
-            }
-
-            return this;
-        }
 
-        /// <summary>
-        /// Append the given type as if it were exposed by MoonSharp.
-        /// </summary>
-        /// <param name="type">The type to append.</param>
-        /// <returns>The builder instance.</returns>
-        public EmmySharpBuilder AppendType(Type type)
-        {
-            var fields = new List<EmmySharpValue>();
+                foreach (var val in ty.GetProperties()
+                    .Where(t => t.GetAccessors().First().IsPublic)
+                    .Where(t => t.DeclaringType == ty)
+                    .Where(EmmyHelpers.Visible))
+                {
+                    (val.GetAccessors().First().IsStatic ? statics : members).Add(new EmmyValue
+                    {
+                        Doc = val.EmmyDoc(),
+                        Name = val.EmmyName(camelCase: true),
+                        Type = val.GetEmmyType(val.PropertyType, aliases),
+                    });
+                }
 
-            foreach (var val in type.GetFields()
-                .Where(t => t.IsPublic)
-                .Where(t => t.DeclaringType == type)
-                .Where(t => !Attribute.IsDefined(t, typeof(MoonSharpHiddenAttribute))))
-            {
-                fields.Add(new EmmySharpValue(val.EmmyDoc(), val.Name, val.FieldType, val.EmmyChoice(), val.IsStatic));
-            }
+                foreach (var met in ty.GetMethods()
+                    .Where(t => t.IsPublic)
+                    .Where(t => !t.IsSpecialName)
+                    .Where(t => t.DeclaringType == ty)
+                    .Where(EmmyHelpers.Visible))
+                {
+                    // TODO: handle operators
+                    (met.IsStatic ? statics : members).Add(LoadMethod(met));
+                }
 
-            foreach (var val in type.GetProperties()
-                .Where(t => t.GetAccessors().Any(a => a.IsPublic))
-                .Where(t => t.DeclaringType == type)
-                .Where(t => !Attribute.IsDefined(t, typeof(MoonSharpHiddenAttribute))))
-            {
-                fields.Add(new EmmySharpValue(val.EmmyDoc(), val.Name, val.PropertyType, val.EmmyChoice(), val.GetGetMethod().IsStatic));
-            }
+                cls.IsStatic = ty.IsAbstract && ty.IsSealed;
+                cls.IsSingleton = ty.GetAttrOrNull<EmmySingletonAttribute>() != null;
 
-            AppendClassDefinition(type, fields);
+                Add(ty, cls);
 
-            foreach (var met in type.GetMethods()
-                .Where(t => t.IsPublic)
-                .Where(t => !t.IsSpecialName)
-                .Where(t => t.DeclaringType == type)
-                .Where(t => !Attribute.IsDefined(t, typeof(MoonSharpHiddenAttribute))))
-            {
-                AppendFunction(met, type);
+                return cls;
             }
 
+            BuildClass(type);
             return this;
         }
 
@@ -371,6 +784,19 @@ public EmmySharpBuilder AppendType(Type type)
         public EmmySharpBuilder AppendType<T>()
             => AppendType(typeof(T));
 
+        public EmmySharpBuilder AppendAlias(string name, EmmyType type)
+        {
+            var alias = EmmyType.Alias(name, type);
+            aliases[name] = alias;
+
+            foreach (var constituent in alias.Constituents)
+            {
+                constituentTypes.Add(constituent);
+            }
+
+            return this;
+        }
+
         /// <summary>
         /// Append all type information from the given assembly, including types not added to MoonSharp,
         /// which fall under the given group.
@@ -381,8 +807,8 @@ public EmmySharpBuilder AppendType<T>()
         public EmmySharpBuilder AppendGroup(Assembly assembly, string group)
         {
             foreach (var ty in assembly.GetTypes()
-                .Where(t => Attribute.IsDefined(t, typeof(EmmyGroupAttribute)))
-                .Where(t => ((EmmyGroupAttribute)Attribute.GetCustomAttribute(t, typeof(EmmyGroupAttribute))).GroupName == group))
+                .Where(EmmyHelpers.Visible)
+                .Where(t => t.GetAttrOrNull<EmmyGroupAttribute>()?.GroupName == group))
             {
                 AppendType(ty);
             }
@@ -406,7 +832,7 @@ public EmmySharpBuilder AppendGroup(string group)
         /// <returns>The builder instance.</returns>
         public EmmySharpBuilder AppendAssembly(Assembly assembly)
         {
-            foreach (var ty in assembly.GetTypes().Where(t => Attribute.IsDefined(t, typeof(MoonSharpUserDataAttribute))))
+            foreach (var ty in assembly.GetTypes().Where(EmmyHelpers.Visible))
             {
                 AppendType(ty);
             }
@@ -436,38 +862,8 @@ public void Build(string filepath)
 
             filepath = Path.Combine(filepath, "lib.lua");
             File.WriteAllText(filepath, c);
-        }
 
-        private string RenameAvoidKeyword(string name)
-        {
-            switch (name)
-            {
-                case "and":
-                case "break":
-                case "do":
-                case "else":
-                case "elseif":
-                case "end":
-                case "false":
-                case "for":
-                case "function":
-                case "goto":
-                case "if":
-                case "in":
-                case "local":
-                case "nil":
-                case "not":
-                case "or":
-                case "repeat":
-                case "return":
-                case "then":
-                case "true":
-                case "until":
-                case "while":
-                    return "_" + name;
-                default:
-                    return name;
-            }
+            Debug.Log("Built emmylua to " + filepath);
         }
     }
 }
\ No newline at end of file
diff --git a/Assets/Scripts/Utility/EmmySharp/EmmyTypeAttribute.cs b/Assets/Scripts/Utility/EmmySharp/EmmyTypeAttribute.cs
new file mode 100644
index 00000000..6a5a2490
--- /dev/null
+++ b/Assets/Scripts/Utility/EmmySharp/EmmyTypeAttribute.cs
@@ -0,0 +1,27 @@
+/// <summary>
+/// EmmySharp, created by Dylan Rafael (floofer++) for use by 0thElement.
+/// The below classes help to generate EmmyLua workspace files from MoonSharp classes.
+/// </summary>
+
+using System;
+
+namespace EmmySharp
+{
+    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
+    public class EmmyTypeAttribute : Attribute
+    {
+        public EmmyTypeAttribute(Type type)
+        {
+            Type = type;
+        }
+
+        public EmmyTypeAttribute(string raw)
+        {
+            Raw = raw;
+        }
+
+        public Type Type { get; }
+
+        public string Raw { get; }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Utility/EmmySharp/EmmyTypeAttribute.cs.meta b/Assets/Scripts/Utility/EmmySharp/EmmyTypeAttribute.cs.meta
new file mode 100644
index 00000000..0e5bc1b5
--- /dev/null
+++ b/Assets/Scripts/Utility/EmmySharp/EmmyTypeAttribute.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0692feb8171682848a465b80d6bef242
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Scripts/Utility/Extension/CollectionExtension.cs b/Assets/Scripts/Utility/Extension/CollectionExtension.cs
index b594cbec..cf30dd6f 100644
--- a/Assets/Scripts/Utility/Extension/CollectionExtension.cs
+++ b/Assets/Scripts/Utility/Extension/CollectionExtension.cs
@@ -7,6 +7,21 @@ namespace ArcCreate.Utility.Extension
 {
     public static class CollectionExtension
     {
+        /// <summary>
+        /// Add all the elements from one list to another without
+        /// any conversion to IEnumerable.
+        /// </summary>
+        /// <param name="list">The list to add to.</param>
+        /// <param name="other">The source list to take elements from.</param>
+        /// <typeparam name="T">The type of the element in each list.</typeparam>
+        public static void FastAddRange<T>(this IList<T> list, IReadOnlyList<T> other)
+        {
+            for (int i = 0; i < other.Count; i++)
+            {
+                list.Add(other[i]);
+            }
+        }
+
         /// <summary>
         /// Whether or not the index is outside the range of the collection.
         /// </summary>
diff --git a/Assets/Scripts/Utility/TRS.cs b/Assets/Scripts/Utility/TRS.cs
new file mode 100644
index 00000000..edac91bc
--- /dev/null
+++ b/Assets/Scripts/Utility/TRS.cs
@@ -0,0 +1,67 @@
+using System;
+using UnityEngine;
+
+namespace ArcCreate.Utility
+{
+    /// <summary>
+    /// A simple data structure which represents a TRS transformation,
+    /// implementing two operations, (+) and (*), to simplify their
+    /// combination.
+    /// The main reasoning for this type's existence is to dimplify access
+    /// to the components of a TRS matrix transformation.
+    /// </summary>
+    public struct TRS
+    {
+        public TRS(Vector3 position, Quaternion rot, Vector3 scale)
+        {
+            Translation = position;
+            Rotation = rot;
+            Scale = scale;
+        }
+
+        public TRS(Matrix4x4 matrix)
+        {
+            if (!matrix.ValidTRS())
+            {
+                throw new InvalidOperationException("Cannot create a transform from an invalid TRS matrix!");
+            }
+
+            Translation = matrix.GetColumn(3);
+            Rotation = matrix.rotation;
+            Scale = matrix.lossyScale;
+
+            // Debug.Log("Scale = " + Scale);
+        }
+
+        #pragma warning disable
+        public static TRS identity
+        #pragma warning restore
+            => new TRS(Vector3.zero, Quaternion.identity, Vector3.one);
+
+        public Vector3 Translation { get; set; }
+
+        public Quaternion Rotation { get; set; }
+
+        public Vector3 Scale { get; set; }
+
+        public Matrix4x4 Matrix => Matrix4x4.TRS(Translation, Rotation, Scale);
+
+        public static implicit operator Matrix4x4(TRS a)
+            => a.Matrix;
+
+        public static TRS operator +(TRS a, TRS b)
+            => new TRS(
+                a.Translation + b.Translation,
+                a.Rotation * b.Rotation,
+                new Vector3(a.Scale.x * b.Scale.x, a.Scale.y * b.Scale.y, a.Scale.z * b.Scale.z));
+
+        public static TRS operator *(TRS a, TRS b)
+            => new TRS(a.Matrix * b.Matrix);
+
+        public static TRS TranslateOnly(Vector3 translate)
+            => new TRS(translate, Quaternion.identity, Vector3.one);
+
+        public static TRS ScaleOnly(Vector3 scale)
+            => new TRS(Vector3.zero, Quaternion.identity, scale);
+    }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Utility/TRS.cs.meta b/Assets/Scripts/Utility/TRS.cs.meta
new file mode 100644
index 00000000..c532caa3
--- /dev/null
+++ b/Assets/Scripts/Utility/TRS.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 708159425c6c5f24cb36567fe813df00
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/StreamingAssets/default.arcpkg.meta b/Assets/StreamingAssets/default.arcpkg.meta
index fa1ead5f..c85dedf8 100644
--- a/Assets/StreamingAssets/default.arcpkg.meta
+++ b/Assets/StreamingAssets/default.arcpkg.meta
@@ -1,5 +1,5 @@
 fileFormatVersion: 2
-guid: e973375a25be4344bb963c2bd2e1e236
+guid: 23da70d899e8de4449a1a7f4edd0b55c
 DefaultImporter:
   externalObjects: {}
   userData: 
diff --git a/Assets/Tests/Scenecontrol/TriggerTest.cs b/Assets/Tests/Scenecontrol/TriggerTest.cs
index 6fb15f60..49ad7a44 100644
--- a/Assets/Tests/Scenecontrol/TriggerTest.cs
+++ b/Assets/Tests/Scenecontrol/TriggerTest.cs
@@ -149,6 +149,7 @@ public override List<object> SerializeProperties(ScenecontrolSerialization seria
             }
         }
 
+        [SerializationExempt]
         private class ManualChannel : ValueChannel
         {
             public float Value { get; set; }