From 153af071fa75bb70dff410c48a6b97783829f103 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 15:45:13 +0100 Subject: [PATCH 01/97] Setup initial editor scene --- Source/Game.cs | 7 ++++--- Source/Mod/Core/GameMod.cs | 5 +++++ Source/Mod/Editor/EditorScene.cs | 33 +++++++++++++++++++++++++++++++ Source/Mod/ImGui/FujiDebugMenu.cs | 4 ++-- Source/Mod/ImGui/ImGuiManager.cs | 11 ++++++++++- Source/Scenes/Scene.cs | 4 +++- Source/Scenes/World.cs | 8 ++++++++ 7 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 Source/Mod/Editor/EditorScene.cs diff --git a/Source/Game.cs b/Source/Game.cs index 3aa9904e..3e451a52 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Text; using Celeste64.Mod; +using Celeste64.Mod.Editor; using Celeste64.Mod.Patches; namespace Celeste64; @@ -74,7 +75,7 @@ public static float ResolutionScale private static Game? instance; public static Game Instance => instance ?? throw new Exception("Game isn't running"); - private readonly Stack scenes = new(); + internal readonly Stack scenes = new(); private Target target = new(Width, Height, [TextureFormat.Color, TextureFormat.Depth24Stencil8]); private readonly Batcher batcher = new(); private Transition transition; @@ -91,8 +92,8 @@ public static float ResolutionScale public SoundHandle? AmbienceWav; public SoundHandle? MusicWav; - public Scene? Scene { get { return scenes.TryPeek(out Scene? scene) ? scene : null; } } - public World? World { get { return Scene is World world ? world : null; } } + public static Scene? Scene => Instance.scenes.TryPeek(out var scene) ? scene : null; + public World? World => Scene is World world ? world : null; internal bool NeedsReload = false; diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs index 56d4ad01..fa80ef5e 100644 --- a/Source/Mod/Core/GameMod.cs +++ b/Source/Mod/Core/GameMod.cs @@ -56,6 +56,11 @@ public abstract class GameMod public World? World { get { return Game != null ? Game.World : null; } } public Map? Map { get { return World != null ? World.Map : null; } } public Player? Player { get { return World != null ? World.Get() : null; } } + + // public Game? Game => Game.Instance; + // public World? World => Game.Scene as World; + // public Map? Map => World?.Map; + // public Player? Player => World?.Get(); // Common Metadata about this mod. public bool Enabled { get { return this is VanillaGameMod || ModSaveData.Enabled; } } diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs new file mode 100644 index 00000000..98de7d91 --- /dev/null +++ b/Source/Mod/Editor/EditorScene.cs @@ -0,0 +1,33 @@ +namespace Celeste64.Mod.Editor; + +public class EditorScene : Scene +{ + private World.EntryInfo Entry; + + internal readonly EditorHandler Handler = new(); + + internal EditorScene(World.EntryInfo entry) + { + Entry = entry; + } + + public override void Update() + { + // Toggle to in-game + if (Input.Keyboard.Pressed(Keys.F3)) + { + Game.Instance.scenes.Pop(); + Game.Instance.scenes.Push(new World(Entry)); + } + } + + public override void Render(Target target) + { + target.Clear(Color.Black); + } +} + +internal class EditorHandler : ImGuiHandler +{ + +} \ No newline at end of file diff --git a/Source/Mod/ImGui/FujiDebugMenu.cs b/Source/Mod/ImGui/FujiDebugMenu.cs index f7ce7c0a..9a6b5806 100644 --- a/Source/Mod/ImGui/FujiDebugMenu.cs +++ b/Source/Mod/ImGui/FujiDebugMenu.cs @@ -21,7 +21,7 @@ public override void Render() { ImGui.SetNextWindowSizeConstraints(new Vec2(300, 300), new Vec2(float.PositiveInfinity, float.PositiveInfinity)); ImGui.Begin("Celeste 64 - Debug Menu"); - if (Game.Instance.Scene is World && ModManager.Instance.CurrentLevelMod != null) + if (Game.Scene is World && ModManager.Instance.CurrentLevelMod != null) { if (ImGui.BeginMenu("Open Map")) { @@ -44,7 +44,7 @@ public override void Render() { } } - if (Game.Instance.Scene is World world && world.Get() is Player player) + if (Game.Scene is World world && world.Get() is { } player) { if (world.All().Any() && ImGui.BeginMenu("Go to Checkpoint")) { diff --git a/Source/Mod/ImGui/ImGuiManager.cs b/Source/Mod/ImGui/ImGuiManager.cs index 43960997..2d721b99 100644 --- a/Source/Mod/ImGui/ImGuiManager.cs +++ b/Source/Mod/ImGui/ImGuiManager.cs @@ -1,3 +1,4 @@ +using Celeste64.Mod.Editor; using ImGuiNET; namespace Celeste64.Mod; @@ -28,8 +29,11 @@ internal void UpdateHandlers() { renderer.Update(); - if(debugMenu.Active) + if (debugMenu.Active) debugMenu.Update(); + + if (Game.Scene is EditorScene editor) + editor.Handler.Update(); foreach (var handler in Handlers) { @@ -40,8 +44,13 @@ internal void UpdateHandlers() internal void RenderHandlers() { renderer.BeforeRender(); + if (debugMenu.Visible) debugMenu.Render(); + + if (Game.Scene is EditorScene editor) + editor.Handler.Render(); + foreach (var handler in Handlers) { if (handler.Visible) handler.Render(); diff --git a/Source/Scenes/Scene.cs b/Source/Scenes/Scene.cs index 6a56cd25..3843c7b3 100644 --- a/Source/Scenes/Scene.cs +++ b/Source/Scenes/Scene.cs @@ -1,4 +1,6 @@ +using Celeste64.Mod; + namespace Celeste64; public abstract class Scene @@ -8,7 +10,7 @@ public abstract class Scene public string MusicWav = string.Empty; public string AmbienceWav = string.Empty; - + public virtual void Entered() {} public virtual void Exited() {} public virtual void Disposed() {} diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 6e01ad0d..05b7919e 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using ModelEntry = (Celeste64.Actor Actor, Celeste64.Model Model); using Celeste64.Mod; +using Celeste64.Mod.Editor; namespace Celeste64; @@ -392,6 +393,13 @@ public override void Update() return; } // don't pour salt in wounds + // Toggle to editor + if (Input.Keyboard.Pressed(Keys.F3)) + { + Game.Instance.scenes.Pop(); + Game.Instance.scenes.Push(new EditorScene(Entry)); + } + try { debugUpdTimer.Restart(); From 2d1214bbe6ff7315c21c0d4a91fd9fc14494e805 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 16:04:53 +0100 Subject: [PATCH 02/97] Use Renogare font --- Source/Mod/ImGui/ImGuiRenderer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Mod/ImGui/ImGuiRenderer.cs b/Source/Mod/ImGui/ImGuiRenderer.cs index 5c674c18..b1098ba8 100644 --- a/Source/Mod/ImGui/ImGuiRenderer.cs +++ b/Source/Mod/ImGui/ImGuiRenderer.cs @@ -30,8 +30,8 @@ public ImGuiRenderer() io.ConfigDockingAlwaysTabBar = true; io.ConfigDockingTransparentPayload = true; - io.Fonts.AddFontDefault(); - io.FontGlobalScale = 1.5f; + io.Fonts.AddFontFromFileTTF($"{Assets.ContentPath}/{Assets.FontsFolder}/Renogare.otf", 14); + io.FontGlobalScale = 1.0f; Input.OnTextEvent += chr => io.AddInputCharacter(chr); } From 27abb151e45caa2eafc17b6333f9c9335569a23b Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 16:05:54 +0100 Subject: [PATCH 03/97] Only show debug menu when inside a World --- Source/Mod/ImGui/FujiDebugMenu.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Mod/ImGui/FujiDebugMenu.cs b/Source/Mod/ImGui/FujiDebugMenu.cs index 9a6b5806..17d1da6f 100644 --- a/Source/Mod/ImGui/FujiDebugMenu.cs +++ b/Source/Mod/ImGui/FujiDebugMenu.cs @@ -18,10 +18,13 @@ public override void Update() } public override void Render() { + if (Game.Scene is not World world) + return; + ImGui.SetNextWindowSizeConstraints(new Vec2(300, 300), new Vec2(float.PositiveInfinity, float.PositiveInfinity)); ImGui.Begin("Celeste 64 - Debug Menu"); - if (Game.Scene is World && ModManager.Instance.CurrentLevelMod != null) + if (ModManager.Instance.CurrentLevelMod != null) { if (ImGui.BeginMenu("Open Map")) { @@ -44,7 +47,7 @@ public override void Render() { } } - if (Game.Scene is World world && world.Get() is { } player) + if (world.Get() is { } player) { if (world.All().Any() && ImGui.BeginMenu("Go to Checkpoint")) { From af38d56fdb872eef6deda11ff4353793602d1244 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 16:20:47 +0100 Subject: [PATCH 04/97] Add test window --- Source/Mod/Editor/EditorScene.cs | 4 +++- Source/Mod/Editor/EditorWindow.cs | 16 ++++++++++++++++ Source/Mod/Editor/Windows/TestWindow.cs | 13 +++++++++++++ Source/Mod/ImGui/ImGuiManager.cs | 16 +++++++++++++--- 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 Source/Mod/Editor/EditorWindow.cs create mode 100644 Source/Mod/Editor/Windows/TestWindow.cs diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 98de7d91..86386d2d 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -4,7 +4,9 @@ public class EditorScene : Scene { private World.EntryInfo Entry; - internal readonly EditorHandler Handler = new(); + internal readonly ImGuiHandler[] Handlers = [ + new TestWindow(), + ]; internal EditorScene(World.EntryInfo entry) { diff --git a/Source/Mod/Editor/EditorWindow.cs b/Source/Mod/Editor/EditorWindow.cs new file mode 100644 index 00000000..c890fbe1 --- /dev/null +++ b/Source/Mod/Editor/EditorWindow.cs @@ -0,0 +1,16 @@ +using ImGuiNET; + +namespace Celeste64.Mod.Editor; + +public abstract class EditorWindow : ImGuiHandler +{ + protected abstract string Title { get; } + + protected abstract void RenderWindow(); + public sealed override void Render() + { + ImGui.Begin(Title); + RenderWindow(); + ImGui.End(); + } +} \ No newline at end of file diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs new file mode 100644 index 00000000..a5c32363 --- /dev/null +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -0,0 +1,13 @@ +using ImGuiNET; + +namespace Celeste64.Mod.Editor; + +public class TestWindow : EditorWindow +{ + protected override string Title => "Test"; + + protected override void RenderWindow() + { + ImGui.Text("Testing"); + } +} \ No newline at end of file diff --git a/Source/Mod/ImGui/ImGuiManager.cs b/Source/Mod/ImGui/ImGuiManager.cs index 2d721b99..ac5da5a2 100644 --- a/Source/Mod/ImGui/ImGuiManager.cs +++ b/Source/Mod/ImGui/ImGuiManager.cs @@ -33,7 +33,12 @@ internal void UpdateHandlers() debugMenu.Update(); if (Game.Scene is EditorScene editor) - editor.Handler.Update(); + { + foreach (var handler in editor.Handlers) + { + if (handler.Active) handler.Update(); + } + } foreach (var handler in Handlers) { @@ -49,8 +54,13 @@ internal void RenderHandlers() debugMenu.Render(); if (Game.Scene is EditorScene editor) - editor.Handler.Render(); - + { + foreach (var handler in editor.Handlers) + { + if (handler.Visible) handler.Render(); + } + } + foreach (var handler in Handlers) { if (handler.Visible) handler.Render(); From bc4be558e2c1bf0d497839ed817261e00eba6e61 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 18:33:32 +0100 Subject: [PATCH 05/97] Render basic cube --- Content/Shaders/Editor.glsl | 72 ++++++++ Source/Mod/Editor/Definition.cs | 7 + Source/Mod/Editor/EditorMaterial.cs | 224 +++++++++++++++++++++++++ Source/Mod/Editor/EditorRenderState.cs | 36 ++++ Source/Mod/Editor/EditorScene.cs | 5 +- Source/Mod/Editor/EditorVertex.cs | 19 +++ Source/Mod/Editor/TestDefinition.cs | 80 +++++++++ Source/Mod/Editor/WorldRenderer.cs | 45 +++++ 8 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 Content/Shaders/Editor.glsl create mode 100644 Source/Mod/Editor/Definition.cs create mode 100644 Source/Mod/Editor/EditorMaterial.cs create mode 100644 Source/Mod/Editor/EditorRenderState.cs create mode 100644 Source/Mod/Editor/EditorVertex.cs create mode 100644 Source/Mod/Editor/TestDefinition.cs create mode 100644 Source/Mod/Editor/WorldRenderer.cs diff --git a/Content/Shaders/Editor.glsl b/Content/Shaders/Editor.glsl new file mode 100644 index 00000000..3f2ec0af --- /dev/null +++ b/Content/Shaders/Editor.glsl @@ -0,0 +1,72 @@ +VERTEX: +#version 330 +#include Partials/Methods.gl + +uniform mat4 u_mvp; +uniform mat4 u_model; + +layout(location=0) in vec3 a_position; +layout(location=1) in vec2 a_tex; +layout(location=2) in vec3 a_color; +layout(location=3) in vec3 a_normal; + +out vec2 v_tex; +out vec3 v_color; +out vec3 v_normal; +out vec3 v_world; + +void main(void) +{ + gl_Position = u_mvp * vec4(a_position, 1.0); + + v_tex = a_tex; + v_color = a_color; + v_normal = TransformNormal(a_normal, u_model); + v_world = vec3(u_model * vec4(a_position, 1.0)); +} + +FRAGMENT: +#version 330 +#include Partials/Methods.gl + +uniform sampler2D u_texture; +uniform vec4 u_color; +uniform float u_near; +uniform float u_far; +uniform vec3 u_sun; +//uniform float u_cutout; + +in vec2 v_tex; +in vec3 v_normal; +in vec3 v_color; +in vec3 v_world; + +layout(location = 0) out vec4 o_color; + +void main(void) +{ + // get texture color + vec4 src = texture(u_texture, v_tex) * u_color * vec4(v_color, 1); + + // TODO: only enable if you want ModelFlags.Cutout types to work, didn't end up using +// if (src.a < u_cutout) +// discard; + + float depth = LinearizeDepth(gl_FragCoord.z, u_near, u_far); + float fall = Map(v_world.z, 50, 0, 0, 1); + float fade = Map(depth, 0.9, 1, 1, 0); + vec3 col = src.rgb; + + // apply depth values + gl_FragDepth = depth; + + // lighten texture color based on normal + float lighten = max(0.0, -dot(v_normal, u_sun)); + col = mix(col, vec3(1,1,1), lighten * 0.10); + + // shadow + float darken = max(0.0, dot(v_normal, u_sun)); + col = mix(col, vec3(4/255.0, 27/255.0, 44/255.0), darken * 0.80); + + o_color = vec4(col, src.a) * fade; +} \ No newline at end of file diff --git a/Source/Mod/Editor/Definition.cs b/Source/Mod/Editor/Definition.cs new file mode 100644 index 00000000..3b8cb5a9 --- /dev/null +++ b/Source/Mod/Editor/Definition.cs @@ -0,0 +1,7 @@ +namespace Celeste64.Mod.Editor; + +// TODO: Find a more descriptive name +public class Definition +{ + public virtual void Render(ref EditorRenderState state) { } +} \ No newline at end of file diff --git a/Source/Mod/Editor/EditorMaterial.cs b/Source/Mod/Editor/EditorMaterial.cs new file mode 100644 index 00000000..55d2cdbc --- /dev/null +++ b/Source/Mod/Editor/EditorMaterial.cs @@ -0,0 +1,224 @@ +namespace Celeste64.Mod.Editor; + +public class EditorMaterial : Material +{ + public const string MatrixUniformName = "u_mvp"; + + private Texture? texture = null; + private Matrix matrix; + private Matrix model; + private Color color; + private float near; + private float far; + // private bool cutout; + // private bool silhouette; + // private Color silhouetteColor; + // private float time; + private Vec3 sun; + // private Color verticalFogColor; + + public EditorMaterial(Texture? texture = null) + : base(Assets.Shaders["Editor"]) + { + if (!(Shader?.Has(MatrixUniformName) ?? false)) + { + Log.Warning($"Shader '{Shader?.Name}' is missing '{MatrixUniformName}' uniform"); + } + + Texture = texture; + Color = Color.White; + } + + public Matrix MVP + { + get => matrix; + set + { + matrix = value; + if (Shader?.Has(MatrixUniformName) ?? false) + Set(MatrixUniformName, value); + } + } + + public Matrix Model + { + get => model; + set + { + model = value; + if (Shader?.Has("u_model") ?? false) + Set("u_model", value); + } + } + + public Texture? Texture + { + get => texture; + set + { + if (texture != value) + { + texture = value; + if (Shader?.Has("u_texture") ?? false) + Set("u_texture", value); + } + } + } + + public Color Color + { + get => color; + set + { + if (color != value) + { + color = value; + if (Shader?.Has("u_color") ?? false) + Set("u_color", value); + } + } + } + + public float NearPlane + { + get => near; + set + { + if (near != value) + { + near = value; + if (Shader?.Has("u_near") ?? false) + Set("u_near", value); + } + } + } + + public float FarPlane + { + get => far; + set + { + if (far != value) + { + far = value; + if (Shader?.Has("u_far") ?? false) + Set("u_far", value); + } + } + } + + // public float Effects + // { + // get => effects; + // set + // { + // if (effects != value) + // { + // effects = value; + // if (Shader?.Has("u_effects") ?? false) + // Set("u_effects", value); + // } + // } + // } + // + // public bool Cutout + // { + // get => cutout; + // set + // { + // if (cutout != value) + // { + // cutout = value; + // if (Shader?.Has("u_cutout") ?? false) + // Set("u_cutout", value ? 0.50f : 0.0f); + // } + // } + // } + // + // public bool Silhouette + // { + // get => silhouette; + // set + // { + // if (silhouette != value) + // { + // silhouette = value; + // if (Shader?.Has("u_silhouette") ?? false) + // Set("u_silhouette", value ? 1.0f : 0.0f); + // } + // } + // } + // + // public Color SilhouetteColor + // { + // get => silhouetteColor; + // set + // { + // if (silhouetteColor != value) + // { + // silhouetteColor = value; + // if (Shader?.Has("u_silhouette_color") ?? false) + // Set("u_silhouette_color", value); + // } + // } + // } + // + // public Color VerticalFogColor + // { + // get => verticalFogColor; + // set + // { + // if (verticalFogColor != value) + // { + // verticalFogColor = value; + // if (Shader?.Has("u_vertical_fog_color") ?? false) + // Set("u_vertical_fog_color", value); + // } + // } + // } + // + public Vec3 SunDirection + { + get => sun; + set + { + if (sun != value) + { + sun = value; + if (Shader?.Has("u_sun") ?? false) + Set("u_sun", value); + } + } + } + // + // public float Time + // { + // get => time; + // set + // { + // if (time != value) + // { + // time = value; + // if (Shader?.Has("u_time") ?? false) + // Set("u_time", value); + // } + // } + // } + + public virtual EditorMaterial Clone() + { + var copy = new EditorMaterial(Texture); + CopyTo(copy); + copy.MVP = MVP; + copy.Model = model; + copy.Texture = Texture; + copy.Color = Color; + // copy.Silhouette = Silhouette; + // copy.SilhouetteColor = SilhouetteColor; + // copy.Time = Time; + // copy.SunDirection = SunDirection; + copy.NearPlane = NearPlane; + copy.FarPlane = FarPlane; + return copy; + } +} \ No newline at end of file diff --git a/Source/Mod/Editor/EditorRenderState.cs b/Source/Mod/Editor/EditorRenderState.cs new file mode 100644 index 00000000..b5d59af1 --- /dev/null +++ b/Source/Mod/Editor/EditorRenderState.cs @@ -0,0 +1,36 @@ +namespace Celeste64.Mod.Editor; + +public class EditorRenderState +{ + public Camera Camera; + public Matrix ModelMatrix; + + // public bool Silhouette; + public Vec3 SunDirection; + // public Color VerticalFogColor; + + public DepthCompare DepthCompare; + public bool DepthMask; + public bool CutoutMode; + + // Stats + public int Calls; + public int Triangles; + + public void ApplyToMaterial(EditorMaterial mat, in Matrix localTransformation) + { + if (mat.Shader == null) + return; + + mat.Model = localTransformation * ModelMatrix; + mat.MVP = mat.Model * Camera.ViewProjection; + + mat.NearPlane = Camera.NearPlane; + mat.FarPlane = Camera.FarPlane; + // mat.Silhouette = Silhouette; + // mat.Time = (float)Time.Duration.TotalSeconds; + mat.SunDirection = SunDirection; + // mat.VerticalFogColor = VerticalFogColor; + // mat.Cutout = CutoutMode; + } +} \ No newline at end of file diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 86386d2d..7956019f 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -8,6 +8,8 @@ public class EditorScene : Scene new TestWindow(), ]; + private readonly WorldRenderer worldRenderer = new(); + internal EditorScene(World.EntryInfo entry) { Entry = entry; @@ -25,7 +27,8 @@ public override void Update() public override void Render(Target target) { - target.Clear(Color.Black); + target.Clear(Color.Black, 1.0f, 0, ClearMask.All); + worldRenderer.Render(target); } } diff --git a/Source/Mod/Editor/EditorVertex.cs b/Source/Mod/Editor/EditorVertex.cs new file mode 100644 index 00000000..25bc9506 --- /dev/null +++ b/Source/Mod/Editor/EditorVertex.cs @@ -0,0 +1,19 @@ +namespace Celeste64.Mod.Editor; + +public readonly struct EditorVertex(Vec3 position, Vec2 texcoord, Vec3 color, Vec3 normal) : IVertex +{ + public readonly Vec3 Pos = position; + public readonly Vec2 Tex = texcoord; + public readonly Vec3 Col = color; + public readonly Vec3 Normal = normal; + + public VertexFormat Format => VertexFormat; + + private static readonly VertexFormat VertexFormat = VertexFormat.Create( + [ + new (0, VertexType.Float3, normalized: false), + new (1, VertexType.Float2, normalized: false), + new (2, VertexType.Float3, normalized: true), + new (3, VertexType.Float3, normalized: false), + ]); +} \ No newline at end of file diff --git a/Source/Mod/Editor/TestDefinition.cs b/Source/Mod/Editor/TestDefinition.cs new file mode 100644 index 00000000..712adee6 --- /dev/null +++ b/Source/Mod/Editor/TestDefinition.cs @@ -0,0 +1,80 @@ +namespace Celeste64.Mod.Editor; + +public class TestDefinition : Definition +{ + private readonly Mesh mesh = new(); + private readonly EditorMaterial material = new(); + + public TestDefinition() + { + Vec3 size = Vec3.One * 3.0f; + Vec3 color = Vec3.One; + + mesh.SetVertices([ + // Front + /* 0*/ new(new Vec3(0, 0, 0), Vec2.Zero, color, -Vec3.UnitY), + /* 1*/ new(new Vec3(size.X, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitY), + /* 2*/ new(new Vec3(0, 0, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitY), + /* 3*/ new(new Vec3(size.X, 0, size.Z), new Vec2(size.X, size.Z), color, -Vec3.UnitY), + // Back + /* 4*/ new(new Vec3(0, size.Y, 0), Vec2.UnitX * size.X, color, Vec3.UnitY), + /* 5*/ new(new Vec3(size.X, size.Y, 0), Vec2.Zero, color, Vec3.UnitY), + /* 6*/ new(new Vec3(0, size.Y, size.Z), new Vec2(size.X, size.Z), color, Vec3.UnitY), + /* 7*/ new(new Vec3(size.X, size.Y, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitY), + // Left + /* 8*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.Y, color, -Vec3.UnitX), + /* 9*/ new(new Vec3(0, 0, size.Z), new Vec2(size.Y, size.Z), color, -Vec3.UnitX), + /*10*/ new(new Vec3(0, size.Y, 0), Vec2.Zero, color, -Vec3.UnitX), + /*11*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitX), + // Right + /*12*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, Vec3.UnitX), + /*13*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitX), + /*14*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitX * size.Y, color, Vec3.UnitX), + /*15*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.Y, size.Z), color, Vec3.UnitX), + // Top + /*16*/ new(new Vec3(0, 0, size.Z), Vec2.Zero, color, Vec3.UnitZ), + /*17*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitX * size.X, color, Vec3.UnitZ), + /*18*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Y, color, Vec3.UnitZ), + /*19*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.X, size.Y), color, Vec3.UnitZ), + // Bottom + /*20*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitZ), + /*21*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, -Vec3.UnitZ), + /*22*/ new(new Vec3(0, size.Y, 0), new Vec2(size.X, size.Y), color, -Vec3.UnitZ), + /*23*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitY * size.Y, color, -Vec3.UnitZ), + ]); + mesh.SetIndices([ + // Front + 0, 1, 2, + 2, 1, 3, + // Back + 5, 4, 7, + 7, 4, 6, + // Left + 10, 8, 11, + 11, 8, 9, + // Right + 12, 14, 13, + 13, 14, 15, + // Top + 16, 17, 18, + 18, 17, 19, + // Bottom + 22, 23, 20, + 20, 23, 21 + ]); + } + + public override void Render(ref EditorRenderState state) + { + state.ApplyToMaterial(material, Matrix.Identity); + + new DrawCommand(state.Camera.Target, mesh, material) + { + DepthCompare = state.DepthCompare, + DepthMask = state.DepthMask, + CullMode = CullMode.None, + }.Submit(); + state.Calls++; + state.Triangles += mesh.IndexCount / 3; + } +} \ No newline at end of file diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs new file mode 100644 index 00000000..ffbe229f --- /dev/null +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -0,0 +1,45 @@ +namespace Celeste64.Mod.Editor; + +public class WorldRenderer +{ + private Camera camera = new(); + private readonly List definitions = []; + + public WorldRenderer() + { + camera.NearPlane = 20; + camera.FarPlane = 800; + camera.Position = new Vec3(0, -10, 0); + camera.LookAt = Vec3.Zero; + camera.FOVMultiplier = 1; + + definitions.Add(new TestDefinition()); + } + + public void Update() + { + + } + + public void Render(Target target) + { + camera.Target = target; + EditorRenderState state = new(); + { + state.Camera = camera; + state.ModelMatrix = Matrix.Identity; + state.SunDirection = new Vec3(0, -.7f, -1).Normalized(); + // state.Silhouette = false; + state.DepthCompare = DepthCompare.Less; + state.DepthMask = true; + // state.VerticalFogColor = 0xdceaf0; + } + + foreach (var definition in definitions) + { + definition.Render(ref state); + } + + Log.Info($"Editor: {state.Calls} draw calls with {state.Triangles} triangles"); + } +} \ No newline at end of file From 08db58c0305a2feb747b12d1825658e61211bf59 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 18:48:28 +0100 Subject: [PATCH 06/97] Add camera movement --- Source/Mod/Editor/EditorScene.cs | 5 ++- Source/Mod/Editor/WorldRenderer.cs | 50 ++++++++++++++++++++++++++++-- Source/Scenes/World.cs | 3 +- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 7956019f..95db271c 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -21,8 +21,11 @@ public override void Update() if (Input.Keyboard.Pressed(Keys.F3)) { Game.Instance.scenes.Pop(); - Game.Instance.scenes.Push(new World(Entry)); + Game.Instance.scenes.Push(new World(Entry)); + return; } + + worldRenderer.Update(); } public override void Render(Target target) diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs index ffbe229f..b7dc78d4 100644 --- a/Source/Mod/Editor/WorldRenderer.cs +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -3,14 +3,16 @@ namespace Celeste64.Mod.Editor; public class WorldRenderer { private Camera camera = new(); + private Vec3 cameraPos = new(0, -10, 0); + private Vec2 cameraRot = new(0, 0); + private readonly List definitions = []; public WorldRenderer() { - camera.NearPlane = 20; + camera.NearPlane = 5; camera.FarPlane = 800; camera.Position = new Vec3(0, -10, 0); - camera.LookAt = Vec3.Zero; camera.FOVMultiplier = 1; definitions.Add(new TestDefinition()); @@ -18,7 +20,51 @@ public WorldRenderer() public void Update() { + // Camera movement + var cameraForward = new Vec3( + MathF.Sin(cameraRot.X), + MathF.Cos(cameraRot.X), + 0.0f); + var cameraRight = new Vec3( + MathF.Sin(cameraRot.X - Calc.HalfPI), + MathF.Cos(cameraRot.X - Calc.HalfPI), + 0.0f); + + float moveSpeed = 50.0f; + + if (Input.Keyboard.Down(Keys.W)) + cameraPos += cameraForward * moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.S)) + cameraPos -= cameraForward * moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.A)) + cameraPos += cameraRight * moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.D)) + cameraPos -= cameraRight * moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.Space)) + cameraPos.Z += moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.LeftShift)) + cameraPos.Z -= moveSpeed * Time.Delta; + + // Camera rotation + float rotateSpeed = 60.0f * Calc.DegToRad; + if (Input.Keyboard.Down(Keys.Up)) + cameraRot.Y -= rotateSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.Down)) + cameraRot.Y += rotateSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.Left)) + cameraRot.X -= rotateSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.Right)) + cameraRot.X += rotateSpeed * Time.Delta; + cameraRot.X %= 360.0f * Calc.DegToRad; + cameraRot.Y = Math.Clamp(cameraRot.Y, -89.9f * Calc.DegToRad, 89.9f * Calc.DegToRad); + // Update camera + var forward = new Vec3( + MathF.Sin(cameraRot.X) * MathF.Cos(cameraRot.Y), + MathF.Cos(cameraRot.X) * MathF.Cos(cameraRot.Y), + MathF.Sin(-cameraRot.Y)); + camera.Position = cameraPos; + camera.LookAt = cameraPos + forward; } public void Render(Target target) diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 05b7919e..6161debd 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -397,7 +397,8 @@ public override void Update() if (Input.Keyboard.Pressed(Keys.F3)) { Game.Instance.scenes.Pop(); - Game.Instance.scenes.Push(new EditorScene(Entry)); + Game.Instance.scenes.Push(new EditorScene(Entry)); + return; } try { From 174e6e93d39edbf3394f278a189a1b406d469ebf Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 18:55:51 +0100 Subject: [PATCH 07/97] Use mouse for rotation --- Source/Mod/Editor/WorldRenderer.cs | 23 +++++++++++------------ Source/Mod/Helpers/InputHelper.cs | 8 ++++++++ 2 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 Source/Mod/Helpers/InputHelper.cs diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs index b7dc78d4..3279f2fd 100644 --- a/Source/Mod/Editor/WorldRenderer.cs +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -1,3 +1,5 @@ +using Celeste64.Mod.Helpers; + namespace Celeste64.Mod.Editor; public class WorldRenderer @@ -30,7 +32,7 @@ public void Update() MathF.Cos(cameraRot.X - Calc.HalfPI), 0.0f); - float moveSpeed = 50.0f; + float moveSpeed = 30.0f; if (Input.Keyboard.Down(Keys.W)) cameraPos += cameraForward * moveSpeed * Time.Delta; @@ -46,17 +48,14 @@ public void Update() cameraPos.Z -= moveSpeed * Time.Delta; // Camera rotation - float rotateSpeed = 60.0f * Calc.DegToRad; - if (Input.Keyboard.Down(Keys.Up)) - cameraRot.Y -= rotateSpeed * Time.Delta; - if (Input.Keyboard.Down(Keys.Down)) - cameraRot.Y += rotateSpeed * Time.Delta; - if (Input.Keyboard.Down(Keys.Left)) - cameraRot.X -= rotateSpeed * Time.Delta; - if (Input.Keyboard.Down(Keys.Right)) - cameraRot.X += rotateSpeed * Time.Delta; - cameraRot.X %= 360.0f * Calc.DegToRad; - cameraRot.Y = Math.Clamp(cameraRot.Y, -89.9f * Calc.DegToRad, 89.9f * Calc.DegToRad); + float rotateSpeed = 15.0f * Calc.DegToRad; + if (Input.Mouse.Down(MouseButtons.Right)) + { + cameraRot.X += InputHelper.MouseDelta.X * rotateSpeed * Time.Delta; + cameraRot.Y += InputHelper.MouseDelta.Y * rotateSpeed * Time.Delta; + cameraRot.X %= 360.0f * Calc.DegToRad; + cameraRot.Y = Math.Clamp(cameraRot.Y, -89.9f * Calc.DegToRad, 89.9f * Calc.DegToRad); + } // Update camera var forward = new Vec3( diff --git a/Source/Mod/Helpers/InputHelper.cs b/Source/Mod/Helpers/InputHelper.cs new file mode 100644 index 00000000..70007b4d --- /dev/null +++ b/Source/Mod/Helpers/InputHelper.cs @@ -0,0 +1,8 @@ +namespace Celeste64.Mod.Helpers; + +public static class InputHelper +{ + public static Vec2 MouseDelta => ImGuiManager.WantCaptureMouse + ? Vec2.Zero + : Input.State.Mouse.Position - Input.LastState.Mouse.Position; +} \ No newline at end of file From c8f51179abb20991ecd4f93420bf3f5d52b463be Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 22:08:06 +0100 Subject: [PATCH 08/97] Add selection support --- Content/Shaders/Editor.glsl | 4 ++ .../{Definition.cs => EditorDefinition.cs} | 3 +- Source/Mod/Editor/EditorMaterial.cs | 25 +++++++-- Source/Mod/Editor/EditorRenderState.cs | 3 ++ Source/Mod/Editor/EditorScene.cs | 8 ++- ...tDefinition.cs => TestEditorDefinition.cs} | 4 +- .../Mod/Editor/{ => Windows}/EditorWindow.cs | 4 +- Source/Mod/Editor/Windows/TestWindow.cs | 3 +- Source/Mod/Editor/WorldRenderer.cs | 53 +++++++++++++++---- 9 files changed, 85 insertions(+), 22 deletions(-) rename Source/Mod/Editor/{Definition.cs => EditorDefinition.cs} (61%) rename Source/Mod/Editor/{TestDefinition.cs => TestEditorDefinition.cs} (97%) rename Source/Mod/Editor/{ => Windows}/EditorWindow.cs (68%) diff --git a/Content/Shaders/Editor.glsl b/Content/Shaders/Editor.glsl index 3f2ec0af..411066f8 100644 --- a/Content/Shaders/Editor.glsl +++ b/Content/Shaders/Editor.glsl @@ -35,6 +35,7 @@ uniform float u_near; uniform float u_far; uniform vec3 u_sun; //uniform float u_cutout; +uniform float u_objectID; in vec2 v_tex; in vec3 v_normal; @@ -42,6 +43,7 @@ in vec3 v_color; in vec3 v_world; layout(location = 0) out vec4 o_color; +layout(location = 1) out float o_objectID; void main(void) { @@ -69,4 +71,6 @@ void main(void) col = mix(col, vec3(4/255.0, 27/255.0, 44/255.0), darken * 0.80); o_color = vec4(col, src.a) * fade; + // TODO: Support object IDs above 255, since its just 8bits + o_objectID = u_objectID / 255.0; } \ No newline at end of file diff --git a/Source/Mod/Editor/Definition.cs b/Source/Mod/Editor/EditorDefinition.cs similarity index 61% rename from Source/Mod/Editor/Definition.cs rename to Source/Mod/Editor/EditorDefinition.cs index 3b8cb5a9..2e140f91 100644 --- a/Source/Mod/Editor/Definition.cs +++ b/Source/Mod/Editor/EditorDefinition.cs @@ -1,7 +1,6 @@ namespace Celeste64.Mod.Editor; -// TODO: Find a more descriptive name -public class Definition +public class EditorDefinition { public virtual void Render(ref EditorRenderState state) { } } \ No newline at end of file diff --git a/Source/Mod/Editor/EditorMaterial.cs b/Source/Mod/Editor/EditorMaterial.cs index 55d2cdbc..e8f5ce6b 100644 --- a/Source/Mod/Editor/EditorMaterial.cs +++ b/Source/Mod/Editor/EditorMaterial.cs @@ -10,12 +10,15 @@ public class EditorMaterial : Material private Color color; private float near; private float far; - // private bool cutout; - // private bool silhouette; + // private bool cutout; + // private bool silhouette; // private Color silhouetteColor; - // private float time; + // private float time; private Vec3 sun; - // private Color verticalFogColor; + // private Color verticalFogColor; + + private int objectID = -1; + public EditorMaterial(Texture? texture = null) : base(Assets.Shaders["Editor"]) @@ -204,6 +207,20 @@ public Vec3 SunDirection // } // } // } + + public int ObjectID + { + get => objectID; + set + { + if (objectID != value) + { + objectID = value; + if (Shader?.Has("u_objectID") ?? false) + Set("u_objectID", (float)value); + } + } + } public virtual EditorMaterial Clone() { diff --git a/Source/Mod/Editor/EditorRenderState.cs b/Source/Mod/Editor/EditorRenderState.cs index b5d59af1..b73d539c 100644 --- a/Source/Mod/Editor/EditorRenderState.cs +++ b/Source/Mod/Editor/EditorRenderState.cs @@ -13,6 +13,8 @@ public class EditorRenderState public bool DepthMask; public bool CutoutMode; + public int ObjectID; + // Stats public int Calls; public int Triangles; @@ -32,5 +34,6 @@ public void ApplyToMaterial(EditorMaterial mat, in Matrix localTransformation) mat.SunDirection = SunDirection; // mat.VerticalFogColor = VerticalFogColor; // mat.Cutout = CutoutMode; + mat.ObjectID = ObjectID; } } \ No newline at end of file diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 95db271c..8c935a6a 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -8,11 +8,15 @@ public class EditorScene : Scene new TestWindow(), ]; + public readonly List Definitions = []; + public EditorDefinition? Selected { internal set; get; } = null; + private readonly WorldRenderer worldRenderer = new(); internal EditorScene(World.EntryInfo entry) { Entry = entry; + Definitions.Add(new TestEditorDefinition()); } public override void Update() @@ -25,13 +29,13 @@ public override void Update() return; } - worldRenderer.Update(); + worldRenderer.Update(this); } public override void Render(Target target) { target.Clear(Color.Black, 1.0f, 0, ClearMask.All); - worldRenderer.Render(target); + worldRenderer.Render(this, target); } } diff --git a/Source/Mod/Editor/TestDefinition.cs b/Source/Mod/Editor/TestEditorDefinition.cs similarity index 97% rename from Source/Mod/Editor/TestDefinition.cs rename to Source/Mod/Editor/TestEditorDefinition.cs index 712adee6..520dda75 100644 --- a/Source/Mod/Editor/TestDefinition.cs +++ b/Source/Mod/Editor/TestEditorDefinition.cs @@ -1,11 +1,11 @@ namespace Celeste64.Mod.Editor; -public class TestDefinition : Definition +public class TestEditorDefinition : EditorDefinition { private readonly Mesh mesh = new(); private readonly EditorMaterial material = new(); - public TestDefinition() + public TestEditorDefinition() { Vec3 size = Vec3.One * 3.0f; Vec3 color = Vec3.One; diff --git a/Source/Mod/Editor/EditorWindow.cs b/Source/Mod/Editor/Windows/EditorWindow.cs similarity index 68% rename from Source/Mod/Editor/EditorWindow.cs rename to Source/Mod/Editor/Windows/EditorWindow.cs index c890fbe1..b07b71a4 100644 --- a/Source/Mod/Editor/EditorWindow.cs +++ b/Source/Mod/Editor/Windows/EditorWindow.cs @@ -6,11 +6,11 @@ public abstract class EditorWindow : ImGuiHandler { protected abstract string Title { get; } - protected abstract void RenderWindow(); + protected abstract void RenderWindow(EditorScene editor); public sealed override void Render() { ImGui.Begin(Title); - RenderWindow(); + RenderWindow((Game.Scene as EditorScene)!); ImGui.End(); } } \ No newline at end of file diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs index a5c32363..3d38351d 100644 --- a/Source/Mod/Editor/Windows/TestWindow.cs +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -6,8 +6,9 @@ public class TestWindow : EditorWindow { protected override string Title => "Test"; - protected override void RenderWindow() + protected override void RenderWindow(EditorScene editor) { ImGui.Text("Testing"); + ImGui.Text($"Selected: {editor.Selected}"); } } \ No newline at end of file diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs index 3279f2fd..755d6880 100644 --- a/Source/Mod/Editor/WorldRenderer.cs +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -8,7 +8,8 @@ public class WorldRenderer private Vec3 cameraPos = new(0, -10, 0); private Vec2 cameraRot = new(0, 0); - private readonly List definitions = []; + private Target? worldTarget = null; + private readonly Batcher batch = new(); public WorldRenderer() { @@ -16,11 +17,9 @@ public WorldRenderer() camera.FarPlane = 800; camera.Position = new Vec3(0, -10, 0); camera.FOVMultiplier = 1; - - definitions.Add(new TestDefinition()); } - public void Update() + public void Update(EditorScene editor) { // Camera movement var cameraForward = new Vec3( @@ -66,9 +65,17 @@ public void Update() camera.LookAt = cameraPos + forward; } - public void Render(Target target) + public void Render(EditorScene editor, Target target) { - camera.Target = target; + // TODO: Maybe render at a higher resolution in the editor? + if (worldTarget == null || worldTarget.Width != target.Width || worldTarget.Height != target.Height) + { + worldTarget?.Dispose(); + worldTarget = new Target(target.Width, target.Height, [TextureFormat.Color, TextureFormat.R8, TextureFormat.Depth24Stencil8]); + } + worldTarget.Clear(Color.Black, 1.0f, 0, ClearMask.All); + + camera.Target = worldTarget; EditorRenderState state = new(); { state.Camera = camera; @@ -80,11 +87,39 @@ public void Render(Target target) // state.VerticalFogColor = 0xdceaf0; } - foreach (var definition in definitions) + for (int i = 0; i < editor.Definitions.Count; i++) + { + state.ObjectID = i + 1; // Use 0 as "nothing selected" + editor.Definitions[i].Render(ref state); + } + + // Try to select the object under the cursor + if (Input.Mouse.LeftPressed) { - definition.Render(ref state); + // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios + var scale = Math.Min(App.WidthInPixels / (float)target.Width, App.HeightInPixels / (float)target.Height); + var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - target.Bounds.Size / 2 * scale); + // Convert it into a pixel position inside the target + var pixelPos = imageRelativePos / scale; + // Round to integer values + pixelPos = new Vec2(MathF.Round(pixelPos.X), MathF.Round(pixelPos.Y)); + + if (pixelPos.X >= 0 && pixelPos.Y >= 0 && pixelPos.X < worldTarget.Width && pixelPos.Y < worldTarget.Height) + { + var data = new byte[worldTarget.Width * worldTarget.Height]; + worldTarget.Attachments[1].GetData(data); + + // NOTE: OpenGL flips the image vertically + byte objectID = data[worldTarget.Width * (target.Height - (int)pixelPos.Y - 1) + (int)pixelPos.X]; + editor.Selected = objectID == 0 + ? null // Nothing selected + : editor.Definitions[objectID - 1]; + } } - Log.Info($"Editor: {state.Calls} draw calls with {state.Triangles} triangles"); + // Render to the main target + batch.Image(worldTarget.Attachments[0], Color.White); + batch.Render(target); + batch.Clear(); } } \ No newline at end of file From 6b29d78e31218e8ea358552a5c614b011530f51e Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 22:28:16 +0100 Subject: [PATCH 09/97] Add position, rotation and scale definition properties --- Source/Mod/Editor/EditorDefinition.cs | 5 +++++ Source/Mod/Editor/TestEditorDefinition.cs | 2 +- Source/Mod/Editor/Windows/TestWindow.cs | 15 +++++++++++++++ Source/Mod/Editor/WorldRenderer.cs | 17 +++++++++++++---- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Source/Mod/Editor/EditorDefinition.cs b/Source/Mod/Editor/EditorDefinition.cs index 2e140f91..7621b0b6 100644 --- a/Source/Mod/Editor/EditorDefinition.cs +++ b/Source/Mod/Editor/EditorDefinition.cs @@ -2,5 +2,10 @@ namespace Celeste64.Mod.Editor; public class EditorDefinition { + // TODO: Figure out how to let definitions mark support for certain special properties like position / rotation / scale + public virtual Vec3 Position { get; set; } = Vector3.Zero; + public virtual Vec3 Rotation { get; set; } = Vector3.Zero; + public virtual Vec3 Scale { get; set; } = Vector3.One; + public virtual void Render(ref EditorRenderState state) { } } \ No newline at end of file diff --git a/Source/Mod/Editor/TestEditorDefinition.cs b/Source/Mod/Editor/TestEditorDefinition.cs index 520dda75..4250d1fe 100644 --- a/Source/Mod/Editor/TestEditorDefinition.cs +++ b/Source/Mod/Editor/TestEditorDefinition.cs @@ -72,7 +72,7 @@ public override void Render(ref EditorRenderState state) { DepthCompare = state.DepthCompare, DepthMask = state.DepthMask, - CullMode = CullMode.None, + CullMode = CullMode.Back, }.Submit(); state.Calls++; state.Triangles += mesh.IndexCount / 3; diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs index 3d38351d..91b454fb 100644 --- a/Source/Mod/Editor/Windows/TestWindow.cs +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -10,5 +10,20 @@ protected override void RenderWindow(EditorScene editor) { ImGui.Text("Testing"); ImGui.Text($"Selected: {editor.Selected}"); + + if (editor.Selected is { } selected) + { + var pos = selected.Position; + ImGui.DragFloat3("Position", ref pos, 0.1f); + selected.Position = pos; + + var rot = selected.Rotation; + ImGui.DragFloat3("Rotation", ref rot, 0.1f); + selected.Rotation = rot; + + var scale = selected.Scale; + ImGui.DragFloat3("Scale", ref scale, 0.1f); + selected.Scale = scale; + } } } \ No newline at end of file diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs index 755d6880..2b9937b7 100644 --- a/Source/Mod/Editor/WorldRenderer.cs +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -89,12 +89,21 @@ public void Render(EditorScene editor, Target target) for (int i = 0; i < editor.Definitions.Count; i++) { + var def = editor.Definitions[i]; + state.ObjectID = i + 1; // Use 0 as "nothing selected" - editor.Definitions[i].Render(ref state); + state.ModelMatrix = + Matrix.CreateScale(def.Scale) * + Matrix.CreateRotationX(def.Rotation.X * Calc.DegToRad) * + Matrix.CreateRotationY(def.Rotation.Y * Calc.DegToRad) * + Matrix.CreateRotationZ(def.Rotation.Z * Calc.DegToRad) * + Matrix.CreateTranslation(def.Position); + + def.Render(ref state); } // Try to select the object under the cursor - if (Input.Mouse.LeftPressed) + if (!ImGuiManager.WantCaptureMouse && Input.Mouse.LeftPressed) { // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios var scale = Math.Min(App.WidthInPixels / (float)target.Width, App.HeightInPixels / (float)target.Height); @@ -108,10 +117,10 @@ public void Render(EditorScene editor, Target target) { var data = new byte[worldTarget.Width * worldTarget.Height]; worldTarget.Attachments[1].GetData(data); - + // NOTE: OpenGL flips the image vertically byte objectID = data[worldTarget.Width * (target.Height - (int)pixelPos.Y - 1) + (int)pixelPos.X]; - editor.Selected = objectID == 0 + editor.Selected = objectID == 0 || (objectID - 1) >= editor.Definitions.Count ? null // Nothing selected : editor.Definitions[objectID - 1]; } From 7e5dee31c52f07cc289821bca84de16a618d8233 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 10 Mar 2024 22:41:58 +0100 Subject: [PATCH 10/97] Add example color property --- Source/Mod/Editor/EditorDefinition.cs | 17 +++++++++++++++++ Source/Mod/Editor/TestEditorDefinition.cs | 16 ++++++++++++++++ Source/Mod/Editor/Windows/TestWindow.cs | 12 +----------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Source/Mod/Editor/EditorDefinition.cs b/Source/Mod/Editor/EditorDefinition.cs index 7621b0b6..01981fa7 100644 --- a/Source/Mod/Editor/EditorDefinition.cs +++ b/Source/Mod/Editor/EditorDefinition.cs @@ -1,3 +1,5 @@ +using ImGuiNET; + namespace Celeste64.Mod.Editor; public class EditorDefinition @@ -8,4 +10,19 @@ public class EditorDefinition public virtual Vec3 Scale { get; set; } = Vector3.One; public virtual void Render(ref EditorRenderState state) { } + + public virtual void RenderGUI(EditorScene editor) + { + var pos = Position; + ImGui.DragFloat3("Position", ref pos, 0.1f); + Position = pos; + + var rot = Rotation; + ImGui.DragFloat3("Rotation", ref rot, 0.1f); + Rotation = rot; + + var scale = Scale; + ImGui.DragFloat3("Scale", ref scale, 0.1f); + Scale = scale; + } } \ No newline at end of file diff --git a/Source/Mod/Editor/TestEditorDefinition.cs b/Source/Mod/Editor/TestEditorDefinition.cs index 4250d1fe..11f15ae2 100644 --- a/Source/Mod/Editor/TestEditorDefinition.cs +++ b/Source/Mod/Editor/TestEditorDefinition.cs @@ -1,3 +1,6 @@ +using ImGuiNET; +using Sledge.Formats; + namespace Celeste64.Mod.Editor; public class TestEditorDefinition : EditorDefinition @@ -5,6 +8,8 @@ public class TestEditorDefinition : EditorDefinition private readonly Mesh mesh = new(); private readonly EditorMaterial material = new(); + public Color Color { get; set; } = Color.White; + public TestEditorDefinition() { Vec3 size = Vec3.One * 3.0f; @@ -67,6 +72,7 @@ public TestEditorDefinition() public override void Render(ref EditorRenderState state) { state.ApplyToMaterial(material, Matrix.Identity); + material.Color = Color; new DrawCommand(state.Camera.Target, mesh, material) { @@ -77,4 +83,14 @@ public override void Render(ref EditorRenderState state) state.Calls++; state.Triangles += mesh.IndexCount / 3; } + + public override void RenderGUI(EditorScene editor) + { + base.RenderGUI(editor); + + var col = Color.ToVector3(); + // ImGui.ColorPicker3("Color", ref col, ImGuiColorEditFlags.DefaultOptions); + ImGui.ColorEdit3("Color", ref col); + Color = new Color(col); + } } \ No newline at end of file diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs index 91b454fb..4c7173ee 100644 --- a/Source/Mod/Editor/Windows/TestWindow.cs +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -13,17 +13,7 @@ protected override void RenderWindow(EditorScene editor) if (editor.Selected is { } selected) { - var pos = selected.Position; - ImGui.DragFloat3("Position", ref pos, 0.1f); - selected.Position = pos; - - var rot = selected.Rotation; - ImGui.DragFloat3("Rotation", ref rot, 0.1f); - selected.Rotation = rot; - - var scale = selected.Scale; - ImGui.DragFloat3("Scale", ref scale, 0.1f); - selected.Scale = scale; + selected.RenderGUI(editor); } } } \ No newline at end of file From cc132d3216319880dbb3839481669a745bac7691 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 15:33:40 +0100 Subject: [PATCH 11/97] Add reload support for the editor --- Source/Game.cs | 11 +++++++++++ Source/Mod/Editor/EditorScene.cs | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Source/Game.cs b/Source/Game.cs index 3e451a52..e4167d12 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -371,6 +371,17 @@ internal void ReloadAssets() PerformAssetReload = true }); } + else if (scene is EditorScene editor) + { + Goto(new Transition() + { + Mode = Transition.Modes.Replace, + Scene = () => new EditorScene(editor.Entry), + ToPause = true, + ToBlack = new AngledWipe(), + PerformAssetReload = true + }); + } else { Goto(new Transition() diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 8c935a6a..cf0239c0 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -2,7 +2,7 @@ namespace Celeste64.Mod.Editor; public class EditorScene : Scene { - private World.EntryInfo Entry; + public World.EntryInfo Entry; internal readonly ImGuiHandler[] Handlers = [ new TestWindow(), From cbc626508f04b1510d80b76afecd96cac758b88c Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 15:43:32 +0100 Subject: [PATCH 12/97] Rename Map to SledgeMap --- Source/Data/Assets.cs | 8 ++++---- Source/Data/{Map.cs => SledgeMap.cs} | 17 +++++++++++------ Source/Mod/Core/GameMod.cs | 16 ++++++++-------- Source/Mod/Core/ModManager.cs | 4 ++-- Source/Scenes/World.cs | 2 +- 5 files changed, 26 insertions(+), 21 deletions(-) rename Source/Data/{Map.cs => SledgeMap.cs} (98%) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 690b0a2d..4f7f14e8 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -77,7 +77,7 @@ public static string ContentPath } } - public static readonly ModAssetDictionary Maps = new(gameMod => gameMod.Maps); + public static readonly ModAssetDictionary Maps = new(gameMod => gameMod.Maps); public static readonly ModAssetDictionary Shaders = new(gameMod => gameMod.Shaders); public static readonly ModAssetDictionary Textures = new(gameMod => gameMod.Textures); public static readonly ModAssetDictionary Models = new(gameMod => gameMod.Models); @@ -115,10 +115,10 @@ public static void Load() Music.Clear(); Audio.Unload(); - Map.ModActorFactories.Clear(); + SledgeMap.ModActorFactories.Clear(); ModLoader.RegisterAllMods(); - var maps = new ConcurrentBag<(Map, GameMod)>(); + var maps = new ConcurrentBag<(SledgeMap, GameMod)>(); var images = new ConcurrentBag<(string, Image, GameMod)>(); var models = new ConcurrentBag<(string, SkinnedTemplate, GameMod)>(); var sounds = new ConcurrentBag<(string, FMOD.Sound, GameMod)>(); @@ -138,7 +138,7 @@ public static void Load() tasks.Add(Task.Run(() => { if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, - stream => new Map(GetResourceNameFromVirt(file, MapsFolder), file, stream), out var map)) + stream => new SledgeMap(GetResourceNameFromVirt(file, MapsFolder), file, stream), out var map)) { maps.Add((map, mod)); } diff --git a/Source/Data/Map.cs b/Source/Data/SledgeMap.cs similarity index 98% rename from Source/Data/Map.cs rename to Source/Data/SledgeMap.cs index e54ca0c2..4547ec19 100644 --- a/Source/Data/Map.cs +++ b/Source/Data/SledgeMap.cs @@ -4,20 +4,25 @@ using SledgeSolid = Sledge.Formats.Map.Objects.Solid; using SledgeEntity = Sledge.Formats.Map.Objects.Entity; using SledgeFace = Sledge.Formats.Map.Objects.Face; -using SledgeMap = Sledge.Formats.Map.Objects.MapFile; using System.Runtime.CompilerServices; using Celeste64.Mod; using System.Globalization; +using Sledge.Formats.Map.Objects; +using Path = System.IO.Path; namespace Celeste64; -public class Map +/// +/// Vanilla Celeste 64 map parser using Sledge. +/// Originally called Map. +/// +public class SledgeMap { - public class ActorFactory(Func create) + public class ActorFactory(Func create) { public bool UseSolidsAsBounds; public bool IsSolidGeometry; - public Func Create = create; + public Func Create = create; } private const string StartCheckpoint = "Start"; @@ -25,7 +30,7 @@ public class ActorFactory(Func create) public readonly string Name; public readonly string Filename; public readonly string Folder; - public readonly SledgeMap? Data; + public readonly MapFile? Data; public readonly string? Skybox; public readonly float SnowAmount; public readonly Vec3 SnowWind; @@ -123,7 +128,7 @@ public class ActorFactory(Func create) public World? LoadWorld; public int LoadStrawberryCounter = 0; - public Map(string name, string virtPath, Stream stream) + public SledgeMap(string name, string virtPath, Stream stream) { Name = name; Filename = virtPath; diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs index fa80ef5e..29013b4e 100644 --- a/Source/Mod/Core/GameMod.cs +++ b/Source/Mod/Core/GameMod.cs @@ -13,7 +13,7 @@ public abstract class GameMod internal ModInfo ModInfo { get; set; } = null!; // Used for storing the assets loaded for this mod specifically. - internal readonly Dictionary Maps = new(StringComparer.OrdinalIgnoreCase); + internal readonly Dictionary Maps = new(StringComparer.OrdinalIgnoreCase); internal readonly Dictionary Shaders = new(StringComparer.OrdinalIgnoreCase); internal readonly Dictionary Textures = new(StringComparer.OrdinalIgnoreCase); internal readonly Dictionary Subtextures = new(StringComparer.OrdinalIgnoreCase); @@ -38,7 +38,7 @@ public abstract class GameMod // Modders should not be manually changing these at runtime, which is why they are readonly. // This lets you bypass going through the assets system which might be more efficient, and will prioritize loading from this mod. // It will also bypass any asset replacements. - public IReadOnlyDictionary ModMaps => Maps; + public IReadOnlyDictionary ModMaps => Maps; public IReadOnlyDictionary ModShaders => Shaders; public IReadOnlyDictionary ModTextures => Textures; public IReadOnlyDictionary ModSubTextures => Subtextures; @@ -54,7 +54,7 @@ public abstract class GameMod // Warning, these may be null if they haven't been initialized yet, so you should always do a null check before using them. public Game? Game { get { return Game.Instance; } } public World? World { get { return Game != null ? Game.World : null; } } - public Map? Map { get { return World != null ? World.Map : null; } } + public SledgeMap? Map { get { return World != null ? World.Map : null; } } public Player? Player { get { return World != null ? World.Get() : null; } } // public Game? Game => Game.Instance; @@ -489,11 +489,11 @@ public void EnableDependencies() /// /// /// - public void AddActorFactory(string name, Map.ActorFactory factory) + public void AddActorFactory(string name, SledgeMap.ActorFactory factory) { - if (Map.ModActorFactories.TryAdd(name, factory)) + if (SledgeMap.ModActorFactories.TryAdd(name, factory)) { - OnUnloadedCleanup += () => Map.ModActorFactories.Remove(name); + OnUnloadedCleanup += () => SledgeMap.ModActorFactories.Remove(name); } else { @@ -557,13 +557,13 @@ public virtual void OnAssetsLoaded() { } /// /// A reference to the world /// A reference to the map that was loaded - public virtual void OnPreMapLoaded(World world, Map map){} + public virtual void OnPreMapLoaded(World world, SledgeMap map){} /// /// Called after a map is finished loading. /// /// A reference to the map that was loaded - public virtual void OnMapLoaded(Map map){} + public virtual void OnMapLoaded(SledgeMap map){} /// /// Called after a scene transistion either when a scene is first loaded, or reloaded. diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index ee1c45af..078e9844 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -155,7 +155,7 @@ internal void OnGameLoaded(Game game) } } - internal void OnPreMapLoaded(World world, Map map) + internal void OnPreMapLoaded(World world, SledgeMap map) { foreach (var mod in EnabledMods) { @@ -163,7 +163,7 @@ internal void OnPreMapLoaded(World world, Map map) } } - internal void OnMapLoaded(Map map) + internal void OnMapLoaded(SledgeMap map) { foreach (var mod in EnabledMods) { diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 6161debd..255c039d 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -67,7 +67,7 @@ private bool IsPauseEnabled private int debugUpdateCount; public static bool DebugDraw { get; private set; } = false; - public Map? Map { get; private set; } + public SledgeMap? Map { get; private set; } public World(EntryInfo entry) { From 04d83b9da487a08f386ba4ddedbc51d94e43e4cd Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 15:55:04 +0100 Subject: [PATCH 13/97] Abstract away sledge map loading to common Map type --- Source/Data/Assets.cs | 4 ++-- Source/Data/Map.cs | 22 ++++++++++++++++++++++ Source/Data/SledgeMap.cs | 15 ++------------- Source/Mod/Core/GameMod.cs | 10 +++++----- Source/Mod/Core/ModManager.cs | 4 ++-- Source/Scenes/World.cs | 2 +- 6 files changed, 34 insertions(+), 23 deletions(-) create mode 100644 Source/Data/Map.cs diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 4f7f14e8..a7a1e7b2 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -77,7 +77,7 @@ public static string ContentPath } } - public static readonly ModAssetDictionary Maps = new(gameMod => gameMod.Maps); + public static readonly ModAssetDictionary Maps = new(gameMod => gameMod.Maps); public static readonly ModAssetDictionary Shaders = new(gameMod => gameMod.Shaders); public static readonly ModAssetDictionary Textures = new(gameMod => gameMod.Textures); public static readonly ModAssetDictionary Models = new(gameMod => gameMod.Models); @@ -118,7 +118,7 @@ public static void Load() SledgeMap.ModActorFactories.Clear(); ModLoader.RegisterAllMods(); - var maps = new ConcurrentBag<(SledgeMap, GameMod)>(); + var maps = new ConcurrentBag<(Map, GameMod)>(); var images = new ConcurrentBag<(string, Image, GameMod)>(); var models = new ConcurrentBag<(string, SkinnedTemplate, GameMod)>(); var sounds = new ConcurrentBag<(string, FMOD.Sound, GameMod)>(); diff --git a/Source/Data/Map.cs b/Source/Data/Map.cs new file mode 100644 index 00000000..4c2d1a91 --- /dev/null +++ b/Source/Data/Map.cs @@ -0,0 +1,22 @@ +namespace Celeste64; + +/// +/// Common base calls for all map types. +/// The vanilla map parser was renamed to SledgeMap. +/// +public abstract class Map +{ + public bool isMalformed { get; init; } = false; + public string? readExceptionMessage { get; init; } = null; + + public string Name { get; init; } + public string Filename { get; init; } + public string Folder { get; init; } + public string? Skybox { get; init; } + public float SnowAmount { get; init; } + public Vec3 SnowWind { get; init; } + public string? Music { get; init; } + public string? Ambience { get; init; } + + public abstract void Load(World world); +} \ No newline at end of file diff --git a/Source/Data/SledgeMap.cs b/Source/Data/SledgeMap.cs index 4547ec19..422362cc 100644 --- a/Source/Data/SledgeMap.cs +++ b/Source/Data/SledgeMap.cs @@ -16,7 +16,7 @@ namespace Celeste64; /// Vanilla Celeste 64 map parser using Sledge. /// Originally called Map. /// -public class SledgeMap +public class SledgeMap : Map { public class ActorFactory(Func create) { @@ -27,18 +27,7 @@ public class ActorFactory(Func create) private const string StartCheckpoint = "Start"; - public readonly string Name; - public readonly string Filename; - public readonly string Folder; public readonly MapFile? Data; - public readonly string? Skybox; - public readonly float SnowAmount; - public readonly Vec3 SnowWind; - public readonly string? Music; - public readonly string? Ambience; - - public readonly bool isMalformed = false; - public readonly string? readExceptionMessage; public static readonly Dictionary ActorFactories = new() { @@ -230,7 +219,7 @@ void QueryObjects(SledgeMapObject obj) // .... } - public void Load(World world) + public override void Load(World world) { LoadWorld = world; LoadStrawberryCounter = 0; diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs index 29013b4e..070c2ec8 100644 --- a/Source/Mod/Core/GameMod.cs +++ b/Source/Mod/Core/GameMod.cs @@ -13,7 +13,7 @@ public abstract class GameMod internal ModInfo ModInfo { get; set; } = null!; // Used for storing the assets loaded for this mod specifically. - internal readonly Dictionary Maps = new(StringComparer.OrdinalIgnoreCase); + internal readonly Dictionary Maps = new(StringComparer.OrdinalIgnoreCase); internal readonly Dictionary Shaders = new(StringComparer.OrdinalIgnoreCase); internal readonly Dictionary Textures = new(StringComparer.OrdinalIgnoreCase); internal readonly Dictionary Subtextures = new(StringComparer.OrdinalIgnoreCase); @@ -38,7 +38,7 @@ public abstract class GameMod // Modders should not be manually changing these at runtime, which is why they are readonly. // This lets you bypass going through the assets system which might be more efficient, and will prioritize loading from this mod. // It will also bypass any asset replacements. - public IReadOnlyDictionary ModMaps => Maps; + public IReadOnlyDictionary ModMaps => Maps; public IReadOnlyDictionary ModShaders => Shaders; public IReadOnlyDictionary ModTextures => Textures; public IReadOnlyDictionary ModSubTextures => Subtextures; @@ -54,7 +54,7 @@ public abstract class GameMod // Warning, these may be null if they haven't been initialized yet, so you should always do a null check before using them. public Game? Game { get { return Game.Instance; } } public World? World { get { return Game != null ? Game.World : null; } } - public SledgeMap? Map { get { return World != null ? World.Map : null; } } + public Map? Map { get { return World != null ? World.Map : null; } } public Player? Player { get { return World != null ? World.Get() : null; } } // public Game? Game => Game.Instance; @@ -557,13 +557,13 @@ public virtual void OnAssetsLoaded() { } /// /// A reference to the world /// A reference to the map that was loaded - public virtual void OnPreMapLoaded(World world, SledgeMap map){} + public virtual void OnPreMapLoaded(World world, Map map){} /// /// Called after a map is finished loading. /// /// A reference to the map that was loaded - public virtual void OnMapLoaded(SledgeMap map){} + public virtual void OnMapLoaded(Map map){} /// /// Called after a scene transistion either when a scene is first loaded, or reloaded. diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index 078e9844..ee1c45af 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -155,7 +155,7 @@ internal void OnGameLoaded(Game game) } } - internal void OnPreMapLoaded(World world, SledgeMap map) + internal void OnPreMapLoaded(World world, Map map) { foreach (var mod in EnabledMods) { @@ -163,7 +163,7 @@ internal void OnPreMapLoaded(World world, SledgeMap map) } } - internal void OnMapLoaded(SledgeMap map) + internal void OnMapLoaded(Map map) { foreach (var mod in EnabledMods) { diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 255c039d..569c7510 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -67,7 +67,7 @@ private bool IsPauseEnabled private int debugUpdateCount; public static bool DebugDraw { get; private set; } = false; - public SledgeMap? Map { get; private set; } + public Map? Map { get; private set; } public World(EntryInfo entry) { From cd8865df07d4711737e03a47e52b4944b3f66b04 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 16:07:17 +0100 Subject: [PATCH 14/97] Setup initial Fuji map format loading --- Source/Data/Assets.cs | 21 +++++++++++++++++++-- Source/Mod/Core/ModManager.cs | 2 +- Source/Mod/Editor/Map/FujiMap.cs | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 Source/Mod/Editor/Map/FujiMap.cs diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index a7a1e7b2..fbca74d7 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; using Celeste64.Mod; +using Celeste64.Mod.Editor; namespace Celeste64; @@ -12,7 +13,8 @@ public static class Assets public const string AssetFolder = "Content"; public const string MapsFolder = "Maps"; - public const string MapsExtension = "map"; + public const string MapsExtensionSledge = "map"; + public const string MapsExtensionFuji = "bin"; public const string TexturesFolder = "Textures"; public const string TexturesExtension = "png"; @@ -129,7 +131,7 @@ public static void Load() // NOTE: Make sure to update ModManager.OnModFileChanged() as well, for hot-reloading to work! var globalFs = ModManager.Instance.GlobalFilesystem; - foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(MapsFolder, MapsExtension)) + foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(MapsFolder, MapsExtensionSledge)) { // Skip the "autosave" folder if (file.StartsWith($"{MapsFolder}/autosave", StringComparison.OrdinalIgnoreCase)) @@ -144,6 +146,21 @@ public static void Load() } })); } + foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(MapsFolder, MapsExtensionFuji)) + { + // Skip the "autosave" folder + if (file.StartsWith($"{MapsFolder}/autosave", StringComparison.OrdinalIgnoreCase)) + continue; + + tasks.Add(Task.Run(() => + { + if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, + stream => new FujiMap(GetResourceNameFromVirt(file, MapsFolder), file, stream), out var map)) + { + maps.Add((map, mod)); + } + })); + } // load texture pngs foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(TexturesFolder, TexturesExtension)) diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index ee1c45af..ab74bd0b 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -91,7 +91,7 @@ internal void OnModFileChanged(ModFileChangedCtx ctx) // Important assets taken from Assets.Load() // TODO: Support non-toplevel mods? - if ((dir.StartsWith(Assets.MapsFolder) && extension == $".{Assets.MapsExtension}" && !dir.StartsWith($"{Assets.MapsFolder}/autosave")) || + if ((dir.StartsWith(Assets.MapsFolder) && extension is $".{Assets.MapsExtensionSledge}" or $".{Assets.MapsExtensionFuji}" && !dir.StartsWith($"{Assets.MapsFolder}/autosave")) || (dir.StartsWith(Assets.TexturesFolder) && extension == $".{Assets.TexturesExtension}") || (dir.StartsWith(Assets.FacesFolder) && extension == $".{Assets.FacesExtension}") || (dir.StartsWith(Assets.ModelsFolder) && extension == $".{Assets.ModelsExtension}") || diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/Map/FujiMap.cs new file mode 100644 index 00000000..0abfa2f0 --- /dev/null +++ b/Source/Mod/Editor/Map/FujiMap.cs @@ -0,0 +1,19 @@ +namespace Celeste64.Mod.Editor; + +/// +/// Map parser for the custom Fuji map format. +/// +public class FujiMap : Map +{ + public FujiMap(string name, string virtPath, Stream stream) + { + Name = name; + Filename = virtPath; + Folder = Path.GetDirectoryName(virtPath) ?? string.Empty; + } + + public override void Load(World world) + { + world.Add(new Player { Position = new Vec3(0, 0, 1000) }); + } +} \ No newline at end of file From cb7bd9cb220c4c7bc71fbcc4af8d51cea301199e Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 17:04:53 +0100 Subject: [PATCH 15/97] Load basic metadata in Fuji format --- Source/Data/Map.cs | 1 + Source/Mod/Editor/EditorScene.cs | 10 +++++ Source/Mod/Editor/Map/FujiMap.cs | 37 ++++++++++++++++ Source/Mod/Editor/Map/FujiMapWriter.cs | 61 ++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 Source/Mod/Editor/Map/FujiMapWriter.cs diff --git a/Source/Data/Map.cs b/Source/Data/Map.cs index 4c2d1a91..dce45151 100644 --- a/Source/Data/Map.cs +++ b/Source/Data/Map.cs @@ -12,6 +12,7 @@ public abstract class Map public string Name { get; init; } public string Filename { get; init; } public string Folder { get; init; } + public string? Skybox { get; init; } public float SnowAmount { get; init; } public Vec3 SnowWind { get; init; } diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index cf0239c0..aaafd65d 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -29,6 +29,16 @@ public override void Update() return; } + if (Input.Keyboard.Ctrl && Input.Keyboard.Pressed(Keys.S)) + { + // TODO: Dont actually hardcode this lol + var path = "/media/Storage/Code/C#/Fuji/Content/Maps/test.bin"; + Log.Info($"Saving map to '{path}'"); + + using var fs = File.Open(path, FileMode.Create); + FujiMapWriter.WriteTo(this, fs); + } + worldRenderer.Update(this); } diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/Map/FujiMap.cs index 0abfa2f0..680a9f25 100644 --- a/Source/Mod/Editor/Map/FujiMap.cs +++ b/Source/Mod/Editor/Map/FujiMap.cs @@ -10,6 +10,43 @@ public FujiMap(string name, string virtPath, Stream stream) Name = name; Filename = virtPath; Folder = Path.GetDirectoryName(virtPath) ?? string.Empty; + + using var reader = new BinaryReader(stream); + + try + { + #region Metadata + + var magic = reader.ReadBytes(4); + if (!magic.SequenceEqual(FujiMapWriter.FormatMagic)) + { + isMalformed = true; + readExceptionMessage = $"Invalid magic bytes! Found '{(char)magic[0]}{(char)magic[1]}{(char)magic[2]}{(char)magic[3]}'"; + return; + } + + var version = reader.ReadByte(); // Not currently used + + #endregion + + #region Metadata + + Skybox = reader.ReadString(); + SnowAmount = reader.ReadSingle(); + SnowWind = reader.ReadVec3(); + Ambience = reader.ReadString(); + Music = reader.ReadString(); + + #endregion + } + catch (Exception ex) + { + isMalformed = true; + readExceptionMessage = ex.Message; + + Log.Error($"Failed to load map {name}, more details below."); + Log.Error(ex.ToString()); + } } public override void Load(World world) diff --git a/Source/Mod/Editor/Map/FujiMapWriter.cs b/Source/Mod/Editor/Map/FujiMapWriter.cs new file mode 100644 index 00000000..4352591e --- /dev/null +++ b/Source/Mod/Editor/Map/FujiMapWriter.cs @@ -0,0 +1,61 @@ +using System.Reflection.PortableExecutable; +using System.Runtime.InteropServices; +using System.Runtime.Serialization.Formatters.Binary; +using MonoMod.Utils; + +namespace Celeste64.Mod.Editor; + +public static class FujiMapWriter +{ + /// + /// Magic 4 bytes at the start of the file, to indicate the format. + /// + public static readonly byte[] FormatMagic = [(byte)'F', (byte)'U', (byte)'J', (byte)'I']; + + /// + /// Current version of the map format. Needs to be incremented with every change to it. + /// + public const byte FormatVersion = 1; + + public static void WriteTo(EditorScene editor, Stream stream) + { + using var writer = new BinaryWriter(stream); + + #region Header + + writer.Write(FormatMagic); + writer.Write(FormatVersion); + + #endregion + + #region Metadata + + // Skybox + writer.WriteNullTerminatedString("city"); + // Snow amount + writer.Write(1.0f); + // Snow direction + writer.Write(new Vec3(0.0f, 0.0f, -1.0f)); + // Ambience + writer.Write("mountain"); + // Music + writer.Write("mus_lvl1"); + + #endregion + } + + public static void Write(this BinaryWriter writer, Vec2 value) + { + writer.Write(value.X); + writer.Write(value.Y); + } + public static void Write(this BinaryWriter writer, Vec3 value) + { + writer.Write(value.X); + writer.Write(value.Y); + writer.Write(value.Z); + } + + public static Vec2 ReadVec2(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle()); + public static Vec3 ReadVec3(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); +} \ No newline at end of file From c1afe06be3b38b9d71190ae143274890954df981 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 18:47:24 +0100 Subject: [PATCH 16/97] Add editor definition (de)serialization --- .../{ => Definition}/EditorDefinition.cs | 3 + .../Definition/SerializeIgnoreAttribute.cs | 25 ++++ .../Editor/Definition/TestEditorDefinition.cs | 131 ++++++++++++++++++ Source/Mod/Editor/Map/FujiMap.cs | 75 ++++++++-- Source/Mod/Editor/Map/FujiMapWriter.cs | 102 ++++++++++++-- Source/Mod/Editor/TestEditorDefinition.cs | 96 ------------- Source/Scenes/World.cs | 8 +- 7 files changed, 323 insertions(+), 117 deletions(-) rename Source/Mod/Editor/{ => Definition}/EditorDefinition.cs (90%) create mode 100644 Source/Mod/Editor/Definition/SerializeIgnoreAttribute.cs create mode 100644 Source/Mod/Editor/Definition/TestEditorDefinition.cs delete mode 100644 Source/Mod/Editor/TestEditorDefinition.cs diff --git a/Source/Mod/Editor/EditorDefinition.cs b/Source/Mod/Editor/Definition/EditorDefinition.cs similarity index 90% rename from Source/Mod/Editor/EditorDefinition.cs rename to Source/Mod/Editor/Definition/EditorDefinition.cs index 01981fa7..d561f5d1 100644 --- a/Source/Mod/Editor/EditorDefinition.cs +++ b/Source/Mod/Editor/Definition/EditorDefinition.cs @@ -9,6 +9,9 @@ public class EditorDefinition public virtual Vec3 Rotation { get; set; } = Vector3.Zero; public virtual Vec3 Scale { get; set; } = Vector3.One; + public virtual void GameLoad() { } + public virtual void EditorLoad() { } + public virtual void Render(ref EditorRenderState state) { } public virtual void RenderGUI(EditorScene editor) diff --git a/Source/Mod/Editor/Definition/SerializeIgnoreAttribute.cs b/Source/Mod/Editor/Definition/SerializeIgnoreAttribute.cs new file mode 100644 index 00000000..bf8e2d95 --- /dev/null +++ b/Source/Mod/Editor/Definition/SerializeIgnoreAttribute.cs @@ -0,0 +1,25 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Celeste64.Mod.Editor; + +[AttributeUsage(AttributeTargets.Property)] +public class SerializeIgnoreAttribute : Attribute; + +[AttributeUsage(AttributeTargets.Property)] +public class SerializeCustomAttribute(Type type) : Attribute +{ + private readonly MethodInfo m_Serialize = type.GetMethod(nameof(CustomPropertySerializer.Serialize), BindingFlags.Public | BindingFlags.Static) + ?? throw new Exception($"Custom property serializer {type} does not inherit from CustomPropertySerializer"); + private readonly MethodInfo m_Deserialize = type.GetMethod(nameof(CustomPropertySerializer.Deserialize), BindingFlags.Public | BindingFlags.Static) + ?? throw new Exception($"Custom property serializer {type} does not inherit from CustomPropertySerializer"); + + internal void Serialize(object value, BinaryWriter writer) => m_Serialize.Invoke(null, [value, writer]); + internal object Deserialize(BinaryReader reader) => m_Deserialize.Invoke(null, [reader])!; +} + +public interface CustomPropertySerializer +{ + public static abstract void Serialize(T value, BinaryWriter writer); + public static abstract T Deserialize(BinaryReader reader); +} \ No newline at end of file diff --git a/Source/Mod/Editor/Definition/TestEditorDefinition.cs b/Source/Mod/Editor/Definition/TestEditorDefinition.cs new file mode 100644 index 00000000..2abc5890 --- /dev/null +++ b/Source/Mod/Editor/Definition/TestEditorDefinition.cs @@ -0,0 +1,131 @@ +using System.Text.Json.Serialization; +using ImGuiNET; +using Sledge.Formats; + +namespace Celeste64.Mod.Editor; + +[Serializable] +public class TestEditorDefinition : EditorDefinition +{ + // TODO: Figure out how to handle editor-only data inside definitions + private Mesh? mesh; + private EditorMaterial? material; + + public Color Color { get; set; } = Color.White; + + [SerializeIgnore] + public float TestProp { get; set; } + [SerializeCustom(typeof(TestProp2Serializer))] + public float TestProp2 { get; set; } + + private class TestProp2Serializer : CustomPropertySerializer + { + public static void Serialize(float value, BinaryWriter writer) + { + Log.Info("Serializing custom prop"); + writer.Write(value * 2.0f); + } + + public static float Deserialize(BinaryReader reader) + { + Log.Info("Deserializing custom prop"); + return reader.ReadSingle() / 2.0f; + } + } + + public TestEditorDefinition() + { + + } + + ~TestEditorDefinition() + { + mesh?.Dispose(); + } + + public override void Render(ref EditorRenderState state) + { + if (mesh == null) + { + Vec3 size = Vec3.One * 3.0f; + Vec3 color = Vec3.One; + + mesh = new(); + mesh.SetVertices([ + // Front + /* 0*/ new(new Vec3(0, 0, 0), Vec2.Zero, color, -Vec3.UnitY), + /* 1*/ new(new Vec3(size.X, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitY), + /* 2*/ new(new Vec3(0, 0, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitY), + /* 3*/ new(new Vec3(size.X, 0, size.Z), new Vec2(size.X, size.Z), color, -Vec3.UnitY), + // Back + /* 4*/ new(new Vec3(0, size.Y, 0), Vec2.UnitX * size.X, color, Vec3.UnitY), + /* 5*/ new(new Vec3(size.X, size.Y, 0), Vec2.Zero, color, Vec3.UnitY), + /* 6*/ new(new Vec3(0, size.Y, size.Z), new Vec2(size.X, size.Z), color, Vec3.UnitY), + /* 7*/ new(new Vec3(size.X, size.Y, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitY), + // Left + /* 8*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.Y, color, -Vec3.UnitX), + /* 9*/ new(new Vec3(0, 0, size.Z), new Vec2(size.Y, size.Z), color, -Vec3.UnitX), + /*10*/ new(new Vec3(0, size.Y, 0), Vec2.Zero, color, -Vec3.UnitX), + /*11*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitX), + // Right + /*12*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, Vec3.UnitX), + /*13*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitX), + /*14*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitX * size.Y, color, Vec3.UnitX), + /*15*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.Y, size.Z), color, Vec3.UnitX), + // Top + /*16*/ new(new Vec3(0, 0, size.Z), Vec2.Zero, color, Vec3.UnitZ), + /*17*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitX * size.X, color, Vec3.UnitZ), + /*18*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Y, color, Vec3.UnitZ), + /*19*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.X, size.Y), color, Vec3.UnitZ), + // Bottom + /*20*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitZ), + /*21*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, -Vec3.UnitZ), + /*22*/ new(new Vec3(0, size.Y, 0), new Vec2(size.X, size.Y), color, -Vec3.UnitZ), + /*23*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitY * size.Y, color, -Vec3.UnitZ), + ]); + mesh.SetIndices([ + // Front + 0, 1, 2, + 2, 1, 3, + // Back + 5, 4, 7, + 7, 4, 6, + // Left + 10, 8, 11, + 11, 8, 9, + // Right + 12, 14, 13, + 13, 14, 15, + // Top + 16, 17, 18, + 18, 17, 19, + // Bottom + 22, 23, 20, + 20, 23, 21 + ]); + } + material ??= new(); + + state.ApplyToMaterial(material, Matrix.Identity); + material.Color = Color; + + new DrawCommand(state.Camera.Target, mesh, material) + { + DepthCompare = state.DepthCompare, + DepthMask = state.DepthMask, + CullMode = CullMode.Back, + }.Submit(); + state.Calls++; + state.Triangles += mesh.IndexCount / 3; + } + + public override void RenderGUI(EditorScene editor) + { + base.RenderGUI(editor); + + var col = Color.ToVector3(); + // ImGui.ColorPicker3("Color", ref col, ImGuiColorEditFlags.DefaultOptions); + ImGui.ColorEdit3("Color", ref col); + Color = new Color(col); + } +} \ No newline at end of file diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/Map/FujiMap.cs index 680a9f25..6e28a23d 100644 --- a/Source/Mod/Editor/Map/FujiMap.cs +++ b/Source/Mod/Editor/Map/FujiMap.cs @@ -1,3 +1,6 @@ +using System.Reflection; +using System.Reflection.Emit; + namespace Celeste64.Mod.Editor; /// @@ -15,8 +18,7 @@ public FujiMap(string name, string virtPath, Stream stream) try { - #region Metadata - + // Header var magic = reader.ReadBytes(4); if (!magic.SequenceEqual(FujiMapWriter.FormatMagic)) { @@ -24,20 +26,75 @@ public FujiMap(string name, string virtPath, Stream stream) readExceptionMessage = $"Invalid magic bytes! Found '{(char)magic[0]}{(char)magic[1]}{(char)magic[2]}{(char)magic[3]}'"; return; } - var version = reader.ReadByte(); // Not currently used - - #endregion - - #region Metadata + // Metadata Skybox = reader.ReadString(); SnowAmount = reader.ReadSingle(); SnowWind = reader.ReadVec3(); Ambience = reader.ReadString(); Music = reader.ReadString(); - - #endregion + + // Definitions + var defCount = reader.ReadInt32(); + for (int i = 0; i < defCount; i++) + { + var fullName = reader.ReadString(); + var defType = Assembly.GetExecutingAssembly().GetType(fullName)!; + var def = Activator.CreateInstance(defType); + + Log.Info($"Def: {def}"); + + var props = defType + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => !prop.HasAttr()); + + foreach (var prop in props) + { + if (prop.GetCustomAttribute() is { } custom) + { + prop.SetValue(def, custom.Deserialize(reader)); + continue; + } + + // Primitives + if (prop.PropertyType == typeof(bool)) + prop.SetValue(def, reader.ReadBoolean()); + else if (prop.PropertyType == typeof(byte)) + prop.SetValue(def, reader.ReadByte()); + else if (prop.PropertyType == typeof(byte[])) + prop.SetValue(def, reader.ReadBytes(reader.Read7BitEncodedInt())); + else if (prop.PropertyType == typeof(char)) + prop.SetValue(def, reader.ReadChar()); + else if (prop.PropertyType == typeof(char[])) + prop.SetValue(def, reader.ReadChars(reader.Read7BitEncodedInt())); + else if (prop.PropertyType == typeof(decimal)) + prop.SetValue(def, reader.ReadDecimal()); + else if (prop.PropertyType == typeof(double)) + prop.SetValue(def, reader.ReadDouble()); + else if (prop.PropertyType == typeof(float)) + prop.SetValue(def, reader.ReadSingle()); + else if (prop.PropertyType == typeof(int)) + prop.SetValue(def, reader.ReadInt32()); + else if (prop.PropertyType == typeof(long)) + prop.SetValue(def, reader.ReadInt64()); + else if (prop.PropertyType == typeof(sbyte)) + prop.SetValue(def, reader.ReadSByte()); + else if (prop.PropertyType == typeof(short)) + prop.SetValue(def, reader.ReadInt16()); + else if (prop.PropertyType == typeof(Half)) + prop.SetValue(def, reader.ReadHalf()); + // Special support + else if (prop.PropertyType == typeof(Vec2)) + prop.SetValue(def, reader.ReadVec2()); + else if (prop.PropertyType == typeof(Vec3)) + prop.SetValue(def, reader.ReadVec3()); + else if (prop.PropertyType == typeof(Color)) + prop.SetValue(def, reader.ReadColor()); + + Log.Info($" - {prop.Name}: {prop.GetValue(def)}"); + } + } } catch (Exception ex) { diff --git a/Source/Mod/Editor/Map/FujiMapWriter.cs b/Source/Mod/Editor/Map/FujiMapWriter.cs index 4352591e..64ce185b 100644 --- a/Source/Mod/Editor/Map/FujiMapWriter.cs +++ b/Source/Mod/Editor/Map/FujiMapWriter.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; using System.Runtime.Serialization.Formatters.Binary; @@ -21,17 +22,13 @@ public static void WriteTo(EditorScene editor, Stream stream) { using var writer = new BinaryWriter(stream); - #region Header - + // Header writer.Write(FormatMagic); writer.Write(FormatVersion); - - #endregion - #region Metadata - + // Metadata // Skybox - writer.WriteNullTerminatedString("city"); + writer.Write("city"); // Snow amount writer.Write(1.0f); // Snow direction @@ -41,7 +38,88 @@ public static void WriteTo(EditorScene editor, Stream stream) // Music writer.Write("mus_lvl1"); - #endregion + // Definitions + writer.Write(editor.Definitions.Count); + foreach (var def in editor.Definitions) + { + Log.Info($"Def: {def}"); + writer.Write(def.GetType().FullName!); + + var props = def.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => !prop.HasAttr()); + + foreach (var prop in props) + { + if (prop.GetCustomAttribute() is { } custom) + { + custom.Serialize(prop.GetValue(def)!, writer); + continue; + } + + switch (prop.GetValue(def)) + { + // Primitives + case bool v: + writer.Write(v); + break; + case byte v: + writer.Write(v); + break; + case byte[] v: + writer.Write7BitEncodedInt(v.Length); + writer.Write(v); + break; + case char v: + writer.Write(v); + break; + case char[] v: + writer.Write7BitEncodedInt(v.Length); + writer.Write(v); + break; + case decimal v: + writer.Write(v); + break; + case double v: + writer.Write(v); + break; + case float v: + writer.Write(v); + break; + case int v: + writer.Write(v); + break; + case long v: + writer.Write(v); + break; + case sbyte v: + writer.Write(v); + break; + case short v: + writer.Write(v); + break; + case Half v: + writer.Write(v); + break; + + // Special support + case Vec2 v: + writer.Write(v); + break; + case Vec3 v: + writer.Write(v); + break; + case Color v: + writer.Write(v); + break; + + default: + throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def}' cannot be serialized"); + } + + Log.Info($" - {prop.Name}: {prop.GetValue(def)}"); + } + } } public static void Write(this BinaryWriter writer, Vec2 value) @@ -55,7 +133,15 @@ public static void Write(this BinaryWriter writer, Vec3 value) writer.Write(value.Y); writer.Write(value.Z); } + public static void Write(this BinaryWriter writer, Color value) + { + writer.Write(value.R); + writer.Write(value.G); + writer.Write(value.B); + writer.Write(value.A); + } public static Vec2 ReadVec2(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle()); public static Vec3 ReadVec3(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + public static Color ReadColor(this BinaryReader reader) => new(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); } \ No newline at end of file diff --git a/Source/Mod/Editor/TestEditorDefinition.cs b/Source/Mod/Editor/TestEditorDefinition.cs deleted file mode 100644 index 11f15ae2..00000000 --- a/Source/Mod/Editor/TestEditorDefinition.cs +++ /dev/null @@ -1,96 +0,0 @@ -using ImGuiNET; -using Sledge.Formats; - -namespace Celeste64.Mod.Editor; - -public class TestEditorDefinition : EditorDefinition -{ - private readonly Mesh mesh = new(); - private readonly EditorMaterial material = new(); - - public Color Color { get; set; } = Color.White; - - public TestEditorDefinition() - { - Vec3 size = Vec3.One * 3.0f; - Vec3 color = Vec3.One; - - mesh.SetVertices([ - // Front - /* 0*/ new(new Vec3(0, 0, 0), Vec2.Zero, color, -Vec3.UnitY), - /* 1*/ new(new Vec3(size.X, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitY), - /* 2*/ new(new Vec3(0, 0, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitY), - /* 3*/ new(new Vec3(size.X, 0, size.Z), new Vec2(size.X, size.Z), color, -Vec3.UnitY), - // Back - /* 4*/ new(new Vec3(0, size.Y, 0), Vec2.UnitX * size.X, color, Vec3.UnitY), - /* 5*/ new(new Vec3(size.X, size.Y, 0), Vec2.Zero, color, Vec3.UnitY), - /* 6*/ new(new Vec3(0, size.Y, size.Z), new Vec2(size.X, size.Z), color, Vec3.UnitY), - /* 7*/ new(new Vec3(size.X, size.Y, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitY), - // Left - /* 8*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.Y, color, -Vec3.UnitX), - /* 9*/ new(new Vec3(0, 0, size.Z), new Vec2(size.Y, size.Z), color, -Vec3.UnitX), - /*10*/ new(new Vec3(0, size.Y, 0), Vec2.Zero, color, -Vec3.UnitX), - /*11*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitX), - // Right - /*12*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, Vec3.UnitX), - /*13*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitX), - /*14*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitX * size.Y, color, Vec3.UnitX), - /*15*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.Y, size.Z), color, Vec3.UnitX), - // Top - /*16*/ new(new Vec3(0, 0, size.Z), Vec2.Zero, color, Vec3.UnitZ), - /*17*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitX * size.X, color, Vec3.UnitZ), - /*18*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Y, color, Vec3.UnitZ), - /*19*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.X, size.Y), color, Vec3.UnitZ), - // Bottom - /*20*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitZ), - /*21*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, -Vec3.UnitZ), - /*22*/ new(new Vec3(0, size.Y, 0), new Vec2(size.X, size.Y), color, -Vec3.UnitZ), - /*23*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitY * size.Y, color, -Vec3.UnitZ), - ]); - mesh.SetIndices([ - // Front - 0, 1, 2, - 2, 1, 3, - // Back - 5, 4, 7, - 7, 4, 6, - // Left - 10, 8, 11, - 11, 8, 9, - // Right - 12, 14, 13, - 13, 14, 15, - // Top - 16, 17, 18, - 18, 17, 19, - // Bottom - 22, 23, 20, - 20, 23, 21 - ]); - } - - public override void Render(ref EditorRenderState state) - { - state.ApplyToMaterial(material, Matrix.Identity); - material.Color = Color; - - new DrawCommand(state.Camera.Target, mesh, material) - { - DepthCompare = state.DepthCompare, - DepthMask = state.DepthMask, - CullMode = CullMode.Back, - }.Submit(); - state.Calls++; - state.Triangles += mesh.IndexCount / 3; - } - - public override void RenderGUI(EditorScene editor) - { - base.RenderGUI(editor); - - var col = Color.ToVector3(); - // ImGui.ColorPicker3("Color", ref col, ImGuiColorEditFlags.DefaultOptions); - ImGui.ColorEdit3("Color", ref col); - Color = new Color(col); - } -} \ No newline at end of file diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 569c7510..2a777c66 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -388,10 +388,6 @@ public override void Update() pauseMenu.Update(); } } - - if(Panicked) { - return; - } // don't pour salt in wounds // Toggle to editor if (Input.Keyboard.Pressed(Keys.F3)) @@ -400,6 +396,10 @@ public override void Update() Game.Instance.scenes.Push(new EditorScene(Entry)); return; } + + if(Panicked) { + return; + } // don't pour salt in wounds try { debugUpdTimer.Restart(); From 86666321f8f103583135d185dc89f187789ccbde Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 19:24:31 +0100 Subject: [PATCH 17/97] Separate the data and editor definition --- .../Mod/Editor/Definition/EditorDefinition.cs | 35 +++- .../Editor/Definition/TestEditorDefinition.cs | 170 +++++++++--------- Source/Mod/Editor/Map/FujiMap.cs | 45 ++--- Source/Mod/Editor/Map/FujiMapWriter.cs | 12 +- Source/Mod/Editor/WorldRenderer.cs | 10 +- 5 files changed, 144 insertions(+), 128 deletions(-) diff --git a/Source/Mod/Editor/Definition/EditorDefinition.cs b/Source/Mod/Editor/Definition/EditorDefinition.cs index d561f5d1..1c4b43f7 100644 --- a/Source/Mod/Editor/Definition/EditorDefinition.cs +++ b/Source/Mod/Editor/Definition/EditorDefinition.cs @@ -2,30 +2,47 @@ namespace Celeste64.Mod.Editor; -public class EditorDefinition +public abstract class EditorDefinitionData { // TODO: Figure out how to let definitions mark support for certain special properties like position / rotation / scale public virtual Vec3 Position { get; set; } = Vector3.Zero; public virtual Vec3 Rotation { get; set; } = Vector3.Zero; public virtual Vec3 Scale { get; set; } = Vector3.One; +} + +public abstract class EditorDefinition +{ + /// + /// Type of the associated . + /// + public readonly Type DataType; - public virtual void GameLoad() { } - public virtual void EditorLoad() { } + /// + /// Instance of the data type associated with this definition. + /// Needs to be casted to the appropriate type inside the sub class. + /// + public readonly EditorDefinitionData _Data; + + protected EditorDefinition(Type dataType) + { + DataType = dataType; + _Data = (EditorDefinitionData)Activator.CreateInstance(DataType)!; + } public virtual void Render(ref EditorRenderState state) { } public virtual void RenderGUI(EditorScene editor) { - var pos = Position; + var pos = _Data.Position; ImGui.DragFloat3("Position", ref pos, 0.1f); - Position = pos; + _Data.Position = pos; - var rot = Rotation; + var rot = _Data.Rotation; ImGui.DragFloat3("Rotation", ref rot, 0.1f); - Rotation = rot; + _Data.Rotation = rot; - var scale = Scale; + var scale = _Data.Scale; ImGui.DragFloat3("Scale", ref scale, 0.1f); - Scale = scale; + _Data.Scale = scale; } } \ No newline at end of file diff --git a/Source/Mod/Editor/Definition/TestEditorDefinition.cs b/Source/Mod/Editor/Definition/TestEditorDefinition.cs index 2abc5890..b5b71ba0 100644 --- a/Source/Mod/Editor/Definition/TestEditorDefinition.cs +++ b/Source/Mod/Editor/Definition/TestEditorDefinition.cs @@ -7,107 +7,105 @@ namespace Celeste64.Mod.Editor; [Serializable] public class TestEditorDefinition : EditorDefinition { - // TODO: Figure out how to handle editor-only data inside definitions - private Mesh? mesh; - private EditorMaterial? material; - - public Color Color { get; set; } = Color.White; - - [SerializeIgnore] - public float TestProp { get; set; } - [SerializeCustom(typeof(TestProp2Serializer))] - public float TestProp2 { get; set; } - - private class TestProp2Serializer : CustomPropertySerializer + public class DefinitionData : EditorDefinitionData { - public static void Serialize(float value, BinaryWriter writer) + public Color Color { get; set; } = Color.White; + + [SerializeIgnore] + public float TestProp { get; set; } + [SerializeCustom(typeof(TestProp2Serializer))] + public float TestProp2 { get; set; } + + private class TestProp2Serializer : CustomPropertySerializer { - Log.Info("Serializing custom prop"); - writer.Write(value * 2.0f); - } + public static void Serialize(float value, BinaryWriter writer) + { + Log.Info("Serializing custom prop"); + writer.Write(value * 2.0f); + } - public static float Deserialize(BinaryReader reader) - { - Log.Info("Deserializing custom prop"); - return reader.ReadSingle() / 2.0f; + public static float Deserialize(BinaryReader reader) + { + Log.Info("Deserializing custom prop"); + return reader.ReadSingle() / 2.0f; + } } } - public TestEditorDefinition() + // TODO: Figure out how to handle editor-only data inside definitions + private readonly Mesh mesh = new(); + private readonly EditorMaterial material = new(); + + public DefinitionData Data => (DefinitionData)_Data; + + public TestEditorDefinition() : base(typeof(DefinitionData)) { + Vec3 size = Vec3.One * 3.0f; + Vec3 color = Vec3.One; + mesh.SetVertices([ + // Front + /* 0*/ new(new Vec3(0, 0, 0), Vec2.Zero, color, -Vec3.UnitY), + /* 1*/ new(new Vec3(size.X, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitY), + /* 2*/ new(new Vec3(0, 0, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitY), + /* 3*/ new(new Vec3(size.X, 0, size.Z), new Vec2(size.X, size.Z), color, -Vec3.UnitY), + // Back + /* 4*/ new(new Vec3(0, size.Y, 0), Vec2.UnitX * size.X, color, Vec3.UnitY), + /* 5*/ new(new Vec3(size.X, size.Y, 0), Vec2.Zero, color, Vec3.UnitY), + /* 6*/ new(new Vec3(0, size.Y, size.Z), new Vec2(size.X, size.Z), color, Vec3.UnitY), + /* 7*/ new(new Vec3(size.X, size.Y, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitY), + // Left + /* 8*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.Y, color, -Vec3.UnitX), + /* 9*/ new(new Vec3(0, 0, size.Z), new Vec2(size.Y, size.Z), color, -Vec3.UnitX), + /*10*/ new(new Vec3(0, size.Y, 0), Vec2.Zero, color, -Vec3.UnitX), + /*11*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitX), + // Right + /*12*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, Vec3.UnitX), + /*13*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitX), + /*14*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitX * size.Y, color, Vec3.UnitX), + /*15*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.Y, size.Z), color, Vec3.UnitX), + // Top + /*16*/ new(new Vec3(0, 0, size.Z), Vec2.Zero, color, Vec3.UnitZ), + /*17*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitX * size.X, color, Vec3.UnitZ), + /*18*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Y, color, Vec3.UnitZ), + /*19*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.X, size.Y), color, Vec3.UnitZ), + // Bottom + /*20*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitZ), + /*21*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, -Vec3.UnitZ), + /*22*/ new(new Vec3(0, size.Y, 0), new Vec2(size.X, size.Y), color, -Vec3.UnitZ), + /*23*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitY * size.Y, color, -Vec3.UnitZ), + ]); + mesh.SetIndices([ + // Front + 0, 1, 2, + 2, 1, 3, + // Back + 5, 4, 7, + 7, 4, 6, + // Left + 10, 8, 11, + 11, 8, 9, + // Right + 12, 14, 13, + 13, 14, 15, + // Top + 16, 17, 18, + 18, 17, 19, + // Bottom + 22, 23, 20, + 20, 23, 21 + ]); } ~TestEditorDefinition() { mesh?.Dispose(); } - + public override void Render(ref EditorRenderState state) { - if (mesh == null) - { - Vec3 size = Vec3.One * 3.0f; - Vec3 color = Vec3.One; - - mesh = new(); - mesh.SetVertices([ - // Front - /* 0*/ new(new Vec3(0, 0, 0), Vec2.Zero, color, -Vec3.UnitY), - /* 1*/ new(new Vec3(size.X, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitY), - /* 2*/ new(new Vec3(0, 0, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitY), - /* 3*/ new(new Vec3(size.X, 0, size.Z), new Vec2(size.X, size.Z), color, -Vec3.UnitY), - // Back - /* 4*/ new(new Vec3(0, size.Y, 0), Vec2.UnitX * size.X, color, Vec3.UnitY), - /* 5*/ new(new Vec3(size.X, size.Y, 0), Vec2.Zero, color, Vec3.UnitY), - /* 6*/ new(new Vec3(0, size.Y, size.Z), new Vec2(size.X, size.Z), color, Vec3.UnitY), - /* 7*/ new(new Vec3(size.X, size.Y, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitY), - // Left - /* 8*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.Y, color, -Vec3.UnitX), - /* 9*/ new(new Vec3(0, 0, size.Z), new Vec2(size.Y, size.Z), color, -Vec3.UnitX), - /*10*/ new(new Vec3(0, size.Y, 0), Vec2.Zero, color, -Vec3.UnitX), - /*11*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitX), - // Right - /*12*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, Vec3.UnitX), - /*13*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitX), - /*14*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitX * size.Y, color, Vec3.UnitX), - /*15*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.Y, size.Z), color, Vec3.UnitX), - // Top - /*16*/ new(new Vec3(0, 0, size.Z), Vec2.Zero, color, Vec3.UnitZ), - /*17*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitX * size.X, color, Vec3.UnitZ), - /*18*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Y, color, Vec3.UnitZ), - /*19*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.X, size.Y), color, Vec3.UnitZ), - // Bottom - /*20*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitZ), - /*21*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, -Vec3.UnitZ), - /*22*/ new(new Vec3(0, size.Y, 0), new Vec2(size.X, size.Y), color, -Vec3.UnitZ), - /*23*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitY * size.Y, color, -Vec3.UnitZ), - ]); - mesh.SetIndices([ - // Front - 0, 1, 2, - 2, 1, 3, - // Back - 5, 4, 7, - 7, 4, 6, - // Left - 10, 8, 11, - 11, 8, 9, - // Right - 12, 14, 13, - 13, 14, 15, - // Top - 16, 17, 18, - 18, 17, 19, - // Bottom - 22, 23, 20, - 20, 23, 21 - ]); - } - material ??= new(); - state.ApplyToMaterial(material, Matrix.Identity); - material.Color = Color; + material.Color = Data.Color; new DrawCommand(state.Camera.Target, mesh, material) { @@ -123,9 +121,9 @@ public override void RenderGUI(EditorScene editor) { base.RenderGUI(editor); - var col = Color.ToVector3(); + var col = Data.Color.ToVector3(); // ImGui.ColorPicker3("Color", ref col, ImGuiColorEditFlags.DefaultOptions); ImGui.ColorEdit3("Color", ref col); - Color = new Color(col); + Data.Color = new Color(col); } } \ No newline at end of file diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/Map/FujiMap.cs index 6e28a23d..61a47d05 100644 --- a/Source/Mod/Editor/Map/FujiMap.cs +++ b/Source/Mod/Editor/Map/FujiMap.cs @@ -39,13 +39,14 @@ public FujiMap(string name, string virtPath, Stream stream) var defCount = reader.ReadInt32(); for (int i = 0; i < defCount; i++) { + // Get the definition data type, by the full name var fullName = reader.ReadString(); - var defType = Assembly.GetExecutingAssembly().GetType(fullName)!; - var def = Activator.CreateInstance(defType); + var defDataType = Assembly.GetExecutingAssembly().GetType(fullName)!; + var defData = Activator.CreateInstance(defDataType); - Log.Info($"Def: {def}"); + Log.Info($"Def: {defData}"); - var props = defType + var props = defDataType .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(prop => !prop.HasAttr()); @@ -53,46 +54,46 @@ public FujiMap(string name, string virtPath, Stream stream) { if (prop.GetCustomAttribute() is { } custom) { - prop.SetValue(def, custom.Deserialize(reader)); + prop.SetValue(defData, custom.Deserialize(reader)); continue; } // Primitives if (prop.PropertyType == typeof(bool)) - prop.SetValue(def, reader.ReadBoolean()); + prop.SetValue(defData, reader.ReadBoolean()); else if (prop.PropertyType == typeof(byte)) - prop.SetValue(def, reader.ReadByte()); + prop.SetValue(defData, reader.ReadByte()); else if (prop.PropertyType == typeof(byte[])) - prop.SetValue(def, reader.ReadBytes(reader.Read7BitEncodedInt())); + prop.SetValue(defData, reader.ReadBytes(reader.Read7BitEncodedInt())); else if (prop.PropertyType == typeof(char)) - prop.SetValue(def, reader.ReadChar()); + prop.SetValue(defData, reader.ReadChar()); else if (prop.PropertyType == typeof(char[])) - prop.SetValue(def, reader.ReadChars(reader.Read7BitEncodedInt())); + prop.SetValue(defData, reader.ReadChars(reader.Read7BitEncodedInt())); else if (prop.PropertyType == typeof(decimal)) - prop.SetValue(def, reader.ReadDecimal()); + prop.SetValue(defData, reader.ReadDecimal()); else if (prop.PropertyType == typeof(double)) - prop.SetValue(def, reader.ReadDouble()); + prop.SetValue(defData, reader.ReadDouble()); else if (prop.PropertyType == typeof(float)) - prop.SetValue(def, reader.ReadSingle()); + prop.SetValue(defData, reader.ReadSingle()); else if (prop.PropertyType == typeof(int)) - prop.SetValue(def, reader.ReadInt32()); + prop.SetValue(defData, reader.ReadInt32()); else if (prop.PropertyType == typeof(long)) - prop.SetValue(def, reader.ReadInt64()); + prop.SetValue(defData, reader.ReadInt64()); else if (prop.PropertyType == typeof(sbyte)) - prop.SetValue(def, reader.ReadSByte()); + prop.SetValue(defData, reader.ReadSByte()); else if (prop.PropertyType == typeof(short)) - prop.SetValue(def, reader.ReadInt16()); + prop.SetValue(defData, reader.ReadInt16()); else if (prop.PropertyType == typeof(Half)) - prop.SetValue(def, reader.ReadHalf()); + prop.SetValue(defData, reader.ReadHalf()); // Special support else if (prop.PropertyType == typeof(Vec2)) - prop.SetValue(def, reader.ReadVec2()); + prop.SetValue(defData, reader.ReadVec2()); else if (prop.PropertyType == typeof(Vec3)) - prop.SetValue(def, reader.ReadVec3()); + prop.SetValue(defData, reader.ReadVec3()); else if (prop.PropertyType == typeof(Color)) - prop.SetValue(def, reader.ReadColor()); + prop.SetValue(defData, reader.ReadColor()); - Log.Info($" - {prop.Name}: {prop.GetValue(def)}"); + Log.Info($" - {prop.Name}: {prop.GetValue(defData)}"); } } } diff --git a/Source/Mod/Editor/Map/FujiMapWriter.cs b/Source/Mod/Editor/Map/FujiMapWriter.cs index 64ce185b..f1193d74 100644 --- a/Source/Mod/Editor/Map/FujiMapWriter.cs +++ b/Source/Mod/Editor/Map/FujiMapWriter.cs @@ -43,9 +43,9 @@ public static void WriteTo(EditorScene editor, Stream stream) foreach (var def in editor.Definitions) { Log.Info($"Def: {def}"); - writer.Write(def.GetType().FullName!); + writer.Write(def.DataType.FullName!); - var props = def.GetType() + var props = def.DataType .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(prop => !prop.HasAttr()); @@ -53,11 +53,11 @@ public static void WriteTo(EditorScene editor, Stream stream) { if (prop.GetCustomAttribute() is { } custom) { - custom.Serialize(prop.GetValue(def)!, writer); + custom.Serialize(prop.GetValue(def._Data)!, writer); continue; } - switch (prop.GetValue(def)) + switch (prop.GetValue(def._Data)) { // Primitives case bool v: @@ -114,10 +114,10 @@ public static void WriteTo(EditorScene editor, Stream stream) break; default: - throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def}' cannot be serialized"); + throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def._Data}' cannot be serialized"); } - Log.Info($" - {prop.Name}: {prop.GetValue(def)}"); + Log.Info($" - {prop.Name}: {prop.GetValue(def._Data)}"); } } } diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs index 2b9937b7..fbeb3808 100644 --- a/Source/Mod/Editor/WorldRenderer.cs +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -93,11 +93,11 @@ public void Render(EditorScene editor, Target target) state.ObjectID = i + 1; // Use 0 as "nothing selected" state.ModelMatrix = - Matrix.CreateScale(def.Scale) * - Matrix.CreateRotationX(def.Rotation.X * Calc.DegToRad) * - Matrix.CreateRotationY(def.Rotation.Y * Calc.DegToRad) * - Matrix.CreateRotationZ(def.Rotation.Z * Calc.DegToRad) * - Matrix.CreateTranslation(def.Position); + Matrix.CreateScale(def._Data.Scale) * + Matrix.CreateRotationX(def._Data.Rotation.X * Calc.DegToRad) * + Matrix.CreateRotationY(def._Data.Rotation.Y * Calc.DegToRad) * + Matrix.CreateRotationZ(def._Data.Rotation.Z * Calc.DegToRad) * + Matrix.CreateTranslation(def._Data.Position); def.Render(ref state); } From 5220192fde6b853cb888cacb634e0760c7894c26 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 20:06:13 +0100 Subject: [PATCH 18/97] Load solid quad for test definition --- Source/Mod/Editor/Map/FujiMap.cs | 53 +++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/Map/FujiMap.cs index 61a47d05..f8a3e938 100644 --- a/Source/Mod/Editor/Map/FujiMap.cs +++ b/Source/Mod/Editor/Map/FujiMap.cs @@ -8,6 +8,8 @@ namespace Celeste64.Mod.Editor; /// public class FujiMap : Map { + private readonly List _definitionData = []; + public FujiMap(string name, string virtPath, Stream stream) { Name = name; @@ -95,6 +97,8 @@ public FujiMap(string name, string virtPath, Stream stream) Log.Info($" - {prop.Name}: {prop.GetValue(defData)}"); } + + _definitionData.Add((EditorDefinitionData)defData!); } } catch (Exception ex) @@ -109,6 +113,53 @@ public FujiMap(string name, string virtPath, Stream stream) public override void Load(World world) { - world.Add(new Player { Position = new Vec3(0, 0, 1000) }); + foreach (var def in _definitionData) + { + // TODO: Probably move this into the definition data itself? + if (def is TestEditorDefinition.DefinitionData test) + { + var matrix = Matrix.Identity; + Vec3[] verts = [ + Vec3.Zero, + Vec3.UnitX * test.Scale.X, + Vec3.UnitX * test.Scale.X + Vec3.UnitY * test.Scale.Y, + Vec3.UnitY * test.Scale.Y, + ]; + + Log.Info(verts[0]); + + var solid = new Solid + { + LocalBounds = new BoundingBox( + verts.Aggregate(Vec3.Min), + verts.Aggregate(Vec3.Max) + ), + LocalVertices = verts, + LocalFaces = + [ + new Solid.Face + { + Plane = new Plane(Vec3.UnitZ, 0.01f), + VertexStart = 0, + VertexCount = 4, + }, + ], + Position = test.Position + test.Scale / 2.0f, + }; + + solid.Model.Materials.Add(new DefaultMaterial(Assets.Textures["wall"])); + solid.Model.Parts.Add(new SimpleModel.Part(0, 0, 6)); + solid.Model.Mesh.SetVertices([ + new Vertex(verts[0], Vec2.Zero, test.Color.ToVector3(), Vec3.UnitZ), + new Vertex(verts[1], Vec2.UnitX, test.Color.ToVector3(), Vec3.UnitZ), + new Vertex(verts[2], Vec2.One, test.Color.ToVector3(), Vec3.UnitZ), + new Vertex(verts[3], Vec2.UnitY, test.Color.ToVector3(), Vec3.UnitZ), + ]); + solid.Model.Mesh.SetIndices([0, 1, 2, 0, 2, 3]); + + world.Add(solid); + } + } + world.Add(new Player { Position = new Vec3(0, 0, 100) }); } } \ No newline at end of file From 8f377727b9fcfbc9f4ad3021e9473569402abfe6 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 20:47:23 +0100 Subject: [PATCH 19/97] Load the map in the editor --- Source/Mod/Core/ModManager.cs | 2 ++ .../Mod/Editor/Definition/EditorDefinition.cs | 7 +++++- Source/Mod/Editor/EditorScene.cs | 25 +++++++++++++++++-- Source/Mod/Editor/Map/FujiMap.cs | 12 +++++---- Source/Mod/Editor/Map/FujiMapWriter.cs | 3 +++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index ab74bd0b..731a5217 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -89,6 +89,8 @@ internal void OnModFileChanged(ModFileChangedCtx ctx) var extension = Path.GetExtension(filepath); var dir = Path.GetDirectoryName(filepath) ?? ""; + Log.Info($"changed {filepath}"); + // Important assets taken from Assets.Load() // TODO: Support non-toplevel mods? if ((dir.StartsWith(Assets.MapsFolder) && extension is $".{Assets.MapsExtensionSledge}" or $".{Assets.MapsExtensionFuji}" && !dir.StartsWith($"{Assets.MapsFolder}/autosave")) || diff --git a/Source/Mod/Editor/Definition/EditorDefinition.cs b/Source/Mod/Editor/Definition/EditorDefinition.cs index 1c4b43f7..d0ca4192 100644 --- a/Source/Mod/Editor/Definition/EditorDefinition.cs +++ b/Source/Mod/Editor/Definition/EditorDefinition.cs @@ -4,6 +4,10 @@ namespace Celeste64.Mod.Editor; public abstract class EditorDefinitionData { + // Used to link back to the EditorDefinition + // TODO: Maybe remove this?? + public string DefinitionFullName { get; set; } + // TODO: Figure out how to let definitions mark support for certain special properties like position / rotation / scale public virtual Vec3 Position { get; set; } = Vector3.Zero; public virtual Vec3 Rotation { get; set; } = Vector3.Zero; @@ -21,12 +25,13 @@ public abstract class EditorDefinition /// Instance of the data type associated with this definition. /// Needs to be casted to the appropriate type inside the sub class. /// - public readonly EditorDefinitionData _Data; + public EditorDefinitionData _Data { get; internal set; } protected EditorDefinition(Type dataType) { DataType = dataType; _Data = (EditorDefinitionData)Activator.CreateInstance(DataType)!; + _Data.DefinitionFullName = GetType().FullName!; } public virtual void Render(ref EditorRenderState state) { } diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index aaafd65d..7b810bfe 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -1,3 +1,5 @@ +using System.Reflection; + namespace Celeste64.Mod.Editor; public class EditorScene : Scene @@ -16,7 +18,24 @@ public class EditorScene : Scene internal EditorScene(World.EntryInfo entry) { Entry = entry; - Definitions.Add(new TestEditorDefinition()); + //Definitions.Add(new TestEditorDefinition()); + + // Load the map + if (Assets.Maps[entry.Map] is not FujiMap map) + { + // Not a Fuji map, return to level + Game.Instance.scenes.Pop(); + Game.Instance.scenes.Push(new World(Entry)); + return; + } + + foreach (var defData in map.DefinitionData) + { + var defType = Assembly.GetExecutingAssembly().GetType(defData.DefinitionFullName)!; + var def = (EditorDefinition)Activator.CreateInstance(defType)!; + def._Data = defData; + Definitions.Add(def); + } } public override void Update() @@ -32,11 +51,13 @@ public override void Update() if (Input.Keyboard.Ctrl && Input.Keyboard.Pressed(Keys.S)) { // TODO: Dont actually hardcode this lol - var path = "/media/Storage/Code/C#/Fuji/Content/Maps/test.bin"; + var path = "/media/Storage/Code/C#/Fuji/Mods/Template-BasicCassetteLevel/Maps/test.bin"; Log.Info($"Saving map to '{path}'"); using var fs = File.Open(path, FileMode.Create); FujiMapWriter.WriteTo(this, fs); + + return; } worldRenderer.Update(this); diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/Map/FujiMap.cs index f8a3e938..1905aa98 100644 --- a/Source/Mod/Editor/Map/FujiMap.cs +++ b/Source/Mod/Editor/Map/FujiMap.cs @@ -8,7 +8,7 @@ namespace Celeste64.Mod.Editor; /// public class FujiMap : Map { - private readonly List _definitionData = []; + internal readonly List DefinitionData = []; public FujiMap(string name, string virtPath, Stream stream) { @@ -87,6 +87,8 @@ public FujiMap(string name, string virtPath, Stream stream) prop.SetValue(defData, reader.ReadInt16()); else if (prop.PropertyType == typeof(Half)) prop.SetValue(defData, reader.ReadHalf()); + else if (prop.PropertyType == typeof(string)) + prop.SetValue(defData, reader.ReadString()); // Special support else if (prop.PropertyType == typeof(Vec2)) prop.SetValue(defData, reader.ReadVec2()); @@ -98,7 +100,7 @@ public FujiMap(string name, string virtPath, Stream stream) Log.Info($" - {prop.Name}: {prop.GetValue(defData)}"); } - _definitionData.Add((EditorDefinitionData)defData!); + DefinitionData.Add((EditorDefinitionData)defData!); } } catch (Exception ex) @@ -113,7 +115,7 @@ public FujiMap(string name, string virtPath, Stream stream) public override void Load(World world) { - foreach (var def in _definitionData) + foreach (var def in DefinitionData) { // TODO: Probably move this into the definition data itself? if (def is TestEditorDefinition.DefinitionData test) @@ -126,7 +128,7 @@ public override void Load(World world) Vec3.UnitY * test.Scale.Y, ]; - Log.Info(verts[0]); + Log.Info(test.Color); var solid = new Solid { @@ -139,7 +141,7 @@ public override void Load(World world) [ new Solid.Face { - Plane = new Plane(Vec3.UnitZ, 0.01f), + Plane = new Plane(Vec3.UnitZ, 0.001f), VertexStart = 0, VertexCount = 4, }, diff --git a/Source/Mod/Editor/Map/FujiMapWriter.cs b/Source/Mod/Editor/Map/FujiMapWriter.cs index f1193d74..01b4a64c 100644 --- a/Source/Mod/Editor/Map/FujiMapWriter.cs +++ b/Source/Mod/Editor/Map/FujiMapWriter.cs @@ -101,6 +101,9 @@ public static void WriteTo(EditorScene editor, Stream stream) case Half v: writer.Write(v); break; + case string v: + writer.Write(v); + break; // Special support case Vec2 v: From a2a830f4bbcb7c69bb5cd52ba7cf0cc8ccece637 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 20:54:58 +0100 Subject: [PATCH 20/97] Make editor camera "smaller" --- Source/Mod/Editor/WorldRenderer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs index fbeb3808..62764334 100644 --- a/Source/Mod/Editor/WorldRenderer.cs +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -15,7 +15,7 @@ public WorldRenderer() { camera.NearPlane = 5; camera.FarPlane = 800; - camera.Position = new Vec3(0, -10, 0); + camera.Position = new Vec3(0, -100, 0); camera.FOVMultiplier = 1; } @@ -31,7 +31,7 @@ public void Update(EditorScene editor) MathF.Cos(cameraRot.X - Calc.HalfPI), 0.0f); - float moveSpeed = 30.0f; + float moveSpeed = 250.0f; if (Input.Keyboard.Down(Keys.W)) cameraPos += cameraForward * moveSpeed * Time.Delta; From 2911562419ce2652c6dfc74697c3c121555becc0 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 21:09:20 +0100 Subject: [PATCH 21/97] Use view-normal based shading, like Blender --- Content/Shaders/Editor.glsl | 19 ++++++++----------- Source/Mod/Editor/EditorMaterial.cs | 14 +++++++++++++- Source/Mod/Editor/EditorRenderState.cs | 1 + 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Content/Shaders/Editor.glsl b/Content/Shaders/Editor.glsl index 411066f8..30bbef83 100644 --- a/Content/Shaders/Editor.glsl +++ b/Content/Shaders/Editor.glsl @@ -4,6 +4,7 @@ VERTEX: uniform mat4 u_mvp; uniform mat4 u_model; +uniform mat4 u_view; layout(location=0) in vec3 a_position; layout(location=1) in vec2 a_tex; @@ -21,7 +22,7 @@ void main(void) v_tex = a_tex; v_color = a_color; - v_normal = TransformNormal(a_normal, u_model); + v_normal = TransformNormal(a_normal, u_view * u_model); v_world = vec3(u_model * vec4(a_position, 1.0)); } @@ -47,7 +48,7 @@ layout(location = 1) out float o_objectID; void main(void) { - // get texture color + // Get texture color vec4 src = texture(u_texture, v_tex) * u_color * vec4(v_color, 1); // TODO: only enable if you want ModelFlags.Cutout types to work, didn't end up using @@ -59,17 +60,13 @@ void main(void) float fade = Map(depth, 0.9, 1, 1, 0); vec3 col = src.rgb; - // apply depth values + // Apply depth values gl_FragDepth = depth; - // lighten texture color based on normal - float lighten = max(0.0, -dot(v_normal, u_sun)); - col = mix(col, vec3(1,1,1), lighten * 0.10); - - // shadow - float darken = max(0.0, dot(v_normal, u_sun)); - col = mix(col, vec3(4/255.0, 27/255.0, 44/255.0), darken * 0.80); - + // Apply shading based on normal relative to the camera + float shade = clamp(dot(v_normal, vec3(0, 0, 1)), 0.2, 1.0); + col *= vec3(shade); + o_color = vec4(col, src.a) * fade; // TODO: Support object IDs above 255, since its just 8bits o_objectID = u_objectID / 255.0; diff --git a/Source/Mod/Editor/EditorMaterial.cs b/Source/Mod/Editor/EditorMaterial.cs index e8f5ce6b..b8ed7144 100644 --- a/Source/Mod/Editor/EditorMaterial.cs +++ b/Source/Mod/Editor/EditorMaterial.cs @@ -5,8 +5,11 @@ public class EditorMaterial : Material public const string MatrixUniformName = "u_mvp"; private Texture? texture = null; + private Matrix matrix; private Matrix model; + private Matrix view; + private Color color; private float near; private float far; @@ -42,7 +45,6 @@ public Matrix MVP Set(MatrixUniformName, value); } } - public Matrix Model { get => model; @@ -53,6 +55,16 @@ public Matrix Model Set("u_model", value); } } + public Matrix View + { + get => view; + set + { + view = value; + if (Shader?.Has("u_view") ?? false) + Set("u_view", value); + } + } public Texture? Texture { diff --git a/Source/Mod/Editor/EditorRenderState.cs b/Source/Mod/Editor/EditorRenderState.cs index b73d539c..09e9d043 100644 --- a/Source/Mod/Editor/EditorRenderState.cs +++ b/Source/Mod/Editor/EditorRenderState.cs @@ -25,6 +25,7 @@ public void ApplyToMaterial(EditorMaterial mat, in Matrix localTransformation) return; mat.Model = localTransformation * ModelMatrix; + mat.View = Camera.View; mat.MVP = mat.Model * Camera.ViewProjection; mat.NearPlane = Camera.NearPlane; From b0246b9128960837b15c51f67e34f34033948544 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 22:04:19 +0100 Subject: [PATCH 22/97] Highlight selected object with an outline --- Content/Shaders/EditorEdge.glsl | 56 +++++++++++++++++++ .../Shaders/{Editor.glsl => EditorWorld.glsl} | 15 ++--- Source/Mod/Editor/EditorMaterial.cs | 2 +- Source/Mod/Editor/WorldRenderer.cs | 50 +++++++++++++++++ 4 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 Content/Shaders/EditorEdge.glsl rename Content/Shaders/{Editor.glsl => EditorWorld.glsl} (85%) diff --git a/Content/Shaders/EditorEdge.glsl b/Content/Shaders/EditorEdge.glsl new file mode 100644 index 00000000..e37df5f0 --- /dev/null +++ b/Content/Shaders/EditorEdge.glsl @@ -0,0 +1,56 @@ +VERTEX: +#version 330 + +layout(location=0) in vec2 a_pos; +layout(location=1) in vec2 a_tex; + + +out vec2 v_tex; + +void main(void) +{ + gl_Position = vec4(a_pos, 0, 1); + v_tex = a_tex; + +} + +FRAGMENT: +#version 330 +#include Partials/Methods.gl + +uniform sampler2D u_objectID; +uniform float u_selectedID; + +uniform vec2 u_pixel; +uniform vec4 u_edge; + +in vec2 v_tex; +out vec4 o_color; + +float objectID(vec2 uv) +{ + return texture(u_objectID, uv).r; +} + +void main(void) +{ + float a = objectID(v_tex + vec2(u_pixel.x, 0)); + float b = objectID(v_tex + vec2(-u_pixel.x, 0)); + float c = objectID(v_tex + vec2(0, u_pixel.y)); + float d = objectID(v_tex + vec2(0, -u_pixel.y)); + + float it = objectID(v_tex); + float other = + a * 0.25 + + b * 0.25 + + c * 0.25 + + d * 0.25; + + float edge = step(0.0001, other - it); + + if (u_selectedID == 0 || edge == 0 || !(a == u_selectedID || b == u_selectedID || c == u_selectedID || d == u_selectedID || it == u_selectedID)) + discard; + + o_color = vec4(vec3(u_edge), 1); +} + \ No newline at end of file diff --git a/Content/Shaders/Editor.glsl b/Content/Shaders/EditorWorld.glsl similarity index 85% rename from Content/Shaders/Editor.glsl rename to Content/Shaders/EditorWorld.glsl index 30bbef83..9a1e74fc 100644 --- a/Content/Shaders/Editor.glsl +++ b/Content/Shaders/EditorWorld.glsl @@ -44,7 +44,7 @@ in vec3 v_color; in vec3 v_world; layout(location = 0) out vec4 o_color; -layout(location = 1) out float o_objectID; +layout(location = 1) out vec4 o_objectID; void main(void) { @@ -55,19 +55,16 @@ void main(void) // if (src.a < u_cutout) // discard; - float depth = LinearizeDepth(gl_FragCoord.z, u_near, u_far); - float fall = Map(v_world.z, 50, 0, 0, 1); - float fade = Map(depth, 0.9, 1, 1, 0); - vec3 col = src.rgb; - // Apply depth values + float depth = LinearizeDepth(gl_FragCoord.z, u_near, u_far); gl_FragDepth = depth; // Apply shading based on normal relative to the camera float shade = clamp(dot(v_normal, vec3(0, 0, 1)), 0.2, 1.0); - col *= vec3(shade); + src.rgb *= vec3(shade); - o_color = vec4(col, src.a) * fade; + o_color = vec4(src.rgb, 1); // TODO: Support object IDs above 255, since its just 8bits - o_objectID = u_objectID / 255.0; + // NOTE: This is still only a float output, however we need to set alpha to avoid weird blending. + o_objectID = vec4(u_objectID / 255.0, 0, 0, 1); } \ No newline at end of file diff --git a/Source/Mod/Editor/EditorMaterial.cs b/Source/Mod/Editor/EditorMaterial.cs index b8ed7144..fed7e671 100644 --- a/Source/Mod/Editor/EditorMaterial.cs +++ b/Source/Mod/Editor/EditorMaterial.cs @@ -24,7 +24,7 @@ public class EditorMaterial : Material public EditorMaterial(Texture? texture = null) - : base(Assets.Shaders["Editor"]) + : base(Assets.Shaders["EditorWorld"]) { if (!(Shader?.Has(MatrixUniformName) ?? false)) { diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs index 62764334..dcb76141 100644 --- a/Source/Mod/Editor/WorldRenderer.cs +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -1,9 +1,25 @@ +using System.Runtime.InteropServices; using Celeste64.Mod.Helpers; namespace Celeste64.Mod.Editor; public class WorldRenderer { + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private readonly struct ScreenVertex(Vec2 position, Vec2 texcoord) : IVertex + { + public readonly Vec2 Pos = position; + public readonly Vec2 Tex = texcoord; + public VertexFormat Format => VertexFormat; + + private static readonly VertexFormat VertexFormat = VertexFormat.Create( + [ + new VertexFormat.Element(0, VertexType.Float2, normalized: false), + new VertexFormat.Element(1, VertexType.Float2, normalized: false), + ]); + } + + private Camera camera = new(); private Vec3 cameraPos = new(0, -10, 0); private Vec2 cameraRot = new(0, 0); @@ -11,12 +27,26 @@ public class WorldRenderer private Target? worldTarget = null; private readonly Batcher batch = new(); + private readonly Mesh screenMesh = new(); + private readonly Material selectionHighlightMaterial = new(Assets.Shaders["EditorEdge"]); + public WorldRenderer() { camera.NearPlane = 5; camera.FarPlane = 800; camera.Position = new Vec3(0, -100, 0); camera.FOVMultiplier = 1; + + screenMesh.SetVertices([ + new ScreenVertex(new Vec2(-1.0f, -1.0f), Vec2.Zero), + new ScreenVertex(new Vec2(-1.0f, 1.0f), Vec2.UnitY), + new ScreenVertex(new Vec2( 1.0f, -1.0f), Vec2.UnitX), + new ScreenVertex(new Vec2( 1.0f, 1.0f), Vec2.One), + ]); + screenMesh.SetIndices([ + 0, 1, 2, + 3, 1, 2, + ]); } public void Update(EditorScene editor) @@ -126,6 +156,26 @@ public void Render(EditorScene editor, Target target) } } + // Perform edge detection pass + if (selectionHighlightMaterial.Shader?.Has("u_objectID") ?? false) + selectionHighlightMaterial.Set("u_objectID", worldTarget.Attachments[1]); + if (selectionHighlightMaterial.Shader?.Has("u_selectedID") ?? false) + selectionHighlightMaterial.Set("u_selectedID", editor.Selected == null ? 0.0f : (editor.Definitions.IndexOf(editor.Selected) + 1) / 255.0f); + + const float EdgeSize = 2.0f; + if (selectionHighlightMaterial.Shader?.Has("u_pixel") ?? false) + selectionHighlightMaterial.Set("u_pixel", new Vec2(1.0f / worldTarget.Width * Game.RelativeScale * EdgeSize, 1.0f / worldTarget.Height * Game.RelativeScale * EdgeSize)); + if (selectionHighlightMaterial.Shader?.Has("u_edge") ?? false) + selectionHighlightMaterial.Set("u_edge", new Color(0x9999ee)); // TODO: Pick a good color + + new DrawCommand(worldTarget, screenMesh, selectionHighlightMaterial) + { + DepthMask = false, + MeshIndexCount = 2 * 3, + }.Submit(); + state.Calls++; + state.Triangles += 2; + // Render to the main target batch.Image(worldTarget.Attachments[0], Color.White); batch.Render(target); From 64ce2a86bfee819802b676b8f5ebb0d280158de1 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 22:07:17 +0100 Subject: [PATCH 23/97] Fix highlight line wrapping around the screen edges --- Content/Shaders/EditorEdge.glsl | 3 ++- Source/Mod/Editor/WorldRenderer.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Content/Shaders/EditorEdge.glsl b/Content/Shaders/EditorEdge.glsl index e37df5f0..62ee0f40 100644 --- a/Content/Shaders/EditorEdge.glsl +++ b/Content/Shaders/EditorEdge.glsl @@ -29,7 +29,8 @@ out vec4 o_color; float objectID(vec2 uv) { - return texture(u_objectID, uv).r; + const float Eps = 0.0001; + return texture(u_objectID, clamp(uv, vec2(Eps), vec2(1.0 - Eps))).r; } void main(void) diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs index dcb76141..0cd39cf1 100644 --- a/Source/Mod/Editor/WorldRenderer.cs +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -157,6 +157,7 @@ public void Render(EditorScene editor, Target target) } // Perform edge detection pass + // TODO: Maybe render the outline through other solids? Probably by re-rendering the selected object? if (selectionHighlightMaterial.Shader?.Has("u_objectID") ?? false) selectionHighlightMaterial.Set("u_objectID", worldTarget.Attachments[1]); if (selectionHighlightMaterial.Shader?.Has("u_selectedID") ?? false) From e8e5998b6a9bcb651e8bb16073303b08f076c1b8 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 22:10:00 +0100 Subject: [PATCH 24/97] Clean-up some accidental changes --- Source/Mod/Core/GameMod.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs index 070c2ec8..07b244c5 100644 --- a/Source/Mod/Core/GameMod.cs +++ b/Source/Mod/Core/GameMod.cs @@ -52,15 +52,10 @@ public abstract class GameMod // This is here to give mods easier access to these objects, so they don't have to get them themselves // Warning, these may be null if they haven't been initialized yet, so you should always do a null check before using them. - public Game? Game { get { return Game.Instance; } } - public World? World { get { return Game != null ? Game.World : null; } } - public Map? Map { get { return World != null ? World.Map : null; } } - public Player? Player { get { return World != null ? World.Get() : null; } } - - // public Game? Game => Game.Instance; - // public World? World => Game.Scene as World; - // public Map? Map => World?.Map; - // public Player? Player => World?.Get(); + public Game? Game => Game.Instance; + public World? World => Game.Scene as World; + public Map? Map => World?.Map; + public Player? Player => World?.Get(); // Common Metadata about this mod. public bool Enabled { get { return this is VanillaGameMod || ModSaveData.Enabled; } } From f93b48419dd1842d592964cff6ef74ca8c0a8ff9 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Mon, 11 Mar 2024 22:13:08 +0100 Subject: [PATCH 25/97] Revert more accidental changes --- Source/Mod/Core/ModManager.cs | 2 -- Source/Scenes/Scene.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/Mod/Core/ModManager.cs b/Source/Mod/Core/ModManager.cs index 731a5217..ab74bd0b 100644 --- a/Source/Mod/Core/ModManager.cs +++ b/Source/Mod/Core/ModManager.cs @@ -89,8 +89,6 @@ internal void OnModFileChanged(ModFileChangedCtx ctx) var extension = Path.GetExtension(filepath); var dir = Path.GetDirectoryName(filepath) ?? ""; - Log.Info($"changed {filepath}"); - // Important assets taken from Assets.Load() // TODO: Support non-toplevel mods? if ((dir.StartsWith(Assets.MapsFolder) && extension is $".{Assets.MapsExtensionSledge}" or $".{Assets.MapsExtensionFuji}" && !dir.StartsWith($"{Assets.MapsFolder}/autosave")) || diff --git a/Source/Scenes/Scene.cs b/Source/Scenes/Scene.cs index 3843c7b3..1c5c8672 100644 --- a/Source/Scenes/Scene.cs +++ b/Source/Scenes/Scene.cs @@ -10,7 +10,7 @@ public abstract class Scene public string MusicWav = string.Empty; public string AmbienceWav = string.Empty; - + public virtual void Entered() {} public virtual void Exited() {} public virtual void Disposed() {} From 409eb26342a14021634bf81ece17017ae6f9a905 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 15:47:09 +0100 Subject: [PATCH 26/97] Inherit World for editor --- .../Mod/Editor/Definition/ActorDefinition.cs | 6 + .../Mod/Editor/Definition/EditorDefinition.cs | 2 +- Source/Mod/Editor/EditorScene.cs | 49 +++-- Source/Mod/Editor/Map/FujiMap.cs | 4 +- Source/Mod/Editor/Map/FujiMapWriter.cs | 168 +++++++++--------- Source/Mod/Editor/Windows/TestWindow.cs | 8 +- Source/Mod/Editor/WorldRenderer.cs | 168 +++++++++--------- 7 files changed, 204 insertions(+), 201 deletions(-) create mode 100644 Source/Mod/Editor/Definition/ActorDefinition.cs diff --git a/Source/Mod/Editor/Definition/ActorDefinition.cs b/Source/Mod/Editor/Definition/ActorDefinition.cs new file mode 100644 index 00000000..3f22c9a8 --- /dev/null +++ b/Source/Mod/Editor/Definition/ActorDefinition.cs @@ -0,0 +1,6 @@ +namespace Celeste64.Mod.Editor; + +public abstract class ActorDefinition +{ + public abstract Actor Load(); +} \ No newline at end of file diff --git a/Source/Mod/Editor/Definition/EditorDefinition.cs b/Source/Mod/Editor/Definition/EditorDefinition.cs index d0ca4192..cfd46ab2 100644 --- a/Source/Mod/Editor/Definition/EditorDefinition.cs +++ b/Source/Mod/Editor/Definition/EditorDefinition.cs @@ -6,7 +6,7 @@ public abstract class EditorDefinitionData { // Used to link back to the EditorDefinition // TODO: Maybe remove this?? - public string DefinitionFullName { get; set; } + internal string DefinitionFullName { get; set; } // TODO: Figure out how to let definitions mark support for certain special properties like position / rotation / scale public virtual Vec3 Position { get; set; } = Vector3.Zero; diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 7b810bfe..172fa1ef 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -2,40 +2,36 @@ namespace Celeste64.Mod.Editor; -public class EditorScene : Scene +public class EditorScene : World { - public World.EntryInfo Entry; - internal readonly ImGuiHandler[] Handlers = [ new TestWindow(), ]; - public readonly List Definitions = []; - public EditorDefinition? Selected { internal set; get; } = null; + public Actor? Selected { internal set; get; } = null; - private readonly WorldRenderer worldRenderer = new(); + // private readonly WorldRenderer worldRenderer = new(); - internal EditorScene(World.EntryInfo entry) + internal EditorScene(EntryInfo entry) : base(entry) { - Entry = entry; //Definitions.Add(new TestEditorDefinition()); // Load the map - if (Assets.Maps[entry.Map] is not FujiMap map) - { - // Not a Fuji map, return to level - Game.Instance.scenes.Pop(); - Game.Instance.scenes.Push(new World(Entry)); - return; - } - - foreach (var defData in map.DefinitionData) - { - var defType = Assembly.GetExecutingAssembly().GetType(defData.DefinitionFullName)!; - var def = (EditorDefinition)Activator.CreateInstance(defType)!; - def._Data = defData; - Definitions.Add(def); - } + // if (Assets.Maps[entry.Map] is not FujiMap map) + // { + // // Not a Fuji map, return to level + // Game.Instance.scenes.Pop(); + // Game.Instance.scenes.Push(new World(Entry)); + // return; + // } + // + // foreach (var defData in map.DefinitionData) + // { + // var defType = Assembly.GetExecutingAssembly().GetType(defData.DefinitionFullName)!; + // var def = (EditorDefinition)Activator.CreateInstance(defType)!; + // def._Data = defData; + // Definitions.Add(def); + // } } public override void Update() @@ -60,13 +56,14 @@ public override void Update() return; } - worldRenderer.Update(this); + // worldRenderer.Update(this); } public override void Render(Target target) { - target.Clear(Color.Black, 1.0f, 0, ClearMask.All); - worldRenderer.Render(this, target); + // target.Clear(Color.Black, 1.0f, 0, ClearMask.All); + // worldRenderer.Render(this, target); + base.Render(target); } } diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/Map/FujiMap.cs index 1905aa98..22c6a7ab 100644 --- a/Source/Mod/Editor/Map/FujiMap.cs +++ b/Source/Mod/Editor/Map/FujiMap.cs @@ -49,7 +49,7 @@ public FujiMap(string name, string virtPath, Stream stream) Log.Info($"Def: {defData}"); var props = defDataType - .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) .Where(prop => !prop.HasAttr()); foreach (var prop in props) @@ -149,7 +149,7 @@ public override void Load(World world) Position = test.Position + test.Scale / 2.0f, }; - solid.Model.Materials.Add(new DefaultMaterial(Assets.Textures["wall"])); + solid.Model.Materials.Add(new DefaultMaterial(Assets.Textures["white"])); solid.Model.Parts.Add(new SimpleModel.Part(0, 0, 6)); solid.Model.Mesh.SetVertices([ new Vertex(verts[0], Vec2.Zero, test.Color.ToVector3(), Vec3.UnitZ), diff --git a/Source/Mod/Editor/Map/FujiMapWriter.cs b/Source/Mod/Editor/Map/FujiMapWriter.cs index 01b4a64c..55c622f5 100644 --- a/Source/Mod/Editor/Map/FujiMapWriter.cs +++ b/Source/Mod/Editor/Map/FujiMapWriter.cs @@ -39,90 +39,90 @@ public static void WriteTo(EditorScene editor, Stream stream) writer.Write("mus_lvl1"); // Definitions - writer.Write(editor.Definitions.Count); - foreach (var def in editor.Definitions) - { - Log.Info($"Def: {def}"); - writer.Write(def.DataType.FullName!); - - var props = def.DataType - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(prop => !prop.HasAttr()); - - foreach (var prop in props) - { - if (prop.GetCustomAttribute() is { } custom) - { - custom.Serialize(prop.GetValue(def._Data)!, writer); - continue; - } - - switch (prop.GetValue(def._Data)) - { - // Primitives - case bool v: - writer.Write(v); - break; - case byte v: - writer.Write(v); - break; - case byte[] v: - writer.Write7BitEncodedInt(v.Length); - writer.Write(v); - break; - case char v: - writer.Write(v); - break; - case char[] v: - writer.Write7BitEncodedInt(v.Length); - writer.Write(v); - break; - case decimal v: - writer.Write(v); - break; - case double v: - writer.Write(v); - break; - case float v: - writer.Write(v); - break; - case int v: - writer.Write(v); - break; - case long v: - writer.Write(v); - break; - case sbyte v: - writer.Write(v); - break; - case short v: - writer.Write(v); - break; - case Half v: - writer.Write(v); - break; - case string v: - writer.Write(v); - break; - - // Special support - case Vec2 v: - writer.Write(v); - break; - case Vec3 v: - writer.Write(v); - break; - case Color v: - writer.Write(v); - break; - - default: - throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def._Data}' cannot be serialized"); - } - - Log.Info($" - {prop.Name}: {prop.GetValue(def._Data)}"); - } - } + // writer.Write(editor.Definitions.Count); + // foreach (var def in editor.Definitions) + // { + // Log.Info($"Def: {def}"); + // writer.Write(def.DataType.FullName!); + // + // var props = def.DataType + // .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) + // .Where(prop => !prop.HasAttr()); + // + // foreach (var prop in props) + // { + // if (prop.GetCustomAttribute() is { } custom) + // { + // custom.Serialize(prop.GetValue(def._Data)!, writer); + // continue; + // } + // + // switch (prop.GetValue(def._Data)) + // { + // // Primitives + // case bool v: + // writer.Write(v); + // break; + // case byte v: + // writer.Write(v); + // break; + // case byte[] v: + // writer.Write7BitEncodedInt(v.Length); + // writer.Write(v); + // break; + // case char v: + // writer.Write(v); + // break; + // case char[] v: + // writer.Write7BitEncodedInt(v.Length); + // writer.Write(v); + // break; + // case decimal v: + // writer.Write(v); + // break; + // case double v: + // writer.Write(v); + // break; + // case float v: + // writer.Write(v); + // break; + // case int v: + // writer.Write(v); + // break; + // case long v: + // writer.Write(v); + // break; + // case sbyte v: + // writer.Write(v); + // break; + // case short v: + // writer.Write(v); + // break; + // case Half v: + // writer.Write(v); + // break; + // case string v: + // writer.Write(v); + // break; + // + // // Special support + // case Vec2 v: + // writer.Write(v); + // break; + // case Vec3 v: + // writer.Write(v); + // break; + // case Color v: + // writer.Write(v); + // break; + // + // default: + // throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def._Data}' cannot be serialized"); + // } + // + // Log.Info($" - {prop.Name}: {prop.GetValue(def._Data)}"); + // } + // } } public static void Write(this BinaryWriter writer, Vec2 value) diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs index 4c7173ee..118021c2 100644 --- a/Source/Mod/Editor/Windows/TestWindow.cs +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -11,9 +11,9 @@ protected override void RenderWindow(EditorScene editor) ImGui.Text("Testing"); ImGui.Text($"Selected: {editor.Selected}"); - if (editor.Selected is { } selected) - { - selected.RenderGUI(editor); - } + // if (editor.Selected is { } selected) + // { + // selected.RenderGUI(editor); + // } } } \ No newline at end of file diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs index 0cd39cf1..c9282af8 100644 --- a/Source/Mod/Editor/WorldRenderer.cs +++ b/Source/Mod/Editor/WorldRenderer.cs @@ -97,89 +97,89 @@ public void Update(EditorScene editor) public void Render(EditorScene editor, Target target) { - // TODO: Maybe render at a higher resolution in the editor? - if (worldTarget == null || worldTarget.Width != target.Width || worldTarget.Height != target.Height) - { - worldTarget?.Dispose(); - worldTarget = new Target(target.Width, target.Height, [TextureFormat.Color, TextureFormat.R8, TextureFormat.Depth24Stencil8]); - } - worldTarget.Clear(Color.Black, 1.0f, 0, ClearMask.All); - - camera.Target = worldTarget; - EditorRenderState state = new(); - { - state.Camera = camera; - state.ModelMatrix = Matrix.Identity; - state.SunDirection = new Vec3(0, -.7f, -1).Normalized(); - // state.Silhouette = false; - state.DepthCompare = DepthCompare.Less; - state.DepthMask = true; - // state.VerticalFogColor = 0xdceaf0; - } - - for (int i = 0; i < editor.Definitions.Count; i++) - { - var def = editor.Definitions[i]; - - state.ObjectID = i + 1; // Use 0 as "nothing selected" - state.ModelMatrix = - Matrix.CreateScale(def._Data.Scale) * - Matrix.CreateRotationX(def._Data.Rotation.X * Calc.DegToRad) * - Matrix.CreateRotationY(def._Data.Rotation.Y * Calc.DegToRad) * - Matrix.CreateRotationZ(def._Data.Rotation.Z * Calc.DegToRad) * - Matrix.CreateTranslation(def._Data.Position); - - def.Render(ref state); - } - - // Try to select the object under the cursor - if (!ImGuiManager.WantCaptureMouse && Input.Mouse.LeftPressed) - { - // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios - var scale = Math.Min(App.WidthInPixels / (float)target.Width, App.HeightInPixels / (float)target.Height); - var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - target.Bounds.Size / 2 * scale); - // Convert it into a pixel position inside the target - var pixelPos = imageRelativePos / scale; - // Round to integer values - pixelPos = new Vec2(MathF.Round(pixelPos.X), MathF.Round(pixelPos.Y)); - - if (pixelPos.X >= 0 && pixelPos.Y >= 0 && pixelPos.X < worldTarget.Width && pixelPos.Y < worldTarget.Height) - { - var data = new byte[worldTarget.Width * worldTarget.Height]; - worldTarget.Attachments[1].GetData(data); - - // NOTE: OpenGL flips the image vertically - byte objectID = data[worldTarget.Width * (target.Height - (int)pixelPos.Y - 1) + (int)pixelPos.X]; - editor.Selected = objectID == 0 || (objectID - 1) >= editor.Definitions.Count - ? null // Nothing selected - : editor.Definitions[objectID - 1]; - } - } - - // Perform edge detection pass - // TODO: Maybe render the outline through other solids? Probably by re-rendering the selected object? - if (selectionHighlightMaterial.Shader?.Has("u_objectID") ?? false) - selectionHighlightMaterial.Set("u_objectID", worldTarget.Attachments[1]); - if (selectionHighlightMaterial.Shader?.Has("u_selectedID") ?? false) - selectionHighlightMaterial.Set("u_selectedID", editor.Selected == null ? 0.0f : (editor.Definitions.IndexOf(editor.Selected) + 1) / 255.0f); - - const float EdgeSize = 2.0f; - if (selectionHighlightMaterial.Shader?.Has("u_pixel") ?? false) - selectionHighlightMaterial.Set("u_pixel", new Vec2(1.0f / worldTarget.Width * Game.RelativeScale * EdgeSize, 1.0f / worldTarget.Height * Game.RelativeScale * EdgeSize)); - if (selectionHighlightMaterial.Shader?.Has("u_edge") ?? false) - selectionHighlightMaterial.Set("u_edge", new Color(0x9999ee)); // TODO: Pick a good color - - new DrawCommand(worldTarget, screenMesh, selectionHighlightMaterial) - { - DepthMask = false, - MeshIndexCount = 2 * 3, - }.Submit(); - state.Calls++; - state.Triangles += 2; - - // Render to the main target - batch.Image(worldTarget.Attachments[0], Color.White); - batch.Render(target); - batch.Clear(); + // // TODO: Maybe render at a higher resolution in the editor? + // if (worldTarget == null || worldTarget.Width != target.Width || worldTarget.Height != target.Height) + // { + // worldTarget?.Dispose(); + // worldTarget = new Target(target.Width, target.Height, [TextureFormat.Color, TextureFormat.R8, TextureFormat.Depth24Stencil8]); + // } + // worldTarget.Clear(Color.Black, 1.0f, 0, ClearMask.All); + // + // camera.Target = worldTarget; + // EditorRenderState state = new(); + // { + // state.Camera = camera; + // state.ModelMatrix = Matrix.Identity; + // state.SunDirection = new Vec3(0, -.7f, -1).Normalized(); + // // state.Silhouette = false; + // state.DepthCompare = DepthCompare.Less; + // state.DepthMask = true; + // // state.VerticalFogColor = 0xdceaf0; + // } + // + // // for (int i = 0; i < editor.Definitions.Count; i++) + // // { + // // var def = editor.Definitions[i]; + // // + // // state.ObjectID = i + 1; // Use 0 as "nothing selected" + // // state.ModelMatrix = + // // Matrix.CreateScale(def._Data.Scale) * + // // Matrix.CreateRotationX(def._Data.Rotation.X * Calc.DegToRad) * + // // Matrix.CreateRotationY(def._Data.Rotation.Y * Calc.DegToRad) * + // // Matrix.CreateRotationZ(def._Data.Rotation.Z * Calc.DegToRad) * + // // Matrix.CreateTranslation(def._Data.Position); + // // + // // def.Render(ref state); + // // } + // + // // Try to select the object under the cursor + // if (!ImGuiManager.WantCaptureMouse && Input.Mouse.LeftPressed) + // { + // // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios + // var scale = Math.Min(App.WidthInPixels / (float)target.Width, App.HeightInPixels / (float)target.Height); + // var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - target.Bounds.Size / 2 * scale); + // // Convert it into a pixel position inside the target + // var pixelPos = imageRelativePos / scale; + // // Round to integer values + // pixelPos = new Vec2(MathF.Round(pixelPos.X), MathF.Round(pixelPos.Y)); + // + // if (pixelPos.X >= 0 && pixelPos.Y >= 0 && pixelPos.X < worldTarget.Width && pixelPos.Y < worldTarget.Height) + // { + // var data = new byte[worldTarget.Width * worldTarget.Height]; + // worldTarget.Attachments[1].GetData(data); + // + // // NOTE: OpenGL flips the image vertically + // byte objectID = data[worldTarget.Width * (target.Height - (int)pixelPos.Y - 1) + (int)pixelPos.X]; + // editor.Selected = objectID == 0 || (objectID - 1) >= editor.Definitions.Count + // ? null // Nothing selected + // : editor.Definitions[objectID - 1]; + // } + // } + // + // // Perform edge detection pass + // // TODO: Maybe render the outline through other solids? Probably by re-rendering the selected object? + // if (selectionHighlightMaterial.Shader?.Has("u_objectID") ?? false) + // selectionHighlightMaterial.Set("u_objectID", worldTarget.Attachments[1]); + // if (selectionHighlightMaterial.Shader?.Has("u_selectedID") ?? false) + // selectionHighlightMaterial.Set("u_selectedID", editor.Selected == null ? 0.0f : (editor.Definitions.IndexOf(editor.Selected) + 1) / 255.0f); + // + // const float EdgeSize = 2.0f; + // if (selectionHighlightMaterial.Shader?.Has("u_pixel") ?? false) + // selectionHighlightMaterial.Set("u_pixel", new Vec2(1.0f / worldTarget.Width * Game.RelativeScale * EdgeSize, 1.0f / worldTarget.Height * Game.RelativeScale * EdgeSize)); + // if (selectionHighlightMaterial.Shader?.Has("u_edge") ?? false) + // selectionHighlightMaterial.Set("u_edge", new Color(0x9999ee)); // TODO: Pick a good color + // + // new DrawCommand(worldTarget, screenMesh, selectionHighlightMaterial) + // { + // DepthMask = false, + // MeshIndexCount = 2 * 3, + // }.Submit(); + // state.Calls++; + // state.Triangles += 2; + // + // // Render to the main target + // batch.Image(worldTarget.Attachments[0], Color.White); + // batch.Render(target); + // batch.Clear(); } } \ No newline at end of file From 31617ab6afec1d2b176b3436a8e8ac6abfe7605c Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 15:53:47 +0100 Subject: [PATCH 27/97] Re-add camera controls to the editor --- Source/Mod/Editor/EditorScene.cs | 53 +++++++++++++++++++++++++++++++- Source/Scenes/World.cs | 2 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 172fa1ef..2f9478a4 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Celeste64.Mod.Helpers; namespace Celeste64.Mod.Editor; @@ -10,6 +11,9 @@ public class EditorScene : World public Actor? Selected { internal set; get; } = null; + private Vec3 cameraPos = new(0, -10, 0); + private Vec2 cameraRot = new(0, 0); + // private readonly WorldRenderer worldRenderer = new(); internal EditorScene(EntryInfo entry) : base(entry) @@ -56,9 +60,56 @@ public override void Update() return; } + // Camera movement + var cameraForward = new Vec3( + MathF.Sin(cameraRot.X), + MathF.Cos(cameraRot.X), + 0.0f); + var cameraRight = new Vec3( + MathF.Sin(cameraRot.X - Calc.HalfPI), + MathF.Cos(cameraRot.X - Calc.HalfPI), + 0.0f); + + float moveSpeed = 250.0f; + + if (Input.Keyboard.Down(Keys.W)) + cameraPos += cameraForward * moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.S)) + cameraPos -= cameraForward * moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.A)) + cameraPos += cameraRight * moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.D)) + cameraPos -= cameraRight * moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.Space)) + cameraPos.Z += moveSpeed * Time.Delta; + if (Input.Keyboard.Down(Keys.LeftShift)) + cameraPos.Z -= moveSpeed * Time.Delta; + + // Camera rotation + float rotateSpeed = 15.0f * Calc.DegToRad; + if (Input.Mouse.Down(MouseButtons.Right)) + { + cameraRot.X += InputHelper.MouseDelta.X * rotateSpeed * Time.Delta; + cameraRot.Y += InputHelper.MouseDelta.Y * rotateSpeed * Time.Delta; + cameraRot.X %= 360.0f * Calc.DegToRad; + cameraRot.Y = Math.Clamp(cameraRot.Y, -89.9f * Calc.DegToRad, 89.9f * Calc.DegToRad); + } + + // Update camera + var forward = new Vec3( + MathF.Sin(cameraRot.X) * MathF.Cos(cameraRot.Y), + MathF.Cos(cameraRot.X) * MathF.Cos(cameraRot.Y), + MathF.Sin(-cameraRot.Y)); + Camera.Position = cameraPos; + Camera.LookAt = cameraPos + forward; + + // Don't call base.Update, since we don't want the actors to update + // Instead we manually call only the things which we want for the editor + ResolveChanges(); + // worldRenderer.Update(this); } - + public override void Render(Target target) { // target.Clear(Color.Black, 1.0f, 0, ClearMask.All); diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 2a777c66..190395b9 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -308,7 +308,7 @@ private List GetTypesOf() return list; } - private void ResolveChanges() + protected void ResolveChanges() { // resolve adding/removing actors while (adding.Count > 0 || destroying.Count > 0) From 1d84b968e207d8e4ce6cc5e796e5a03db9d85b97 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 16:10:22 +0100 Subject: [PATCH 28/97] Tweak world rendering for editor --- Source/Mod/Editor/EditorScene.cs | 21 +++++++++++++++-- Source/Scenes/World.cs | 39 ++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 2f9478a4..3577fdee 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -5,6 +5,8 @@ namespace Celeste64.Mod.Editor; public class EditorScene : World { + private const float EditorResolutionScale = 3.0f; + internal readonly ImGuiHandler[] Handlers = [ new TestWindow(), ]; @@ -18,6 +20,10 @@ public class EditorScene : World internal EditorScene(EntryInfo entry) : base(entry) { + Camera.NearPlane = 0.1f; + Camera.FarPlane = 4000; // Increase render distance + Camera.FOVMultiplier = 1.25f; + //Definitions.Add(new TestEditorDefinition()); // Load the map @@ -37,7 +43,18 @@ internal EditorScene(EntryInfo entry) : base(entry) // Definitions.Add(def); // } } - + + private float previousScale = 1.0f; + public override void Entered() + { + previousScale = Game.ResolutionScale; + Game.ResolutionScale = EditorResolutionScale; + } + public override void Exited() + { + Game.ResolutionScale = previousScale; + } + public override void Update() { // Toggle to in-game @@ -86,7 +103,7 @@ public override void Update() cameraPos.Z -= moveSpeed * Time.Delta; // Camera rotation - float rotateSpeed = 15.0f * Calc.DegToRad; + float rotateSpeed = 16.5f * Calc.DegToRad; if (Input.Mouse.Down(MouseButtons.Right)) { cameraRot.X += InputHelper.MouseDelta.X * rotateSpeed * Time.Delta; diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 190395b9..d2dcee36 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -9,6 +9,9 @@ public class World : Scene { public enum EntryReasons { Entered, Returned, Respawned } public readonly record struct EntryInfo(string Map, string CheckPoint, bool Submap, EntryReasons Reason); + + public enum WorldType { Game, Editor } + public readonly WorldType Type; public Camera Camera = new(); public Rng Rng = new(0); @@ -100,6 +103,7 @@ public World(EntryInfo entry) }))); Entry = entry; + Type = this is EditorScene ? WorldType.Editor : WorldType.Game; var stopwatch = Stopwatch.StartNew(); @@ -130,6 +134,7 @@ public World(EntryInfo entry) strawbCounterWiggle = 0; // setup pause menu + if (Type == WorldType.Game) // Don't create pause menu in the editor { Menu optionsMenu = new GameOptionsMenu(pauseMenu); @@ -173,25 +178,11 @@ public World(EntryInfo entry) } // environment + if (Type == WorldType.Game) // Don't create environment effects in the editor { if (map.SnowAmount > 0) Add(new Snow(map.SnowAmount, map.SnowWind)); - if (!string.IsNullOrEmpty(map.Skybox)) - { - // single skybox - if (Assets.Textures.TryGetValue($"skyboxes/{map.Skybox}", out var skybox)) - { - skyboxes.Add(new(skybox)); - } - // group - else - { - while (Assets.Textures.TryGetValue($"skyboxes/{map.Skybox}_{skyboxes.Count}", out var nextSkybox)) - skyboxes.Add(new(nextSkybox)); - } - } - // Fuji Custom: Allows playing music and ambience from wav files if available. // Otherwise, uses fmod events like normal. if (map.Music != null && Assets.Music.ContainsKey(map.Music)) @@ -216,6 +207,22 @@ public World(EntryInfo entry) Ambience = $"event:/sfx/ambience/{map.Ambience}"; } } + + // But still show the skybox + if (!string.IsNullOrEmpty(map.Skybox)) + { + // single skybox + if (Assets.Textures.TryGetValue($"skyboxes/{map.Skybox}", out var skybox)) + { + skyboxes.Add(new(skybox)); + } + // group + else + { + while (Assets.Textures.TryGetValue($"skyboxes/{map.Skybox}_{skyboxes.Count}", out var nextSkybox)) + skyboxes.Add(new(nextSkybox)); + } + } ModManager.Instance.OnPreMapLoaded(this, map); @@ -899,7 +906,9 @@ public override void Render(Target target) } // ui + if (Type == WorldType.Game) // Don't render UI in editor { + Log.Info(Type); batch.SetSampler(new TextureSampler(TextureFilter.Linear, TextureWrap.ClampToEdge, TextureWrap.ClampToEdge)); var bounds = new Rect(0, 0, target.Width, target.Height); var font = Language.Current.SpriteFont; From 557948f0b6ca7625aaa72c620a4e41a384a0d22c Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 16:26:27 +0100 Subject: [PATCH 29/97] Properly call Entered/Exited --- Source/Mod/Editor/EditorScene.cs | 4 +++- Source/Scenes/World.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 3577fdee..98725ffb 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -5,7 +5,7 @@ namespace Celeste64.Mod.Editor; public class EditorScene : World { - private const float EditorResolutionScale = 3.0f; + private const float EditorResolutionScale = 2.0f; internal readonly ImGuiHandler[] Handlers = [ new TestWindow(), @@ -60,8 +60,10 @@ public override void Update() // Toggle to in-game if (Input.Keyboard.Pressed(Keys.F3)) { + Game.Scene!.Exited(); Game.Instance.scenes.Pop(); Game.Instance.scenes.Push(new World(Entry)); + Game.Scene.Entered(); return; } diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index d2dcee36..f1654326 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -399,8 +399,10 @@ public override void Update() // Toggle to editor if (Input.Keyboard.Pressed(Keys.F3)) { + Game.Scene!.Exited(); Game.Instance.scenes.Pop(); Game.Instance.scenes.Push(new EditorScene(Entry)); + Game.Scene!.Entered(); return; } @@ -908,7 +910,6 @@ public override void Render(Target target) // ui if (Type == WorldType.Game) // Don't render UI in editor { - Log.Info(Type); batch.SetSampler(new TextureSampler(TextureFilter.Linear, TextureWrap.ClampToEdge, TextureWrap.ClampToEdge)); var bounds = new Rect(0, 0, target.Width, target.Height); var font = Language.Current.SpriteFont; From 8a4a66876eb8d3ed3fc91e3b631293d66370d42b Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 17:25:01 +0100 Subject: [PATCH 30/97] Setup ray casting for selecting actors --- Source/Mod/Editor/EditorScene.cs | 103 ++++++++++++++++++++++++++++++- Source/Mod/Helpers/ModUtils.cs | 51 +++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 Source/Mod/Helpers/ModUtils.cs diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 98725ffb..e3f5c798 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -5,7 +5,7 @@ namespace Celeste64.Mod.Editor; public class EditorScene : World { - private const float EditorResolutionScale = 2.0f; + private const float EditorResolutionScale = 3.0f; internal readonly ImGuiHandler[] Handlers = [ new TestWindow(), @@ -122,6 +122,16 @@ public override void Update() Camera.Position = cameraPos; Camera.LookAt = cameraPos + forward; + // Shoot ray cast for selection + if (Input.Mouse.LeftPressed) + { + Log.Info($"Casting at {Camera.Position} into {Camera.Forward}"); + if (ActorRayCast(Camera.Position, Camera.Forward, 100.0f, out var hit, ignoreBackfaces: false)) + { + Log.Info($"hit Point: {hit.Point} Normal: {hit.Normal} Distance: {hit.Distance} Actor: {hit.Actor} Intersections: {hit.Intersections}"); + } + } + // Don't call base.Update, since we don't want the actors to update // Instead we manually call only the things which we want for the editor ResolveChanges(); @@ -135,6 +145,97 @@ public override void Render(Target target) // worldRenderer.Render(this, target); base.Render(target); } + + public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out RayHit hit, bool ignoreBackfaces = true, bool ignoreTransparent = false) + { + hit = default; + float? closest = null; + + var p0 = point; + var p1 = point + direction * distance; + var box = new BoundingBox(Vec3.Min(p0, p1), Vec3.Max(p0, p1)).Inflate(1); + + foreach (var actor in Actors) + { + if (!actor.WorldBounds.Intersects(box)) + continue; + + if (actor is not Solid solid) + { + if (ModUtils.RayIntersectsBox(point, direction, actor.WorldBounds, out float dist)) + { + // too far away + if (dist > distance) + continue; + + hit.Intersections++; + + // we have a closer value + if (closest.HasValue && dist > closest.Value) + continue; + + // store as closest + hit.Point = point + direction * dist; + hit.Distance = dist; + hit.Actor = actor; + closest = dist; + } + + continue; + } + + // Special handling for solid to properly check against mesh + if (!solid.Collidable || solid.Destroying) + continue; + + if (solid.Transparent && ignoreTransparent) + continue; + + var verts = solid.WorldVertices; + var faces = solid.WorldFaces; + + foreach (var face in faces) + { + // only do planes that are facing against us + if (ignoreBackfaces && Vec3.Dot(face.Plane.Normal, direction) >= 0) + continue; + + // ignore faces that are definitely too far away + if (Utils.DistanceToPlane(point, face.Plane) > distance) + continue; + + // check against each triangle in the face + for (int i = 0; i < face.VertexCount - 2; i ++) + { + if (Utils.RayIntersectsTriangle(point, direction, + verts[face.VertexStart + 0], + verts[face.VertexStart + i + 1], + verts[face.VertexStart + i + 2], out float dist)) + { + // too far away + if (dist > distance) + continue; + + hit.Intersections++; + + // we have a closer value + if (closest.HasValue && dist > closest.Value) + continue; + + // store as closest + hit.Point = point + direction * dist; + hit.Normal = face.Plane.Normal; + hit.Distance = dist; + hit.Actor = solid; + closest = dist; + break; + } + } + } + } + + return closest.HasValue; + } } internal class EditorHandler : ImGuiHandler diff --git a/Source/Mod/Helpers/ModUtils.cs b/Source/Mod/Helpers/ModUtils.cs new file mode 100644 index 00000000..b0874160 --- /dev/null +++ b/Source/Mod/Helpers/ModUtils.cs @@ -0,0 +1,51 @@ +namespace Celeste64.Mod; + +public static class ModUtils +{ + public static bool RayIntersectsBox(Vec3 origin, Vec3 direction, BoundingBox box, out float t) + { + t = 0.0f; + + // X + float tMin = (box.Min.X - origin.X) / direction.X; + float tMax = (box.Max.X - origin.X) / direction.X; + + if (tMin > tMax) + (tMin, tMax) = (tMax, tMin); + + // Y + float tyMin = (box.Min.Y - origin.Y) / direction.Y; + float tyMax = (box.Max.Y - origin.Y) / direction.Y; + + if (tyMin > tyMax) + (tyMin, tyMax) = (tyMax, tyMin); + + if (tMin > tyMax || tyMin > tMax) + return false; + + if (tyMin > tMin) + tMin = tyMin; + + if (tyMax < tMax) + tMax = tyMax; + + // Z + float tzMin = (box.Min.Z - origin.Z) / direction.Z; + float tzMax = (box.Max.Z - origin.Z) / direction.Z; + + if (tzMin > tzMax) + (tzMin, tzMax) = (tzMax, tzMin); + + if (tMin > tzMax || tzMin > tMax) + return false; + + if (tzMin > tMin) + tMin = tzMin; + + if (tzMax < tMax) + tMax = tzMax; + + t = tMin; + return t > 0.0f; + } +} \ No newline at end of file From b4feb3e7894b22b79029596b56891353f91b1c23 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 18:19:33 +0100 Subject: [PATCH 31/97] Determine ray cast direction based on mouse position --- Source/Mod/Editor/EditorScene.cs | 45 ++++++++++++++----- Source/Mod/Helpers/ModUtils.cs | 74 +++++++++++++------------------- 2 files changed, 66 insertions(+), 53 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index e3f5c798..4baee8f1 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -125,10 +125,28 @@ public override void Update() // Shoot ray cast for selection if (Input.Mouse.LeftPressed) { - Log.Info($"Casting at {Camera.Position} into {Camera.Forward}"); - if (ActorRayCast(Camera.Position, Camera.Forward, 100.0f, out var hit, ignoreBackfaces: false)) + if (Matrix.Invert(Camera.ViewProjection, out var inverse) && Camera.Target != null) { - Log.Info($"hit Point: {hit.Point} Normal: {hit.Normal} Distance: {hit.Distance} Actor: {hit.Actor} Intersections: {hit.Intersections}"); + // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios + var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); + var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); + // Convert into normalized-device-coordinates + var ncdPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; + // Turn it back into a world position (with distance 0 from the camera) + var worldPos = Vec4.Transform(new Vec4(ncdPos, 0.0f, 1.0f), inverse); + worldPos.X /= worldPos.W; + worldPos.Y /= worldPos.W; + worldPos.Z /= worldPos.W; + var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z) - Camera.Position; + // direction *= 10.0f; + direction = direction.Normalized(); + + Log.Info($"Casting at {Camera.Position} into {direction} (vs {Camera.Forward}"); + if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) + { + Log.Info($"hit Point: {hit.Point} Normal: {hit.Normal} Distance: {hit.Distance} Actor: {hit.Actor} Intersections: {hit.Intersections}"); + Selected = hit.Actor; + } } } @@ -160,25 +178,31 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R if (!actor.WorldBounds.Intersects(box)) continue; + // TODO: Allow selecting decorations, since they're currently one giant object + if (actor is Decoration or FloatingDecoration) + continue; + if (actor is not Solid solid) { - if (ModUtils.RayIntersectsBox(point, direction, actor.WorldBounds, out float dist)) + if (ModUtils.RayIntersectsBox(point, direction, actor.WorldBounds, out float distEnter, out float distExit)) { // too far away - if (dist > distance) + if (distEnter > distance) continue; - + + Log.Info($"intersected non-solid {actor} @ {distEnter} / {distExit}"); + hit.Intersections++; // we have a closer value - if (closest.HasValue && dist > closest.Value) + if (closest.HasValue && distEnter > closest.Value) continue; // store as closest - hit.Point = point + direction * dist; - hit.Distance = dist; + hit.Point = point + direction * distEnter; + hit.Distance = distEnter; hit.Actor = actor; - closest = dist; + closest = distEnter; } continue; @@ -216,6 +240,7 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R if (dist > distance) continue; + Log.Info($"intersected solid {actor} @ {dist}"); hit.Intersections++; // we have a closer value diff --git a/Source/Mod/Helpers/ModUtils.cs b/Source/Mod/Helpers/ModUtils.cs index b0874160..91db8555 100644 --- a/Source/Mod/Helpers/ModUtils.cs +++ b/Source/Mod/Helpers/ModUtils.cs @@ -2,50 +2,38 @@ namespace Celeste64.Mod; public static class ModUtils { - public static bool RayIntersectsBox(Vec3 origin, Vec3 direction, BoundingBox box, out float t) + // Based on Unity's implementation + // See: https://github.com/Unity-Technologies/Graphics/blob/17c1d4655ea4685128801d617bdc8d624e586bc6/Packages/com.unity.render-pipelines.core/ShaderLibrary/GeometricTools.hlsl#L48-L75 + public static bool RayIntersectsBox(Vec3 origin, Vec3 direction, BoundingBox box, out float tEnter, out float tExit) { - t = 0.0f; + // Could be precomputed. Clamp to avoid INF. clamp() is a single ALU on GCN. + // rcp(FLT_EPS) = 16,777,216, which is large enough for our purposes, + // yet doesn't cause a lot of numerical issues associated with FLT_MAX. + // float3 rayDirInv = clamp(rcp(rayDirection), -rcp(FLT_EPS), rcp(FLT_EPS)); + var dirInv = Vec3.Clamp(Vec3.One / direction, new Vec3(-1.0f / float.Epsilon), new Vec3(1.0f / float.Epsilon)); + + // Perform ray-slab intersection (component-wise). + // float3 t0 = boxMin * rayDirInv - (rayOrigin * rayDirInv); + // float3 t1 = boxMax * rayDirInv - (rayOrigin * rayDirInv); + var t0 = box.Min * dirInv - (origin * dirInv); + var t1 = box.Max * dirInv - (origin * dirInv); + + // Find the closest/farthest distance (component-wise). + // float3 tSlabEntr = min(t0, t1); + // float3 tSlabExit = max(t0, t1); + var tSlabEnter = Vec3.Min(t0, t1); + var tSlabExit = Vec3.Max(t0, t1); + + // Find the farthest entry and the nearest exit. + // tEntr = Max3(tSlabEntr.x, tSlabEntr.y, tSlabEntr.z); + // tExit = Min3(tSlabExit.x, tSlabExit.y, tSlabExit.z); + tEnter = Math.Max(tSlabEnter.X, Math.Max(tSlabEnter.Y, tSlabEnter.Z)); + tExit = Math.Min(tSlabExit.X, Math.Min(tSlabExit.Y, tSlabExit.Z)); + + // Clamp to the range. + // tEntr = max(tEntr, tMin); + // tExit = min(tExit, tMax); - // X - float tMin = (box.Min.X - origin.X) / direction.X; - float tMax = (box.Max.X - origin.X) / direction.X; - - if (tMin > tMax) - (tMin, tMax) = (tMax, tMin); - - // Y - float tyMin = (box.Min.Y - origin.Y) / direction.Y; - float tyMax = (box.Max.Y - origin.Y) / direction.Y; - - if (tyMin > tyMax) - (tyMin, tyMax) = (tyMax, tyMin); - - if (tMin > tyMax || tyMin > tMax) - return false; - - if (tyMin > tMin) - tMin = tyMin; - - if (tyMax < tMax) - tMax = tyMax; - - // Z - float tzMin = (box.Min.Z - origin.Z) / direction.Z; - float tzMax = (box.Max.Z - origin.Z) / direction.Z; - - if (tzMin > tzMax) - (tzMin, tzMax) = (tzMax, tzMin); - - if (tMin > tzMax || tzMin > tMax) - return false; - - if (tzMin > tMin) - tMin = tzMin; - - if (tzMax < tMax) - tMax = tzMax; - - t = tMin; - return t > 0.0f; + return tEnter < tExit; } } \ No newline at end of file From adcef605ab82deaf640a9b5c41808eab3ffe54e1 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 18:20:59 +0100 Subject: [PATCH 32/97] Port over Batcher3D from Celeste64-TAS --- Source/Mod/Helpers/Batcher3D.cs | 504 ++++++++++++++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 Source/Mod/Helpers/Batcher3D.cs diff --git a/Source/Mod/Helpers/Batcher3D.cs b/Source/Mod/Helpers/Batcher3D.cs new file mode 100644 index 00000000..31263c6d --- /dev/null +++ b/Source/Mod/Helpers/Batcher3D.cs @@ -0,0 +1,504 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Celeste64.Mod; + +/// +/// A version of the in 3D instead of 2D. +/// +public class Batcher3D +{ + /// + /// Vertex Format of Batcher.Vertex + /// + private static readonly VertexFormat VertexFormat = VertexFormat.Create( + new VertexFormat.Element(0, VertexType.Float3, false), + new VertexFormat.Element(1, VertexType.Float2, false), + new VertexFormat.Element(2, VertexType.UByte4, true) + ); + + /// + /// The Vertex Layout used for Sprite Batching + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Vertex(Vec3 position, Vec2 texcoord, Color color) : IVertex + { + public Vec3 Pos = position; + public Vec2 Tex = texcoord; + public Color Col = color; + + public readonly VertexFormat Format => VertexFormat; + } + + ~Batcher3D() + { + if (vertexPtr != IntPtr.Zero) + Marshal.FreeHGlobal(vertexPtr); + if (indexPtr != IntPtr.Zero) + Marshal.FreeHGlobal(indexPtr); + } + + private IntPtr vertexPtr = IntPtr.Zero; + private int vertexCount = 0; + private int vertexCapacity = 0; + + private IntPtr indexPtr = IntPtr.Zero; + private int indexCount = 0; + private int indexCapacity = 0; + + private readonly Mesh mesh = new(); + private readonly Material material = new(Assets.Shaders["Sprite"]); + private bool dirty = false; + + public void Line(Vec3 from, Vec3 to, Color color, float thickness = 0.1f) => Line(from, to, color, Matrix4x4.Identity, thickness); + public void Line(Vec3 from, Vec3 to, Color color, Matrix transform, float thickness = 0.1f) + { + var normal = (to - from).Normalized(); + // The other vector for the cross product can't be parallel to the normal + var tangent = Math.Abs(Vec3.Dot(normal, Vec3.UnitX)) < 0.5f ? Vec3.Cross(normal, Vec3.UnitX) : Vec3.Cross(normal, Vec3.UnitY); + var bitangent = Vec3.Cross(normal, tangent); + + tangent *= thickness; + bitangent *= thickness; + + Box(from - tangent - bitangent, from - tangent + bitangent, from + tangent - bitangent, from + tangent + bitangent, + to - tangent - bitangent, to - tangent + bitangent, to + tangent - bitangent, to + tangent + bitangent, + color, transform); + } + + public void Cube(Vec3 center, Color color, float thickness = 0.1f) => Cube(center, color, Matrix4x4.Identity, thickness); + public void Cube(Vec3 center, Color color, Matrix transform, float thickness = 0.1f) + { + Box(center + new Vec3(-thickness, -thickness, -thickness), + center + new Vec3(thickness, -thickness, -thickness), + center + new Vec3(-thickness, thickness, -thickness), + center + new Vec3(thickness, thickness, -thickness), + center + new Vec3(-thickness, -thickness, thickness), + center + new Vec3(thickness, -thickness, thickness), + center + new Vec3(-thickness, thickness, thickness), + center + new Vec3(thickness, thickness, thickness), + color, transform + ); + } + + public void Torus(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Torus(center, radius, resolution, color, Matrix4x4.Identity, thickness); + public void Torus(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) + { + var points = new Vec3[resolution]; + + float angleStep = Calc.TAU / resolution; + float angle = 0.0f; + for (int i = 0; i < resolution; i++, angle += angleStep) + { + points[i] = new Vec3(Calc.AngleToVector(angle, radius), 0.0f); + } + + EnsureVertexCapacity(vertexCount + resolution * 4); // 4 vertices each + EnsureIndexCapacity(indexCount + resolution * 4 * 2 * 3); // 4 faces * 2 triangles * 3 vertices each + + unsafe + { + Span vertices = new((Vertex*)vertexPtr + vertexCount, resolution * 4); + Span indices = new((int*)indexPtr + indexCount, resolution * 4 * 2 * 3); + + for (int i = 0; i < resolution; i++) + { + var normal = points[i].Normalized() * thickness; + var up = new Vec3(0.0f, 0.0f, thickness); + + vertices[i * 4 + 0].Pos = Vec3.Transform(center + points[i] + normal - up, transform); + vertices[i * 4 + 1].Pos = Vec3.Transform(center + points[i] - normal - up, transform); + vertices[i * 4 + 2].Pos = Vec3.Transform(center + points[i] + normal + up, transform); + vertices[i * 4 + 3].Pos = Vec3.Transform(center + points[i] - normal + up, transform); + vertices[i * 4 + 0].Col = color; + vertices[i * 4 + 1].Col = color; + vertices[i * 4 + 2].Col = color; + vertices[i * 4 + 3].Col = color; + } + + for (int i = 0; i < resolution; i++) + { + int curr = i; + int prev = i == 0 ? resolution - 1 : i - 1; // Wrap around to the end + + // Bottom + indices[i * (4 * 2 * 3) + 0] = vertexCount + prev * 4 + 0; + indices[i * (4 * 2 * 3) + 1] = vertexCount + curr * 4 + 0; + indices[i * (4 * 2 * 3) + 2] = vertexCount + prev * 4 + 1; + indices[i * (4 * 2 * 3) + 3] = vertexCount + prev * 4 + 0; + indices[i * (4 * 2 * 3) + 4] = vertexCount + curr * 4 + 0; + indices[i * (4 * 2 * 3) + 5] = vertexCount + curr * 4 + 1; + // Top + indices[i * (4 * 2 * 3) + 6] = vertexCount + prev * 4 + 2; + indices[i * (4 * 2 * 3) + 7] = vertexCount + prev * 4 + 3; + indices[i * (4 * 2 * 3) + 8] = vertexCount + curr * 4 + 3; + indices[i * (4 * 2 * 3) + 9] = vertexCount + prev * 4 + 2; + indices[i * (4 * 2 * 3) + 10] = vertexCount + curr * 4 + 3; + indices[i * (4 * 2 * 3) + 11] = vertexCount + curr * 4 + 2; + // Outer + indices[i * (4 * 2 * 3) + 12] = vertexCount + prev * 4 + 0; + indices[i * (4 * 2 * 3) + 13] = vertexCount + curr * 4 + 2; + indices[i * (4 * 2 * 3) + 14] = vertexCount + prev * 4 + 2; + indices[i * (4 * 2 * 3) + 15] = vertexCount + prev * 4 + 0; + indices[i * (4 * 2 * 3) + 16] = vertexCount + curr * 4 + 0; + indices[i * (4 * 2 * 3) + 17] = vertexCount + curr * 4 + 2; + // Inner + indices[i * (4 * 2 * 3) + 18] = vertexCount + prev * 4 + 1; + indices[i * (4 * 2 * 3) + 19] = vertexCount + prev * 4 + 3; + indices[i * (4 * 2 * 3) + 20] = vertexCount + curr * 4 + 3; + indices[i * (4 * 2 * 3) + 21] = vertexCount + prev * 4 + 1; + indices[i * (4 * 2 * 3) + 22] = vertexCount + curr * 4 + 3; + indices[i * (4 * 2 * 3) + 23] = vertexCount + curr * 4 + 1; + } + } + + vertexCount += resolution * 4; + indexCount += resolution * 4 * 2 * 3; + dirty = true; + } + + public void Disk(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Disk(center, radius, resolution, color, Matrix4x4.Identity, thickness); + public void Disk(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) + { + var points = new Vec3[resolution]; + + float angleStep = Calc.TAU / resolution; + float angle = 0.0f; + for (int i = 0; i < resolution; i++, angle += angleStep) + { + points[i] = new Vec3(Calc.AngleToVector(angle, radius), 0.0f); + } + + EnsureVertexCapacity(vertexCount + resolution * 2 + 2); // 2 vertices each + 2 in the center + EnsureIndexCapacity(indexCount + resolution * 4 * 3); // 1 faces for outside + 2 triangles on top/bottom = 4 triangles * 3 vertices each + + unsafe + { + Span vertices = new((Vertex*)vertexPtr + vertexCount, resolution * 2 + 2); + Span indices = new((int*)indexPtr + indexCount, resolution * 4 * 3); + + var up = new Vec3(0.0f, 0.0f, thickness); + vertices[0].Pos = Vec3.Transform(center - up, transform); + vertices[1].Pos = Vec3.Transform(center + up, transform); + vertices[0].Col = color; + vertices[1].Col = color; + + for (int i = 0; i < resolution; i++) + { + vertices[(i * 2 + 2) + 0].Pos = Vec3.Transform(center + points[i] - up, transform); + vertices[(i * 2 + 2) + 1].Pos = Vec3.Transform(center + points[i] + up, transform); + vertices[(i * 2 + 2) + 0].Col = color; + vertices[(i * 2 + 2) + 1].Col = color; + } + + for (int i = 0; i < resolution; i++) + { + int curr = i; + int prev = i == 0 ? resolution - 1 : i - 1; // Wrap around to the end + + // Bottom + indices[i * (4 * 3) + 0] = vertexCount + (prev * 2 + 2) + 0; + indices[i * (4 * 3) + 1] = vertexCount + (curr * 2 + 2) + 0; + indices[i * (4 * 3) + 2] = vertexCount + 0; + // Top + indices[i * (4 * 3) + 3] = vertexCount + (curr * 2 + 2) + 1; + indices[i * (4 * 3) + 4] = vertexCount + (prev * 2 + 2) + 1; + indices[i * (4 * 3) + 5] = vertexCount + 1; + // Outer + indices[i * (4 * 3) + 6] = vertexCount + (curr * 2 + 2) + 1; + indices[i * (4 * 3) + 7] = vertexCount + (curr * 2 + 2) + 0; + indices[i * (4 * 3) + 8] = vertexCount + (prev * 2 + 2) + 0; + indices[i * (4 * 3) + 9] = vertexCount + (curr * 2 + 2) + 1; + indices[i * (4 * 3) + 10] = vertexCount + (prev * 2 + 2) + 0; + indices[i * (4 * 3) + 11] = vertexCount + (prev * 2 + 2) + 1; + } + } + + vertexCount += resolution * 2 + 2; + indexCount += resolution * 4 * 3; + dirty = true; + } + + public void Sphere(Vec3 center, float radius, int resolution, Color color) => Sphere(center, radius, resolution, color, Matrix4x4.Identity); + public void Sphere(Vec3 center, float radius, int resolution, Color color, Matrix transform) + { + // Taken and adapted from Utils.CreateSphere() + int stackCount = resolution; + int sliceCount = resolution; + + EnsureVertexCapacity(vertexCount + 2 + (stackCount - 1) * sliceCount); + EnsureIndexCapacity(indexCount + sliceCount * 6 + (stackCount - 2) * sliceCount * 6); + + unsafe + { + Span vertices = new((Vertex*)vertexPtr + vertexCount, 2 + (stackCount - 1) * sliceCount); + Span indices = new((int*)indexPtr + indexCount, sliceCount * 6 + (stackCount - 2) * sliceCount * 6); + + int vtx = 0; + int idx = 0; + + // Add top vertex + int v0 = vertexCount + vtx; + vertices[vtx++].Pos = center + new Vec3(0.0f, 0.0f, radius); + + // Generate vertices per stack / slice + for (int i = 0; i < stackCount - 1; i++) + { + float phi = MathF.PI * (i + 1) / (float)(stackCount); + for (int j = 0; j < sliceCount; j++) + { + float theta = 2.0f * MathF.PI * (j) / (float)(sliceCount); + float x = radius * MathF.Sin(phi) * MathF.Cos(theta); + float y = radius * MathF.Sin(phi) * MathF.Sin(theta); + float z = radius * MathF.Cos(phi); + vertices[vtx++].Pos = center + new Vec3(x, y, z); + } + } + + // Add bottom vertex + int v1 = vertexCount + vtx; + vertices[vtx++].Pos = center + new Vec3(0.0f, 0.0f, -radius); + + // Fill-in color + for (int i = 0; i < vtx; i++) + vertices[i].Col = color; + + // Add top / bottom triangles + for (int i = 0; i < sliceCount; ++i) + { + int i0 = i + 1; + int i1 = (i + 1) % sliceCount + 1; + indices[idx++] = v0; + indices[idx++] = vertexCount + i1; + indices[idx++] = vertexCount + i0; + + i0 = i + sliceCount * (stackCount - 2) + 1; + i1 = (i + 1) % sliceCount + sliceCount * (stackCount - 2) + 1; + indices[idx++] = v1; + indices[idx++] = vertexCount + i0; + indices[idx++] = vertexCount + i1; + } + + // Add quads per stack / slice + for (int j = 0; j < stackCount - 2; j++) + { + int j0 = j * sliceCount + 1; + int j1 = (j + 1) * sliceCount + 1; + for (int i = 0; i < sliceCount; i++) + { + int i0 = j0 + i; + int i1 = j0 + (i + 1) % sliceCount; + int i2 = j1 + (i + 1) % sliceCount; + int i3 = j1 + i; + indices[idx++] = vertexCount + i0; + indices[idx++] = vertexCount + i1; + indices[idx++] = vertexCount + i2; + indices[idx++] = vertexCount + i0; + indices[idx++] = vertexCount + i2; + indices[idx++] = vertexCount + i3; + } + } + + vertexCount += vtx; + indexCount += idx; + dirty = true; + } + } + + public void Box(Vec3 min, Vec3 max, Color color) => Box(min, max, color, Matrix.Identity); + public void Box(Vec3 min, Vec3 max, Color color, Matrix transform) => + Box(min with { Z = max.Z }, + min with { X = max.X, Z = max.Z }, + min, + min with { X = max.X }, + + max with { X = min.X }, + max, + max with { X = min.X, Z = min.Z }, + max with { Z = min.Z }, + color, transform); + + /// + /// Renders a box of a solid color. + /// + /// Front Top Left + /// Front Top Right + /// Front Bottom Left + /// Front Bottom Right + /// Back Top Left + /// Back Top Right + /// Back Bottom Left + /// Back Bottom Right + /// Box color + public void Box(Vec3 v0, Vec3 v1, Vec3 v2, Vec3 v3, + Vec3 v4, Vec3 v5, Vec3 v6, Vec3 v7, + Color color, Matrix transform) + { + EnsureVertexCapacity(vertexCount + 8); + EnsureIndexCapacity(indexCount + 6 * 2 * 3); // 6 faces * 2 triangles * 3 vertices + + unsafe + { + Span vertices = new((Vertex*)vertexPtr + vertexCount, 8); + Span indices = new((int*)indexPtr + indexCount, 36); + + vertices[0].Pos = Vec3.Transform(v0, transform); + vertices[1].Pos = Vec3.Transform(v1, transform); + vertices[2].Pos = Vec3.Transform(v2, transform); + vertices[3].Pos = Vec3.Transform(v3, transform); + vertices[4].Pos = Vec3.Transform(v4, transform); + vertices[5].Pos = Vec3.Transform(v5, transform); + vertices[6].Pos = Vec3.Transform(v6, transform); + vertices[7].Pos = Vec3.Transform(v7, transform); + vertices[0].Col = color; + vertices[1].Col = color; + vertices[2].Col = color; + vertices[3].Col = color; + vertices[4].Col = color; + vertices[5].Col = color; + vertices[6].Col = color; + vertices[7].Col = color; + + // Front + indices[0] = vertexCount + 0; + indices[1] = vertexCount + 2; + indices[2] = vertexCount + 1; + indices[3] = vertexCount + 2; + indices[4] = vertexCount + 3; + indices[5] = vertexCount + 1; + // Back + indices[6] = vertexCount + 4; + indices[7] = vertexCount + 5; + indices[8] = vertexCount + 6; + indices[9] = vertexCount + 5; + indices[10] = vertexCount + 7; + indices[11] = vertexCount + 6; + // Left + indices[12] = vertexCount + 0; + indices[13] = vertexCount + 6; + indices[14] = vertexCount + 2; + indices[15] = vertexCount + 0; + indices[16] = vertexCount + 4; + indices[17] = vertexCount + 6; + // Right + indices[18] = vertexCount + 1; + indices[19] = vertexCount + 3; + indices[20] = vertexCount + 7; + indices[21] = vertexCount + 1; + indices[22] = vertexCount + 7; + indices[23] = vertexCount + 5; + // Top + indices[24] = vertexCount + 0; + indices[25] = vertexCount + 1; + indices[26] = vertexCount + 5; + indices[27] = vertexCount + 0; + indices[28] = vertexCount + 5; + indices[29] = vertexCount + 4; + // Bottom + indices[30] = vertexCount + 2; + indices[31] = vertexCount + 7; + indices[32] = vertexCount + 6; + indices[33] = vertexCount + 2; + indices[34] = vertexCount + 3; + indices[35] = vertexCount + 7; + } + + vertexCount += 8; + indexCount += 6 * 2 * 3; + dirty = true; + } + + /// + /// Draws the Batcher3D to the given Target with the given RenderState + /// + public void Render(ref RenderState state) + { + if (indexPtr == IntPtr.Zero || vertexPtr == IntPtr.Zero) + return; + + // Upload our data if we've been modified since the last time we rendered + if (dirty) + { + mesh.SetIndices(indexPtr, indexCount, IndexFormat.ThirtyTwo); + mesh.SetVertices(vertexPtr, vertexCount, VertexFormat); + dirty = false; + } + + if (material.Shader?.Has("u_matrix") ?? false) + material.Set("u_matrix", state.Camera.ViewProjection); + if (material.Shader?.Has("u_far") ?? false) + material.Set("u_far", state.Camera.FarPlane); + if (material.Shader?.Has("u_near") ?? false) + material.Set("u_near", state.Camera.NearPlane); + if (material.Shader?.Has("u_texture") ?? false) + material.Set("u_texture", Assets.Textures["white"]); + + var call = new DrawCommand(state.Camera.Target, mesh, material) + { + // BlendMode = BlendMode.Screen, + DepthCompare = state.DepthCompare, + DepthMask = state.DepthMask, + // DepthMask = false, + // DepthCompare = DepthCompare.Less, + CullMode = CullMode.None, + MeshIndexStart = 0, + MeshIndexCount = indexCount, + }; + call.Submit(); + state.Calls++; + state.Triangles += indexCount / 3; + } + + /// + /// Clears the Batcher3D. + /// + public void Clear() + { + vertexCount = 0; + indexCount = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void EnsureVertexCapacity(int capacity) + { + if (capacity < vertexCapacity) return; + + if (vertexCapacity == 0) + vertexCapacity = 32; + + while (capacity >= vertexCapacity) + vertexCapacity *= 2; + + IntPtr newPtr = Marshal.AllocHGlobal(sizeof(Vertex) * vertexCapacity); + + if (vertexCount > 0) + Buffer.MemoryCopy((void*)vertexPtr, (void*)newPtr, vertexCapacity * sizeof(Vertex), vertexCount * sizeof(Vertex)); + + if (vertexPtr != IntPtr.Zero) + Marshal.FreeHGlobal(vertexPtr); + + vertexPtr = newPtr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void EnsureIndexCapacity(int capacity) + { + if (capacity < indexCapacity) return; + + if (indexCapacity == 0) + indexCapacity = 32; + + while (capacity >= indexCapacity) + indexCapacity *= 2; + + IntPtr newPtr = Marshal.AllocHGlobal(sizeof(int) * indexCapacity); + + if (indexCount > 0) + Buffer.MemoryCopy((void*)indexPtr, (void*)newPtr, indexCapacity * sizeof(int), indexCount * sizeof(int)); + + if (indexPtr != IntPtr.Zero) + Marshal.FreeHGlobal(indexPtr); + + indexPtr = newPtr; + } +} From 73026bdf0daac567278706ad8e11d591fbb9c205 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 19:15:08 +0100 Subject: [PATCH 33/97] Fix object selection sometimes not working --- Source/Mod/Editor/EditorScene.cs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 4baee8f1..1b99bbfb 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -125,27 +125,34 @@ public override void Update() // Shoot ray cast for selection if (Input.Mouse.LeftPressed) { - if (Matrix.Invert(Camera.ViewProjection, out var inverse) && Camera.Target != null) + if (Camera.Target != null && + Matrix.Invert(Camera.Projection, out var inverseProj) && + Matrix.Invert(Camera.View, out var inverseView)) { // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); // Convert into normalized-device-coordinates - var ncdPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; - // Turn it back into a world position (with distance 0 from the camera) - var worldPos = Vec4.Transform(new Vec4(ncdPos, 0.0f, 1.0f), inverse); - worldPos.X /= worldPos.W; - worldPos.Y /= worldPos.W; - worldPos.Z /= worldPos.W; - var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z) - Camera.Position; - // direction *= 10.0f; - direction = direction.Normalized(); + var ndcPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; + // Flip Y, since up is negative in NDC coords + ndcPos.Y *= -1.0f; + var clipPos = new Vec4(ndcPos, -1.0f, 1.0f); + var eyePos = Vec4.Transform(clipPos, inverseProj); + // We only care about XY, so we set ZW to "forward" + eyePos.Z = -1.0f; + eyePos.W = 0.0f; + var worldPos = Vec4.Transform(eyePos, inverseView); + var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); Log.Info($"Casting at {Camera.Position} into {direction} (vs {Camera.Forward}"); if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) { Log.Info($"hit Point: {hit.Point} Normal: {hit.Normal} Distance: {hit.Distance} Actor: {hit.Actor} Intersections: {hit.Intersections}"); Selected = hit.Actor; + } + else + { + Selected = null; } } } From 614da0afc471d7e181863ea606780382d5c75265 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 19:16:52 +0100 Subject: [PATCH 34/97] Add OBB intersection method for the future --- Source/Mod/Helpers/ModUtils.cs | 112 +++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/Source/Mod/Helpers/ModUtils.cs b/Source/Mod/Helpers/ModUtils.cs index 91db8555..ab984800 100644 --- a/Source/Mod/Helpers/ModUtils.cs +++ b/Source/Mod/Helpers/ModUtils.cs @@ -36,4 +36,116 @@ public static bool RayIntersectsBox(Vec3 origin, Vec3 direction, BoundingBox box return tEnter < tExit; } + + // Intersection method from "Real-Time Rendering and Essential Mathematics for Games" + // Reference Implementation: https://github.com/opengl-tutorials/ogl/blob/15e57f6cccef388915e565d8322b8442049e1bd8/misc05_picking/misc05_picking_custom.cpp#L83-L197 + public static bool RayIntersectOBB(Vec3 origin, Vec3 direction, BoundingBox box, Matrix transform, out float t) + { + t = 0.0f; + float tMin = 0.0f, tMax = 100000.0f; + + var oobWorldPos = new Vec3(transform.M41, transform.M42, transform.M43); + var delta = oobWorldPos - origin; + + // Test intersection with the 2 planes perpendicular to the OBB's X axis + { + var xAxis = new Vec3(transform.M11, transform.M12, transform.M13); + float e = Vec3.Dot(xAxis, delta); + float f = Vec3.Dot(direction, xAxis); + + if (Math.Abs(f) > 0.001f) // Standard case + { + float t1 = (e + box.Min.X) / f; // Intersection with the "left" plane + float t2 = (e + box.Max.X) / f; // Intersection with the "right" plane + // t1 and t2 now contain distances between ray origin and ray-plane intersections + + // We want t1 to represent the nearest intersection, + // so if it's not the case, invert t1 and t2 + if (t1>t2) + (t1, t2) = (t2, t1); + + // tMax is the nearest "far" intersection (amongst the X,Y and Z planes pairs) + if ( t2 < tMax ) + tMax = t2; + // tMin is the farthest "near" intersection (amongst the X,Y and Z planes pairs) + if ( t1 > tMin ) + tMin = t1; + + // And here's the trick : + // If "far" is closer than "near", then there is NO intersection. + // See the images in the tutorials for the visual explanation. + if (tMax < tMin ) + return false; + } + else // Rare case: The ray is almost parallel to the planes, so they don't have any "intersection" + { + if(-e + box.Min.X > 0.0f || -e + box.Max.X < 0.0f) + return false; + } + } + + // Test intersection with the 2 planes perpendicular to the OBB's Y axis + // Exactly the same thing than above. + { + var yAxis = new Vec3(transform.M21, transform.M22, transform.M23); + float e = Vec3.Dot(yAxis, delta); + float f = Vec3.Dot(direction, yAxis); + + if (Math.Abs(f) > 0.001f) + { + float t1 = (e + box.Min.Y) / f; + float t2 = (e + box.Max.Y) / f; + + if (t1>t2) + (t1, t2) = (t2, t1); + + if ( t2 < tMax ) + tMax = t2; + if ( t1 > tMin ) + tMin = t1; + + if (tMax < tMin ) + return false; + } + else + { + if(-e + box.Min.Y > 0.0f || -e + box.Max.Y < 0.0f) + return false; + } + } + + + // Test intersection with the 2 planes perpendicular to the OBB's Z axis + // Exactly the same thing than above. + { + var zAxis = new Vec3(transform.M31, transform.M32, transform.M33); + float e = Vec3.Dot(zAxis, delta); + float f = Vec3.Dot(direction, zAxis); + + if (Math.Abs(f) > 0.001f) + { + float t1 = (e + box.Min.Z) / f; + float t2 = (e + box.Max.Z) / f; + + if (t1>t2) + (t1, t2) = (t2, t1); + + if ( t2 < tMax ) + tMax = t2; + if ( t1 > tMin ) + tMin = t1; + + if (tMax < tMin ) + return false; + } + else + { + if(-e + box.Min.Z > 0.0f || -e + box.Max.Z < 0.0f) + return false; + } + } + + t = tMin; + return true; + } } \ No newline at end of file From 067fa49cb8d0339e0a0c483df8757327cb37892e Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 19:39:17 +0100 Subject: [PATCH 35/97] Highlight currently selected actor --- Source/Mod/Editor/EditorScene.cs | 183 ++++++++++++++++++++++++++++--- Source/Scenes/World.cs | 31 +++--- 2 files changed, 184 insertions(+), 30 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 1b99bbfb..97fd08fa 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -17,6 +17,7 @@ public class EditorScene : World private Vec2 cameraRot = new(0, 0); // private readonly WorldRenderer worldRenderer = new(); + private readonly Batcher3D batch3D = new(); internal EditorScene(EntryInfo entry) : base(entry) { @@ -144,31 +145,186 @@ public override void Update() var worldPos = Vec4.Transform(eyePos, inverseView); var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); - Log.Info($"Casting at {Camera.Position} into {direction} (vs {Camera.Forward}"); if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) - { - Log.Info($"hit Point: {hit.Point} Normal: {hit.Normal} Distance: {hit.Distance} Actor: {hit.Actor} Intersections: {hit.Intersections}"); Selected = hit.Actor; - } else - { Selected = null; - } } } // Don't call base.Update, since we don't want the actors to update // Instead we manually call only the things which we want for the editor - ResolveChanges(); + + // toggle debug draw + if (Input.Keyboard.Pressed(Keys.F1)) + DebugDraw = !DebugDraw; - // worldRenderer.Update(this); + // add / remove actors + ResolveChanges(); } public override void Render(Target target) { - // target.Clear(Color.Black, 1.0f, 0, ClearMask.All); - // worldRenderer.Render(this, target); - base.Render(target); + // We copy and modify World.Render, since thats easier + + debugRndTimer.Restart(); + Camera.Target = target; + target.Clear(0x444c83, 1, 0, ClearMask.All); + + // create render state + RenderState state = new(); + { + state.Camera = Camera; + state.ModelMatrix = Matrix.Identity; + state.SunDirection = new Vec3(0, -.7f, -1).Normalized(); + state.Silhouette = false; + state.DepthCompare = DepthCompare.Less; + state.DepthMask = true; + state.VerticalFogColor = 0xdceaf0; + } + + // collect renderable objects + { + sprites.Clear(); + models.Clear(); + + // collect point shadows + foreach (var actor in All()) + { + var alpha = (actor as ICastPointShadow)!.PointShadowAlpha; + if (alpha > 0 && + Camera.Frustum.Contains(actor.WorldBounds.Conflate(actor.WorldBounds - Vec3.UnitZ * 1000))) + sprites.Add(Sprite.CreateShadowSprite(this, actor.Position + Vec3.UnitZ, alpha)); + } + + // collect models & sprites + foreach (var actor in Actors) + { + if (!Camera.Frustum.Contains(actor.WorldBounds.Inflate(1))) + continue; + + (actor as IHaveSprites)?.CollectSprites(sprites); + (actor as IHaveModels)?.CollectModels(models); + } + + // sort models by distance (for transparency) + models.Sort((a, b) => + (int)((b.Actor.Position - Camera.Position).LengthSquared() - + (a.Actor.Position - Camera.Position).LengthSquared())); + + // perp all models + foreach (var it in models) + it.Model.Prepare(); + } + + // draw the skybox first + { + var shift = new Vec3(Camera.Position.X, Camera.Position.Y, Camera.Position.Z); + for (int i = 0; i < skyboxes.Count; i++) + { + skyboxes[i].Render(Camera, + Matrix.CreateRotationZ(i * GeneralTimer * 0.01f) * + Matrix.CreateScale(1, 1, 0.5f) * + Matrix.CreateTranslation(shift), 300); + } + } + + // render solids + RenderModels(ref state, models, ModelFlags.Terrain); + + // render silhouettes + { + var it = state; + it.DepthCompare = DepthCompare.Greater; + it.DepthMask = false; + it.Silhouette = true; + RenderModels(ref it, models, ModelFlags.Silhouette); + state.Triangles = it.Triangles; + state.Calls = it.Calls; + } + + // render main models + RenderModels(ref state, models, ModelFlags.Default); + + // perform post processing effects + ApplyPostEffects(); + + // Render selected actor bounding box on-top of everything else + if (Selected is { } selected) + { + var lineColor = Color.Green; + var innerColor = Color.Green * 0.4f; + var lineThickness = 0.1f; + + batch3D.Line(selected.WorldBounds.Min, selected.WorldBounds.Min with { X = selected.WorldBounds.Max.X }, lineColor, lineThickness); + batch3D.Line(selected.WorldBounds.Min, selected.WorldBounds.Min with { Y = selected.WorldBounds.Max.Y }, lineColor, lineThickness); + batch3D.Line(selected.WorldBounds.Min, selected.WorldBounds.Min with { Z = selected.WorldBounds.Max.Z }, lineColor, lineThickness); + + batch3D.Line(selected.WorldBounds.Max, selected.WorldBounds.Max with { X = selected.WorldBounds.Min.X }, lineColor, lineThickness); + batch3D.Line(selected.WorldBounds.Max, selected.WorldBounds.Max with { Y = selected.WorldBounds.Min.Y }, lineColor, lineThickness); + batch3D.Line(selected.WorldBounds.Max, selected.WorldBounds.Max with { Z = selected.WorldBounds.Min.Z }, lineColor, lineThickness); + + batch3D.Line(selected.WorldBounds.Min with { Y = selected.WorldBounds.Max.Y }, selected.WorldBounds.Max with { X = selected.WorldBounds.Min.X }, lineColor, lineThickness); + batch3D.Line(selected.WorldBounds.Min with { Y = selected.WorldBounds.Max.Y }, selected.WorldBounds.Max with { Z = selected.WorldBounds.Min.Z }, lineColor, lineThickness); + + batch3D.Line(selected.WorldBounds.Max with { Y = selected.WorldBounds.Min.Y }, selected.WorldBounds.Min with { X = selected.WorldBounds.Max.X }, lineColor, lineThickness); + batch3D.Line(selected.WorldBounds.Max with { Y = selected.WorldBounds.Min.Y }, selected.WorldBounds.Min with { Z = selected.WorldBounds.Max.Z }, lineColor, lineThickness); + + batch3D.Line(selected.WorldBounds.Min with { X = selected.WorldBounds.Max.X }, selected.WorldBounds.Max with { Z = selected.WorldBounds.Min.Z }, lineColor, lineThickness); + batch3D.Line(selected.WorldBounds.Min with { Z = selected.WorldBounds.Max.Z }, selected.WorldBounds.Max with { X = selected.WorldBounds.Min.X }, lineColor, lineThickness); + + batch3D.Box(selected.WorldBounds.Min, selected.WorldBounds.Max, innerColor); + + batch3D.Render(ref state); + batch3D.Clear(); + } + + // render alpha threshold transparent stuff + { + state.CutoutMode = true; + RenderModels(ref state, models, ModelFlags.Cutout); + state.CutoutMode = false; + } + + // render 2d sprites + { + spriteRenderer.Render(ref state, sprites, false); + spriteRenderer.Render(ref state, sprites, true); + } + + // render partially transparent models... must be sorted etc + { + state.DepthMask = false; + RenderModels(ref state, models, ModelFlags.Transparent); + state.DepthMask = true; + } + + // ui + { + batch.SetSampler(new TextureSampler(TextureFilter.Linear, TextureWrap.ClampToEdge, TextureWrap.ClampToEdge)); + var bounds = new Rect(0, 0, target.Width, target.Height); + var font = Language.Current.SpriteFont; + + // debug + if (DebugDraw) + { + var updateMs = debugUpdTimer.Elapsed.TotalMilliseconds; + var renderMs = lastDebugRndTime.TotalMilliseconds; + var frameMs = debugFpsTimer.Elapsed.TotalMilliseconds; + var fps = (int)(1000/frameMs); + debugFpsTimer.Restart(); + + batch.Text(font, $"Draws: {state.Calls}, Tris: {state.Triangles}, Upd: {debugUpdateCount}", bounds.BottomLeft, new Vec2(0, 1), Color.Red); + batch.Text(font, $"u:{updateMs:0.00}ms | r:{renderMs:0.00}ms | f:{frameMs:0.00}ms / {fps}fps", bounds.BottomLeft - new Vec2(0, font.LineHeight), new Vec2(0, 1), Color.Red); + batch.Text(font, $"m: {Entry.Map}, c: {Entry.CheckPoint}, s: {Entry.Submap}", bounds.BottomLeft - new Vec2(0, font.LineHeight * 2), new Vec2(0, 1), Color.Red); + } + + batch.Render(Camera.Target); + batch.Clear(); + } + + lastDebugRndTime = debugRndTimer.Elapsed; + debugRndTimer.Stop(); } public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out RayHit hit, bool ignoreBackfaces = true, bool ignoreTransparent = false) @@ -197,8 +353,6 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R if (distEnter > distance) continue; - Log.Info($"intersected non-solid {actor} @ {distEnter} / {distExit}"); - hit.Intersections++; // we have a closer value @@ -232,7 +386,7 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R continue; // ignore faces that are definitely too far away - if (Utils.DistanceToPlane(point, face.Plane) > distance) + if (point.DistanceToPlane(face.Plane) > distance) continue; // check against each triangle in the face @@ -247,7 +401,6 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R if (dist > distance) continue; - Log.Info($"intersected solid {actor} @ {dist}"); hit.Intersections++; // we have a closer value diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index f1654326..237b127a 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -29,14 +29,14 @@ public enum WorldType { Game, Editor } private readonly Dictionary> recycled = []; private readonly List trackedTypes = []; - private readonly List models = []; - private readonly List sprites = []; + protected readonly List models = []; + protected readonly List sprites = []; - private Target? postTarget; - private readonly Material postMaterial = new(); - private readonly Batcher batch = new(); - private readonly List skyboxes = []; - private readonly SpriteRenderer spriteRenderer = new(); + protected Target? postTarget; + protected readonly Material postMaterial = new(); + protected readonly Batcher batch = new(); + protected readonly List skyboxes = []; + protected readonly SpriteRenderer spriteRenderer = new(); // Pause Menu, only drawn when actually paused private Menu pauseMenu = new(); @@ -63,12 +63,13 @@ private bool IsPauseEnabled } } - private readonly Stopwatch debugUpdTimer = new(); - private readonly Stopwatch debugRndTimer = new(); - private readonly Stopwatch debugFpsTimer = new(); - private TimeSpan lastDebugRndTime; - private int debugUpdateCount; - public static bool DebugDraw { get; private set; } = false; + protected readonly Stopwatch debugUpdTimer = new(); + protected readonly Stopwatch debugRndTimer = new(); + protected readonly Stopwatch debugFpsTimer = new(); + protected TimeSpan lastDebugRndTime; + protected int debugUpdateCount; + + public static bool DebugDraw { get; protected set; } = false; public Map? Map { get; private set; } @@ -987,7 +988,7 @@ public override void Render(Target target) debugRndTimer.Stop(); } - private void ApplyPostEffects() + protected void ApplyPostEffects() { // perform post processing effects if (Camera.Target != null) @@ -1021,7 +1022,7 @@ private void ApplyPostEffects() } } - private void RenderModels(ref RenderState state, List models, ModelFlags flags) + protected void RenderModels(ref RenderState state, List models, ModelFlags flags) { foreach (var it in models) { From b48c889a4cc9c6139b18b749fd0dd8c638648606 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 19:54:24 +0100 Subject: [PATCH 36/97] Render selected outline in front of everything else --- Source/Mod/Editor/EditorScene.cs | 77 +++++++++++++++++++------------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 97fd08fa..cb31af4d 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -249,36 +249,6 @@ public override void Render(Target target) // perform post processing effects ApplyPostEffects(); - // Render selected actor bounding box on-top of everything else - if (Selected is { } selected) - { - var lineColor = Color.Green; - var innerColor = Color.Green * 0.4f; - var lineThickness = 0.1f; - - batch3D.Line(selected.WorldBounds.Min, selected.WorldBounds.Min with { X = selected.WorldBounds.Max.X }, lineColor, lineThickness); - batch3D.Line(selected.WorldBounds.Min, selected.WorldBounds.Min with { Y = selected.WorldBounds.Max.Y }, lineColor, lineThickness); - batch3D.Line(selected.WorldBounds.Min, selected.WorldBounds.Min with { Z = selected.WorldBounds.Max.Z }, lineColor, lineThickness); - - batch3D.Line(selected.WorldBounds.Max, selected.WorldBounds.Max with { X = selected.WorldBounds.Min.X }, lineColor, lineThickness); - batch3D.Line(selected.WorldBounds.Max, selected.WorldBounds.Max with { Y = selected.WorldBounds.Min.Y }, lineColor, lineThickness); - batch3D.Line(selected.WorldBounds.Max, selected.WorldBounds.Max with { Z = selected.WorldBounds.Min.Z }, lineColor, lineThickness); - - batch3D.Line(selected.WorldBounds.Min with { Y = selected.WorldBounds.Max.Y }, selected.WorldBounds.Max with { X = selected.WorldBounds.Min.X }, lineColor, lineThickness); - batch3D.Line(selected.WorldBounds.Min with { Y = selected.WorldBounds.Max.Y }, selected.WorldBounds.Max with { Z = selected.WorldBounds.Min.Z }, lineColor, lineThickness); - - batch3D.Line(selected.WorldBounds.Max with { Y = selected.WorldBounds.Min.Y }, selected.WorldBounds.Min with { X = selected.WorldBounds.Max.X }, lineColor, lineThickness); - batch3D.Line(selected.WorldBounds.Max with { Y = selected.WorldBounds.Min.Y }, selected.WorldBounds.Min with { Z = selected.WorldBounds.Max.Z }, lineColor, lineThickness); - - batch3D.Line(selected.WorldBounds.Min with { X = selected.WorldBounds.Max.X }, selected.WorldBounds.Max with { Z = selected.WorldBounds.Min.Z }, lineColor, lineThickness); - batch3D.Line(selected.WorldBounds.Min with { Z = selected.WorldBounds.Max.Z }, selected.WorldBounds.Max with { X = selected.WorldBounds.Min.X }, lineColor, lineThickness); - - batch3D.Box(selected.WorldBounds.Min, selected.WorldBounds.Max, innerColor); - - batch3D.Render(ref state); - batch3D.Clear(); - } - // render alpha threshold transparent stuff { state.CutoutMode = true; @@ -298,6 +268,53 @@ public override void Render(Target target) RenderModels(ref state, models, ModelFlags.Transparent); state.DepthMask = true; } + + // Render selected actor bounding box on-top of everything else + if (Selected is { } selected) + { + var lineColor = Color.Green; + var innerColor = Color.Green * 0.4f; + var lineThickness = 0.1f; + var inflate = 0.25f; + var matrix = Matrix.CreateTranslation(selected.Position); + + var bounds = selected.LocalBounds.Inflate(inflate); + var v000 = bounds.Min; + var v100 = bounds.Min with { X = bounds.Max.X }; + var v010 = bounds.Min with { Y = bounds.Max.Y }; + var v001 = bounds.Min with { Z = bounds.Max.Z }; + var v011 = bounds.Max with { X = bounds.Min.X }; + var v101 = bounds.Max with { Y = bounds.Min.Y }; + var v110 = bounds.Max with { Z = bounds.Min.Z }; + var v111 = bounds.Max; + + batch3D.Box(v000, v111, innerColor, matrix); + batch3D.Render(ref state); + batch3D.Clear(); + + // Ignore depth for outline + state.Camera.Target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); + + batch3D.Line(v000, v100, lineColor, matrix, lineThickness); + batch3D.Line(v000, v010, lineColor, matrix, lineThickness); + batch3D.Line(v000, v001, lineColor, matrix, lineThickness); + + batch3D.Line(v111, v011, lineColor, matrix, lineThickness); + batch3D.Line(v111, v101, lineColor, matrix, lineThickness); + batch3D.Line(v111, v110, lineColor, matrix, lineThickness); + + batch3D.Line(v010, v011, lineColor, matrix, lineThickness); + batch3D.Line(v010, v110, lineColor, matrix, lineThickness); + + batch3D.Line(v101, v100, lineColor, matrix, lineThickness); + batch3D.Line(v101, v001, lineColor, matrix, lineThickness); + + batch3D.Line(v100, v110, lineColor, matrix, lineThickness); + batch3D.Line(v001, v011, lineColor, matrix, lineThickness); + + batch3D.Render(ref state); + batch3D.Clear(); + } // ui { From 5180474cd4c2b17cefa27fab583731190e8a561e Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 19:59:42 +0100 Subject: [PATCH 37/97] Scale line thickness based on distance --- Source/Mod/Editor/EditorScene.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index cb31af4d..0e6059e5 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -274,7 +274,6 @@ public override void Render(Target target) { var lineColor = Color.Green; var innerColor = Color.Green * 0.4f; - var lineThickness = 0.1f; var inflate = 0.25f; var matrix = Matrix.CreateTranslation(selected.Position); @@ -288,12 +287,15 @@ public override void Render(Target target) var v110 = bounds.Max with { Z = bounds.Min.Z }; var v111 = bounds.Max; + // Scale thickness based on distance + var lineThickness = Vec3.Distance(Camera.Position, bounds.Center) * 0.0003f; + batch3D.Box(v000, v111, innerColor, matrix); batch3D.Render(ref state); batch3D.Clear(); // Ignore depth for outline - state.Camera.Target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); + target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); batch3D.Line(v000, v100, lineColor, matrix, lineThickness); batch3D.Line(v000, v010, lineColor, matrix, lineThickness); From daedf3ad770e97098c1f6784c3d2778f87c241b1 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 20:27:07 +0100 Subject: [PATCH 38/97] Attach ActorDefinition to Actor --- Source/Actors/Actor.cs | 16 ++++++++++++- Source/Actors/Attacher.cs | 2 +- Source/Actors/SpikeBlock.cs | 23 ++++++++++++++++++- .../Mod/Editor/Definition/ActorDefinition.cs | 2 +- Source/Mod/Editor/Windows/TestWindow.cs | 16 +++++++++---- 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/Source/Actors/Actor.cs b/Source/Actors/Actor.cs index 2a75762f..8ed53666 100644 --- a/Source/Actors/Actor.cs +++ b/Source/Actors/Actor.cs @@ -1,4 +1,6 @@ +using Celeste64.Mod.Editor; + namespace Celeste64; public class Actor @@ -13,6 +15,18 @@ public class Actor protected BoundingBox worldBounds; protected bool dirty = true; + public readonly Type? DefinitionType; + public readonly ActorDefinition? _Data; + + protected Actor(Type? definitionType = null) + { + DefinitionType = definitionType; + if (DefinitionType != null) + { + _Data = Activator.CreateInstance(DefinitionType) as ActorDefinition; + } + } + /// /// Optional GroupName, used by Strawberries to check what unlocks them. Can /// be used by other stuff for whatever. @@ -38,7 +52,7 @@ public class Actor /// If we should Update while off-screen /// public bool UpdateOffScreen = false; - + public virtual BoundingBox LocalBounds { get => localBounds; diff --git a/Source/Actors/Attacher.cs b/Source/Actors/Attacher.cs index 09955a79..300ab417 100644 --- a/Source/Actors/Attacher.cs +++ b/Source/Actors/Attacher.cs @@ -1,7 +1,7 @@  namespace Celeste64; -public abstract class Attacher : Actor, IRidePlatforms +public abstract class Attacher(Type? definitionType = null) : Actor(definitionType), IRidePlatforms { public virtual Vec3 AttachNormal => -Vec3.UnitZ; public virtual Vec3 AttachOrigin => Position; diff --git a/Source/Actors/SpikeBlock.cs b/Source/Actors/SpikeBlock.cs index 2dc1bf04..fff26b02 100644 --- a/Source/Actors/SpikeBlock.cs +++ b/Source/Actors/SpikeBlock.cs @@ -1,8 +1,29 @@  +using Celeste64.Mod.Editor; + namespace Celeste64; -public class SpikeBlock : Attacher, IHaveModels +public class SpikeBlock() : Attacher(typeof(Definition)), IHaveModels { + private class Definition : ActorDefinition + { + public Vec3 Position { get; set; } + public Vec3 Rotation { get; set; } + public Vec3 Size { get; set; } + + public override void Load(World world) + { + world.Add(new SpikeBlock + { + Position = Position, + RotationXYZ = Rotation, + LocalBounds = new BoundingBox(Vec3.Zero, Size) + }); + } + } + + private Definition Data => (Definition)_Data!; + public SimpleModel? Model; public Vec3 Direction; diff --git a/Source/Mod/Editor/Definition/ActorDefinition.cs b/Source/Mod/Editor/Definition/ActorDefinition.cs index 3f22c9a8..34c0049e 100644 --- a/Source/Mod/Editor/Definition/ActorDefinition.cs +++ b/Source/Mod/Editor/Definition/ActorDefinition.cs @@ -2,5 +2,5 @@ namespace Celeste64.Mod.Editor; public abstract class ActorDefinition { - public abstract Actor Load(); + public abstract void Load(World world); } \ No newline at end of file diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs index 118021c2..2c0b5336 100644 --- a/Source/Mod/Editor/Windows/TestWindow.cs +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -1,3 +1,4 @@ +using System.Reflection; using ImGuiNET; namespace Celeste64.Mod.Editor; @@ -11,9 +12,16 @@ protected override void RenderWindow(EditorScene editor) ImGui.Text("Testing"); ImGui.Text($"Selected: {editor.Selected}"); - // if (editor.Selected is { } selected) - // { - // selected.RenderGUI(editor); - // } + if (editor.Selected is { DefinitionType: { } defType, _Data: { } data }) + { + var props = defType + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => !prop.HasAttr()); + + foreach (var prop in props) + { + ImGui.Text($" - {prop.Name}: {prop.GetValue(data)}"); + } + } } } \ No newline at end of file From 31c0d63d9dc9cc34ba2e1b6e07ff251c6de8100e Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 21:07:56 +0100 Subject: [PATCH 39/97] Mark ActorDefinition as dirty when value is changed --- Source/Actors/SpikeBlock.cs | 6 +++--- Source/Mod/Editor/Definition/ActorDefinition.cs | 2 ++ Source/Mod/Editor/EditorScene.cs | 4 ++-- Source/Mod/Editor/Windows/TestWindow.cs | 15 ++++++++++++++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Source/Actors/SpikeBlock.cs b/Source/Actors/SpikeBlock.cs index fff26b02..8dfe2878 100644 --- a/Source/Actors/SpikeBlock.cs +++ b/Source/Actors/SpikeBlock.cs @@ -7,9 +7,9 @@ public class SpikeBlock() : Attacher(typeof(Definition)), IHaveModels { private class Definition : ActorDefinition { - public Vec3 Position { get; set; } - public Vec3 Rotation { get; set; } - public Vec3 Size { get; set; } + public Vec3 Position { get; set; } = Vec3.Zero; + public Vec3 Rotation { get; set; } = Vec3.Zero; + public Vec3 Size { get; set; } = Vec3.One; public override void Load(World world) { diff --git a/Source/Mod/Editor/Definition/ActorDefinition.cs b/Source/Mod/Editor/Definition/ActorDefinition.cs index 34c0049e..0ae3f79b 100644 --- a/Source/Mod/Editor/Definition/ActorDefinition.cs +++ b/Source/Mod/Editor/Definition/ActorDefinition.cs @@ -2,5 +2,7 @@ namespace Celeste64.Mod.Editor; public abstract class ActorDefinition { + public bool Dirty = false; + public abstract void Load(World world); } \ No newline at end of file diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index 0e6059e5..fe7be13e 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -107,7 +107,7 @@ public override void Update() // Camera rotation float rotateSpeed = 16.5f * Calc.DegToRad; - if (Input.Mouse.Down(MouseButtons.Right)) + if (Input.Mouse.Down(MouseButtons.Right) && !ImGuiManager.WantCaptureMouse) { cameraRot.X += InputHelper.MouseDelta.X * rotateSpeed * Time.Delta; cameraRot.Y += InputHelper.MouseDelta.Y * rotateSpeed * Time.Delta; @@ -124,7 +124,7 @@ public override void Update() Camera.LookAt = cameraPos + forward; // Shoot ray cast for selection - if (Input.Mouse.LeftPressed) + if (Input.Mouse.LeftPressed && !ImGuiManager.WantCaptureMouse) { if (Camera.Target != null && Matrix.Invert(Camera.Projection, out var inverseProj) && diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs index 2c0b5336..3b5258f0 100644 --- a/Source/Mod/Editor/Windows/TestWindow.cs +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -20,7 +20,20 @@ protected override void RenderWindow(EditorScene editor) foreach (var prop in props) { - ImGui.Text($" - {prop.Name}: {prop.GetValue(data)}"); + switch (prop.GetValue(data)) + { + case Vec3 v: + if (ImGui.DragFloat3(prop.Name, ref v)) + { + prop.SetValue(data, v); + data.Dirty = true; + } + break; + + default: + ImGui.Text($" - {prop.Name}: {prop.GetValue(data)}"); + break; + } } } } From a400b8ef8948b023797a4300cd32d1aaeca8ef57 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Tue, 12 Mar 2024 21:14:57 +0100 Subject: [PATCH 40/97] Use oriented bounding boxes --- Source/Mod/Editor/EditorScene.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorScene.cs index fe7be13e..dba668d5 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorScene.cs @@ -275,8 +275,8 @@ public override void Render(Target target) var lineColor = Color.Green; var innerColor = Color.Green * 0.4f; var inflate = 0.25f; - var matrix = Matrix.CreateTranslation(selected.Position); + var matrix = selected.Matrix; var bounds = selected.LocalBounds.Inflate(inflate); var v000 = bounds.Min; var v100 = bounds.Min with { X = bounds.Max.X }; @@ -360,29 +360,33 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R if (!actor.WorldBounds.Intersects(box)) continue; + // Don't re-select an actor + if (actor == Selected) + continue; + // TODO: Allow selecting decorations, since they're currently one giant object if (actor is Decoration or FloatingDecoration) continue; if (actor is not Solid solid) { - if (ModUtils.RayIntersectsBox(point, direction, actor.WorldBounds, out float distEnter, out float distExit)) + if (ModUtils.RayIntersectOBB(point, direction, actor.LocalBounds, actor.Matrix, out float dist)) { // too far away - if (distEnter > distance) + if (dist > distance) continue; hit.Intersections++; // we have a closer value - if (closest.HasValue && distEnter > closest.Value) + if (closest.HasValue && dist > closest.Value) continue; // store as closest - hit.Point = point + direction * distEnter; - hit.Distance = distEnter; + hit.Point = point + direction * dist; + hit.Distance = dist; hit.Actor = actor; - closest = distEnter; + closest = dist; } continue; From 5725fe342e526aaae2d69f865c82c5746f95f632 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Wed, 13 Mar 2024 13:45:35 +0100 Subject: [PATCH 41/97] Remove unused code from editor --- Source/Game.cs | 4 +- .../Mod/Editor/Definition/EditorDefinition.cs | 53 ---- .../Definition/SerializeIgnoreAttribute.cs | 25 -- .../Editor/Definition/TestEditorDefinition.cs | 129 --------- Source/Mod/Editor/EditorMaterial.cs | 253 ------------------ Source/Mod/Editor/EditorRenderState.cs | 40 --- Source/Mod/Editor/EditorVertex.cs | 19 -- .../Editor/{EditorScene.cs => EditorWorld.cs} | 94 +++---- Source/Mod/Editor/Map/FujiMap.cs | 138 +++++----- Source/Mod/Editor/Map/FujiMapWriter.cs | 28 +- .../ActorDefinition.cs | 0 .../Serialization/PropertyCustomAttribute.cs | 28 ++ .../Serialization/PropertyIgnoreAttribute.cs | 4 + Source/Mod/Editor/Windows/EditorWindow.cs | 4 +- Source/Mod/Editor/Windows/TestWindow.cs | 21 +- Source/Mod/Editor/WorldRenderer.cs | 185 ------------- Source/Mod/ImGui/ImGuiManager.cs | 4 +- Source/Scenes/World.cs | 15 +- 18 files changed, 190 insertions(+), 854 deletions(-) delete mode 100644 Source/Mod/Editor/Definition/EditorDefinition.cs delete mode 100644 Source/Mod/Editor/Definition/SerializeIgnoreAttribute.cs delete mode 100644 Source/Mod/Editor/Definition/TestEditorDefinition.cs delete mode 100644 Source/Mod/Editor/EditorMaterial.cs delete mode 100644 Source/Mod/Editor/EditorRenderState.cs delete mode 100644 Source/Mod/Editor/EditorVertex.cs rename Source/Mod/Editor/{EditorScene.cs => EditorWorld.cs} (96%) rename Source/Mod/Editor/{Definition => Serialization}/ActorDefinition.cs (100%) create mode 100644 Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs create mode 100644 Source/Mod/Editor/Serialization/PropertyIgnoreAttribute.cs delete mode 100644 Source/Mod/Editor/WorldRenderer.cs diff --git a/Source/Game.cs b/Source/Game.cs index 5c6233c0..5bb4a556 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -371,12 +371,12 @@ internal void ReloadAssets() PerformAssetReload = true }); } - else if (scene is EditorScene editor) + else if (scene is EditorWorld editor) { Goto(new Transition() { Mode = Transition.Modes.Replace, - Scene = () => new EditorScene(editor.Entry), + Scene = () => new EditorWorld(editor.Entry), ToPause = true, ToBlack = new AngledWipe(), PerformAssetReload = true diff --git a/Source/Mod/Editor/Definition/EditorDefinition.cs b/Source/Mod/Editor/Definition/EditorDefinition.cs deleted file mode 100644 index cfd46ab2..00000000 --- a/Source/Mod/Editor/Definition/EditorDefinition.cs +++ /dev/null @@ -1,53 +0,0 @@ -using ImGuiNET; - -namespace Celeste64.Mod.Editor; - -public abstract class EditorDefinitionData -{ - // Used to link back to the EditorDefinition - // TODO: Maybe remove this?? - internal string DefinitionFullName { get; set; } - - // TODO: Figure out how to let definitions mark support for certain special properties like position / rotation / scale - public virtual Vec3 Position { get; set; } = Vector3.Zero; - public virtual Vec3 Rotation { get; set; } = Vector3.Zero; - public virtual Vec3 Scale { get; set; } = Vector3.One; -} - -public abstract class EditorDefinition -{ - /// - /// Type of the associated . - /// - public readonly Type DataType; - - /// - /// Instance of the data type associated with this definition. - /// Needs to be casted to the appropriate type inside the sub class. - /// - public EditorDefinitionData _Data { get; internal set; } - - protected EditorDefinition(Type dataType) - { - DataType = dataType; - _Data = (EditorDefinitionData)Activator.CreateInstance(DataType)!; - _Data.DefinitionFullName = GetType().FullName!; - } - - public virtual void Render(ref EditorRenderState state) { } - - public virtual void RenderGUI(EditorScene editor) - { - var pos = _Data.Position; - ImGui.DragFloat3("Position", ref pos, 0.1f); - _Data.Position = pos; - - var rot = _Data.Rotation; - ImGui.DragFloat3("Rotation", ref rot, 0.1f); - _Data.Rotation = rot; - - var scale = _Data.Scale; - ImGui.DragFloat3("Scale", ref scale, 0.1f); - _Data.Scale = scale; - } -} \ No newline at end of file diff --git a/Source/Mod/Editor/Definition/SerializeIgnoreAttribute.cs b/Source/Mod/Editor/Definition/SerializeIgnoreAttribute.cs deleted file mode 100644 index bf8e2d95..00000000 --- a/Source/Mod/Editor/Definition/SerializeIgnoreAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace Celeste64.Mod.Editor; - -[AttributeUsage(AttributeTargets.Property)] -public class SerializeIgnoreAttribute : Attribute; - -[AttributeUsage(AttributeTargets.Property)] -public class SerializeCustomAttribute(Type type) : Attribute -{ - private readonly MethodInfo m_Serialize = type.GetMethod(nameof(CustomPropertySerializer.Serialize), BindingFlags.Public | BindingFlags.Static) - ?? throw new Exception($"Custom property serializer {type} does not inherit from CustomPropertySerializer"); - private readonly MethodInfo m_Deserialize = type.GetMethod(nameof(CustomPropertySerializer.Deserialize), BindingFlags.Public | BindingFlags.Static) - ?? throw new Exception($"Custom property serializer {type} does not inherit from CustomPropertySerializer"); - - internal void Serialize(object value, BinaryWriter writer) => m_Serialize.Invoke(null, [value, writer]); - internal object Deserialize(BinaryReader reader) => m_Deserialize.Invoke(null, [reader])!; -} - -public interface CustomPropertySerializer -{ - public static abstract void Serialize(T value, BinaryWriter writer); - public static abstract T Deserialize(BinaryReader reader); -} \ No newline at end of file diff --git a/Source/Mod/Editor/Definition/TestEditorDefinition.cs b/Source/Mod/Editor/Definition/TestEditorDefinition.cs deleted file mode 100644 index b5b71ba0..00000000 --- a/Source/Mod/Editor/Definition/TestEditorDefinition.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.Text.Json.Serialization; -using ImGuiNET; -using Sledge.Formats; - -namespace Celeste64.Mod.Editor; - -[Serializable] -public class TestEditorDefinition : EditorDefinition -{ - public class DefinitionData : EditorDefinitionData - { - public Color Color { get; set; } = Color.White; - - [SerializeIgnore] - public float TestProp { get; set; } - [SerializeCustom(typeof(TestProp2Serializer))] - public float TestProp2 { get; set; } - - private class TestProp2Serializer : CustomPropertySerializer - { - public static void Serialize(float value, BinaryWriter writer) - { - Log.Info("Serializing custom prop"); - writer.Write(value * 2.0f); - } - - public static float Deserialize(BinaryReader reader) - { - Log.Info("Deserializing custom prop"); - return reader.ReadSingle() / 2.0f; - } - } - } - - // TODO: Figure out how to handle editor-only data inside definitions - private readonly Mesh mesh = new(); - private readonly EditorMaterial material = new(); - - public DefinitionData Data => (DefinitionData)_Data; - - public TestEditorDefinition() : base(typeof(DefinitionData)) - { - Vec3 size = Vec3.One * 3.0f; - Vec3 color = Vec3.One; - - mesh.SetVertices([ - // Front - /* 0*/ new(new Vec3(0, 0, 0), Vec2.Zero, color, -Vec3.UnitY), - /* 1*/ new(new Vec3(size.X, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitY), - /* 2*/ new(new Vec3(0, 0, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitY), - /* 3*/ new(new Vec3(size.X, 0, size.Z), new Vec2(size.X, size.Z), color, -Vec3.UnitY), - // Back - /* 4*/ new(new Vec3(0, size.Y, 0), Vec2.UnitX * size.X, color, Vec3.UnitY), - /* 5*/ new(new Vec3(size.X, size.Y, 0), Vec2.Zero, color, Vec3.UnitY), - /* 6*/ new(new Vec3(0, size.Y, size.Z), new Vec2(size.X, size.Z), color, Vec3.UnitY), - /* 7*/ new(new Vec3(size.X, size.Y, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitY), - // Left - /* 8*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.Y, color, -Vec3.UnitX), - /* 9*/ new(new Vec3(0, 0, size.Z), new Vec2(size.Y, size.Z), color, -Vec3.UnitX), - /*10*/ new(new Vec3(0, size.Y, 0), Vec2.Zero, color, -Vec3.UnitX), - /*11*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Z, color, -Vec3.UnitX), - // Right - /*12*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, Vec3.UnitX), - /*13*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitY * size.Z, color, Vec3.UnitX), - /*14*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitX * size.Y, color, Vec3.UnitX), - /*15*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.Y, size.Z), color, Vec3.UnitX), - // Top - /*16*/ new(new Vec3(0, 0, size.Z), Vec2.Zero, color, Vec3.UnitZ), - /*17*/ new(new Vec3(size.X, 0, size.Z), Vec2.UnitX * size.X, color, Vec3.UnitZ), - /*18*/ new(new Vec3(0, size.Y, size.Z), Vec2.UnitY * size.Y, color, Vec3.UnitZ), - /*19*/ new(new Vec3(size.X, size.Y, size.Z), new Vec2(size.X, size.Y), color, Vec3.UnitZ), - // Bottom - /*20*/ new(new Vec3(0, 0, 0), Vec2.UnitX * size.X, color, -Vec3.UnitZ), - /*21*/ new(new Vec3(size.X, 0, 0), Vec2.Zero, color, -Vec3.UnitZ), - /*22*/ new(new Vec3(0, size.Y, 0), new Vec2(size.X, size.Y), color, -Vec3.UnitZ), - /*23*/ new(new Vec3(size.X, size.Y, 0), Vec2.UnitY * size.Y, color, -Vec3.UnitZ), - ]); - mesh.SetIndices([ - // Front - 0, 1, 2, - 2, 1, 3, - // Back - 5, 4, 7, - 7, 4, 6, - // Left - 10, 8, 11, - 11, 8, 9, - // Right - 12, 14, 13, - 13, 14, 15, - // Top - 16, 17, 18, - 18, 17, 19, - // Bottom - 22, 23, 20, - 20, 23, 21 - ]); - } - - ~TestEditorDefinition() - { - mesh?.Dispose(); - } - - public override void Render(ref EditorRenderState state) - { - state.ApplyToMaterial(material, Matrix.Identity); - material.Color = Data.Color; - - new DrawCommand(state.Camera.Target, mesh, material) - { - DepthCompare = state.DepthCompare, - DepthMask = state.DepthMask, - CullMode = CullMode.Back, - }.Submit(); - state.Calls++; - state.Triangles += mesh.IndexCount / 3; - } - - public override void RenderGUI(EditorScene editor) - { - base.RenderGUI(editor); - - var col = Data.Color.ToVector3(); - // ImGui.ColorPicker3("Color", ref col, ImGuiColorEditFlags.DefaultOptions); - ImGui.ColorEdit3("Color", ref col); - Data.Color = new Color(col); - } -} \ No newline at end of file diff --git a/Source/Mod/Editor/EditorMaterial.cs b/Source/Mod/Editor/EditorMaterial.cs deleted file mode 100644 index fed7e671..00000000 --- a/Source/Mod/Editor/EditorMaterial.cs +++ /dev/null @@ -1,253 +0,0 @@ -namespace Celeste64.Mod.Editor; - -public class EditorMaterial : Material -{ - public const string MatrixUniformName = "u_mvp"; - - private Texture? texture = null; - - private Matrix matrix; - private Matrix model; - private Matrix view; - - private Color color; - private float near; - private float far; - // private bool cutout; - // private bool silhouette; - // private Color silhouetteColor; - // private float time; - private Vec3 sun; - // private Color verticalFogColor; - - private int objectID = -1; - - - public EditorMaterial(Texture? texture = null) - : base(Assets.Shaders["EditorWorld"]) - { - if (!(Shader?.Has(MatrixUniformName) ?? false)) - { - Log.Warning($"Shader '{Shader?.Name}' is missing '{MatrixUniformName}' uniform"); - } - - Texture = texture; - Color = Color.White; - } - - public Matrix MVP - { - get => matrix; - set - { - matrix = value; - if (Shader?.Has(MatrixUniformName) ?? false) - Set(MatrixUniformName, value); - } - } - public Matrix Model - { - get => model; - set - { - model = value; - if (Shader?.Has("u_model") ?? false) - Set("u_model", value); - } - } - public Matrix View - { - get => view; - set - { - view = value; - if (Shader?.Has("u_view") ?? false) - Set("u_view", value); - } - } - - public Texture? Texture - { - get => texture; - set - { - if (texture != value) - { - texture = value; - if (Shader?.Has("u_texture") ?? false) - Set("u_texture", value); - } - } - } - - public Color Color - { - get => color; - set - { - if (color != value) - { - color = value; - if (Shader?.Has("u_color") ?? false) - Set("u_color", value); - } - } - } - - public float NearPlane - { - get => near; - set - { - if (near != value) - { - near = value; - if (Shader?.Has("u_near") ?? false) - Set("u_near", value); - } - } - } - - public float FarPlane - { - get => far; - set - { - if (far != value) - { - far = value; - if (Shader?.Has("u_far") ?? false) - Set("u_far", value); - } - } - } - - // public float Effects - // { - // get => effects; - // set - // { - // if (effects != value) - // { - // effects = value; - // if (Shader?.Has("u_effects") ?? false) - // Set("u_effects", value); - // } - // } - // } - // - // public bool Cutout - // { - // get => cutout; - // set - // { - // if (cutout != value) - // { - // cutout = value; - // if (Shader?.Has("u_cutout") ?? false) - // Set("u_cutout", value ? 0.50f : 0.0f); - // } - // } - // } - // - // public bool Silhouette - // { - // get => silhouette; - // set - // { - // if (silhouette != value) - // { - // silhouette = value; - // if (Shader?.Has("u_silhouette") ?? false) - // Set("u_silhouette", value ? 1.0f : 0.0f); - // } - // } - // } - // - // public Color SilhouetteColor - // { - // get => silhouetteColor; - // set - // { - // if (silhouetteColor != value) - // { - // silhouetteColor = value; - // if (Shader?.Has("u_silhouette_color") ?? false) - // Set("u_silhouette_color", value); - // } - // } - // } - // - // public Color VerticalFogColor - // { - // get => verticalFogColor; - // set - // { - // if (verticalFogColor != value) - // { - // verticalFogColor = value; - // if (Shader?.Has("u_vertical_fog_color") ?? false) - // Set("u_vertical_fog_color", value); - // } - // } - // } - // - public Vec3 SunDirection - { - get => sun; - set - { - if (sun != value) - { - sun = value; - if (Shader?.Has("u_sun") ?? false) - Set("u_sun", value); - } - } - } - // - // public float Time - // { - // get => time; - // set - // { - // if (time != value) - // { - // time = value; - // if (Shader?.Has("u_time") ?? false) - // Set("u_time", value); - // } - // } - // } - - public int ObjectID - { - get => objectID; - set - { - if (objectID != value) - { - objectID = value; - if (Shader?.Has("u_objectID") ?? false) - Set("u_objectID", (float)value); - } - } - } - - public virtual EditorMaterial Clone() - { - var copy = new EditorMaterial(Texture); - CopyTo(copy); - copy.MVP = MVP; - copy.Model = model; - copy.Texture = Texture; - copy.Color = Color; - // copy.Silhouette = Silhouette; - // copy.SilhouetteColor = SilhouetteColor; - // copy.Time = Time; - // copy.SunDirection = SunDirection; - copy.NearPlane = NearPlane; - copy.FarPlane = FarPlane; - return copy; - } -} \ No newline at end of file diff --git a/Source/Mod/Editor/EditorRenderState.cs b/Source/Mod/Editor/EditorRenderState.cs deleted file mode 100644 index 09e9d043..00000000 --- a/Source/Mod/Editor/EditorRenderState.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Celeste64.Mod.Editor; - -public class EditorRenderState -{ - public Camera Camera; - public Matrix ModelMatrix; - - // public bool Silhouette; - public Vec3 SunDirection; - // public Color VerticalFogColor; - - public DepthCompare DepthCompare; - public bool DepthMask; - public bool CutoutMode; - - public int ObjectID; - - // Stats - public int Calls; - public int Triangles; - - public void ApplyToMaterial(EditorMaterial mat, in Matrix localTransformation) - { - if (mat.Shader == null) - return; - - mat.Model = localTransformation * ModelMatrix; - mat.View = Camera.View; - mat.MVP = mat.Model * Camera.ViewProjection; - - mat.NearPlane = Camera.NearPlane; - mat.FarPlane = Camera.FarPlane; - // mat.Silhouette = Silhouette; - // mat.Time = (float)Time.Duration.TotalSeconds; - mat.SunDirection = SunDirection; - // mat.VerticalFogColor = VerticalFogColor; - // mat.Cutout = CutoutMode; - mat.ObjectID = ObjectID; - } -} \ No newline at end of file diff --git a/Source/Mod/Editor/EditorVertex.cs b/Source/Mod/Editor/EditorVertex.cs deleted file mode 100644 index 25bc9506..00000000 --- a/Source/Mod/Editor/EditorVertex.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Celeste64.Mod.Editor; - -public readonly struct EditorVertex(Vec3 position, Vec2 texcoord, Vec3 color, Vec3 normal) : IVertex -{ - public readonly Vec3 Pos = position; - public readonly Vec2 Tex = texcoord; - public readonly Vec3 Col = color; - public readonly Vec3 Normal = normal; - - public VertexFormat Format => VertexFormat; - - private static readonly VertexFormat VertexFormat = VertexFormat.Create( - [ - new (0, VertexType.Float3, normalized: false), - new (1, VertexType.Float2, normalized: false), - new (2, VertexType.Float3, normalized: true), - new (3, VertexType.Float3, normalized: false), - ]); -} \ No newline at end of file diff --git a/Source/Mod/Editor/EditorScene.cs b/Source/Mod/Editor/EditorWorld.cs similarity index 96% rename from Source/Mod/Editor/EditorScene.cs rename to Source/Mod/Editor/EditorWorld.cs index dba668d5..02e5b699 100644 --- a/Source/Mod/Editor/EditorScene.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -3,30 +3,30 @@ namespace Celeste64.Mod.Editor; -public class EditorScene : World +public class EditorWorld : World { - private const float EditorResolutionScale = 3.0f; - + private const float EditorResolutionScale = 3.0f; + internal readonly ImGuiHandler[] Handlers = [ new TestWindow(), ]; - + public Actor? Selected { internal set; get; } = null; - + private Vec3 cameraPos = new(0, -10, 0); private Vec2 cameraRot = new(0, 0); - + // private readonly WorldRenderer worldRenderer = new(); private readonly Batcher3D batch3D = new(); - - internal EditorScene(EntryInfo entry) : base(entry) + + internal EditorWorld(EntryInfo entry) : base(entry) { Camera.NearPlane = 0.1f; Camera.FarPlane = 4000; // Increase render distance Camera.FOVMultiplier = 1.25f; - + //Definitions.Add(new TestEditorDefinition()); - + // Load the map // if (Assets.Maps[entry.Map] is not FujiMap map) // { @@ -67,7 +67,7 @@ public override void Update() Game.Scene.Entered(); return; } - + if (Input.Keyboard.Ctrl && Input.Keyboard.Pressed(Keys.S)) { // TODO: Dont actually hardcode this lol @@ -76,10 +76,10 @@ public override void Update() using var fs = File.Open(path, FileMode.Create); FujiMapWriter.WriteTo(this, fs); - + return; } - + // Camera movement var cameraForward = new Vec3( MathF.Sin(cameraRot.X), @@ -89,9 +89,9 @@ public override void Update() MathF.Sin(cameraRot.X - Calc.HalfPI), MathF.Cos(cameraRot.X - Calc.HalfPI), 0.0f); - + float moveSpeed = 250.0f; - + if (Input.Keyboard.Down(Keys.W)) cameraPos += cameraForward * moveSpeed * Time.Delta; if (Input.Keyboard.Down(Keys.S)) @@ -104,7 +104,7 @@ public override void Update() cameraPos.Z += moveSpeed * Time.Delta; if (Input.Keyboard.Down(Keys.LeftShift)) cameraPos.Z -= moveSpeed * Time.Delta; - + // Camera rotation float rotateSpeed = 16.5f * Calc.DegToRad; if (Input.Mouse.Down(MouseButtons.Right) && !ImGuiManager.WantCaptureMouse) @@ -114,7 +114,7 @@ public override void Update() cameraRot.X %= 360.0f * Calc.DegToRad; cameraRot.Y = Math.Clamp(cameraRot.Y, -89.9f * Calc.DegToRad, 89.9f * Calc.DegToRad); } - + // Update camera var forward = new Vec3( MathF.Sin(cameraRot.X) * MathF.Cos(cameraRot.Y), @@ -122,12 +122,12 @@ public override void Update() MathF.Sin(-cameraRot.Y)); Camera.Position = cameraPos; Camera.LookAt = cameraPos + forward; - + // Shoot ray cast for selection if (Input.Mouse.LeftPressed && !ImGuiManager.WantCaptureMouse) { if (Camera.Target != null && - Matrix.Invert(Camera.Projection, out var inverseProj) && + Matrix.Invert(Camera.Projection, out var inverseProj) && Matrix.Invert(Camera.View, out var inverseView)) { // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios @@ -144,29 +144,29 @@ public override void Update() eyePos.W = 0.0f; var worldPos = Vec4.Transform(eyePos, inverseView); var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); - + if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) Selected = hit.Actor; else Selected = null; } } - + // Don't call base.Update, since we don't want the actors to update // Instead we manually call only the things which we want for the editor // toggle debug draw if (Input.Keyboard.Pressed(Keys.F1)) DebugDraw = !DebugDraw; - + // add / remove actors ResolveChanges(); } - + public override void Render(Target target) { // We copy and modify World.Render, since thats easier - + debugRndTimer.Restart(); Camera.Target = target; target.Clear(0x444c83, 1, 0, ClearMask.All); @@ -192,7 +192,7 @@ public override void Render(Target target) foreach (var actor in All()) { var alpha = (actor as ICastPointShadow)!.PointShadowAlpha; - if (alpha > 0 && + if (alpha > 0 && Camera.Frustum.Contains(actor.WorldBounds.Conflate(actor.WorldBounds - Vec3.UnitZ * 1000))) sprites.Add(Sprite.CreateShadowSprite(this, actor.Position + Vec3.UnitZ, alpha)); } @@ -222,7 +222,7 @@ public override void Render(Target target) var shift = new Vec3(Camera.Position.X, Camera.Position.Y, Camera.Position.Z); for (int i = 0; i < skyboxes.Count; i++) { - skyboxes[i].Render(Camera, + skyboxes[i].Render(Camera, Matrix.CreateRotationZ(i * GeneralTimer * 0.01f) * Matrix.CreateScale(1, 1, 0.5f) * Matrix.CreateTranslation(shift), 300); @@ -245,10 +245,10 @@ public override void Render(Target target) // render main models RenderModels(ref state, models, ModelFlags.Default); - + // perform post processing effects ApplyPostEffects(); - + // render alpha threshold transparent stuff { state.CutoutMode = true; @@ -268,14 +268,14 @@ public override void Render(Target target) RenderModels(ref state, models, ModelFlags.Transparent); state.DepthMask = true; } - + // Render selected actor bounding box on-top of everything else if (Selected is { } selected) { var lineColor = Color.Green; var innerColor = Color.Green * 0.4f; var inflate = 0.25f; - + var matrix = selected.Matrix; var bounds = selected.LocalBounds.Inflate(inflate); var v000 = bounds.Min; @@ -286,17 +286,17 @@ public override void Render(Target target) var v101 = bounds.Max with { Y = bounds.Min.Y }; var v110 = bounds.Max with { Z = bounds.Min.Z }; var v111 = bounds.Max; - + // Scale thickness based on distance var lineThickness = Vec3.Distance(Camera.Position, bounds.Center) * 0.0003f; - + batch3D.Box(v000, v111, innerColor, matrix); batch3D.Render(ref state); batch3D.Clear(); - + // Ignore depth for outline target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); - + batch3D.Line(v000, v100, lineColor, matrix, lineThickness); batch3D.Line(v000, v010, lineColor, matrix, lineThickness); batch3D.Line(v000, v001, lineColor, matrix, lineThickness); @@ -313,7 +313,7 @@ public override void Render(Target target) batch3D.Line(v100, v110, lineColor, matrix, lineThickness); batch3D.Line(v001, v011, lineColor, matrix, lineThickness); - + batch3D.Render(ref state); batch3D.Clear(); } @@ -345,7 +345,7 @@ public override void Render(Target target) lastDebugRndTime = debugRndTimer.Elapsed; debugRndTimer.Stop(); } - + public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out RayHit hit, bool ignoreBackfaces = true, bool ignoreTransparent = false) { hit = default; @@ -359,15 +359,15 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R { if (!actor.WorldBounds.Intersects(box)) continue; - + // Don't re-select an actor if (actor == Selected) continue; - + // TODO: Allow selecting decorations, since they're currently one giant object if (actor is Decoration or FloatingDecoration) continue; - + if (actor is not Solid solid) { if (ModUtils.RayIntersectOBB(point, direction, actor.LocalBounds, actor.Matrix, out float dist)) @@ -375,30 +375,30 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R // too far away if (dist > distance) continue; - + hit.Intersections++; // we have a closer value if (closest.HasValue && dist > closest.Value) continue; - + // store as closest hit.Point = point + direction * dist; hit.Distance = dist; hit.Actor = actor; closest = dist; } - + continue; } - + // Special handling for solid to properly check against mesh if (!solid.Collidable || solid.Destroying) continue; if (solid.Transparent && ignoreTransparent) continue; - + var verts = solid.WorldVertices; var faces = solid.WorldFaces; @@ -415,7 +415,7 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R // check against each triangle in the face for (int i = 0; i < face.VertexCount - 2; i ++) { - if (Utils.RayIntersectsTriangle(point, direction, + if (Utils.RayIntersectsTriangle(point, direction, verts[face.VertexStart + 0], verts[face.VertexStart + i + 1], verts[face.VertexStart + i + 2], out float dist)) @@ -448,5 +448,5 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R internal class EditorHandler : ImGuiHandler { - -} \ No newline at end of file + +} diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/Map/FujiMap.cs index 22c6a7ab..8113d1b0 100644 --- a/Source/Mod/Editor/Map/FujiMap.cs +++ b/Source/Mod/Editor/Map/FujiMap.cs @@ -8,16 +8,16 @@ namespace Celeste64.Mod.Editor; /// public class FujiMap : Map { - internal readonly List DefinitionData = []; - + internal readonly List Definitions = []; + public FujiMap(string name, string virtPath, Stream stream) { Name = name; Filename = virtPath; Folder = Path.GetDirectoryName(virtPath) ?? string.Empty; - + using var reader = new BinaryReader(stream); - + try { // Header @@ -36,7 +36,7 @@ public FujiMap(string name, string virtPath, Stream stream) SnowWind = reader.ReadVec3(); Ambience = reader.ReadString(); Music = reader.ReadString(); - + // Definitions var defCount = reader.ReadInt32(); for (int i = 0; i < defCount; i++) @@ -45,43 +45,43 @@ public FujiMap(string name, string virtPath, Stream stream) var fullName = reader.ReadString(); var defDataType = Assembly.GetExecutingAssembly().GetType(fullName)!; var defData = Activator.CreateInstance(defDataType); - + Log.Info($"Def: {defData}"); - + var props = defDataType .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) - .Where(prop => !prop.HasAttr()); - + .Where(prop => !prop.HasAttr()); + foreach (var prop in props) { - if (prop.GetCustomAttribute() is { } custom) + if (prop.GetCustomAttribute() is { } custom) { prop.SetValue(defData, custom.Deserialize(reader)); continue; } - + // Primitives - if (prop.PropertyType == typeof(bool)) + if (prop.PropertyType == typeof(bool)) prop.SetValue(defData, reader.ReadBoolean()); - else if (prop.PropertyType == typeof(byte)) + else if (prop.PropertyType == typeof(byte)) prop.SetValue(defData, reader.ReadByte()); else if (prop.PropertyType == typeof(byte[])) prop.SetValue(defData, reader.ReadBytes(reader.Read7BitEncodedInt())); - else if (prop.PropertyType == typeof(char)) + else if (prop.PropertyType == typeof(char)) prop.SetValue(defData, reader.ReadChar()); - else if (prop.PropertyType == typeof(char[])) + else if (prop.PropertyType == typeof(char[])) prop.SetValue(defData, reader.ReadChars(reader.Read7BitEncodedInt())); else if (prop.PropertyType == typeof(decimal)) prop.SetValue(defData, reader.ReadDecimal()); else if (prop.PropertyType == typeof(double)) prop.SetValue(defData, reader.ReadDouble()); - else if (prop.PropertyType == typeof(float)) + else if (prop.PropertyType == typeof(float)) prop.SetValue(defData, reader.ReadSingle()); else if (prop.PropertyType == typeof(int)) prop.SetValue(defData, reader.ReadInt32()); - else if (prop.PropertyType == typeof(long)) + else if (prop.PropertyType == typeof(long)) prop.SetValue(defData, reader.ReadInt64()); - else if (prop.PropertyType == typeof(sbyte)) + else if (prop.PropertyType == typeof(sbyte)) prop.SetValue(defData, reader.ReadSByte()); else if (prop.PropertyType == typeof(short)) prop.SetValue(defData, reader.ReadInt16()); @@ -96,72 +96,72 @@ public FujiMap(string name, string virtPath, Stream stream) prop.SetValue(defData, reader.ReadVec3()); else if (prop.PropertyType == typeof(Color)) prop.SetValue(defData, reader.ReadColor()); - + Log.Info($" - {prop.Name}: {prop.GetValue(defData)}"); } - - DefinitionData.Add((EditorDefinitionData)defData!); + + Definitions.Add((ActorDefinition)defData!); } } catch (Exception ex) { isMalformed = true; readExceptionMessage = ex.Message; - + Log.Error($"Failed to load map {name}, more details below."); Log.Error(ex.ToString()); } } - + public override void Load(World world) { - foreach (var def in DefinitionData) + foreach (var def in Definitions) { // TODO: Probably move this into the definition data itself? - if (def is TestEditorDefinition.DefinitionData test) - { - var matrix = Matrix.Identity; - Vec3[] verts = [ - Vec3.Zero, - Vec3.UnitX * test.Scale.X, - Vec3.UnitX * test.Scale.X + Vec3.UnitY * test.Scale.Y, - Vec3.UnitY * test.Scale.Y, - ]; - - Log.Info(test.Color); - - var solid = new Solid - { - LocalBounds = new BoundingBox( - verts.Aggregate(Vec3.Min), - verts.Aggregate(Vec3.Max) - ), - LocalVertices = verts, - LocalFaces = - [ - new Solid.Face - { - Plane = new Plane(Vec3.UnitZ, 0.001f), - VertexStart = 0, - VertexCount = 4, - }, - ], - Position = test.Position + test.Scale / 2.0f, - }; - - solid.Model.Materials.Add(new DefaultMaterial(Assets.Textures["white"])); - solid.Model.Parts.Add(new SimpleModel.Part(0, 0, 6)); - solid.Model.Mesh.SetVertices([ - new Vertex(verts[0], Vec2.Zero, test.Color.ToVector3(), Vec3.UnitZ), - new Vertex(verts[1], Vec2.UnitX, test.Color.ToVector3(), Vec3.UnitZ), - new Vertex(verts[2], Vec2.One, test.Color.ToVector3(), Vec3.UnitZ), - new Vertex(verts[3], Vec2.UnitY, test.Color.ToVector3(), Vec3.UnitZ), - ]); - solid.Model.Mesh.SetIndices([0, 1, 2, 0, 2, 3]); - - world.Add(solid); - } + // if (def is TestEditorDefinition.DefinitionData test) + // { + // var matrix = Matrix.Identity; + // Vec3[] verts = [ + // Vec3.Zero, + // Vec3.UnitX * test.Scale.X, + // Vec3.UnitX * test.Scale.X + Vec3.UnitY * test.Scale.Y, + // Vec3.UnitY * test.Scale.Y, + // ]; + // + // Log.Info(test.Color); + // + // var solid = new Solid + // { + // LocalBounds = new BoundingBox( + // verts.Aggregate(Vec3.Min), + // verts.Aggregate(Vec3.Max) + // ), + // LocalVertices = verts, + // LocalFaces = + // [ + // new Solid.Face + // { + // Plane = new Plane(Vec3.UnitZ, 0.001f), + // VertexStart = 0, + // VertexCount = 4, + // }, + // ], + // Position = test.Position + test.Scale / 2.0f, + // }; + // + // solid.Model.Materials.Add(new DefaultMaterial(Assets.Textures["white"])); + // solid.Model.Parts.Add(new SimpleModel.Part(0, 0, 6)); + // solid.Model.Mesh.SetVertices([ + // new Vertex(verts[0], Vec2.Zero, test.Color.ToVector3(), Vec3.UnitZ), + // new Vertex(verts[1], Vec2.UnitX, test.Color.ToVector3(), Vec3.UnitZ), + // new Vertex(verts[2], Vec2.One, test.Color.ToVector3(), Vec3.UnitZ), + // new Vertex(verts[3], Vec2.UnitY, test.Color.ToVector3(), Vec3.UnitZ), + // ]); + // solid.Model.Mesh.SetIndices([0, 1, 2, 0, 2, 3]); + // + // world.Add(solid); + // } } - world.Add(new Player { Position = new Vec3(0, 0, 100) }); + world.Add(new Player { Position = new Vec3(0, 0, 100) }); } -} \ No newline at end of file +} diff --git a/Source/Mod/Editor/Map/FujiMapWriter.cs b/Source/Mod/Editor/Map/FujiMapWriter.cs index 55c622f5..ce1b3799 100644 --- a/Source/Mod/Editor/Map/FujiMapWriter.cs +++ b/Source/Mod/Editor/Map/FujiMapWriter.cs @@ -17,16 +17,16 @@ public static class FujiMapWriter /// Current version of the map format. Needs to be incremented with every change to it. /// public const byte FormatVersion = 1; - - public static void WriteTo(EditorScene editor, Stream stream) + + public static void WriteTo(EditorWorld editor, Stream stream) { using var writer = new BinaryWriter(stream); - + // Header writer.Write(FormatMagic); writer.Write(FormatVersion); - // Metadata + // Metadata // Skybox writer.Write("city"); // Snow amount @@ -37,18 +37,18 @@ public static void WriteTo(EditorScene editor, Stream stream) writer.Write("mountain"); // Music writer.Write("mus_lvl1"); - + // Definitions // writer.Write(editor.Definitions.Count); // foreach (var def in editor.Definitions) // { // Log.Info($"Def: {def}"); // writer.Write(def.DataType.FullName!); - // + // // var props = def.DataType // .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) // .Where(prop => !prop.HasAttr()); - // + // // foreach (var prop in props) // { // if (prop.GetCustomAttribute() is { } custom) @@ -56,7 +56,7 @@ public static void WriteTo(EditorScene editor, Stream stream) // custom.Serialize(prop.GetValue(def._Data)!, writer); // continue; // } - // + // // switch (prop.GetValue(def._Data)) // { // // Primitives @@ -104,7 +104,7 @@ public static void WriteTo(EditorScene editor, Stream stream) // case string v: // writer.Write(v); // break; - // + // // // Special support // case Vec2 v: // writer.Write(v); @@ -115,16 +115,16 @@ public static void WriteTo(EditorScene editor, Stream stream) // case Color v: // writer.Write(v); // break; - // + // // default: // throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def._Data}' cannot be serialized"); // } - // + // // Log.Info($" - {prop.Name}: {prop.GetValue(def._Data)}"); // } // } } - + public static void Write(this BinaryWriter writer, Vec2 value) { writer.Write(value.X); @@ -143,8 +143,8 @@ public static void Write(this BinaryWriter writer, Color value) writer.Write(value.B); writer.Write(value.A); } - + public static Vec2 ReadVec2(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle()); public static Vec3 ReadVec3(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); public static Color ReadColor(this BinaryReader reader) => new(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); -} \ No newline at end of file +} diff --git a/Source/Mod/Editor/Definition/ActorDefinition.cs b/Source/Mod/Editor/Serialization/ActorDefinition.cs similarity index 100% rename from Source/Mod/Editor/Definition/ActorDefinition.cs rename to Source/Mod/Editor/Serialization/ActorDefinition.cs diff --git a/Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs b/Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs new file mode 100644 index 00000000..294516cd --- /dev/null +++ b/Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs @@ -0,0 +1,28 @@ +using System.Reflection; + +namespace Celeste64.Mod.Editor; + +public interface ICustomProperty +{ + public static abstract void Serialize(T value, BinaryWriter writer); + public static abstract T Deserialize(BinaryReader reader); + public static abstract void RenderGui(ref T value); +} + +[AttributeUsage(AttributeTargets.Property)] +public class PropertyCustomAttribute(Type type) : Attribute +{ + private readonly MethodInfo m_Serialize = type.GetMethod(nameof(ICustomProperty.Serialize), BindingFlags.Public | BindingFlags.Static) + ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); + + private readonly MethodInfo m_Deserialize = type.GetMethod(nameof(ICustomProperty.Deserialize), BindingFlags.Public | BindingFlags.Static) + ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); + + private readonly MethodInfo m_RenderGui = type.GetMethod(nameof(ICustomProperty.Deserialize), BindingFlags.Public | BindingFlags.Static) + ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); + + internal void Serialize(object value, BinaryWriter writer) => m_Serialize.Invoke(null, [value, writer]); + internal object Deserialize(BinaryReader reader) => m_Deserialize.Invoke(null, [reader])!; + internal void RenderGui(ref object value) => m_RenderGui.Invoke(null, [value]); +} + diff --git a/Source/Mod/Editor/Serialization/PropertyIgnoreAttribute.cs b/Source/Mod/Editor/Serialization/PropertyIgnoreAttribute.cs new file mode 100644 index 00000000..a747b6e6 --- /dev/null +++ b/Source/Mod/Editor/Serialization/PropertyIgnoreAttribute.cs @@ -0,0 +1,4 @@ +namespace Celeste64.Mod.Editor; + +[AttributeUsage(AttributeTargets.Property)] +public class PropertyIgnoreAttribute : Attribute; diff --git a/Source/Mod/Editor/Windows/EditorWindow.cs b/Source/Mod/Editor/Windows/EditorWindow.cs index b07b71a4..e40c99f9 100644 --- a/Source/Mod/Editor/Windows/EditorWindow.cs +++ b/Source/Mod/Editor/Windows/EditorWindow.cs @@ -6,11 +6,11 @@ public abstract class EditorWindow : ImGuiHandler { protected abstract string Title { get; } - protected abstract void RenderWindow(EditorScene editor); + protected abstract void RenderWindow(EditorWorld editor); public sealed override void Render() { ImGui.Begin(Title); - RenderWindow((Game.Scene as EditorScene)!); + RenderWindow((Game.Scene as EditorWorld)!); ImGui.End(); } } \ No newline at end of file diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs index 3b5258f0..76af79b3 100644 --- a/Source/Mod/Editor/Windows/TestWindow.cs +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -7,19 +7,28 @@ public class TestWindow : EditorWindow { protected override string Title => "Test"; - protected override void RenderWindow(EditorScene editor) + protected override void RenderWindow(EditorWorld editor) { ImGui.Text("Testing"); ImGui.Text($"Selected: {editor.Selected}"); - + if (editor.Selected is { DefinitionType: { } defType, _Data: { } data }) { var props = defType .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(prop => !prop.HasAttr()); - + .Where(prop => !prop.HasAttr()); + foreach (var prop in props) { + if (prop.GetCustomAttribute() is { } custom) + { + var obj = prop.GetValue(data)!; + custom.RenderGui(ref obj); + prop.SetValue(data, obj); + + continue; + } + switch (prop.GetValue(data)) { case Vec3 v: @@ -29,7 +38,7 @@ protected override void RenderWindow(EditorScene editor) data.Dirty = true; } break; - + default: ImGui.Text($" - {prop.Name}: {prop.GetValue(data)}"); break; @@ -37,4 +46,4 @@ protected override void RenderWindow(EditorScene editor) } } } -} \ No newline at end of file +} diff --git a/Source/Mod/Editor/WorldRenderer.cs b/Source/Mod/Editor/WorldRenderer.cs deleted file mode 100644 index c9282af8..00000000 --- a/Source/Mod/Editor/WorldRenderer.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System.Runtime.InteropServices; -using Celeste64.Mod.Helpers; - -namespace Celeste64.Mod.Editor; - -public class WorldRenderer -{ - [StructLayout(LayoutKind.Sequential, Pack = 1)] - private readonly struct ScreenVertex(Vec2 position, Vec2 texcoord) : IVertex - { - public readonly Vec2 Pos = position; - public readonly Vec2 Tex = texcoord; - public VertexFormat Format => VertexFormat; - - private static readonly VertexFormat VertexFormat = VertexFormat.Create( - [ - new VertexFormat.Element(0, VertexType.Float2, normalized: false), - new VertexFormat.Element(1, VertexType.Float2, normalized: false), - ]); - } - - - private Camera camera = new(); - private Vec3 cameraPos = new(0, -10, 0); - private Vec2 cameraRot = new(0, 0); - - private Target? worldTarget = null; - private readonly Batcher batch = new(); - - private readonly Mesh screenMesh = new(); - private readonly Material selectionHighlightMaterial = new(Assets.Shaders["EditorEdge"]); - - public WorldRenderer() - { - camera.NearPlane = 5; - camera.FarPlane = 800; - camera.Position = new Vec3(0, -100, 0); - camera.FOVMultiplier = 1; - - screenMesh.SetVertices([ - new ScreenVertex(new Vec2(-1.0f, -1.0f), Vec2.Zero), - new ScreenVertex(new Vec2(-1.0f, 1.0f), Vec2.UnitY), - new ScreenVertex(new Vec2( 1.0f, -1.0f), Vec2.UnitX), - new ScreenVertex(new Vec2( 1.0f, 1.0f), Vec2.One), - ]); - screenMesh.SetIndices([ - 0, 1, 2, - 3, 1, 2, - ]); - } - - public void Update(EditorScene editor) - { - // Camera movement - var cameraForward = new Vec3( - MathF.Sin(cameraRot.X), - MathF.Cos(cameraRot.X), - 0.0f); - var cameraRight = new Vec3( - MathF.Sin(cameraRot.X - Calc.HalfPI), - MathF.Cos(cameraRot.X - Calc.HalfPI), - 0.0f); - - float moveSpeed = 250.0f; - - if (Input.Keyboard.Down(Keys.W)) - cameraPos += cameraForward * moveSpeed * Time.Delta; - if (Input.Keyboard.Down(Keys.S)) - cameraPos -= cameraForward * moveSpeed * Time.Delta; - if (Input.Keyboard.Down(Keys.A)) - cameraPos += cameraRight * moveSpeed * Time.Delta; - if (Input.Keyboard.Down(Keys.D)) - cameraPos -= cameraRight * moveSpeed * Time.Delta; - if (Input.Keyboard.Down(Keys.Space)) - cameraPos.Z += moveSpeed * Time.Delta; - if (Input.Keyboard.Down(Keys.LeftShift)) - cameraPos.Z -= moveSpeed * Time.Delta; - - // Camera rotation - float rotateSpeed = 15.0f * Calc.DegToRad; - if (Input.Mouse.Down(MouseButtons.Right)) - { - cameraRot.X += InputHelper.MouseDelta.X * rotateSpeed * Time.Delta; - cameraRot.Y += InputHelper.MouseDelta.Y * rotateSpeed * Time.Delta; - cameraRot.X %= 360.0f * Calc.DegToRad; - cameraRot.Y = Math.Clamp(cameraRot.Y, -89.9f * Calc.DegToRad, 89.9f * Calc.DegToRad); - } - - // Update camera - var forward = new Vec3( - MathF.Sin(cameraRot.X) * MathF.Cos(cameraRot.Y), - MathF.Cos(cameraRot.X) * MathF.Cos(cameraRot.Y), - MathF.Sin(-cameraRot.Y)); - camera.Position = cameraPos; - camera.LookAt = cameraPos + forward; - } - - public void Render(EditorScene editor, Target target) - { - // // TODO: Maybe render at a higher resolution in the editor? - // if (worldTarget == null || worldTarget.Width != target.Width || worldTarget.Height != target.Height) - // { - // worldTarget?.Dispose(); - // worldTarget = new Target(target.Width, target.Height, [TextureFormat.Color, TextureFormat.R8, TextureFormat.Depth24Stencil8]); - // } - // worldTarget.Clear(Color.Black, 1.0f, 0, ClearMask.All); - // - // camera.Target = worldTarget; - // EditorRenderState state = new(); - // { - // state.Camera = camera; - // state.ModelMatrix = Matrix.Identity; - // state.SunDirection = new Vec3(0, -.7f, -1).Normalized(); - // // state.Silhouette = false; - // state.DepthCompare = DepthCompare.Less; - // state.DepthMask = true; - // // state.VerticalFogColor = 0xdceaf0; - // } - // - // // for (int i = 0; i < editor.Definitions.Count; i++) - // // { - // // var def = editor.Definitions[i]; - // // - // // state.ObjectID = i + 1; // Use 0 as "nothing selected" - // // state.ModelMatrix = - // // Matrix.CreateScale(def._Data.Scale) * - // // Matrix.CreateRotationX(def._Data.Rotation.X * Calc.DegToRad) * - // // Matrix.CreateRotationY(def._Data.Rotation.Y * Calc.DegToRad) * - // // Matrix.CreateRotationZ(def._Data.Rotation.Z * Calc.DegToRad) * - // // Matrix.CreateTranslation(def._Data.Position); - // // - // // def.Render(ref state); - // // } - // - // // Try to select the object under the cursor - // if (!ImGuiManager.WantCaptureMouse && Input.Mouse.LeftPressed) - // { - // // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios - // var scale = Math.Min(App.WidthInPixels / (float)target.Width, App.HeightInPixels / (float)target.Height); - // var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - target.Bounds.Size / 2 * scale); - // // Convert it into a pixel position inside the target - // var pixelPos = imageRelativePos / scale; - // // Round to integer values - // pixelPos = new Vec2(MathF.Round(pixelPos.X), MathF.Round(pixelPos.Y)); - // - // if (pixelPos.X >= 0 && pixelPos.Y >= 0 && pixelPos.X < worldTarget.Width && pixelPos.Y < worldTarget.Height) - // { - // var data = new byte[worldTarget.Width * worldTarget.Height]; - // worldTarget.Attachments[1].GetData(data); - // - // // NOTE: OpenGL flips the image vertically - // byte objectID = data[worldTarget.Width * (target.Height - (int)pixelPos.Y - 1) + (int)pixelPos.X]; - // editor.Selected = objectID == 0 || (objectID - 1) >= editor.Definitions.Count - // ? null // Nothing selected - // : editor.Definitions[objectID - 1]; - // } - // } - // - // // Perform edge detection pass - // // TODO: Maybe render the outline through other solids? Probably by re-rendering the selected object? - // if (selectionHighlightMaterial.Shader?.Has("u_objectID") ?? false) - // selectionHighlightMaterial.Set("u_objectID", worldTarget.Attachments[1]); - // if (selectionHighlightMaterial.Shader?.Has("u_selectedID") ?? false) - // selectionHighlightMaterial.Set("u_selectedID", editor.Selected == null ? 0.0f : (editor.Definitions.IndexOf(editor.Selected) + 1) / 255.0f); - // - // const float EdgeSize = 2.0f; - // if (selectionHighlightMaterial.Shader?.Has("u_pixel") ?? false) - // selectionHighlightMaterial.Set("u_pixel", new Vec2(1.0f / worldTarget.Width * Game.RelativeScale * EdgeSize, 1.0f / worldTarget.Height * Game.RelativeScale * EdgeSize)); - // if (selectionHighlightMaterial.Shader?.Has("u_edge") ?? false) - // selectionHighlightMaterial.Set("u_edge", new Color(0x9999ee)); // TODO: Pick a good color - // - // new DrawCommand(worldTarget, screenMesh, selectionHighlightMaterial) - // { - // DepthMask = false, - // MeshIndexCount = 2 * 3, - // }.Submit(); - // state.Calls++; - // state.Triangles += 2; - // - // // Render to the main target - // batch.Image(worldTarget.Attachments[0], Color.White); - // batch.Render(target); - // batch.Clear(); - } -} \ No newline at end of file diff --git a/Source/Mod/ImGui/ImGuiManager.cs b/Source/Mod/ImGui/ImGuiManager.cs index 6de5dfac..a2314955 100644 --- a/Source/Mod/ImGui/ImGuiManager.cs +++ b/Source/Mod/ImGui/ImGuiManager.cs @@ -32,7 +32,7 @@ internal void UpdateHandlers() if (debugMenu.Active) debugMenu.Update(); - if (Game.Scene is EditorScene editor) + if (Game.Scene is EditorWorld editor) { foreach (var handler in editor.Handlers) { @@ -53,7 +53,7 @@ internal void RenderHandlers() if (debugMenu.Visible) debugMenu.Render(); - if (Game.Scene is EditorScene editor) + if (Game.Scene is EditorWorld editor) { foreach (var handler in editor.Handlers) { diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index b2320fab..1c4bf080 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -10,7 +10,7 @@ public class World : Scene { public enum EntryReasons { Entered, Returned, Respawned } public readonly record struct EntryInfo(string Map, string CheckPoint, bool Submap, EntryReasons Reason); - + public enum WorldType { Game, Editor } public readonly WorldType Type; @@ -69,7 +69,7 @@ private bool IsPauseEnabled protected readonly Stopwatch debugFpsTimer = new(); protected TimeSpan lastDebugRndTime; protected int debugUpdateCount; - + public static bool DebugDraw { get; protected set; } = false; public Map? Map { get; private set; } @@ -106,7 +106,7 @@ public World(EntryInfo entry) }))); Entry = entry; - Type = this is EditorScene ? WorldType.Editor : WorldType.Game; + Type = this is EditorWorld ? WorldType.Editor : WorldType.Game; var stopwatch = Stopwatch.StartNew(); @@ -210,7 +210,7 @@ public World(EntryInfo entry) Ambience = $"event:/sfx/ambience/{map.Ambience}"; } } - + // But still show the skybox if (!string.IsNullOrEmpty(map.Skybox)) { @@ -398,17 +398,17 @@ public override void Update() pauseMenu.Update(); } } - + // Toggle to editor if (Input.Keyboard.Pressed(Keys.F3)) { Game.Scene!.Exited(); Game.Instance.scenes.Pop(); - Game.Instance.scenes.Push(new EditorScene(Entry)); + Game.Instance.scenes.Push(new EditorWorld(Entry)); Game.Scene!.Entered(); return; } - + if(Panicked) { return; } // don't pour salt in wounds @@ -915,7 +915,6 @@ public override void Render(Target target) } // ui - if (Type == WorldType.Game) // Don't render UI in editor { batch.SetSampler(new TextureSampler(TextureFilter.Linear, TextureWrap.ClampToEdge, TextureWrap.ClampToEdge)); var bounds = new Rect(0, 0, target.Width, target.Height); From 1a110fb1297a7dc796fe046cf9aaaf1eac3c99ca Mon Sep 17 00:00:00 2001 From: psyGamer Date: Wed, 13 Mar 2024 14:37:57 +0100 Subject: [PATCH 42/97] Recreate actors of definition every time its changed --- Source/Actors/SpikeBlock.cs | 16 +-- Source/Mod/Editor/EditorWorld.cs | 105 ++++++++++++------ Source/Mod/Editor/Map/FujiMapWriter.cs | 2 +- .../Editor/Serialization/ActorDefinition.cs | 8 +- Source/Mod/Editor/Windows/TestWindow.cs | 22 ++-- Source/Scenes/World.cs | 14 ++- 6 files changed, 109 insertions(+), 58 deletions(-) diff --git a/Source/Actors/SpikeBlock.cs b/Source/Actors/SpikeBlock.cs index 5418a509..1e03526c 100644 --- a/Source/Actors/SpikeBlock.cs +++ b/Source/Actors/SpikeBlock.cs @@ -5,20 +5,20 @@ namespace Celeste64; public class SpikeBlock() : Attacher(typeof(Definition)), IHaveModels { - private class Definition : ActorDefinition + public class Definition : ActorDefinition { public Vec3 Position { get; set; } = Vec3.Zero; public Vec3 Rotation { get; set; } = Vec3.Zero; - public Vec3 Size { get; set; } = Vec3.One; - - public override void Load(World world) + public Vec3 Size { get; set; } = new Vec3(50.0f, 10.0f, 100.0f); + + public override Actor[] Load(World.WorldType type) { - world.Add(new SpikeBlock + return [new SpikeBlock { Position = Position, - RotationXYZ = Rotation, - LocalBounds = new BoundingBox(Vec3.Zero, Size) - }); + RotationXYZ = Rotation * Calc.DegToRad, + LocalBounds = new BoundingBox(-Size / 2.0f, Size / 2.0f) + }]; } } diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 02e5b699..6990358c 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -1,5 +1,6 @@ using System.Reflection; using Celeste64.Mod.Helpers; +using System.Collections.ObjectModel; namespace Celeste64.Mod.Editor; @@ -11,7 +12,15 @@ public class EditorWorld : World new TestWindow(), ]; - public Actor? Selected { internal set; get; } = null; + public readonly List Definitions = []; + public ReadOnlyDictionary ActorsFromDefinition => actorsFromDefinition.AsReadOnly(); + public ReadOnlyDictionary DefinitionFromActors => definitionFromActors.AsReadOnly(); + + private readonly Dictionary actorsFromDefinition = new(); + private readonly Dictionary definitionFromActors = new(); + + public ActorDefinition? Selected { internal set; get; } = null; + public Actor[] SelectedActors => Selected is not null && ActorsFromDefinition.TryGetValue(Selected, out var actors) ? actors : []; private Vec3 cameraPos = new(0, -10, 0); private Vec2 cameraRot = new(0, 0); @@ -146,12 +155,42 @@ public override void Update() var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) - Selected = hit.Actor; + Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; else Selected = null; } } + // Update actors of definitions + foreach (var def in Definitions) + { + Log.Info($"{def} {def.Dirty} {(def as SpikeBlock.Definition).Position}"); + } + foreach (var def in Definitions.Where(def => def.Dirty)) + { + Log.Info($"Recreating {def}"); + + if (actorsFromDefinition.Remove(def, out var actors)) + { + foreach (var actor in actors) + { + definitionFromActors.Remove(actor); + Destroy(actor); + } + } + + var newActors = def.Load(WorldType.Editor); + actorsFromDefinition[def] = newActors; + + foreach (var actor in newActors) + { + definitionFromActors.Add(actor, def); + Add(actor); + } + + def.Dirty = false; + } + // Don't call base.Update, since we don't want the actors to update // Instead we manually call only the things which we want for the editor @@ -269,15 +308,27 @@ public override void Render(Target target) state.DepthMask = true; } - // Render selected actor bounding box on-top of everything else - if (Selected is { } selected) + var selectedLineColor = Color.Green; + var selectedInnerColor = Color.Green * 0.4f; + const float selectedBoundsInflate = 0.25f; + + // Render selected actors bounding box + foreach (var selected in SelectedActors) { - var lineColor = Color.Green; - var innerColor = Color.Green * 0.4f; - var inflate = 0.25f; + var matrix = selected.Matrix; + var bounds = selected.LocalBounds.Inflate(selectedBoundsInflate); + batch3D.Box(bounds.Min, bounds.Max, selectedInnerColor, matrix); + } + batch3D.Render(ref state); + batch3D.Clear(); + + // Render outline on-top of everything else + target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); + foreach (var selected in SelectedActors) + { var matrix = selected.Matrix; - var bounds = selected.LocalBounds.Inflate(inflate); + var bounds = selected.LocalBounds.Inflate(selectedBoundsInflate); var v000 = bounds.Min; var v100 = bounds.Min with { X = bounds.Max.X }; var v010 = bounds.Min with { Y = bounds.Max.Y }; @@ -290,33 +341,25 @@ public override void Render(Target target) // Scale thickness based on distance var lineThickness = Vec3.Distance(Camera.Position, bounds.Center) * 0.0003f; - batch3D.Box(v000, v111, innerColor, matrix); - batch3D.Render(ref state); - batch3D.Clear(); - - // Ignore depth for outline - target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); - - batch3D.Line(v000, v100, lineColor, matrix, lineThickness); - batch3D.Line(v000, v010, lineColor, matrix, lineThickness); - batch3D.Line(v000, v001, lineColor, matrix, lineThickness); - - batch3D.Line(v111, v011, lineColor, matrix, lineThickness); - batch3D.Line(v111, v101, lineColor, matrix, lineThickness); - batch3D.Line(v111, v110, lineColor, matrix, lineThickness); + batch3D.Line(v000, v100, selectedLineColor, matrix, lineThickness); + batch3D.Line(v000, v010, selectedLineColor, matrix, lineThickness); + batch3D.Line(v000, v001, selectedLineColor, matrix, lineThickness); - batch3D.Line(v010, v011, lineColor, matrix, lineThickness); - batch3D.Line(v010, v110, lineColor, matrix, lineThickness); + batch3D.Line(v111, v011, selectedLineColor, matrix, lineThickness); + batch3D.Line(v111, v101, selectedLineColor, matrix, lineThickness); + batch3D.Line(v111, v110, selectedLineColor, matrix, lineThickness); - batch3D.Line(v101, v100, lineColor, matrix, lineThickness); - batch3D.Line(v101, v001, lineColor, matrix, lineThickness); + batch3D.Line(v010, v011, selectedLineColor, matrix, lineThickness); + batch3D.Line(v010, v110, selectedLineColor, matrix, lineThickness); - batch3D.Line(v100, v110, lineColor, matrix, lineThickness); - batch3D.Line(v001, v011, lineColor, matrix, lineThickness); + batch3D.Line(v101, v100, selectedLineColor, matrix, lineThickness); + batch3D.Line(v101, v001, selectedLineColor, matrix, lineThickness); - batch3D.Render(ref state); - batch3D.Clear(); + batch3D.Line(v100, v110, selectedLineColor, matrix, lineThickness); + batch3D.Line(v001, v011, selectedLineColor, matrix, lineThickness); } + batch3D.Render(ref state); + batch3D.Clear(); // ui { @@ -361,7 +404,7 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R continue; // Don't re-select an actor - if (actor == Selected) + if (SelectedActors.Contains(actor)) continue; // TODO: Allow selecting decorations, since they're currently one giant object diff --git a/Source/Mod/Editor/Map/FujiMapWriter.cs b/Source/Mod/Editor/Map/FujiMapWriter.cs index ce1b3799..f33fca6d 100644 --- a/Source/Mod/Editor/Map/FujiMapWriter.cs +++ b/Source/Mod/Editor/Map/FujiMapWriter.cs @@ -39,7 +39,7 @@ public static void WriteTo(EditorWorld editor, Stream stream) writer.Write("mus_lvl1"); // Definitions - // writer.Write(editor.Definitions.Count); + writer.Write(editor.Definitions.Count); // foreach (var def in editor.Definitions) // { // Log.Info($"Def: {def}"); diff --git a/Source/Mod/Editor/Serialization/ActorDefinition.cs b/Source/Mod/Editor/Serialization/ActorDefinition.cs index 0ae3f79b..cd35f048 100644 --- a/Source/Mod/Editor/Serialization/ActorDefinition.cs +++ b/Source/Mod/Editor/Serialization/ActorDefinition.cs @@ -2,7 +2,7 @@ namespace Celeste64.Mod.Editor; public abstract class ActorDefinition { - public bool Dirty = false; - - public abstract void Load(World world); -} \ No newline at end of file + public bool Dirty = true; + + public abstract Actor[] Load(World.WorldType type); +} diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs index 76af79b3..3ca93306 100644 --- a/Source/Mod/Editor/Windows/TestWindow.cs +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -10,11 +10,17 @@ public class TestWindow : EditorWindow protected override void RenderWindow(EditorWorld editor) { ImGui.Text("Testing"); + + if (ImGui.Button("Add Spikes")) + { + editor.Definitions.Add(new SpikeBlock.Definition()); + } + ImGui.Text($"Selected: {editor.Selected}"); - if (editor.Selected is { DefinitionType: { } defType, _Data: { } data }) + if (editor.Selected is { } selected) { - var props = defType + var props = selected.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(prop => !prop.HasAttr()); @@ -22,25 +28,25 @@ protected override void RenderWindow(EditorWorld editor) { if (prop.GetCustomAttribute() is { } custom) { - var obj = prop.GetValue(data)!; + var obj = prop.GetValue(selected)!; custom.RenderGui(ref obj); - prop.SetValue(data, obj); + prop.SetValue(selected, obj); continue; } - switch (prop.GetValue(data)) + switch (prop.GetValue(selected)) { case Vec3 v: if (ImGui.DragFloat3(prop.Name, ref v)) { - prop.SetValue(data, v); - data.Dirty = true; + prop.SetValue(selected, v); + selected.Dirty = true; } break; default: - ImGui.Text($" - {prop.Name}: {prop.GetValue(data)}"); + ImGui.Text($" - {prop.Name}: {prop.GetValue(selected)}"); break; } } diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 1c4bf080..750da8b6 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -227,12 +227,14 @@ public World(EntryInfo entry) } } - ModManager.Instance.OnPreMapLoaded(this, map); - - // load content - map.Load(this); - - ModManager.Instance.OnWorldLoaded(this); + // The editor handles loading itself + if (Type == WorldType.Game) + { + ModManager.Instance.OnPreMapLoaded(this, map); + // load content + map.Load(this); + ModManager.Instance.OnWorldLoaded(this); + } Log.Info($"Loaded Map '{Entry.Map}' in {stopwatch.ElapsedMilliseconds}ms"); } From 01fac4e3f1d1880e0e37d88798722dcff64de3f1 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Wed, 13 Mar 2024 14:46:21 +0100 Subject: [PATCH 43/97] Re-enable map serialization --- Source/Mod/Editor/EditorWorld.cs | 29 +---- Source/Mod/Editor/Map/FujiMap.cs | 99 +++++---------- Source/Mod/Editor/Map/FujiMapWriter.cs | 166 ++++++++++++------------- 3 files changed, 118 insertions(+), 176 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 6990358c..279d8bcc 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -34,24 +34,11 @@ internal EditorWorld(EntryInfo entry) : base(entry) Camera.FarPlane = 4000; // Increase render distance Camera.FOVMultiplier = 1.25f; - //Definitions.Add(new TestEditorDefinition()); - - // Load the map - // if (Assets.Maps[entry.Map] is not FujiMap map) - // { - // // Not a Fuji map, return to level - // Game.Instance.scenes.Pop(); - // Game.Instance.scenes.Push(new World(Entry)); - // return; - // } - // - // foreach (var defData in map.DefinitionData) - // { - // var defType = Assembly.GetExecutingAssembly().GetType(defData.DefinitionFullName)!; - // var def = (EditorDefinition)Activator.CreateInstance(defType)!; - // def._Data = defData; - // Definitions.Add(def); - // } + if (Map is not FujiMap fujiMap) + return; // We can't load sledge map (for now at least) + + // Load map + Definitions.AddRange(fujiMap.Definitions); } private float previousScale = 1.0f; @@ -162,14 +149,8 @@ public override void Update() } // Update actors of definitions - foreach (var def in Definitions) - { - Log.Info($"{def} {def.Dirty} {(def as SpikeBlock.Definition).Position}"); - } foreach (var def in Definitions.Where(def => def.Dirty)) { - Log.Info($"Recreating {def}"); - if (actorsFromDefinition.Remove(def, out var actors)) { foreach (var actor in actors) diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/Map/FujiMap.cs index 8113d1b0..201d370a 100644 --- a/Source/Mod/Editor/Map/FujiMap.cs +++ b/Source/Mod/Editor/Map/FujiMap.cs @@ -43,64 +43,64 @@ public FujiMap(string name, string virtPath, Stream stream) { // Get the definition data type, by the full name var fullName = reader.ReadString(); - var defDataType = Assembly.GetExecutingAssembly().GetType(fullName)!; - var defData = Activator.CreateInstance(defDataType); + var defType = Assembly.GetExecutingAssembly().GetType(fullName)!; + var def = Activator.CreateInstance(defType); - Log.Info($"Def: {defData}"); + Log.Info($"Reading def: {def}"); - var props = defDataType - .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) + var props = defType + .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(prop => !prop.HasAttr()); foreach (var prop in props) { if (prop.GetCustomAttribute() is { } custom) { - prop.SetValue(defData, custom.Deserialize(reader)); + prop.SetValue(def, custom.Deserialize(reader)); continue; } // Primitives if (prop.PropertyType == typeof(bool)) - prop.SetValue(defData, reader.ReadBoolean()); + prop.SetValue(def, reader.ReadBoolean()); else if (prop.PropertyType == typeof(byte)) - prop.SetValue(defData, reader.ReadByte()); + prop.SetValue(def, reader.ReadByte()); else if (prop.PropertyType == typeof(byte[])) - prop.SetValue(defData, reader.ReadBytes(reader.Read7BitEncodedInt())); + prop.SetValue(def, reader.ReadBytes(reader.Read7BitEncodedInt())); else if (prop.PropertyType == typeof(char)) - prop.SetValue(defData, reader.ReadChar()); + prop.SetValue(def, reader.ReadChar()); else if (prop.PropertyType == typeof(char[])) - prop.SetValue(defData, reader.ReadChars(reader.Read7BitEncodedInt())); + prop.SetValue(def, reader.ReadChars(reader.Read7BitEncodedInt())); else if (prop.PropertyType == typeof(decimal)) - prop.SetValue(defData, reader.ReadDecimal()); + prop.SetValue(def, reader.ReadDecimal()); else if (prop.PropertyType == typeof(double)) - prop.SetValue(defData, reader.ReadDouble()); + prop.SetValue(def, reader.ReadDouble()); else if (prop.PropertyType == typeof(float)) - prop.SetValue(defData, reader.ReadSingle()); + prop.SetValue(def, reader.ReadSingle()); else if (prop.PropertyType == typeof(int)) - prop.SetValue(defData, reader.ReadInt32()); + prop.SetValue(def, reader.ReadInt32()); else if (prop.PropertyType == typeof(long)) - prop.SetValue(defData, reader.ReadInt64()); + prop.SetValue(def, reader.ReadInt64()); else if (prop.PropertyType == typeof(sbyte)) - prop.SetValue(defData, reader.ReadSByte()); + prop.SetValue(def, reader.ReadSByte()); else if (prop.PropertyType == typeof(short)) - prop.SetValue(defData, reader.ReadInt16()); + prop.SetValue(def, reader.ReadInt16()); else if (prop.PropertyType == typeof(Half)) - prop.SetValue(defData, reader.ReadHalf()); + prop.SetValue(def, reader.ReadHalf()); else if (prop.PropertyType == typeof(string)) - prop.SetValue(defData, reader.ReadString()); + prop.SetValue(def, reader.ReadString()); // Special support else if (prop.PropertyType == typeof(Vec2)) - prop.SetValue(defData, reader.ReadVec2()); + prop.SetValue(def, reader.ReadVec2()); else if (prop.PropertyType == typeof(Vec3)) - prop.SetValue(defData, reader.ReadVec3()); + prop.SetValue(def, reader.ReadVec3()); else if (prop.PropertyType == typeof(Color)) - prop.SetValue(defData, reader.ReadColor()); + prop.SetValue(def, reader.ReadColor()); - Log.Info($" - {prop.Name}: {prop.GetValue(defData)}"); + Log.Info($" - {prop.Name}: {prop.GetValue(def)}"); } - Definitions.Add((ActorDefinition)defData!); + Definitions.Add((ActorDefinition)def!); } } catch (Exception ex) @@ -117,50 +117,11 @@ public override void Load(World world) { foreach (var def in Definitions) { - // TODO: Probably move this into the definition data itself? - // if (def is TestEditorDefinition.DefinitionData test) - // { - // var matrix = Matrix.Identity; - // Vec3[] verts = [ - // Vec3.Zero, - // Vec3.UnitX * test.Scale.X, - // Vec3.UnitX * test.Scale.X + Vec3.UnitY * test.Scale.Y, - // Vec3.UnitY * test.Scale.Y, - // ]; - // - // Log.Info(test.Color); - // - // var solid = new Solid - // { - // LocalBounds = new BoundingBox( - // verts.Aggregate(Vec3.Min), - // verts.Aggregate(Vec3.Max) - // ), - // LocalVertices = verts, - // LocalFaces = - // [ - // new Solid.Face - // { - // Plane = new Plane(Vec3.UnitZ, 0.001f), - // VertexStart = 0, - // VertexCount = 4, - // }, - // ], - // Position = test.Position + test.Scale / 2.0f, - // }; - // - // solid.Model.Materials.Add(new DefaultMaterial(Assets.Textures["white"])); - // solid.Model.Parts.Add(new SimpleModel.Part(0, 0, 6)); - // solid.Model.Mesh.SetVertices([ - // new Vertex(verts[0], Vec2.Zero, test.Color.ToVector3(), Vec3.UnitZ), - // new Vertex(verts[1], Vec2.UnitX, test.Color.ToVector3(), Vec3.UnitZ), - // new Vertex(verts[2], Vec2.One, test.Color.ToVector3(), Vec3.UnitZ), - // new Vertex(verts[3], Vec2.UnitY, test.Color.ToVector3(), Vec3.UnitZ), - // ]); - // solid.Model.Mesh.SetIndices([0, 1, 2, 0, 2, 3]); - // - // world.Add(solid); - // } + var newActors = def.Load(world.Type); + foreach (var actor in newActors) + { + world.Add(actor); + } } world.Add(new Player { Position = new Vec3(0, 0, 100) }); } diff --git a/Source/Mod/Editor/Map/FujiMapWriter.cs b/Source/Mod/Editor/Map/FujiMapWriter.cs index f33fca6d..34c7867d 100644 --- a/Source/Mod/Editor/Map/FujiMapWriter.cs +++ b/Source/Mod/Editor/Map/FujiMapWriter.cs @@ -40,89 +40,89 @@ public static void WriteTo(EditorWorld editor, Stream stream) // Definitions writer.Write(editor.Definitions.Count); - // foreach (var def in editor.Definitions) - // { - // Log.Info($"Def: {def}"); - // writer.Write(def.DataType.FullName!); - // - // var props = def.DataType - // .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) - // .Where(prop => !prop.HasAttr()); - // - // foreach (var prop in props) - // { - // if (prop.GetCustomAttribute() is { } custom) - // { - // custom.Serialize(prop.GetValue(def._Data)!, writer); - // continue; - // } - // - // switch (prop.GetValue(def._Data)) - // { - // // Primitives - // case bool v: - // writer.Write(v); - // break; - // case byte v: - // writer.Write(v); - // break; - // case byte[] v: - // writer.Write7BitEncodedInt(v.Length); - // writer.Write(v); - // break; - // case char v: - // writer.Write(v); - // break; - // case char[] v: - // writer.Write7BitEncodedInt(v.Length); - // writer.Write(v); - // break; - // case decimal v: - // writer.Write(v); - // break; - // case double v: - // writer.Write(v); - // break; - // case float v: - // writer.Write(v); - // break; - // case int v: - // writer.Write(v); - // break; - // case long v: - // writer.Write(v); - // break; - // case sbyte v: - // writer.Write(v); - // break; - // case short v: - // writer.Write(v); - // break; - // case Half v: - // writer.Write(v); - // break; - // case string v: - // writer.Write(v); - // break; - // - // // Special support - // case Vec2 v: - // writer.Write(v); - // break; - // case Vec3 v: - // writer.Write(v); - // break; - // case Color v: - // writer.Write(v); - // break; - // - // default: - // throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def._Data}' cannot be serialized"); - // } - // - // Log.Info($" - {prop.Name}: {prop.GetValue(def._Data)}"); - // } - // } + foreach (var def in editor.Definitions) + { + Log.Info($"Writing def: {def}"); + writer.Write(def.GetType().FullName!); + + var props = def.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) + .Where(prop => !prop.HasAttr()); + + foreach (var prop in props) + { + if (prop.GetCustomAttribute() is { } custom) + { + custom.Serialize(prop.GetValue(def)!, writer); + continue; + } + + switch (prop.GetValue(def)) + { + // Primitives + case bool v: + writer.Write(v); + break; + case byte v: + writer.Write(v); + break; + case byte[] v: + writer.Write7BitEncodedInt(v.Length); + writer.Write(v); + break; + case char v: + writer.Write(v); + break; + case char[] v: + writer.Write7BitEncodedInt(v.Length); + writer.Write(v); + break; + case decimal v: + writer.Write(v); + break; + case double v: + writer.Write(v); + break; + case float v: + writer.Write(v); + break; + case int v: + writer.Write(v); + break; + case long v: + writer.Write(v); + break; + case sbyte v: + writer.Write(v); + break; + case short v: + writer.Write(v); + break; + case Half v: + writer.Write(v); + break; + case string v: + writer.Write(v); + break; + + // Special support + case Vec2 v: + writer.Write(v); + break; + case Vec3 v: + writer.Write(v); + break; + case Color v: + writer.Write(v); + break; + + default: + throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def}' cannot be serialized"); + } + + Log.Info($" * {prop.Name}: {prop.GetValue(def)}"); + } + } } public static void Write(this BinaryWriter writer, Vec2 value) From 13ae81c967593a6e02fa11b7b773010e99a5bd9c Mon Sep 17 00:00:00 2001 From: psyGamer Date: Wed, 13 Mar 2024 15:01:50 +0100 Subject: [PATCH 44/97] Merge FujiMapWriter with FujiMap --- Source/Data/Assets.cs | 17 +-- Source/Mod/Editor/EditorWorld.cs | 37 ++---- Source/Mod/Editor/{Map => }/FujiMap.cs | 133 +++++++++++++++++++++- Source/Mod/Editor/Map/FujiMapWriter.cs | 150 ------------------------- Source/Mod/Helpers/BinaryExtensions.cs | 27 +++++ 5 files changed, 177 insertions(+), 187 deletions(-) rename Source/Mod/Editor/{Map => }/FujiMap.cs (58%) delete mode 100644 Source/Mod/Editor/Map/FujiMapWriter.cs create mode 100644 Source/Mod/Helpers/BinaryExtensions.cs diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index b3e70702..5029e733 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -15,8 +15,8 @@ public static class Assets public const string MapsFolder = "Maps"; public const string MapsExtensionSledge = "map"; -public const string MapsExtensionFuji = "bin"; - + public const string MapsExtensionFuji = "bin"; + public const string TexturesFolder = "Textures"; public const string TexturesExtension = "png"; @@ -157,13 +157,14 @@ public static void Load() tasks.Add(Task.Run(() => { - if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, - stream => new FujiMap(GetResourceNameFromVirt(file, MapsFolder), file, stream), out var map)) + var fullPath = mod.Filesystem is FolderModFilesystem fs ? fs.VirtToRealPath(file) : null; + if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, + stream => new FujiMap(GetResourceNameFromVirt(file, MapsFolder), file, stream, fullPath), out var map)) { maps.Add((map, mod)); } })); - } + } // load texture pngs foreach (var (file, mod) in globalFs.FindFilesInDirectoryRecursiveWithMod(TexturesFolder, TexturesExtension)) @@ -274,8 +275,8 @@ public static void Load() Levels.AddRange(levels); } - // if (mod.Filesystem != null && mod.Filesystem.TryOpenFile("Dialog.json", - // stream => JsonSerializer.Deserialize(stream, DialogLineDictContext.Default.DictionaryStringListDialogLine) ?? [], + // if (mod.Filesystem != null && mod.Filesystem.TryOpenFile("Dialog.json", + // stream => JsonSerializer.Deserialize(stream, DialogLineDictContext.Default.DictionaryStringListDialogLine) ?? [], // out var dialog)) // { // foreach (var (key, value) in dialog) @@ -454,4 +455,4 @@ internal static string GetResourceNameFromVirt(string virtPath, string folder) fragmentShader: fragment.ToString() )); } -} \ No newline at end of file +} diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 279d8bcc..6f2e4f6e 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -1,4 +1,3 @@ -using System.Reflection; using Celeste64.Mod.Helpers; using System.Collections.ObjectModel; @@ -12,33 +11,28 @@ public class EditorWorld : World new TestWindow(), ]; - public readonly List Definitions = []; + public List Definitions => Map is FujiMap fujiMap ? fujiMap.Definitions : []; public ReadOnlyDictionary ActorsFromDefinition => actorsFromDefinition.AsReadOnly(); public ReadOnlyDictionary DefinitionFromActors => definitionFromActors.AsReadOnly(); - private readonly Dictionary actorsFromDefinition = new(); - private readonly Dictionary definitionFromActors = new(); - public ActorDefinition? Selected { internal set; get; } = null; public Actor[] SelectedActors => Selected is not null && ActorsFromDefinition.TryGetValue(Selected, out var actors) ? actors : []; + private readonly Dictionary actorsFromDefinition = new(); + private readonly Dictionary definitionFromActors = new(); + private Vec3 cameraPos = new(0, -10, 0); private Vec2 cameraRot = new(0, 0); - // private readonly WorldRenderer worldRenderer = new(); private readonly Batcher3D batch3D = new(); internal EditorWorld(EntryInfo entry) : base(entry) { - Camera.NearPlane = 0.1f; + Camera.NearPlane = 0.1f; // Allow getting closer to objects Camera.FarPlane = 4000; // Increase render distance - Camera.FOVMultiplier = 1.25f; + Camera.FOVMultiplier = 1.25f; // Higher FOV feels better in the editor - if (Map is not FujiMap fujiMap) - return; // We can't load sledge map (for now at least) - - // Load map - Definitions.AddRange(fujiMap.Definitions); + // Map gets implicitly loaded, since our Definitions are taken directly from it } private float previousScale = 1.0f; @@ -64,14 +58,10 @@ public override void Update() return; } - if (Input.Keyboard.Ctrl && Input.Keyboard.Pressed(Keys.S)) + if (Input.Keyboard.Ctrl && Input.Keyboard.Pressed(Keys.S) && Map is FujiMap { FullPath: { } fullPath } fujiMap) { - // TODO: Dont actually hardcode this lol - var path = "/media/Storage/Code/C#/Fuji/Mods/Template-BasicCassetteLevel/Maps/test.bin"; - Log.Info($"Saving map to '{path}'"); - - using var fs = File.Open(path, FileMode.Create); - FujiMapWriter.WriteTo(this, fs); + Log.Info($"Saving map to '{fullPath}'"); + fujiMap.SaveToFile(); return; } @@ -185,7 +175,7 @@ public override void Update() public override void Render(Target target) { - // We copy and modify World.Render, since thats easier + // We copy and modify World.Render, since that's easier debugRndTimer.Restart(); Camera.Target = target; @@ -469,8 +459,3 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R return closest.HasValue; } } - -internal class EditorHandler : ImGuiHandler -{ - -} diff --git a/Source/Mod/Editor/Map/FujiMap.cs b/Source/Mod/Editor/FujiMap.cs similarity index 58% rename from Source/Mod/Editor/Map/FujiMap.cs rename to Source/Mod/Editor/FujiMap.cs index 201d370a..07b038ef 100644 --- a/Source/Mod/Editor/Map/FujiMap.cs +++ b/Source/Mod/Editor/FujiMap.cs @@ -8,13 +8,25 @@ namespace Celeste64.Mod.Editor; /// public class FujiMap : Map { - internal readonly List Definitions = []; + /// + /// Magic 4 bytes at the start of the file, to indicate the format. + /// + public static readonly byte[] FormatMagic = [(byte)'F', (byte)'U', (byte)'J', (byte)'I']; - public FujiMap(string name, string virtPath, Stream stream) + /// + /// Current version of the map format. Needs to be incremented with every change to it. + /// + public const byte FormatVersion = 1; + + public readonly string? FullPath; + public readonly List Definitions = []; + + public FujiMap(string name, string virtPath, Stream stream, string? fullPath) { Name = name; Filename = virtPath; Folder = Path.GetDirectoryName(virtPath) ?? string.Empty; + FullPath = fullPath; using var reader = new BinaryReader(stream); @@ -22,7 +34,7 @@ public FujiMap(string name, string virtPath, Stream stream) { // Header var magic = reader.ReadBytes(4); - if (!magic.SequenceEqual(FujiMapWriter.FormatMagic)) + if (!magic.SequenceEqual(FormatMagic)) { isMalformed = true; readExceptionMessage = $"Invalid magic bytes! Found '{(char)magic[0]}{(char)magic[1]}{(char)magic[2]}{(char)magic[3]}'"; @@ -113,6 +125,121 @@ public FujiMap(string name, string virtPath, Stream stream) } } + public void SaveToFile() + { + // Only allow saving when the mod is a folder + if (FullPath == null) + { + Log.Warning("Tried to save zipped map file"); + return; + } + + using var fs = File.Open(FullPath, FileMode.Create); + using var writer = new BinaryWriter(fs); + + // Header + writer.Write(FormatMagic); + writer.Write(FormatVersion); + + // Metadata + // Skybox + writer.Write("city"); + // Snow amount + writer.Write(1.0f); + // Snow direction + writer.Write(new Vec3(0.0f, 0.0f, -1.0f)); + // Ambience + writer.Write("mountain"); + // Music + writer.Write("mus_lvl1"); + + // Definitions + writer.Write(Definitions.Count); + foreach (var def in Definitions) + { + Log.Info($"Writing def: {def}"); + writer.Write(def.GetType().FullName!); + + var props = def.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) + .Where(prop => !prop.HasAttr()); + + foreach (var prop in props) + { + if (prop.GetCustomAttribute() is { } custom) + { + custom.Serialize(prop.GetValue(def)!, writer); + continue; + } + + switch (prop.GetValue(def)) + { + // Primitives + case bool v: + writer.Write(v); + break; + case byte v: + writer.Write(v); + break; + case byte[] v: + writer.Write7BitEncodedInt(v.Length); + writer.Write(v); + break; + case char v: + writer.Write(v); + break; + case char[] v: + writer.Write7BitEncodedInt(v.Length); + writer.Write(v); + break; + case decimal v: + writer.Write(v); + break; + case double v: + writer.Write(v); + break; + case float v: + writer.Write(v); + break; + case int v: + writer.Write(v); + break; + case long v: + writer.Write(v); + break; + case sbyte v: + writer.Write(v); + break; + case short v: + writer.Write(v); + break; + case Half v: + writer.Write(v); + break; + case string v: + writer.Write(v); + break; + + // Special support + case Vec2 v: + writer.Write(v); + break; + case Vec3 v: + writer.Write(v); + break; + case Color v: + writer.Write(v); + break; + + default: + throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def}' cannot be serialized"); + } + + Log.Info($" * {prop.Name}: {prop.GetValue(def)}"); + } + } + } + public override void Load(World world) { foreach (var def in Definitions) diff --git a/Source/Mod/Editor/Map/FujiMapWriter.cs b/Source/Mod/Editor/Map/FujiMapWriter.cs deleted file mode 100644 index 34c7867d..00000000 --- a/Source/Mod/Editor/Map/FujiMapWriter.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System.Reflection; -using System.Reflection.PortableExecutable; -using System.Runtime.InteropServices; -using System.Runtime.Serialization.Formatters.Binary; -using MonoMod.Utils; - -namespace Celeste64.Mod.Editor; - -public static class FujiMapWriter -{ - /// - /// Magic 4 bytes at the start of the file, to indicate the format. - /// - public static readonly byte[] FormatMagic = [(byte)'F', (byte)'U', (byte)'J', (byte)'I']; - - /// - /// Current version of the map format. Needs to be incremented with every change to it. - /// - public const byte FormatVersion = 1; - - public static void WriteTo(EditorWorld editor, Stream stream) - { - using var writer = new BinaryWriter(stream); - - // Header - writer.Write(FormatMagic); - writer.Write(FormatVersion); - - // Metadata - // Skybox - writer.Write("city"); - // Snow amount - writer.Write(1.0f); - // Snow direction - writer.Write(new Vec3(0.0f, 0.0f, -1.0f)); - // Ambience - writer.Write("mountain"); - // Music - writer.Write("mus_lvl1"); - - // Definitions - writer.Write(editor.Definitions.Count); - foreach (var def in editor.Definitions) - { - Log.Info($"Writing def: {def}"); - writer.Write(def.GetType().FullName!); - - var props = def.GetType() - .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) - .Where(prop => !prop.HasAttr()); - - foreach (var prop in props) - { - if (prop.GetCustomAttribute() is { } custom) - { - custom.Serialize(prop.GetValue(def)!, writer); - continue; - } - - switch (prop.GetValue(def)) - { - // Primitives - case bool v: - writer.Write(v); - break; - case byte v: - writer.Write(v); - break; - case byte[] v: - writer.Write7BitEncodedInt(v.Length); - writer.Write(v); - break; - case char v: - writer.Write(v); - break; - case char[] v: - writer.Write7BitEncodedInt(v.Length); - writer.Write(v); - break; - case decimal v: - writer.Write(v); - break; - case double v: - writer.Write(v); - break; - case float v: - writer.Write(v); - break; - case int v: - writer.Write(v); - break; - case long v: - writer.Write(v); - break; - case sbyte v: - writer.Write(v); - break; - case short v: - writer.Write(v); - break; - case Half v: - writer.Write(v); - break; - case string v: - writer.Write(v); - break; - - // Special support - case Vec2 v: - writer.Write(v); - break; - case Vec3 v: - writer.Write(v); - break; - case Color v: - writer.Write(v); - break; - - default: - throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def}' cannot be serialized"); - } - - Log.Info($" * {prop.Name}: {prop.GetValue(def)}"); - } - } - } - - public static void Write(this BinaryWriter writer, Vec2 value) - { - writer.Write(value.X); - writer.Write(value.Y); - } - public static void Write(this BinaryWriter writer, Vec3 value) - { - writer.Write(value.X); - writer.Write(value.Y); - writer.Write(value.Z); - } - public static void Write(this BinaryWriter writer, Color value) - { - writer.Write(value.R); - writer.Write(value.G); - writer.Write(value.B); - writer.Write(value.A); - } - - public static Vec2 ReadVec2(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle()); - public static Vec3 ReadVec3(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - public static Color ReadColor(this BinaryReader reader) => new(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); -} diff --git a/Source/Mod/Helpers/BinaryExtensions.cs b/Source/Mod/Helpers/BinaryExtensions.cs new file mode 100644 index 00000000..7f03084e --- /dev/null +++ b/Source/Mod/Helpers/BinaryExtensions.cs @@ -0,0 +1,27 @@ +namespace Celeste64.Mod; + +public static class BinaryExtensions +{ + public static void Write(this BinaryWriter writer, Vec2 value) + { + writer.Write(value.X); + writer.Write(value.Y); + } + public static void Write(this BinaryWriter writer, Vec3 value) + { + writer.Write(value.X); + writer.Write(value.Y); + writer.Write(value.Z); + } + public static void Write(this BinaryWriter writer, Color value) + { + writer.Write(value.R); + writer.Write(value.G); + writer.Write(value.B); + writer.Write(value.A); + } + + public static Vec2 ReadVec2(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle()); + public static Vec3 ReadVec3(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + public static Color ReadColor(this BinaryReader reader) => new(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); +} From 4e9c97424af182d57d4961a15c2edaa4e756c9f5 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Wed, 13 Mar 2024 15:10:40 +0100 Subject: [PATCH 45/97] Draw world bounds of selected actors --- Source/Mod/Editor/EditorWorld.cs | 65 +++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 6f2e4f6e..5a1e5998 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -279,8 +279,9 @@ public override void Render(Target target) state.DepthMask = true; } - var selectedLineColor = Color.Green; - var selectedInnerColor = Color.Green * 0.4f; + var selectedLocalBoundsFillColor = Color.Green * 0.4f; + var selectedLocalBoundsOutlineColor = Color.Green; + var selectedWorldBoundsOutlineColor = Color.Blue; const float selectedBoundsInflate = 0.25f; // Render selected actors bounding box @@ -289,7 +290,7 @@ public override void Render(Target target) var matrix = selected.Matrix; var bounds = selected.LocalBounds.Inflate(selectedBoundsInflate); - batch3D.Box(bounds.Min, bounds.Max, selectedInnerColor, matrix); + batch3D.Box(bounds.Min, bounds.Max, selectedLocalBoundsFillColor, matrix); } batch3D.Render(ref state); batch3D.Clear(); @@ -298,6 +299,10 @@ public override void Render(Target target) target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); foreach (var selected in SelectedActors) { + // Scale thickness based on distance + var lineThickness = Vec3.Distance(Camera.Position, selected.WorldBounds.Center) * 0.001f; + + // Transformed local bounds var matrix = selected.Matrix; var bounds = selected.LocalBounds.Inflate(selectedBoundsInflate); var v000 = bounds.Min; @@ -309,25 +314,51 @@ public override void Render(Target target) var v110 = bounds.Max with { Z = bounds.Min.Z }; var v111 = bounds.Max; - // Scale thickness based on distance - var lineThickness = Vec3.Distance(Camera.Position, bounds.Center) * 0.0003f; + batch3D.Line(v000, v100, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v000, v010, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v000, v001, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v111, v011, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v111, v101, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v111, v110, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v010, v011, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v010, v110, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v101, v100, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v101, v001, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v100, v110, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v001, v011, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + // World bounds + matrix = Matrix.Identity; + bounds = selected.WorldBounds.Inflate(selectedBoundsInflate); + v000 = bounds.Min; + v100 = bounds.Min with { X = bounds.Max.X }; + v010 = bounds.Min with { Y = bounds.Max.Y }; + v001 = bounds.Min with { Z = bounds.Max.Z }; + v011 = bounds.Max with { X = bounds.Min.X }; + v101 = bounds.Max with { Y = bounds.Min.Y }; + v110 = bounds.Max with { Z = bounds.Min.Z }; + v111 = bounds.Max; - batch3D.Line(v000, v100, selectedLineColor, matrix, lineThickness); - batch3D.Line(v000, v010, selectedLineColor, matrix, lineThickness); - batch3D.Line(v000, v001, selectedLineColor, matrix, lineThickness); + batch3D.Line(v000, v100, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v000, v010, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v000, v001, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v111, v011, selectedLineColor, matrix, lineThickness); - batch3D.Line(v111, v101, selectedLineColor, matrix, lineThickness); - batch3D.Line(v111, v110, selectedLineColor, matrix, lineThickness); + batch3D.Line(v111, v011, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v111, v101, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v111, v110, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v010, v011, selectedLineColor, matrix, lineThickness); - batch3D.Line(v010, v110, selectedLineColor, matrix, lineThickness); + batch3D.Line(v010, v011, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v010, v110, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v101, v100, selectedLineColor, matrix, lineThickness); - batch3D.Line(v101, v001, selectedLineColor, matrix, lineThickness); + batch3D.Line(v101, v100, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v101, v001, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v100, v110, selectedLineColor, matrix, lineThickness); - batch3D.Line(v001, v011, selectedLineColor, matrix, lineThickness); + batch3D.Line(v100, v110, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v001, v011, selectedWorldBoundsOutlineColor, matrix, lineThickness); } batch3D.Render(ref state); batch3D.Clear(); From a2d8454bed09f9f56b8d8e2784f2958027848a84 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 17:04:10 +0100 Subject: [PATCH 46/97] Run format pass over editor --- Source/Actors/Actor.cs | 6 +- Source/Data/Assets.cs | 6 +- Source/Data/Map.cs | 6 +- Source/Data/SledgeMap.cs | 142 +-- Source/Mod/Editor/EditorWorld.cs | 12 +- Source/Mod/Editor/FujiMap.cs | 72 +- .../Serialization/PropertyCustomAttribute.cs | 6 +- Source/Mod/Editor/Windows/EditorWindow.cs | 2 +- Source/Mod/Editor/Windows/TestWindow.cs | 2 +- Source/Mod/Helpers/Batcher3D.cs | 986 +++++++++--------- Source/Mod/Helpers/BinaryExtensions.cs | 40 +- Source/Mod/Helpers/InputHelper.cs | 8 +- Source/Mod/Helpers/ModUtils.cs | 58 +- Source/Mod/ImGui/FujiDebugMenu.cs | 11 +- Source/Mod/ImGui/ImGuiManager.cs | 22 +- Source/Scenes/World.cs | 7 +- 16 files changed, 697 insertions(+), 689 deletions(-) diff --git a/Source/Actors/Actor.cs b/Source/Actors/Actor.cs index 85979834..6d181a75 100644 --- a/Source/Actors/Actor.cs +++ b/Source/Actors/Actor.cs @@ -16,7 +16,7 @@ public class Actor public readonly Type? DefinitionType; public readonly ActorDefinition? _Data; - + protected Actor(Type? definitionType = null) { DefinitionType = definitionType; @@ -25,7 +25,7 @@ protected Actor(Type? definitionType = null) _Data = Activator.CreateInstance(DefinitionType) as ActorDefinition; } } - + /// /// Optional GroupName, used by Strawberries to check what unlocks them. Can /// be used by other stuff for whatever. @@ -51,7 +51,7 @@ protected Actor(Type? definitionType = null) /// If we should Update while off-screen /// public bool UpdateOffScreen = false; - + public virtual BoundingBox LocalBounds { get => localBounds; diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 9d915a1e..46af8a57 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -1,10 +1,10 @@ using Celeste64.Mod; +using Celeste64.Mod; +using Celeste64.Mod.Editor; using System.Collections.Concurrent; using System.Diagnostics; using System.Text; using System.Text.Json; -using Celeste64.Mod; -using Celeste64.Mod.Editor; namespace Celeste64; @@ -154,7 +154,7 @@ public static void Load() { var fullPath = mod.Filesystem is FolderModFilesystem fs ? fs.VirtToRealPath(file) : null; if (mod.Filesystem != null && mod.Filesystem.TryOpenFile(file, - stream => new FujiMap(GetResourceNameFromVirt(file, MapsFolder), file, stream, fullPath), out var map)) + stream => new FujiMap(GetResourceNameFromVirt(file, MapsFolder), file, stream, fullPath), out var map)) { maps.Add((map, mod)); } diff --git a/Source/Data/Map.cs b/Source/Data/Map.cs index dce45151..aaa8d34e 100644 --- a/Source/Data/Map.cs +++ b/Source/Data/Map.cs @@ -8,7 +8,7 @@ public abstract class Map { public bool isMalformed { get; init; } = false; public string? readExceptionMessage { get; init; } = null; - + public string Name { get; init; } public string Filename { get; init; } public string Folder { get; init; } @@ -18,6 +18,6 @@ public abstract class Map public Vec3 SnowWind { get; init; } public string? Music { get; init; } public string? Ambience { get; init; } - + public abstract void Load(World world); -} \ No newline at end of file +} diff --git a/Source/Data/SledgeMap.cs b/Source/Data/SledgeMap.cs index 422362cc..a0351d85 100644 --- a/Source/Data/SledgeMap.cs +++ b/Source/Data/SledgeMap.cs @@ -1,14 +1,14 @@ -using System.Runtime.InteropServices; +using Celeste64.Mod; using Sledge.Formats.Map.Formats; -using SledgeMapObject = Sledge.Formats.Map.Objects.MapObject; -using SledgeSolid = Sledge.Formats.Map.Objects.Solid; -using SledgeEntity = Sledge.Formats.Map.Objects.Entity; -using SledgeFace = Sledge.Formats.Map.Objects.Face; -using System.Runtime.CompilerServices; -using Celeste64.Mod; -using System.Globalization; using Sledge.Formats.Map.Objects; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Path = System.IO.Path; +using SledgeEntity = Sledge.Formats.Map.Objects.Entity; +using SledgeFace = Sledge.Formats.Map.Objects.Face; +using SledgeMapObject = Sledge.Formats.Map.Objects.MapObject; +using SledgeSolid = Sledge.Formats.Map.Objects.Solid; namespace Celeste64; @@ -29,7 +29,7 @@ public class ActorFactory(Func create) public readonly MapFile? Data; - public static readonly Dictionary ActorFactories = new() + public static readonly Dictionary ActorFactories = new() { ["Strawberry"] = new((map, entity) => { @@ -52,13 +52,15 @@ public class ActorFactory(Func create) return new MovingBlock( entity.GetIntProperty("slow", 0) > 0, map.FindTargetNodeFromParam(entity, "target")); - }) { IsSolidGeometry = true }, + }) + { IsSolidGeometry = true }, ["GateBlock"] = new((map, entity) => new GateBlock(map.FindTargetNodeFromParam(entity, "target"))) { IsSolidGeometry = true }, ["TrafficBlock"] = new((map, entity) => new TrafficBlock(map.FindTargetNodeFromParam(entity, "target"))) { IsSolidGeometry = true }, ["FallingBlock"] = new((map, entity) => { return new FallingBlock() { Secret = (entity.GetIntProperty("secret", 0) != 0) }; - }) { IsSolidGeometry = true }, + }) + { IsSolidGeometry = true }, ["FloatyBlock"] = new((map, entity) => new FloatyBlock()) { IsSolidGeometry = true }, ["DeathBlock"] = new((map, entity) => new DeathBlock()) { UseSolidsAsBounds = true }, ["SpikeBlock"] = new((map, entity) => new SpikeBlock()) { UseSolidsAsBounds = true }, @@ -84,7 +86,8 @@ public class ActorFactory(Func create) entity.GetIntProperty("bounces", 0) != 0, entity.GetIntProperty("transparent", 0) != 0, entity.GetIntProperty("secret", 0) != 0); - }) { IsSolidGeometry = true }, + }) + { IsSolidGeometry = true }, ["CassetteBlock"] = new((map, entity) => new CassetteBlock(entity.GetIntProperty("startOn", 1) != 0)) { IsSolidGeometry = true }, ["NonClimbableBlock"] = new((map, entity) => new NonClimbableBlock()) { IsSolidGeometry = true }, ["DoubleDashPuzzleBlock"] = new((map, entity) => new DoubleDashPuzzleBlock()) { IsSolidGeometry = true }, @@ -92,7 +95,8 @@ public class ActorFactory(Func create) ["Fog"] = new((map, entity) => new FogRing(entity)), ["FixedCamera"] = new((map, entity) => new FixedCamera(map.FindTargetNodeFromParam(entity, "target"))) { UseSolidsAsBounds = true }, ["IntroCar"] = new((map, entity) => new IntroCar(entity.GetFloatProperty("scale", 6))), - ["SolidMesh"] = new((map, entity) => { + ["SolidMesh"] = new((map, entity) => + { if (Assets.Models.TryGetValueFromFullPath(entity.GetStringProperty("model", string.Empty), out var model)) { return new SolidMesh(model, entity.GetFloatProperty("scale", 6)); @@ -124,10 +128,11 @@ public SledgeMap(string name, string virtPath, Stream stream) Folder = Path.GetDirectoryName(virtPath) ?? string.Empty; var format = new QuakeMapFormat(); - try + try { Data = format.Read(stream); - } catch (Exception e) + } + catch (Exception e) { Data = null; @@ -139,7 +144,7 @@ public SledgeMap(string name, string virtPath, Stream stream) Log.Error(e.ToString()); } - if(Data != null) + if (Data != null) { Skybox = Data.Worldspawn.GetStringProperty("skybox", "city"); SnowAmount = Data.Worldspawn.GetFloatProperty("snowAmount", 1); @@ -189,7 +194,7 @@ void QueryObjects(SledgeMapObject obj) } } - if(Data != null) + if (Data != null) { QueryObjects(Data.Worldspawn); } @@ -206,10 +211,10 @@ void QueryObjects(SledgeMapObject obj) { var rng = new Rng(); var n = floatingDecorations.Count; - while (n > 1) + while (n > 1) { int k = rng.Int(n--); - (floatingDecorations[k], floatingDecorations[n]) = + (floatingDecorations[k], floatingDecorations[n]) = (floatingDecorations[n], floatingDecorations[k]); } } @@ -244,24 +249,24 @@ public override void Load(World world) // split into a grid so we don't have one massive solid var chunk = new Vec3(1000, 1000, 1000); - for (int x = 0; x < bounds.Size.X / chunk.X; x ++) - for (int y = 0; y < bounds.Size.Y / chunk.Y; y ++) - for (int z = 0; z < bounds.Size.Z / chunk.Z; z ++) - { - var box = new BoundingBox(bounds.Min, bounds.Min + chunk * new Vec3(1 + x, 1 + y, 1 + z)); - - for (int i = available.Count - 1; i >= 0; i --) - if (box.Contains(available[i].Bounds.Center)) + for (int x = 0; x < bounds.Size.X / chunk.X; x++) + for (int y = 0; y < bounds.Size.Y / chunk.Y; y++) + for (int z = 0; z < bounds.Size.Z / chunk.Z; z++) { - combined.Add(available[i].Solid); - available.RemoveAt(i); + var box = new BoundingBox(bounds.Min, bounds.Min + chunk * new Vec3(1 + x, 1 + y, 1 + z)); + + for (int i = available.Count - 1; i >= 0; i--) + if (box.Contains(available[i].Bounds.Center)) + { + combined.Add(available[i].Solid); + available.RemoveAt(i); + } + + var result = new Solid(); + GenerateSolid(result, combined); + world.Add(result); + combined.Clear(); } - - var result = new Solid(); - GenerateSolid(result, combined); - world.Add(result); - combined.Clear(); - } } // load all decorations into one big model *shrug* @@ -285,7 +290,7 @@ public override void Load(World world) var decoration = new FloatingDecoration(); var to = Math.Min(from + floatingDecorations.Count / 4, floatingDecorations.Count); - for (int j = from; j < to; j ++) + for (int j = from; j < to; j++) CollectSolids(floatingDecorations[j], decorations); from = to; @@ -321,7 +326,7 @@ private void LoadActor(World world, SledgeEntity entity) // spawns ther player if the world entry is this checkpoint // OR the world entry has no checkpoint and we're the start // OR the world entry checkpoint is misconfigured and we're the start - var spawnsPlayer = + var spawnsPlayer = (world.Entry.CheckPoint == name) || (string.IsNullOrEmpty(world.Entry.CheckPoint) && name == StartCheckpoint) || (!Checkpoints.Contains(world.Entry.CheckPoint) && name == StartCheckpoint); @@ -340,10 +345,10 @@ private void LoadActor(World world, SledgeEntity entity) HandleActorCreation(world, entity, it, factory); } } - + public void HandleActorCreation(World world, SledgeEntity entity, Actor it, ActorFactory? factory) { - if(it is Solid solid) + if (it is Solid solid) { if ((factory?.IsSolidGeometry ?? false)) { @@ -359,8 +364,8 @@ public void HandleActorCreation(World world, SledgeEntity entity, Actor it, Acto if (entity.Properties.ContainsKey("origin")) it.Position = Vec3.Transform(entity.GetVectorProperty("origin", Vec3.Zero), baseTransform); - if (entity.Properties.ContainsKey("_tb_group") && - groupNames.TryGetValue(entity.GetIntProperty("_tb_group", -1), out var groupName)) + if (entity.Properties.ContainsKey("_tb_group") && + groupNames.TryGetValue(entity.GetIntProperty("_tb_group", -1), out var groupName)) it.GroupName = groupName; @@ -404,7 +409,7 @@ public void HandleActorCreation(World world, SledgeEntity entity, Actor it, Acto bounds.Max -= it.Position; it.LocalBounds = bounds; } - + world.Add(it); } @@ -418,7 +423,7 @@ public void HandleActorCreation(World world, SledgeEntity entity, Actor it, Acto foreach (var child in obj.Children) { - if (FindTargetEntity(child, targetName) is {} it) + if (FindTargetEntity(child, targetName) is { } it) return it; } @@ -427,7 +432,8 @@ public void HandleActorCreation(World world, SledgeEntity entity, Actor it, Acto public bool FindTargetNode(string name, out Vec3 pos) { - if(Data == null) { + if (Data == null) + { pos = Vec3.Zero; return false; } @@ -481,7 +487,7 @@ private BoundingBox CalculateSolidBounds(SledgeSolid sol, in Matrix? transform = private BoundingBox CalculateSolidBounds(List collection, in Matrix transform) { BoundingBox box = new(); - + if (collection.Count > 0) box = CalculateSolidBounds(collection[0]); @@ -595,33 +601,33 @@ private void GenerateSolid(Solid into, List collection) // find all used materials foreach (var solid in collection) - foreach (var face in solid.Faces) - { - if (face.TextureName.StartsWith("__") || face.TextureName == "TB_empty") - continue; - - // add collider vertices - var vertexIndex = colliderVertices.Count; - var last = Vec3.Zero; - for (int i = 0; i < face.Vertices.Count; i++) + foreach (var face in solid.Faces) { - // skip collider vertices that are too close together ... - var it = Vec3.Transform(face.Vertices[i], transform); - if (i == 0 || (last - it).LengthSquared() > 1) - colliderVertices.Add(last = it); - } + if (face.TextureName.StartsWith("__") || face.TextureName == "TB_empty") + continue; - // add collider face - if (colliderVertices.Count > vertexIndex) - { - colliderFaces.Add(new () + // add collider vertices + var vertexIndex = colliderVertices.Count; + var last = Vec3.Zero; + for (int i = 0; i < face.Vertices.Count; i++) { - Plane = Plane.Normalize(Plane.Transform(face.Plane, transform)), - VertexStart = vertexIndex, - VertexCount = colliderVertices.Count - vertexIndex - }); + // skip collider vertices that are too close together ... + var it = Vec3.Transform(face.Vertices[i], transform); + if (i == 0 || (last - it).LengthSquared() > 1) + colliderVertices.Add(last = it); + } + + // add collider face + if (colliderVertices.Count > vertexIndex) + { + colliderFaces.Add(new() + { + Plane = Plane.Normalize(Plane.Transform(face.Plane, transform)), + VertexStart = vertexIndex, + VertexCount = colliderVertices.Count - vertexIndex + }); + } } - } // set up values if (colliderVertices.Count > 0) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 5a1e5998..eca07671 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -114,7 +114,7 @@ public override void Update() { if (Camera.Target != null && Matrix.Invert(Camera.Projection, out var inverseProj) && - Matrix.Invert(Camera.View, out var inverseView)) + Matrix.Invert(Camera.View, out var inverseView)) { // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); @@ -375,7 +375,7 @@ public override void Render(Target target) var updateMs = debugUpdTimer.Elapsed.TotalMilliseconds; var renderMs = lastDebugRndTime.TotalMilliseconds; var frameMs = debugFpsTimer.Elapsed.TotalMilliseconds; - var fps = (int)(1000/frameMs); + var fps = (int)(1000 / frameMs); debugFpsTimer.Restart(); batch.Text(font, $"Draws: {state.Calls}, Tris: {state.Triangles}, Upd: {debugUpdateCount}", bounds.BottomLeft, new Vec2(0, 1), Color.Red); @@ -458,12 +458,12 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R continue; // check against each triangle in the face - for (int i = 0; i < face.VertexCount - 2; i ++) + for (int i = 0; i < face.VertexCount - 2; i++) { if (Utils.RayIntersectsTriangle(point, direction, - verts[face.VertexStart + 0], - verts[face.VertexStart + i + 1], - verts[face.VertexStart + i + 2], out float dist)) + verts[face.VertexStart + 0], + verts[face.VertexStart + i + 1], + verts[face.VertexStart + i + 2], out float dist)) { // too far away if (dist > distance) diff --git a/Source/Mod/Editor/FujiMap.cs b/Source/Mod/Editor/FujiMap.cs index 07b038ef..2f208d53 100644 --- a/Source/Mod/Editor/FujiMap.cs +++ b/Source/Mod/Editor/FujiMap.cs @@ -60,21 +60,21 @@ public FujiMap(string name, string virtPath, Stream stream, string? fullPath) Log.Info($"Reading def: {def}"); - var props = defType - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(prop => !prop.HasAttr()); - - foreach (var prop in props) - { - if (prop.GetCustomAttribute() is { } custom) - { - prop.SetValue(def, custom.Deserialize(reader)); - continue; - } - - // Primitives - if (prop.PropertyType == typeof(bool)) - prop.SetValue(def, reader.ReadBoolean()); + var props = defType + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => !prop.HasAttr()); + + foreach (var prop in props) + { + if (prop.GetCustomAttribute() is { } custom) + { + prop.SetValue(def, custom.Deserialize(reader)); + continue; + } + + // Primitives + if (prop.PropertyType == typeof(bool)) + prop.SetValue(def, reader.ReadBoolean()); else if (prop.PropertyType == typeof(byte)) prop.SetValue(def, reader.ReadByte()); else if (prop.PropertyType == typeof(byte[])) @@ -86,33 +86,33 @@ public FujiMap(string name, string virtPath, Stream stream, string? fullPath) else if (prop.PropertyType == typeof(decimal)) prop.SetValue(def, reader.ReadDecimal()); else if (prop.PropertyType == typeof(double)) - prop.SetValue(def, reader.ReadDouble()); + prop.SetValue(def, reader.ReadDouble()); else if (prop.PropertyType == typeof(float)) - prop.SetValue(def, reader.ReadSingle()); + prop.SetValue(def, reader.ReadSingle()); else if (prop.PropertyType == typeof(int)) - prop.SetValue(def, reader.ReadInt32()); + prop.SetValue(def, reader.ReadInt32()); else if (prop.PropertyType == typeof(long)) - prop.SetValue(def, reader.ReadInt64()); + prop.SetValue(def, reader.ReadInt64()); else if (prop.PropertyType == typeof(sbyte)) - prop.SetValue(def, reader.ReadSByte()); + prop.SetValue(def, reader.ReadSByte()); else if (prop.PropertyType == typeof(short)) - prop.SetValue(def, reader.ReadInt16()); + prop.SetValue(def, reader.ReadInt16()); else if (prop.PropertyType == typeof(Half)) - prop.SetValue(def, reader.ReadHalf()); - else if (prop.PropertyType == typeof(string)) - prop.SetValue(def, reader.ReadString()); - // Special support - else if (prop.PropertyType == typeof(Vec2)) - prop.SetValue(def, reader.ReadVec2()); - else if (prop.PropertyType == typeof(Vec3)) - prop.SetValue(def, reader.ReadVec3()); - else if (prop.PropertyType == typeof(Color)) - prop.SetValue(def, reader.ReadColor()); - - Log.Info($" - {prop.Name}: {prop.GetValue(def)}"); - } - - Definitions.Add((ActorDefinition)def!); + prop.SetValue(def, reader.ReadHalf()); + else if (prop.PropertyType == typeof(string)) + prop.SetValue(def, reader.ReadString()); + // Special support + else if (prop.PropertyType == typeof(Vec2)) + prop.SetValue(def, reader.ReadVec2()); + else if (prop.PropertyType == typeof(Vec3)) + prop.SetValue(def, reader.ReadVec3()); + else if (prop.PropertyType == typeof(Color)) + prop.SetValue(def, reader.ReadColor()); + + Log.Info($" - {prop.Name}: {prop.GetValue(def)}"); + } + + Definitions.Add((ActorDefinition)def!); } } catch (Exception ex) diff --git a/Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs b/Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs index 294516cd..c4b6cf93 100644 --- a/Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs +++ b/Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs @@ -13,13 +13,13 @@ public interface ICustomProperty public class PropertyCustomAttribute(Type type) : Attribute { private readonly MethodInfo m_Serialize = type.GetMethod(nameof(ICustomProperty.Serialize), BindingFlags.Public | BindingFlags.Static) - ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); + ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); private readonly MethodInfo m_Deserialize = type.GetMethod(nameof(ICustomProperty.Deserialize), BindingFlags.Public | BindingFlags.Static) - ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); + ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); private readonly MethodInfo m_RenderGui = type.GetMethod(nameof(ICustomProperty.Deserialize), BindingFlags.Public | BindingFlags.Static) - ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); + ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); internal void Serialize(object value, BinaryWriter writer) => m_Serialize.Invoke(null, [value, writer]); internal object Deserialize(BinaryReader reader) => m_Deserialize.Invoke(null, [reader])!; diff --git a/Source/Mod/Editor/Windows/EditorWindow.cs b/Source/Mod/Editor/Windows/EditorWindow.cs index e40c99f9..675bb20b 100644 --- a/Source/Mod/Editor/Windows/EditorWindow.cs +++ b/Source/Mod/Editor/Windows/EditorWindow.cs @@ -13,4 +13,4 @@ public sealed override void Render() RenderWindow((Game.Scene as EditorWorld)!); ImGui.End(); } -} \ No newline at end of file +} diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/Windows/TestWindow.cs index 3ca93306..ae5eeba3 100644 --- a/Source/Mod/Editor/Windows/TestWindow.cs +++ b/Source/Mod/Editor/Windows/TestWindow.cs @@ -1,5 +1,5 @@ -using System.Reflection; using ImGuiNET; +using System.Reflection; namespace Celeste64.Mod.Editor; diff --git a/Source/Mod/Helpers/Batcher3D.cs b/Source/Mod/Helpers/Batcher3D.cs index 31263c6d..fac31133 100644 --- a/Source/Mod/Helpers/Batcher3D.cs +++ b/Source/Mod/Helpers/Batcher3D.cs @@ -8,497 +8,497 @@ namespace Celeste64.Mod; /// public class Batcher3D { - /// - /// Vertex Format of Batcher.Vertex - /// - private static readonly VertexFormat VertexFormat = VertexFormat.Create( - new VertexFormat.Element(0, VertexType.Float3, false), - new VertexFormat.Element(1, VertexType.Float2, false), - new VertexFormat.Element(2, VertexType.UByte4, true) - ); - - /// - /// The Vertex Layout used for Sprite Batching - /// - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct Vertex(Vec3 position, Vec2 texcoord, Color color) : IVertex - { - public Vec3 Pos = position; - public Vec2 Tex = texcoord; - public Color Col = color; - - public readonly VertexFormat Format => VertexFormat; - } - - ~Batcher3D() - { - if (vertexPtr != IntPtr.Zero) - Marshal.FreeHGlobal(vertexPtr); - if (indexPtr != IntPtr.Zero) - Marshal.FreeHGlobal(indexPtr); - } - - private IntPtr vertexPtr = IntPtr.Zero; - private int vertexCount = 0; - private int vertexCapacity = 0; - - private IntPtr indexPtr = IntPtr.Zero; - private int indexCount = 0; - private int indexCapacity = 0; - - private readonly Mesh mesh = new(); - private readonly Material material = new(Assets.Shaders["Sprite"]); - private bool dirty = false; - - public void Line(Vec3 from, Vec3 to, Color color, float thickness = 0.1f) => Line(from, to, color, Matrix4x4.Identity, thickness); - public void Line(Vec3 from, Vec3 to, Color color, Matrix transform, float thickness = 0.1f) - { - var normal = (to - from).Normalized(); - // The other vector for the cross product can't be parallel to the normal - var tangent = Math.Abs(Vec3.Dot(normal, Vec3.UnitX)) < 0.5f ? Vec3.Cross(normal, Vec3.UnitX) : Vec3.Cross(normal, Vec3.UnitY); - var bitangent = Vec3.Cross(normal, tangent); - - tangent *= thickness; - bitangent *= thickness; - - Box(from - tangent - bitangent, from - tangent + bitangent, from + tangent - bitangent, from + tangent + bitangent, - to - tangent - bitangent, to - tangent + bitangent, to + tangent - bitangent, to + tangent + bitangent, - color, transform); - } - - public void Cube(Vec3 center, Color color, float thickness = 0.1f) => Cube(center, color, Matrix4x4.Identity, thickness); - public void Cube(Vec3 center, Color color, Matrix transform, float thickness = 0.1f) - { - Box(center + new Vec3(-thickness, -thickness, -thickness), - center + new Vec3(thickness, -thickness, -thickness), - center + new Vec3(-thickness, thickness, -thickness), - center + new Vec3(thickness, thickness, -thickness), - center + new Vec3(-thickness, -thickness, thickness), - center + new Vec3(thickness, -thickness, thickness), - center + new Vec3(-thickness, thickness, thickness), - center + new Vec3(thickness, thickness, thickness), - color, transform - ); - } - - public void Torus(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Torus(center, radius, resolution, color, Matrix4x4.Identity, thickness); - public void Torus(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) - { - var points = new Vec3[resolution]; - - float angleStep = Calc.TAU / resolution; - float angle = 0.0f; - for (int i = 0; i < resolution; i++, angle += angleStep) - { - points[i] = new Vec3(Calc.AngleToVector(angle, radius), 0.0f); - } - - EnsureVertexCapacity(vertexCount + resolution * 4); // 4 vertices each - EnsureIndexCapacity(indexCount + resolution * 4 * 2 * 3); // 4 faces * 2 triangles * 3 vertices each - - unsafe - { - Span vertices = new((Vertex*)vertexPtr + vertexCount, resolution * 4); - Span indices = new((int*)indexPtr + indexCount, resolution * 4 * 2 * 3); - - for (int i = 0; i < resolution; i++) - { - var normal = points[i].Normalized() * thickness; - var up = new Vec3(0.0f, 0.0f, thickness); - - vertices[i * 4 + 0].Pos = Vec3.Transform(center + points[i] + normal - up, transform); - vertices[i * 4 + 1].Pos = Vec3.Transform(center + points[i] - normal - up, transform); - vertices[i * 4 + 2].Pos = Vec3.Transform(center + points[i] + normal + up, transform); - vertices[i * 4 + 3].Pos = Vec3.Transform(center + points[i] - normal + up, transform); - vertices[i * 4 + 0].Col = color; - vertices[i * 4 + 1].Col = color; - vertices[i * 4 + 2].Col = color; - vertices[i * 4 + 3].Col = color; - } - - for (int i = 0; i < resolution; i++) - { - int curr = i; - int prev = i == 0 ? resolution - 1 : i - 1; // Wrap around to the end - - // Bottom - indices[i * (4 * 2 * 3) + 0] = vertexCount + prev * 4 + 0; - indices[i * (4 * 2 * 3) + 1] = vertexCount + curr * 4 + 0; - indices[i * (4 * 2 * 3) + 2] = vertexCount + prev * 4 + 1; - indices[i * (4 * 2 * 3) + 3] = vertexCount + prev * 4 + 0; - indices[i * (4 * 2 * 3) + 4] = vertexCount + curr * 4 + 0; - indices[i * (4 * 2 * 3) + 5] = vertexCount + curr * 4 + 1; - // Top - indices[i * (4 * 2 * 3) + 6] = vertexCount + prev * 4 + 2; - indices[i * (4 * 2 * 3) + 7] = vertexCount + prev * 4 + 3; - indices[i * (4 * 2 * 3) + 8] = vertexCount + curr * 4 + 3; - indices[i * (4 * 2 * 3) + 9] = vertexCount + prev * 4 + 2; - indices[i * (4 * 2 * 3) + 10] = vertexCount + curr * 4 + 3; - indices[i * (4 * 2 * 3) + 11] = vertexCount + curr * 4 + 2; - // Outer - indices[i * (4 * 2 * 3) + 12] = vertexCount + prev * 4 + 0; - indices[i * (4 * 2 * 3) + 13] = vertexCount + curr * 4 + 2; - indices[i * (4 * 2 * 3) + 14] = vertexCount + prev * 4 + 2; - indices[i * (4 * 2 * 3) + 15] = vertexCount + prev * 4 + 0; - indices[i * (4 * 2 * 3) + 16] = vertexCount + curr * 4 + 0; - indices[i * (4 * 2 * 3) + 17] = vertexCount + curr * 4 + 2; - // Inner - indices[i * (4 * 2 * 3) + 18] = vertexCount + prev * 4 + 1; - indices[i * (4 * 2 * 3) + 19] = vertexCount + prev * 4 + 3; - indices[i * (4 * 2 * 3) + 20] = vertexCount + curr * 4 + 3; - indices[i * (4 * 2 * 3) + 21] = vertexCount + prev * 4 + 1; - indices[i * (4 * 2 * 3) + 22] = vertexCount + curr * 4 + 3; - indices[i * (4 * 2 * 3) + 23] = vertexCount + curr * 4 + 1; - } - } - - vertexCount += resolution * 4; - indexCount += resolution * 4 * 2 * 3; - dirty = true; - } - - public void Disk(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Disk(center, radius, resolution, color, Matrix4x4.Identity, thickness); - public void Disk(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) - { - var points = new Vec3[resolution]; - - float angleStep = Calc.TAU / resolution; - float angle = 0.0f; - for (int i = 0; i < resolution; i++, angle += angleStep) - { - points[i] = new Vec3(Calc.AngleToVector(angle, radius), 0.0f); - } - - EnsureVertexCapacity(vertexCount + resolution * 2 + 2); // 2 vertices each + 2 in the center - EnsureIndexCapacity(indexCount + resolution * 4 * 3); // 1 faces for outside + 2 triangles on top/bottom = 4 triangles * 3 vertices each - - unsafe - { - Span vertices = new((Vertex*)vertexPtr + vertexCount, resolution * 2 + 2); - Span indices = new((int*)indexPtr + indexCount, resolution * 4 * 3); - - var up = new Vec3(0.0f, 0.0f, thickness); - vertices[0].Pos = Vec3.Transform(center - up, transform); - vertices[1].Pos = Vec3.Transform(center + up, transform); - vertices[0].Col = color; - vertices[1].Col = color; - - for (int i = 0; i < resolution; i++) - { - vertices[(i * 2 + 2) + 0].Pos = Vec3.Transform(center + points[i] - up, transform); - vertices[(i * 2 + 2) + 1].Pos = Vec3.Transform(center + points[i] + up, transform); - vertices[(i * 2 + 2) + 0].Col = color; - vertices[(i * 2 + 2) + 1].Col = color; - } - - for (int i = 0; i < resolution; i++) - { - int curr = i; - int prev = i == 0 ? resolution - 1 : i - 1; // Wrap around to the end - - // Bottom - indices[i * (4 * 3) + 0] = vertexCount + (prev * 2 + 2) + 0; - indices[i * (4 * 3) + 1] = vertexCount + (curr * 2 + 2) + 0; - indices[i * (4 * 3) + 2] = vertexCount + 0; - // Top - indices[i * (4 * 3) + 3] = vertexCount + (curr * 2 + 2) + 1; - indices[i * (4 * 3) + 4] = vertexCount + (prev * 2 + 2) + 1; - indices[i * (4 * 3) + 5] = vertexCount + 1; - // Outer - indices[i * (4 * 3) + 6] = vertexCount + (curr * 2 + 2) + 1; - indices[i * (4 * 3) + 7] = vertexCount + (curr * 2 + 2) + 0; - indices[i * (4 * 3) + 8] = vertexCount + (prev * 2 + 2) + 0; - indices[i * (4 * 3) + 9] = vertexCount + (curr * 2 + 2) + 1; - indices[i * (4 * 3) + 10] = vertexCount + (prev * 2 + 2) + 0; - indices[i * (4 * 3) + 11] = vertexCount + (prev * 2 + 2) + 1; - } - } - - vertexCount += resolution * 2 + 2; - indexCount += resolution * 4 * 3; - dirty = true; - } - - public void Sphere(Vec3 center, float radius, int resolution, Color color) => Sphere(center, radius, resolution, color, Matrix4x4.Identity); - public void Sphere(Vec3 center, float radius, int resolution, Color color, Matrix transform) - { - // Taken and adapted from Utils.CreateSphere() - int stackCount = resolution; - int sliceCount = resolution; - - EnsureVertexCapacity(vertexCount + 2 + (stackCount - 1) * sliceCount); - EnsureIndexCapacity(indexCount + sliceCount * 6 + (stackCount - 2) * sliceCount * 6); - - unsafe - { - Span vertices = new((Vertex*)vertexPtr + vertexCount, 2 + (stackCount - 1) * sliceCount); - Span indices = new((int*)indexPtr + indexCount, sliceCount * 6 + (stackCount - 2) * sliceCount * 6); - - int vtx = 0; - int idx = 0; - - // Add top vertex - int v0 = vertexCount + vtx; - vertices[vtx++].Pos = center + new Vec3(0.0f, 0.0f, radius); - - // Generate vertices per stack / slice - for (int i = 0; i < stackCount - 1; i++) - { - float phi = MathF.PI * (i + 1) / (float)(stackCount); - for (int j = 0; j < sliceCount; j++) - { - float theta = 2.0f * MathF.PI * (j) / (float)(sliceCount); - float x = radius * MathF.Sin(phi) * MathF.Cos(theta); - float y = radius * MathF.Sin(phi) * MathF.Sin(theta); - float z = radius * MathF.Cos(phi); - vertices[vtx++].Pos = center + new Vec3(x, y, z); - } - } - - // Add bottom vertex - int v1 = vertexCount + vtx; - vertices[vtx++].Pos = center + new Vec3(0.0f, 0.0f, -radius); - - // Fill-in color - for (int i = 0; i < vtx; i++) - vertices[i].Col = color; - - // Add top / bottom triangles - for (int i = 0; i < sliceCount; ++i) - { - int i0 = i + 1; - int i1 = (i + 1) % sliceCount + 1; - indices[idx++] = v0; - indices[idx++] = vertexCount + i1; - indices[idx++] = vertexCount + i0; - - i0 = i + sliceCount * (stackCount - 2) + 1; - i1 = (i + 1) % sliceCount + sliceCount * (stackCount - 2) + 1; - indices[idx++] = v1; - indices[idx++] = vertexCount + i0; - indices[idx++] = vertexCount + i1; - } - - // Add quads per stack / slice - for (int j = 0; j < stackCount - 2; j++) - { - int j0 = j * sliceCount + 1; - int j1 = (j + 1) * sliceCount + 1; - for (int i = 0; i < sliceCount; i++) - { - int i0 = j0 + i; - int i1 = j0 + (i + 1) % sliceCount; - int i2 = j1 + (i + 1) % sliceCount; - int i3 = j1 + i; - indices[idx++] = vertexCount + i0; - indices[idx++] = vertexCount + i1; - indices[idx++] = vertexCount + i2; - indices[idx++] = vertexCount + i0; - indices[idx++] = vertexCount + i2; - indices[idx++] = vertexCount + i3; - } - } - - vertexCount += vtx; - indexCount += idx; - dirty = true; - } - } - - public void Box(Vec3 min, Vec3 max, Color color) => Box(min, max, color, Matrix.Identity); - public void Box(Vec3 min, Vec3 max, Color color, Matrix transform) => - Box(min with { Z = max.Z }, - min with { X = max.X, Z = max.Z }, - min, - min with { X = max.X }, - - max with { X = min.X }, - max, - max with { X = min.X, Z = min.Z }, - max with { Z = min.Z }, - color, transform); - - /// - /// Renders a box of a solid color. - /// - /// Front Top Left - /// Front Top Right - /// Front Bottom Left - /// Front Bottom Right - /// Back Top Left - /// Back Top Right - /// Back Bottom Left - /// Back Bottom Right - /// Box color - public void Box(Vec3 v0, Vec3 v1, Vec3 v2, Vec3 v3, - Vec3 v4, Vec3 v5, Vec3 v6, Vec3 v7, - Color color, Matrix transform) - { - EnsureVertexCapacity(vertexCount + 8); - EnsureIndexCapacity(indexCount + 6 * 2 * 3); // 6 faces * 2 triangles * 3 vertices - - unsafe - { - Span vertices = new((Vertex*)vertexPtr + vertexCount, 8); - Span indices = new((int*)indexPtr + indexCount, 36); - - vertices[0].Pos = Vec3.Transform(v0, transform); - vertices[1].Pos = Vec3.Transform(v1, transform); - vertices[2].Pos = Vec3.Transform(v2, transform); - vertices[3].Pos = Vec3.Transform(v3, transform); - vertices[4].Pos = Vec3.Transform(v4, transform); - vertices[5].Pos = Vec3.Transform(v5, transform); - vertices[6].Pos = Vec3.Transform(v6, transform); - vertices[7].Pos = Vec3.Transform(v7, transform); - vertices[0].Col = color; - vertices[1].Col = color; - vertices[2].Col = color; - vertices[3].Col = color; - vertices[4].Col = color; - vertices[5].Col = color; - vertices[6].Col = color; - vertices[7].Col = color; - - // Front - indices[0] = vertexCount + 0; - indices[1] = vertexCount + 2; - indices[2] = vertexCount + 1; - indices[3] = vertexCount + 2; - indices[4] = vertexCount + 3; - indices[5] = vertexCount + 1; - // Back - indices[6] = vertexCount + 4; - indices[7] = vertexCount + 5; - indices[8] = vertexCount + 6; - indices[9] = vertexCount + 5; - indices[10] = vertexCount + 7; - indices[11] = vertexCount + 6; - // Left - indices[12] = vertexCount + 0; - indices[13] = vertexCount + 6; - indices[14] = vertexCount + 2; - indices[15] = vertexCount + 0; - indices[16] = vertexCount + 4; - indices[17] = vertexCount + 6; - // Right - indices[18] = vertexCount + 1; - indices[19] = vertexCount + 3; - indices[20] = vertexCount + 7; - indices[21] = vertexCount + 1; - indices[22] = vertexCount + 7; - indices[23] = vertexCount + 5; - // Top - indices[24] = vertexCount + 0; - indices[25] = vertexCount + 1; - indices[26] = vertexCount + 5; - indices[27] = vertexCount + 0; - indices[28] = vertexCount + 5; - indices[29] = vertexCount + 4; - // Bottom - indices[30] = vertexCount + 2; - indices[31] = vertexCount + 7; - indices[32] = vertexCount + 6; - indices[33] = vertexCount + 2; - indices[34] = vertexCount + 3; - indices[35] = vertexCount + 7; - } - - vertexCount += 8; - indexCount += 6 * 2 * 3; - dirty = true; - } - - /// - /// Draws the Batcher3D to the given Target with the given RenderState - /// - public void Render(ref RenderState state) - { - if (indexPtr == IntPtr.Zero || vertexPtr == IntPtr.Zero) - return; - - // Upload our data if we've been modified since the last time we rendered - if (dirty) - { - mesh.SetIndices(indexPtr, indexCount, IndexFormat.ThirtyTwo); - mesh.SetVertices(vertexPtr, vertexCount, VertexFormat); - dirty = false; - } - - if (material.Shader?.Has("u_matrix") ?? false) - material.Set("u_matrix", state.Camera.ViewProjection); - if (material.Shader?.Has("u_far") ?? false) - material.Set("u_far", state.Camera.FarPlane); - if (material.Shader?.Has("u_near") ?? false) - material.Set("u_near", state.Camera.NearPlane); - if (material.Shader?.Has("u_texture") ?? false) - material.Set("u_texture", Assets.Textures["white"]); - - var call = new DrawCommand(state.Camera.Target, mesh, material) - { - // BlendMode = BlendMode.Screen, - DepthCompare = state.DepthCompare, - DepthMask = state.DepthMask, - // DepthMask = false, - // DepthCompare = DepthCompare.Less, - CullMode = CullMode.None, - MeshIndexStart = 0, - MeshIndexCount = indexCount, - }; - call.Submit(); - state.Calls++; - state.Triangles += indexCount / 3; - } - - /// - /// Clears the Batcher3D. - /// - public void Clear() - { - vertexCount = 0; - indexCount = 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe void EnsureVertexCapacity(int capacity) - { - if (capacity < vertexCapacity) return; - - if (vertexCapacity == 0) - vertexCapacity = 32; - - while (capacity >= vertexCapacity) - vertexCapacity *= 2; - - IntPtr newPtr = Marshal.AllocHGlobal(sizeof(Vertex) * vertexCapacity); - - if (vertexCount > 0) - Buffer.MemoryCopy((void*)vertexPtr, (void*)newPtr, vertexCapacity * sizeof(Vertex), vertexCount * sizeof(Vertex)); - - if (vertexPtr != IntPtr.Zero) - Marshal.FreeHGlobal(vertexPtr); - - vertexPtr = newPtr; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe void EnsureIndexCapacity(int capacity) - { - if (capacity < indexCapacity) return; - - if (indexCapacity == 0) - indexCapacity = 32; - - while (capacity >= indexCapacity) - indexCapacity *= 2; - - IntPtr newPtr = Marshal.AllocHGlobal(sizeof(int) * indexCapacity); - - if (indexCount > 0) - Buffer.MemoryCopy((void*)indexPtr, (void*)newPtr, indexCapacity * sizeof(int), indexCount * sizeof(int)); - - if (indexPtr != IntPtr.Zero) - Marshal.FreeHGlobal(indexPtr); - - indexPtr = newPtr; - } + /// + /// Vertex Format of Batcher.Vertex + /// + private static readonly VertexFormat VertexFormat = VertexFormat.Create( + new VertexFormat.Element(0, VertexType.Float3, false), + new VertexFormat.Element(1, VertexType.Float2, false), + new VertexFormat.Element(2, VertexType.UByte4, true) + ); + + /// + /// The Vertex Layout used for Sprite Batching + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Vertex(Vec3 position, Vec2 texcoord, Color color) : IVertex + { + public Vec3 Pos = position; + public Vec2 Tex = texcoord; + public Color Col = color; + + public readonly VertexFormat Format => VertexFormat; + } + + ~Batcher3D() + { + if (vertexPtr != IntPtr.Zero) + Marshal.FreeHGlobal(vertexPtr); + if (indexPtr != IntPtr.Zero) + Marshal.FreeHGlobal(indexPtr); + } + + private IntPtr vertexPtr = IntPtr.Zero; + private int vertexCount = 0; + private int vertexCapacity = 0; + + private IntPtr indexPtr = IntPtr.Zero; + private int indexCount = 0; + private int indexCapacity = 0; + + private readonly Mesh mesh = new(); + private readonly Material material = new(Assets.Shaders["Sprite"]); + private bool dirty = false; + + public void Line(Vec3 from, Vec3 to, Color color, float thickness = 0.1f) => Line(from, to, color, Matrix4x4.Identity, thickness); + public void Line(Vec3 from, Vec3 to, Color color, Matrix transform, float thickness = 0.1f) + { + var normal = (to - from).Normalized(); + // The other vector for the cross product can't be parallel to the normal + var tangent = Math.Abs(Vec3.Dot(normal, Vec3.UnitX)) < 0.5f ? Vec3.Cross(normal, Vec3.UnitX) : Vec3.Cross(normal, Vec3.UnitY); + var bitangent = Vec3.Cross(normal, tangent); + + tangent *= thickness; + bitangent *= thickness; + + Box(from - tangent - bitangent, from - tangent + bitangent, from + tangent - bitangent, from + tangent + bitangent, + to - tangent - bitangent, to - tangent + bitangent, to + tangent - bitangent, to + tangent + bitangent, + color, transform); + } + + public void Cube(Vec3 center, Color color, float thickness = 0.1f) => Cube(center, color, Matrix4x4.Identity, thickness); + public void Cube(Vec3 center, Color color, Matrix transform, float thickness = 0.1f) + { + Box(center + new Vec3(-thickness, -thickness, -thickness), + center + new Vec3(thickness, -thickness, -thickness), + center + new Vec3(-thickness, thickness, -thickness), + center + new Vec3(thickness, thickness, -thickness), + center + new Vec3(-thickness, -thickness, thickness), + center + new Vec3(thickness, -thickness, thickness), + center + new Vec3(-thickness, thickness, thickness), + center + new Vec3(thickness, thickness, thickness), + color, transform + ); + } + + public void Torus(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Torus(center, radius, resolution, color, Matrix4x4.Identity, thickness); + public void Torus(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) + { + var points = new Vec3[resolution]; + + float angleStep = Calc.TAU / resolution; + float angle = 0.0f; + for (int i = 0; i < resolution; i++, angle += angleStep) + { + points[i] = new Vec3(Calc.AngleToVector(angle, radius), 0.0f); + } + + EnsureVertexCapacity(vertexCount + resolution * 4); // 4 vertices each + EnsureIndexCapacity(indexCount + resolution * 4 * 2 * 3); // 4 faces * 2 triangles * 3 vertices each + + unsafe + { + Span vertices = new((Vertex*)vertexPtr + vertexCount, resolution * 4); + Span indices = new((int*)indexPtr + indexCount, resolution * 4 * 2 * 3); + + for (int i = 0; i < resolution; i++) + { + var normal = points[i].Normalized() * thickness; + var up = new Vec3(0.0f, 0.0f, thickness); + + vertices[i * 4 + 0].Pos = Vec3.Transform(center + points[i] + normal - up, transform); + vertices[i * 4 + 1].Pos = Vec3.Transform(center + points[i] - normal - up, transform); + vertices[i * 4 + 2].Pos = Vec3.Transform(center + points[i] + normal + up, transform); + vertices[i * 4 + 3].Pos = Vec3.Transform(center + points[i] - normal + up, transform); + vertices[i * 4 + 0].Col = color; + vertices[i * 4 + 1].Col = color; + vertices[i * 4 + 2].Col = color; + vertices[i * 4 + 3].Col = color; + } + + for (int i = 0; i < resolution; i++) + { + int curr = i; + int prev = i == 0 ? resolution - 1 : i - 1; // Wrap around to the end + + // Bottom + indices[i * (4 * 2 * 3) + 0] = vertexCount + prev * 4 + 0; + indices[i * (4 * 2 * 3) + 1] = vertexCount + curr * 4 + 0; + indices[i * (4 * 2 * 3) + 2] = vertexCount + prev * 4 + 1; + indices[i * (4 * 2 * 3) + 3] = vertexCount + prev * 4 + 0; + indices[i * (4 * 2 * 3) + 4] = vertexCount + curr * 4 + 0; + indices[i * (4 * 2 * 3) + 5] = vertexCount + curr * 4 + 1; + // Top + indices[i * (4 * 2 * 3) + 6] = vertexCount + prev * 4 + 2; + indices[i * (4 * 2 * 3) + 7] = vertexCount + prev * 4 + 3; + indices[i * (4 * 2 * 3) + 8] = vertexCount + curr * 4 + 3; + indices[i * (4 * 2 * 3) + 9] = vertexCount + prev * 4 + 2; + indices[i * (4 * 2 * 3) + 10] = vertexCount + curr * 4 + 3; + indices[i * (4 * 2 * 3) + 11] = vertexCount + curr * 4 + 2; + // Outer + indices[i * (4 * 2 * 3) + 12] = vertexCount + prev * 4 + 0; + indices[i * (4 * 2 * 3) + 13] = vertexCount + curr * 4 + 2; + indices[i * (4 * 2 * 3) + 14] = vertexCount + prev * 4 + 2; + indices[i * (4 * 2 * 3) + 15] = vertexCount + prev * 4 + 0; + indices[i * (4 * 2 * 3) + 16] = vertexCount + curr * 4 + 0; + indices[i * (4 * 2 * 3) + 17] = vertexCount + curr * 4 + 2; + // Inner + indices[i * (4 * 2 * 3) + 18] = vertexCount + prev * 4 + 1; + indices[i * (4 * 2 * 3) + 19] = vertexCount + prev * 4 + 3; + indices[i * (4 * 2 * 3) + 20] = vertexCount + curr * 4 + 3; + indices[i * (4 * 2 * 3) + 21] = vertexCount + prev * 4 + 1; + indices[i * (4 * 2 * 3) + 22] = vertexCount + curr * 4 + 3; + indices[i * (4 * 2 * 3) + 23] = vertexCount + curr * 4 + 1; + } + } + + vertexCount += resolution * 4; + indexCount += resolution * 4 * 2 * 3; + dirty = true; + } + + public void Disk(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Disk(center, radius, resolution, color, Matrix4x4.Identity, thickness); + public void Disk(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) + { + var points = new Vec3[resolution]; + + float angleStep = Calc.TAU / resolution; + float angle = 0.0f; + for (int i = 0; i < resolution; i++, angle += angleStep) + { + points[i] = new Vec3(Calc.AngleToVector(angle, radius), 0.0f); + } + + EnsureVertexCapacity(vertexCount + resolution * 2 + 2); // 2 vertices each + 2 in the center + EnsureIndexCapacity(indexCount + resolution * 4 * 3); // 1 faces for outside + 2 triangles on top/bottom = 4 triangles * 3 vertices each + + unsafe + { + Span vertices = new((Vertex*)vertexPtr + vertexCount, resolution * 2 + 2); + Span indices = new((int*)indexPtr + indexCount, resolution * 4 * 3); + + var up = new Vec3(0.0f, 0.0f, thickness); + vertices[0].Pos = Vec3.Transform(center - up, transform); + vertices[1].Pos = Vec3.Transform(center + up, transform); + vertices[0].Col = color; + vertices[1].Col = color; + + for (int i = 0; i < resolution; i++) + { + vertices[(i * 2 + 2) + 0].Pos = Vec3.Transform(center + points[i] - up, transform); + vertices[(i * 2 + 2) + 1].Pos = Vec3.Transform(center + points[i] + up, transform); + vertices[(i * 2 + 2) + 0].Col = color; + vertices[(i * 2 + 2) + 1].Col = color; + } + + for (int i = 0; i < resolution; i++) + { + int curr = i; + int prev = i == 0 ? resolution - 1 : i - 1; // Wrap around to the end + + // Bottom + indices[i * (4 * 3) + 0] = vertexCount + (prev * 2 + 2) + 0; + indices[i * (4 * 3) + 1] = vertexCount + (curr * 2 + 2) + 0; + indices[i * (4 * 3) + 2] = vertexCount + 0; + // Top + indices[i * (4 * 3) + 3] = vertexCount + (curr * 2 + 2) + 1; + indices[i * (4 * 3) + 4] = vertexCount + (prev * 2 + 2) + 1; + indices[i * (4 * 3) + 5] = vertexCount + 1; + // Outer + indices[i * (4 * 3) + 6] = vertexCount + (curr * 2 + 2) + 1; + indices[i * (4 * 3) + 7] = vertexCount + (curr * 2 + 2) + 0; + indices[i * (4 * 3) + 8] = vertexCount + (prev * 2 + 2) + 0; + indices[i * (4 * 3) + 9] = vertexCount + (curr * 2 + 2) + 1; + indices[i * (4 * 3) + 10] = vertexCount + (prev * 2 + 2) + 0; + indices[i * (4 * 3) + 11] = vertexCount + (prev * 2 + 2) + 1; + } + } + + vertexCount += resolution * 2 + 2; + indexCount += resolution * 4 * 3; + dirty = true; + } + + public void Sphere(Vec3 center, float radius, int resolution, Color color) => Sphere(center, radius, resolution, color, Matrix4x4.Identity); + public void Sphere(Vec3 center, float radius, int resolution, Color color, Matrix transform) + { + // Taken and adapted from Utils.CreateSphere() + int stackCount = resolution; + int sliceCount = resolution; + + EnsureVertexCapacity(vertexCount + 2 + (stackCount - 1) * sliceCount); + EnsureIndexCapacity(indexCount + sliceCount * 6 + (stackCount - 2) * sliceCount * 6); + + unsafe + { + Span vertices = new((Vertex*)vertexPtr + vertexCount, 2 + (stackCount - 1) * sliceCount); + Span indices = new((int*)indexPtr + indexCount, sliceCount * 6 + (stackCount - 2) * sliceCount * 6); + + int vtx = 0; + int idx = 0; + + // Add top vertex + int v0 = vertexCount + vtx; + vertices[vtx++].Pos = center + new Vec3(0.0f, 0.0f, radius); + + // Generate vertices per stack / slice + for (int i = 0; i < stackCount - 1; i++) + { + float phi = MathF.PI * (i + 1) / (float)(stackCount); + for (int j = 0; j < sliceCount; j++) + { + float theta = 2.0f * MathF.PI * (j) / (float)(sliceCount); + float x = radius * MathF.Sin(phi) * MathF.Cos(theta); + float y = radius * MathF.Sin(phi) * MathF.Sin(theta); + float z = radius * MathF.Cos(phi); + vertices[vtx++].Pos = center + new Vec3(x, y, z); + } + } + + // Add bottom vertex + int v1 = vertexCount + vtx; + vertices[vtx++].Pos = center + new Vec3(0.0f, 0.0f, -radius); + + // Fill-in color + for (int i = 0; i < vtx; i++) + vertices[i].Col = color; + + // Add top / bottom triangles + for (int i = 0; i < sliceCount; ++i) + { + int i0 = i + 1; + int i1 = (i + 1) % sliceCount + 1; + indices[idx++] = v0; + indices[idx++] = vertexCount + i1; + indices[idx++] = vertexCount + i0; + + i0 = i + sliceCount * (stackCount - 2) + 1; + i1 = (i + 1) % sliceCount + sliceCount * (stackCount - 2) + 1; + indices[idx++] = v1; + indices[idx++] = vertexCount + i0; + indices[idx++] = vertexCount + i1; + } + + // Add quads per stack / slice + for (int j = 0; j < stackCount - 2; j++) + { + int j0 = j * sliceCount + 1; + int j1 = (j + 1) * sliceCount + 1; + for (int i = 0; i < sliceCount; i++) + { + int i0 = j0 + i; + int i1 = j0 + (i + 1) % sliceCount; + int i2 = j1 + (i + 1) % sliceCount; + int i3 = j1 + i; + indices[idx++] = vertexCount + i0; + indices[idx++] = vertexCount + i1; + indices[idx++] = vertexCount + i2; + indices[idx++] = vertexCount + i0; + indices[idx++] = vertexCount + i2; + indices[idx++] = vertexCount + i3; + } + } + + vertexCount += vtx; + indexCount += idx; + dirty = true; + } + } + + public void Box(Vec3 min, Vec3 max, Color color) => Box(min, max, color, Matrix.Identity); + public void Box(Vec3 min, Vec3 max, Color color, Matrix transform) => + Box(min with { Z = max.Z }, + min with { X = max.X, Z = max.Z }, + min, + min with { X = max.X }, + + max with { X = min.X }, + max, + max with { X = min.X, Z = min.Z }, + max with { Z = min.Z }, + color, transform); + + /// + /// Renders a box of a solid color. + /// + /// Front Top Left + /// Front Top Right + /// Front Bottom Left + /// Front Bottom Right + /// Back Top Left + /// Back Top Right + /// Back Bottom Left + /// Back Bottom Right + /// Box color + public void Box(Vec3 v0, Vec3 v1, Vec3 v2, Vec3 v3, + Vec3 v4, Vec3 v5, Vec3 v6, Vec3 v7, + Color color, Matrix transform) + { + EnsureVertexCapacity(vertexCount + 8); + EnsureIndexCapacity(indexCount + 6 * 2 * 3); // 6 faces * 2 triangles * 3 vertices + + unsafe + { + Span vertices = new((Vertex*)vertexPtr + vertexCount, 8); + Span indices = new((int*)indexPtr + indexCount, 36); + + vertices[0].Pos = Vec3.Transform(v0, transform); + vertices[1].Pos = Vec3.Transform(v1, transform); + vertices[2].Pos = Vec3.Transform(v2, transform); + vertices[3].Pos = Vec3.Transform(v3, transform); + vertices[4].Pos = Vec3.Transform(v4, transform); + vertices[5].Pos = Vec3.Transform(v5, transform); + vertices[6].Pos = Vec3.Transform(v6, transform); + vertices[7].Pos = Vec3.Transform(v7, transform); + vertices[0].Col = color; + vertices[1].Col = color; + vertices[2].Col = color; + vertices[3].Col = color; + vertices[4].Col = color; + vertices[5].Col = color; + vertices[6].Col = color; + vertices[7].Col = color; + + // Front + indices[0] = vertexCount + 0; + indices[1] = vertexCount + 2; + indices[2] = vertexCount + 1; + indices[3] = vertexCount + 2; + indices[4] = vertexCount + 3; + indices[5] = vertexCount + 1; + // Back + indices[6] = vertexCount + 4; + indices[7] = vertexCount + 5; + indices[8] = vertexCount + 6; + indices[9] = vertexCount + 5; + indices[10] = vertexCount + 7; + indices[11] = vertexCount + 6; + // Left + indices[12] = vertexCount + 0; + indices[13] = vertexCount + 6; + indices[14] = vertexCount + 2; + indices[15] = vertexCount + 0; + indices[16] = vertexCount + 4; + indices[17] = vertexCount + 6; + // Right + indices[18] = vertexCount + 1; + indices[19] = vertexCount + 3; + indices[20] = vertexCount + 7; + indices[21] = vertexCount + 1; + indices[22] = vertexCount + 7; + indices[23] = vertexCount + 5; + // Top + indices[24] = vertexCount + 0; + indices[25] = vertexCount + 1; + indices[26] = vertexCount + 5; + indices[27] = vertexCount + 0; + indices[28] = vertexCount + 5; + indices[29] = vertexCount + 4; + // Bottom + indices[30] = vertexCount + 2; + indices[31] = vertexCount + 7; + indices[32] = vertexCount + 6; + indices[33] = vertexCount + 2; + indices[34] = vertexCount + 3; + indices[35] = vertexCount + 7; + } + + vertexCount += 8; + indexCount += 6 * 2 * 3; + dirty = true; + } + + /// + /// Draws the Batcher3D to the given Target with the given RenderState + /// + public void Render(ref RenderState state) + { + if (indexPtr == IntPtr.Zero || vertexPtr == IntPtr.Zero) + return; + + // Upload our data if we've been modified since the last time we rendered + if (dirty) + { + mesh.SetIndices(indexPtr, indexCount, IndexFormat.ThirtyTwo); + mesh.SetVertices(vertexPtr, vertexCount, VertexFormat); + dirty = false; + } + + if (material.Shader?.Has("u_matrix") ?? false) + material.Set("u_matrix", state.Camera.ViewProjection); + if (material.Shader?.Has("u_far") ?? false) + material.Set("u_far", state.Camera.FarPlane); + if (material.Shader?.Has("u_near") ?? false) + material.Set("u_near", state.Camera.NearPlane); + if (material.Shader?.Has("u_texture") ?? false) + material.Set("u_texture", Assets.Textures["white"]); + + var call = new DrawCommand(state.Camera.Target, mesh, material) + { + // BlendMode = BlendMode.Screen, + DepthCompare = state.DepthCompare, + DepthMask = state.DepthMask, + // DepthMask = false, + // DepthCompare = DepthCompare.Less, + CullMode = CullMode.None, + MeshIndexStart = 0, + MeshIndexCount = indexCount, + }; + call.Submit(); + state.Calls++; + state.Triangles += indexCount / 3; + } + + /// + /// Clears the Batcher3D. + /// + public void Clear() + { + vertexCount = 0; + indexCount = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void EnsureVertexCapacity(int capacity) + { + if (capacity < vertexCapacity) return; + + if (vertexCapacity == 0) + vertexCapacity = 32; + + while (capacity >= vertexCapacity) + vertexCapacity *= 2; + + IntPtr newPtr = Marshal.AllocHGlobal(sizeof(Vertex) * vertexCapacity); + + if (vertexCount > 0) + Buffer.MemoryCopy((void*)vertexPtr, (void*)newPtr, vertexCapacity * sizeof(Vertex), vertexCount * sizeof(Vertex)); + + if (vertexPtr != IntPtr.Zero) + Marshal.FreeHGlobal(vertexPtr); + + vertexPtr = newPtr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void EnsureIndexCapacity(int capacity) + { + if (capacity < indexCapacity) return; + + if (indexCapacity == 0) + indexCapacity = 32; + + while (capacity >= indexCapacity) + indexCapacity *= 2; + + IntPtr newPtr = Marshal.AllocHGlobal(sizeof(int) * indexCapacity); + + if (indexCount > 0) + Buffer.MemoryCopy((void*)indexPtr, (void*)newPtr, indexCapacity * sizeof(int), indexCount * sizeof(int)); + + if (indexPtr != IntPtr.Zero) + Marshal.FreeHGlobal(indexPtr); + + indexPtr = newPtr; + } } diff --git a/Source/Mod/Helpers/BinaryExtensions.cs b/Source/Mod/Helpers/BinaryExtensions.cs index 7f03084e..30583d3f 100644 --- a/Source/Mod/Helpers/BinaryExtensions.cs +++ b/Source/Mod/Helpers/BinaryExtensions.cs @@ -3,25 +3,25 @@ namespace Celeste64.Mod; public static class BinaryExtensions { public static void Write(this BinaryWriter writer, Vec2 value) - { - writer.Write(value.X); - writer.Write(value.Y); - } - public static void Write(this BinaryWriter writer, Vec3 value) - { - writer.Write(value.X); - writer.Write(value.Y); - writer.Write(value.Z); - } - public static void Write(this BinaryWriter writer, Color value) - { - writer.Write(value.R); - writer.Write(value.G); - writer.Write(value.B); - writer.Write(value.A); - } + { + writer.Write(value.X); + writer.Write(value.Y); + } + public static void Write(this BinaryWriter writer, Vec3 value) + { + writer.Write(value.X); + writer.Write(value.Y); + writer.Write(value.Z); + } + public static void Write(this BinaryWriter writer, Color value) + { + writer.Write(value.R); + writer.Write(value.G); + writer.Write(value.B); + writer.Write(value.A); + } - public static Vec2 ReadVec2(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle()); - public static Vec3 ReadVec3(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - public static Color ReadColor(this BinaryReader reader) => new(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); + public static Vec2 ReadVec2(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle()); + public static Vec3 ReadVec3(this BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + public static Color ReadColor(this BinaryReader reader) => new(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); } diff --git a/Source/Mod/Helpers/InputHelper.cs b/Source/Mod/Helpers/InputHelper.cs index 70007b4d..2af57f65 100644 --- a/Source/Mod/Helpers/InputHelper.cs +++ b/Source/Mod/Helpers/InputHelper.cs @@ -2,7 +2,7 @@ namespace Celeste64.Mod.Helpers; public static class InputHelper { - public static Vec2 MouseDelta => ImGuiManager.WantCaptureMouse - ? Vec2.Zero - : Input.State.Mouse.Position - Input.LastState.Mouse.Position; -} \ No newline at end of file + public static Vec2 MouseDelta => ImGuiManager.WantCaptureMouse + ? Vec2.Zero + : Input.State.Mouse.Position - Input.LastState.Mouse.Position; +} diff --git a/Source/Mod/Helpers/ModUtils.cs b/Source/Mod/Helpers/ModUtils.cs index ab984800..6e936e4b 100644 --- a/Source/Mod/Helpers/ModUtils.cs +++ b/Source/Mod/Helpers/ModUtils.cs @@ -22,7 +22,7 @@ public static bool RayIntersectsBox(Vec3 origin, Vec3 direction, BoundingBox box // float3 tSlabEntr = min(t0, t1); // float3 tSlabExit = max(t0, t1); var tSlabEnter = Vec3.Min(t0, t1); - var tSlabExit = Vec3.Max(t0, t1); + var tSlabExit = Vec3.Max(t0, t1); // Find the farthest entry and the nearest exit. // tEntr = Max3(tSlabEntr.x, tSlabEntr.y, tSlabEntr.z); @@ -33,17 +33,17 @@ public static bool RayIntersectsBox(Vec3 origin, Vec3 direction, BoundingBox box // Clamp to the range. // tEntr = max(tEntr, tMin); // tExit = min(tExit, tMax); - + return tEnter < tExit; } - + // Intersection method from "Real-Time Rendering and Essential Mathematics for Games" // Reference Implementation: https://github.com/opengl-tutorials/ogl/blob/15e57f6cccef388915e565d8322b8442049e1bd8/misc05_picking/misc05_picking_custom.cpp#L83-L197 public static bool RayIntersectOBB(Vec3 origin, Vec3 direction, BoundingBox box, Matrix transform, out float t) { t = 0.0f; float tMin = 0.0f, tMax = 100000.0f; - + var oobWorldPos = new Vec3(transform.M41, transform.M42, transform.M43); var delta = oobWorldPos - origin; @@ -52,64 +52,64 @@ public static bool RayIntersectOBB(Vec3 origin, Vec3 direction, BoundingBox box, var xAxis = new Vec3(transform.M11, transform.M12, transform.M13); float e = Vec3.Dot(xAxis, delta); float f = Vec3.Dot(direction, xAxis); - + if (Math.Abs(f) > 0.001f) // Standard case { float t1 = (e + box.Min.X) / f; // Intersection with the "left" plane float t2 = (e + box.Max.X) / f; // Intersection with the "right" plane - // t1 and t2 now contain distances between ray origin and ray-plane intersections + // t1 and t2 now contain distances between ray origin and ray-plane intersections // We want t1 to represent the nearest intersection, // so if it's not the case, invert t1 and t2 - if (t1>t2) + if (t1 > t2) (t1, t2) = (t2, t1); // tMax is the nearest "far" intersection (amongst the X,Y and Z planes pairs) - if ( t2 < tMax ) + if (t2 < tMax) tMax = t2; // tMin is the farthest "near" intersection (amongst the X,Y and Z planes pairs) - if ( t1 > tMin ) + if (t1 > tMin) tMin = t1; // And here's the trick : // If "far" is closer than "near", then there is NO intersection. // See the images in the tutorials for the visual explanation. - if (tMax < tMin ) + if (tMax < tMin) return false; - } + } else // Rare case: The ray is almost parallel to the planes, so they don't have any "intersection" { - if(-e + box.Min.X > 0.0f || -e + box.Max.X < 0.0f) + if (-e + box.Min.X > 0.0f || -e + box.Max.X < 0.0f) return false; } } - + // Test intersection with the 2 planes perpendicular to the OBB's Y axis // Exactly the same thing than above. { var yAxis = new Vec3(transform.M21, transform.M22, transform.M23); float e = Vec3.Dot(yAxis, delta); float f = Vec3.Dot(direction, yAxis); - + if (Math.Abs(f) > 0.001f) { float t1 = (e + box.Min.Y) / f; float t2 = (e + box.Max.Y) / f; - if (t1>t2) + if (t1 > t2) (t1, t2) = (t2, t1); - if ( t2 < tMax ) + if (t2 < tMax) tMax = t2; - if ( t1 > tMin ) + if (t1 > tMin) tMin = t1; - if (tMax < tMin ) + if (tMax < tMin) return false; - } + } else { - if(-e + box.Min.Y > 0.0f || -e + box.Max.Y < 0.0f) + if (-e + box.Min.Y > 0.0f || -e + box.Max.Y < 0.0f) return false; } } @@ -121,31 +121,31 @@ public static bool RayIntersectOBB(Vec3 origin, Vec3 direction, BoundingBox box, var zAxis = new Vec3(transform.M31, transform.M32, transform.M33); float e = Vec3.Dot(zAxis, delta); float f = Vec3.Dot(direction, zAxis); - + if (Math.Abs(f) > 0.001f) { float t1 = (e + box.Min.Z) / f; float t2 = (e + box.Max.Z) / f; - if (t1>t2) + if (t1 > t2) (t1, t2) = (t2, t1); - if ( t2 < tMax ) + if (t2 < tMax) tMax = t2; - if ( t1 > tMin ) + if (t1 > tMin) tMin = t1; - if (tMax < tMin ) + if (tMax < tMin) return false; - } + } else { - if(-e + box.Min.Z > 0.0f || -e + box.Max.Z < 0.0f) + if (-e + box.Min.Z > 0.0f || -e + box.Max.Z < 0.0f) return false; } } - + t = tMin; return true; } -} \ No newline at end of file +} diff --git a/Source/Mod/ImGui/FujiDebugMenu.cs b/Source/Mod/ImGui/FujiDebugMenu.cs index 5064aef5..794699f1 100644 --- a/Source/Mod/ImGui/FujiDebugMenu.cs +++ b/Source/Mod/ImGui/FujiDebugMenu.cs @@ -17,10 +17,11 @@ public override void Update() } } - public override void Render() { + public override void Render() + { if (Game.Scene is not World world) return; - + ImGui.SetNextWindowSizeConstraints(new Vec2(300, 300), new Vec2(float.PositiveInfinity, float.PositiveInfinity)); ImGui.Begin("Celeste 64 - Debug Menu"); @@ -54,7 +55,7 @@ public override void Render() { int i = 0; foreach (var actor in world.All()) { - if(actor is Checkpoint checkpoint) + if (actor is Checkpoint checkpoint) { string checkpointName = string.IsNullOrEmpty(checkpoint.CheckpointName) ? $"Checkpoint {i}" : checkpoint.CheckpointName; if (ImGui.MenuItem(checkpointName)) @@ -62,7 +63,7 @@ public override void Render() { player.Position = checkpoint.Position; } i++; - } + } } ImGui.EndMenu(); } @@ -79,7 +80,7 @@ public override void Render() { } if (ImGui.MenuItem("Toggle Debug Fly")) { - if(player.StateMachine.State != Player.States.DebugFly) + if (player.StateMachine.State != Player.States.DebugFly) { player.StateMachine.State = Player.States.DebugFly; } diff --git a/Source/Mod/ImGui/ImGuiManager.cs b/Source/Mod/ImGui/ImGuiManager.cs index bdfc38f0..ef82282b 100644 --- a/Source/Mod/ImGui/ImGuiManager.cs +++ b/Source/Mod/ImGui/ImGuiManager.cs @@ -31,14 +31,14 @@ internal void UpdateHandlers() if (debugMenu.Active) debugMenu.Update(); - - if (Game.Scene is EditorWorld editor) - { - foreach (var handler in editor.Handlers) - { - if (handler.Active) handler.Update(); - } - } + + if (Game.Scene is EditorWorld editor) + { + foreach (var handler in editor.Handlers) + { + if (handler.Active) handler.Update(); + } + } foreach (var handler in Handlers) { @@ -49,16 +49,16 @@ internal void UpdateHandlers() internal void RenderHandlers() { renderer.BeforeRender(); - + if (debugMenu.Visible) debugMenu.Render(); - + if (Game.Scene is EditorWorld editor) { foreach (var handler in editor.Handlers) { if (handler.Visible) handler.Render(); - } + } } foreach (var handler in Handlers) diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 1713499c..7ffcb3e6 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -1,8 +1,8 @@ using Celeste64.Mod; -using System.Diagnostics; -using ModelEntry = (Celeste64.Actor Actor, Celeste64.Model Model); using Celeste64.Mod; using Celeste64.Mod.Editor; +using System.Diagnostics; +using ModelEntry = (Celeste64.Actor Actor, Celeste64.Model Model); namespace Celeste64; @@ -410,7 +410,8 @@ public override void Update() return; } - if(Panicked) { + if (Panicked) + { return; } // don't pour salt in wounds From 8b2138872f517a88954ca34de02f7ba4f9b3f05e Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 17:38:51 +0100 Subject: [PATCH 47/97] Code cleanup --- Source/Data/Assets.cs | 1 - Source/Mod/Core/GameMod.cs | 2 +- Source/Scenes/World.cs | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/Data/Assets.cs b/Source/Data/Assets.cs index 46af8a57..1fa84758 100644 --- a/Source/Data/Assets.cs +++ b/Source/Data/Assets.cs @@ -1,5 +1,4 @@ using Celeste64.Mod; -using Celeste64.Mod; using Celeste64.Mod.Editor; using System.Collections.Concurrent; using System.Diagnostics; diff --git a/Source/Mod/Core/GameMod.cs b/Source/Mod/Core/GameMod.cs index 4714d610..173f5bda 100644 --- a/Source/Mod/Core/GameMod.cs +++ b/Source/Mod/Core/GameMod.cs @@ -53,7 +53,7 @@ public abstract class GameMod // This is here to give mods easier access to these objects, so they don't have to get them themselves // Warning, these may be null if they haven't been initialized yet, so you should always do a null check before using them. public Game? Game => Game.Instance; - public World? World => Game?.Scene as World; + public World? World => Game.Scene as World; public Map? Map => World?.Map; public Player? Player => World?.Get(); diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 7ffcb3e6..766bef65 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -1,5 +1,4 @@ using Celeste64.Mod; -using Celeste64.Mod; using Celeste64.Mod.Editor; using System.Diagnostics; using ModelEntry = (Celeste64.Actor Actor, Celeste64.Model Model); From 521eb7a2fc582fde0ab844cc07e4a576ec435b42 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 18:49:05 +0100 Subject: [PATCH 48/97] Fix ImGui not receiving keyboard inputs --- Source/Mod/ImGui/ImGuiManager.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Source/Mod/ImGui/ImGuiManager.cs b/Source/Mod/ImGui/ImGuiManager.cs index ef82282b..47fd69e2 100644 --- a/Source/Mod/ImGui/ImGuiManager.cs +++ b/Source/Mod/ImGui/ImGuiManager.cs @@ -27,6 +27,10 @@ internal ImGuiManager() internal void UpdateHandlers() { + // Reset so that ImGui itself actually receives the inputs + WantCaptureKeyboard = false; + WantCaptureMouse = false; + renderer.Update(); if (debugMenu.Active) @@ -44,6 +48,10 @@ internal void UpdateHandlers() { if (handler.Active) handler.Update(); } + + var io = ImGui.GetIO(); + WantCaptureKeyboard = io.WantCaptureKeyboard; + WantCaptureMouse = io.WantCaptureMouse; } internal void RenderHandlers() @@ -66,10 +74,6 @@ internal void RenderHandlers() if (handler.Visible) handler.Render(); } renderer.AfterRender(); - - var io = ImGui.GetIO(); - WantCaptureKeyboard = io.WantCaptureKeyboard; - WantCaptureMouse = io.WantCaptureMouse; } internal void RenderTexture(Batcher batch) From b0ecb9dc6b196315ca58593471efaaa3766ecec4 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 18:49:55 +0100 Subject: [PATCH 49/97] Add settings window for environment --- Source/Data/Map.cs | 10 ++-- Source/Mod/Editor/EditorWorld.cs | 53 +++++++++++++++++++ Source/Mod/Editor/FujiMap.cs | 4 +- .../Mod/Editor/Windows/EnvironmentSettings.cs | 42 +++++++++++++++ 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 Source/Mod/Editor/Windows/EnvironmentSettings.cs diff --git a/Source/Data/Map.cs b/Source/Data/Map.cs index aaa8d34e..24c7b65f 100644 --- a/Source/Data/Map.cs +++ b/Source/Data/Map.cs @@ -13,11 +13,11 @@ public abstract class Map public string Filename { get; init; } public string Folder { get; init; } - public string? Skybox { get; init; } - public float SnowAmount { get; init; } - public Vec3 SnowWind { get; init; } - public string? Music { get; init; } - public string? Ambience { get; init; } + public string? Skybox { get; internal set; } + public float SnowAmount { get; internal set; } + public Vec3 SnowWind { get; internal set; } + public string? Music { get; internal set; } + public string? Ambience { get; internal set; } public abstract void Load(World world); } diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index eca07671..e12763c2 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -9,6 +9,7 @@ public class EditorWorld : World internal readonly ImGuiHandler[] Handlers = [ new TestWindow(), + new EnvironmentSettings(), ]; public List Definitions => Map is FujiMap fujiMap ? fujiMap.Definitions : []; @@ -34,6 +35,58 @@ internal EditorWorld(EntryInfo entry) : base(entry) // Map gets implicitly loaded, since our Definitions are taken directly from it } + + internal void RefreshEnvironment() + { + if (Map == null) + return; + + // Taken from World constructor + // NOTE: Even tho it's currently disabled, it's here for completeness + if (Type == WorldType.Game) + { + if (Map.SnowAmount > 0) + Add(new Snow(Map.SnowAmount, Map.SnowWind)); + + if (Map.Music != null && Assets.Music.ContainsKey(Map.Music)) + { + MusicWav = Map.Music; + Music = $"event:/music/"; + } + else + { + MusicWav = ""; + Music = $"event:/music/{Map.Music}"; + } + + if (Map.Ambience != null && Assets.Music.ContainsKey(Map.Ambience)) + { + AmbienceWav = Map.Ambience; + Ambience = $"event:/sfx/ambience/"; + } + else + { + AmbienceWav = ""; + Ambience = $"event:/sfx/ambience/{Map.Ambience}"; + } + } + + skyboxes.Clear(); + if (!string.IsNullOrEmpty(Map.Skybox)) + { + // single skybox + if (Assets.Textures.TryGetValue($"skyboxes/{Map.Skybox}", out var skybox)) + { + skyboxes.Add(new(skybox)); + } + // group + else + { + while (Assets.Textures.TryGetValue($"skyboxes/{Map.Skybox}_{skyboxes.Count}", out var nextSkybox)) + skyboxes.Add(new(nextSkybox)); + } + } + } private float previousScale = 1.0f; public override void Entered() diff --git a/Source/Mod/Editor/FujiMap.cs b/Source/Mod/Editor/FujiMap.cs index 2f208d53..11d8cb04 100644 --- a/Source/Mod/Editor/FujiMap.cs +++ b/Source/Mod/Editor/FujiMap.cs @@ -11,12 +11,12 @@ public class FujiMap : Map /// /// Magic 4 bytes at the start of the file, to indicate the format. /// - public static readonly byte[] FormatMagic = [(byte)'F', (byte)'U', (byte)'J', (byte)'I']; + private static readonly byte[] FormatMagic = [(byte)'F', (byte)'U', (byte)'J', (byte)'I']; /// /// Current version of the map format. Needs to be incremented with every change to it. /// - public const byte FormatVersion = 1; + private const byte FormatVersion = 1; public readonly string? FullPath; public readonly List Definitions = []; diff --git a/Source/Mod/Editor/Windows/EnvironmentSettings.cs b/Source/Mod/Editor/Windows/EnvironmentSettings.cs new file mode 100644 index 00000000..0b5ba8c4 --- /dev/null +++ b/Source/Mod/Editor/Windows/EnvironmentSettings.cs @@ -0,0 +1,42 @@ +using ImGuiNET; + +namespace Celeste64.Mod.Editor; + +public class EnvironmentSettings : EditorWindow +{ + protected override string Title => "Environment Settings"; + protected override void RenderWindow(EditorWorld editor) + { + if (editor.Map == null) + return; + + bool changed = false; + + // AFAIK just passing a large value works fine for C# strings + const int bufferSize = 32767; + + // TODO: Add an asset picker?? + string skybox = editor.Map.Skybox ?? string.Empty; + changed |= ImGui.InputText("Skybox", ref skybox, bufferSize); + editor.Map.Skybox = skybox; + + float snowAmount = editor.Map.SnowAmount; + changed |= ImGui.DragFloat("Snow Amount", ref snowAmount, v_speed: 0.1f, v_min: 0.0f); + editor.Map.SnowAmount = snowAmount; + + var snowWind = editor.Map.SnowWind; + changed |= ImGui.DragFloat3("Snow Wind", ref snowWind, v_speed: 0.1f); + editor.Map.SnowWind = snowWind; + + string music = editor.Map.Music ?? string.Empty; + changed |= ImGui.InputText("Music", ref music, bufferSize); + editor.Map.Music = music; + + string ambience = editor.Map.Ambience ?? string.Empty; + changed |= ImGui.InputText("Ambience", ref ambience, bufferSize); + editor.Map.Ambience = ambience; + + if (changed) + editor.RefreshEnvironment(); + } +} From 49d1289156aa51b247d81dbfe36c1bc7221dd36d Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 19:06:41 +0100 Subject: [PATCH 50/97] Add main menu bar for changing view settings --- Source/Data/Save.cs | 14 ++++- Source/Mod/Editor/EditorWorld.cs | 56 ++++++++++--------- Source/Mod/Editor/GUI/EditorMenuBar.cs | 40 +++++++++++++ .../Editor/{Windows => GUI}/EditorWindow.cs | 0 .../{Windows => GUI}/EnvironmentSettings.cs | 0 .../Mod/Editor/{Windows => GUI}/TestWindow.cs | 0 Source/Scenes/World.cs | 34 ++++++----- 7 files changed, 100 insertions(+), 44 deletions(-) create mode 100644 Source/Mod/Editor/GUI/EditorMenuBar.cs rename Source/Mod/Editor/{Windows => GUI}/EditorWindow.cs (100%) rename Source/Mod/Editor/{Windows => GUI}/EnvironmentSettings.cs (100%) rename Source/Mod/Editor/{Windows => GUI}/TestWindow.cs (100%) diff --git a/Source/Data/Save.cs b/Source/Data/Save.cs index d485e8b2..c8ef9c95 100644 --- a/Source/Data/Save.cs +++ b/Source/Data/Save.cs @@ -104,6 +104,13 @@ public bool SettingsGetBool(string name, bool defaultValue = false) public bool SettingsSetBool(string name, bool value = false) => SettingsBoolData[name] = value; } + + public class EditorSettings + { + public bool RenderSnow { get; set; } = false; + public bool PlayMusic { get; set; } = false; + public bool PlayAmbience { get; set; } = false; + } public static Save Instance = new(); @@ -163,7 +170,7 @@ public bool SettingsSetBool(string name, bool value = false) public string SkinName { get; set; } = "Madeline"; /// - /// Fuji Custom - Whether we should write to the log file or not. + /// Fuji Custom - Whether we should write to the log file or not /// public bool WriteLog { get; set; } = true; @@ -171,6 +178,11 @@ public bool SettingsSetBool(string name, bool value = false) /// Fuji Custom - Whether The debug menu should be enabled /// public bool EnableDebugMenu { get; set; } = false; + + /// + /// Fuji Custom - Settings for the in-game editor + /// + public EditorSettings Editor { get; set; } = new(); /// /// Fuji Custom - Records for each mod diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index e12763c2..778be759 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -8,6 +8,8 @@ public class EditorWorld : World private const float EditorResolutionScale = 3.0f; internal readonly ImGuiHandler[] Handlers = [ + new EditorMenuBar(), + new TestWindow(), new EnvironmentSettings(), ]; @@ -32,6 +34,9 @@ internal EditorWorld(EntryInfo entry) : base(entry) Camera.NearPlane = 0.1f; // Allow getting closer to objects Camera.FarPlane = 4000; // Increase render distance Camera.FOVMultiplier = 1.25f; // Higher FOV feels better in the editor + + // Load environment + RefreshEnvironment(); // Map gets implicitly loaded, since our Definitions are taken directly from it } @@ -41,34 +46,35 @@ internal void RefreshEnvironment() if (Map == null) return; - // Taken from World constructor - // NOTE: Even tho it's currently disabled, it's here for completeness - if (Type == WorldType.Game) + // Taken from World constructor with added cleanup of previously created stuff + + if (Get() is { } snow) + Destroy(snow); + if (Map.SnowAmount > 0 && Save.Instance.Editor.RenderSnow) { - if (Map.SnowAmount > 0) - Add(new Snow(Map.SnowAmount, Map.SnowWind)); + Add(new Snow(Map.SnowAmount, Map.SnowWind)); + } - if (Map.Music != null && Assets.Music.ContainsKey(Map.Music)) - { - MusicWav = Map.Music; - Music = $"event:/music/"; - } - else - { - MusicWav = ""; - Music = $"event:/music/{Map.Music}"; - } + if (Map.Music != null && Assets.Music.ContainsKey(Map.Music) && Save.Instance.Editor.PlayMusic) + { + MusicWav = Map.Music; + Music = $"event:/music/"; + } + else + { + MusicWav = ""; + Music = $"event:/music/{Map.Music}"; + } - if (Map.Ambience != null && Assets.Music.ContainsKey(Map.Ambience)) - { - AmbienceWav = Map.Ambience; - Ambience = $"event:/sfx/ambience/"; - } - else - { - AmbienceWav = ""; - Ambience = $"event:/sfx/ambience/{Map.Ambience}"; - } + if (Map.Ambience != null && Assets.Music.ContainsKey(Map.Ambience) && Save.Instance.Editor.PlayAmbience) + { + AmbienceWav = Map.Ambience; + Ambience = $"event:/sfx/ambience/"; + } + else + { + AmbienceWav = ""; + Ambience = $"event:/sfx/ambience/{Map.Ambience}"; } skyboxes.Clear(); diff --git a/Source/Mod/Editor/GUI/EditorMenuBar.cs b/Source/Mod/Editor/GUI/EditorMenuBar.cs new file mode 100644 index 00000000..2331f352 --- /dev/null +++ b/Source/Mod/Editor/GUI/EditorMenuBar.cs @@ -0,0 +1,40 @@ +using ImGuiNET; + +namespace Celeste64.Mod.Editor; + +public class EditorMenuBar : ImGuiHandler +{ + public override void Render() + { + bool changed = false; + + ImGui.BeginMainMenuBar(); + + if (ImGui.BeginMenu("Settings")) + { + bool music = Save.Instance.Editor.PlayMusic; + changed |= ImGui.Checkbox("Player Music", ref music); + Save.Instance.Editor.PlayMusic = music; + + bool ambience = Save.Instance.Editor.PlayAmbience; + changed |= ImGui.Checkbox("Play Ambience", ref ambience); + Save.Instance.Editor.PlayAmbience = ambience; + + ImGui.EndMenu(); + } + + if (ImGui.BeginMenu("View")) + { + bool snow = Save.Instance.Editor.RenderSnow; + changed |= ImGui.Checkbox("Show Snow", ref snow); + Save.Instance.Editor.RenderSnow = snow; + + ImGui.EndMenu(); + } + + ImGui.EndMainMenuBar(); + + if (changed) + (Game.Scene as EditorWorld)!.RefreshEnvironment(); + } +} diff --git a/Source/Mod/Editor/Windows/EditorWindow.cs b/Source/Mod/Editor/GUI/EditorWindow.cs similarity index 100% rename from Source/Mod/Editor/Windows/EditorWindow.cs rename to Source/Mod/Editor/GUI/EditorWindow.cs diff --git a/Source/Mod/Editor/Windows/EnvironmentSettings.cs b/Source/Mod/Editor/GUI/EnvironmentSettings.cs similarity index 100% rename from Source/Mod/Editor/Windows/EnvironmentSettings.cs rename to Source/Mod/Editor/GUI/EnvironmentSettings.cs diff --git a/Source/Mod/Editor/Windows/TestWindow.cs b/Source/Mod/Editor/GUI/TestWindow.cs similarity index 100% rename from Source/Mod/Editor/Windows/TestWindow.cs rename to Source/Mod/Editor/GUI/TestWindow.cs diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 766bef65..46c93416 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -180,7 +180,7 @@ public World(EntryInfo entry) } // environment - if (Type == WorldType.Game) // Don't create environment effects in the editor + if (Type == WorldType.Game) // Editor will create environment effects by itself { if (map.SnowAmount > 0) Add(new Snow(map.SnowAmount, map.SnowWind)); @@ -208,26 +208,24 @@ public World(EntryInfo entry) AmbienceWav = ""; Ambience = $"event:/sfx/ambience/{map.Ambience}"; } - } - - // But still show the skybox - if (!string.IsNullOrEmpty(map.Skybox)) - { - // single skybox - if (Assets.Textures.TryGetValue($"skyboxes/{map.Skybox}", out var skybox)) - { - skyboxes.Add(new(skybox)); - } - // group - else + + if (!string.IsNullOrEmpty(map.Skybox)) { - while (Assets.Textures.TryGetValue($"skyboxes/{map.Skybox}_{skyboxes.Count}", out var nextSkybox)) - skyboxes.Add(new(nextSkybox)); + // single skybox + if (Assets.Textures.TryGetValue($"skyboxes/{map.Skybox}", out var skybox)) + { + skyboxes.Add(new(skybox)); + } + // group + else + { + while (Assets.Textures.TryGetValue($"skyboxes/{map.Skybox}_{skyboxes.Count}", out var nextSkybox)) + skyboxes.Add(new(nextSkybox)); + } } } - - // The editor handles loading itself - if (Type == WorldType.Game) + + if (Type == WorldType.Game) // The editor handles loading itself { ModManager.Instance.OnPreMapLoaded(this, map); // load content From f3d874f54b01977a26be3026470954b4fe9ff4bc Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 19:12:53 +0100 Subject: [PATCH 51/97] Properly handle toggling music/ambience --- Source/Mod/Editor/EditorWorld.cs | 44 ++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 778be759..81146602 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -55,27 +55,51 @@ internal void RefreshEnvironment() Add(new Snow(Map.SnowAmount, Map.SnowWind)); } - if (Map.Music != null && Assets.Music.ContainsKey(Map.Music) && Save.Instance.Editor.PlayMusic) + Game.Instance.Music.Stop(); + Game.Instance.MusicWav?.Stop(); + if (Save.Instance.Editor.PlayMusic) { - MusicWav = Map.Music; - Music = $"event:/music/"; + if (Map.Music != null && Assets.Music.ContainsKey(Map.Music)) + { + MusicWav = Map.Music; + Music = $"event:/music/"; + } + else + { + MusicWav = ""; + Music = $"event:/music/{Map.Music}"; + } } else { - MusicWav = ""; - Music = $"event:/music/{Map.Music}"; + MusicWav = string.Empty; + Music = string.Empty; } + Game.Instance.Music = Audio.Play(Music); + Game.Instance.MusicWav = Audio.PlayMusic(MusicWav); - if (Map.Ambience != null && Assets.Music.ContainsKey(Map.Ambience) && Save.Instance.Editor.PlayAmbience) + Game.Instance.Ambience.Stop(); + Game.Instance.AmbienceWav?.Stop(); + if (Save.Instance.Editor.PlayAmbience) { - AmbienceWav = Map.Ambience; - Ambience = $"event:/sfx/ambience/"; + if (Map.Ambience != null && Assets.Music.ContainsKey(Map.Ambience)) + { + AmbienceWav = Map.Ambience; + Ambience = $"event:/sfx/ambience/"; + } + else + { + AmbienceWav = ""; + Ambience = $"event:/sfx/ambience/{Map.Ambience}"; + } } else { - AmbienceWav = ""; - Ambience = $"event:/sfx/ambience/{Map.Ambience}"; + AmbienceWav = string.Empty; + Ambience = string.Empty; } + Game.Instance.Ambience = Audio.Play(Ambience); + Game.Instance.AmbienceWav = Audio.PlayMusic(AmbienceWav); skyboxes.Clear(); if (!string.IsNullOrEmpty(Map.Skybox)) From 30dcb8b3c298fbd31e297507036e8f9a69fe2521 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 19:44:19 +0100 Subject: [PATCH 52/97] Add render distance and resolution options --- Source/Data/Save.cs | 18 ++++++++++++- Source/Mod/Editor/EditorWorld.cs | 18 ++++++++++--- Source/Mod/Editor/GUI/EditorMenuBar.cs | 36 ++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/Source/Data/Save.cs b/Source/Data/Save.cs index c8ef9c95..5d7ff789 100644 --- a/Source/Data/Save.cs +++ b/Source/Data/Save.cs @@ -107,9 +107,25 @@ public bool SettingsSetBool(string name, bool value = false) public class EditorSettings { - public bool RenderSnow { get; set; } = false; + // Settings public bool PlayMusic { get; set; } = false; public bool PlayAmbience { get; set; } = false; + + // View + public bool RenderSnow { get; set; } = false; + public bool RenderSkybox { get; set; } = true; + + public const float MinRenderDistance = 500.0f; + public const float MaxRenderDistance = 5000.0f; + private float renderDistance = 4000.0f; + public float RenderDistance + { + get => renderDistance; + set => renderDistance = Math.Clamp(value, MinRenderDistance, MaxRenderDistance); + } + + public enum Resolution { Game = 0, HD = 1, Native = 2 } + public Resolution ResolutionType { get; set; } = Resolution.HD; } public static Save Instance = new(); diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 81146602..69002e6b 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -32,9 +32,11 @@ public class EditorWorld : World internal EditorWorld(EntryInfo entry) : base(entry) { Camera.NearPlane = 0.1f; // Allow getting closer to objects - Camera.FarPlane = 4000; // Increase render distance Camera.FOVMultiplier = 1.25f; // Higher FOV feels better in the editor + // Store previous game resolution to restore it when exiting + previousScale = Game.ResolutionScale; + // Load environment RefreshEnvironment(); @@ -43,6 +45,15 @@ internal EditorWorld(EntryInfo entry) : base(entry) internal void RefreshEnvironment() { + Camera.FarPlane = Save.Instance.Editor.RenderDistance; + Game.ResolutionScale = Save.Instance.Editor.ResolutionType switch + { + Save.EditorSettings.Resolution.Game => 1.0f, + Save.EditorSettings.Resolution.HD => 3.0f, + Save.EditorSettings.Resolution.Native => Math.Max(App.Width / (float)Game.DefaultWidth, App.Height / (float)Game.DefaultHeight), + _ => throw new ArgumentOutOfRangeException(), + }; + if (Map == null) return; @@ -102,7 +113,7 @@ internal void RefreshEnvironment() Game.Instance.AmbienceWav = Audio.PlayMusic(AmbienceWav); skyboxes.Clear(); - if (!string.IsNullOrEmpty(Map.Skybox)) + if (!string.IsNullOrEmpty(Map.Skybox) && Save.Instance.Editor.RenderSkybox) { // single skybox if (Assets.Textures.TryGetValue($"skyboxes/{Map.Skybox}", out var skybox)) @@ -121,8 +132,7 @@ internal void RefreshEnvironment() private float previousScale = 1.0f; public override void Entered() { - previousScale = Game.ResolutionScale; - Game.ResolutionScale = EditorResolutionScale; + // Game.ResolutionScale = Save.; } public override void Exited() { diff --git a/Source/Mod/Editor/GUI/EditorMenuBar.cs b/Source/Mod/Editor/GUI/EditorMenuBar.cs index 2331f352..dfba0a24 100644 --- a/Source/Mod/Editor/GUI/EditorMenuBar.cs +++ b/Source/Mod/Editor/GUI/EditorMenuBar.cs @@ -29,6 +29,42 @@ public override void Render() changed |= ImGui.Checkbox("Show Snow", ref snow); Save.Instance.Editor.RenderSnow = snow; + bool skybox = Save.Instance.Editor.RenderSkybox; + changed |= ImGui.Checkbox("Show Skybox", ref skybox); + Save.Instance.Editor.RenderSkybox = skybox; + + float renderDistance = Save.Instance.Editor.RenderDistance; + changed |= ImGui.DragFloat("Render Distance", ref renderDistance, v_speed: 10.0f, v_min: Save.EditorSettings.MinRenderDistance, v_max: Save.EditorSettings.MaxRenderDistance); + Save.Instance.Editor.RenderDistance = renderDistance; + + string[] displayStrings = [ + "Game (640x360)", + "HD (1920x1080)", + $"Native ({App.Width}x{App.Height})", + ]; + + var resolutionType = Save.Instance.Editor.ResolutionType; + if (ImGui.BeginCombo("Resolution", displayStrings[(int)resolutionType])) + { + if (ImGui.Selectable(displayStrings[(int)Save.EditorSettings.Resolution.Game], resolutionType == Save.EditorSettings.Resolution.Game)) + { + Save.Instance.Editor.ResolutionType = Save.EditorSettings.Resolution.Game; + changed = true; + } + if (ImGui.Selectable(displayStrings[(int)Save.EditorSettings.Resolution.HD], resolutionType == Save.EditorSettings.Resolution.HD)) + { + Save.Instance.Editor.ResolutionType = Save.EditorSettings.Resolution.HD; + changed = true; + } + if (ImGui.Selectable(displayStrings[(int)Save.EditorSettings.Resolution.Native], resolutionType == Save.EditorSettings.Resolution.Native)) + { + Save.Instance.Editor.ResolutionType = Save.EditorSettings.Resolution.Native; + changed = true; + } + + ImGui.EndCombo(); + } + ImGui.EndMenu(); } From b9f2ed70f9eb3106f5738eb2c51087372dd4b6a1 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 19:46:40 +0100 Subject: [PATCH 53/97] Fix "Audio Event doesn't exist" warnings --- Source/Mod/Editor/EditorWorld.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 69002e6b..859e564f 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -86,8 +86,10 @@ internal void RefreshEnvironment() MusicWav = string.Empty; Music = string.Empty; } - Game.Instance.Music = Audio.Play(Music); - Game.Instance.MusicWav = Audio.PlayMusic(MusicWav); + if (!string.IsNullOrWhiteSpace(Music)) + Game.Instance.Music = Audio.Play(Music); + if (!string.IsNullOrWhiteSpace(MusicWav)) + Game.Instance.MusicWav = Audio.PlayMusic(MusicWav); Game.Instance.Ambience.Stop(); Game.Instance.AmbienceWav?.Stop(); @@ -109,8 +111,10 @@ internal void RefreshEnvironment() AmbienceWav = string.Empty; Ambience = string.Empty; } - Game.Instance.Ambience = Audio.Play(Ambience); - Game.Instance.AmbienceWav = Audio.PlayMusic(AmbienceWav); + if (!string.IsNullOrWhiteSpace(Ambience)) + Game.Instance.Ambience = Audio.Play(Ambience); + if (!string.IsNullOrWhiteSpace(AmbienceWav)) + Game.Instance.AmbienceWav = Audio.PlayMusic(AmbienceWav); skyboxes.Clear(); if (!string.IsNullOrEmpty(Map.Skybox) && Save.Instance.Editor.RenderSkybox) From 27e1e69de19aa007dd5880a54eb943f40f90f8a0 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 20:04:10 +0100 Subject: [PATCH 54/97] Improve actor editor window --- Source/Mod/Editor/EditorWorld.cs | 6 ++++-- .../GUI/{TestWindow.cs => EditActorWindow.cs} | 18 +++++++++++------- Source/Mod/Editor/GUI/EditorMenuBar.cs | 2 +- Source/Mod/Editor/GUI/EditorWindow.cs | 8 ++++---- ...ettings.cs => EnvironmentSettingsWindow.cs} | 3 ++- 5 files changed, 22 insertions(+), 15 deletions(-) rename Source/Mod/Editor/GUI/{TestWindow.cs => EditActorWindow.cs} (72%) rename Source/Mod/Editor/GUI/{EnvironmentSettings.cs => EnvironmentSettingsWindow.cs} (93%) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 859e564f..dbdb8c91 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -10,9 +10,11 @@ public class EditorWorld : World internal readonly ImGuiHandler[] Handlers = [ new EditorMenuBar(), - new TestWindow(), - new EnvironmentSettings(), + new EditActorWindow(), + new EnvironmentSettingsWindow(), ]; + + public static EditorWorld Current => (Game.Scene as EditorWorld)!; public List Definitions => Map is FujiMap fujiMap ? fujiMap.Definitions : []; public ReadOnlyDictionary ActorsFromDefinition => actorsFromDefinition.AsReadOnly(); diff --git a/Source/Mod/Editor/GUI/TestWindow.cs b/Source/Mod/Editor/GUI/EditActorWindow.cs similarity index 72% rename from Source/Mod/Editor/GUI/TestWindow.cs rename to Source/Mod/Editor/GUI/EditActorWindow.cs index ae5eeba3..2944d98e 100644 --- a/Source/Mod/Editor/GUI/TestWindow.cs +++ b/Source/Mod/Editor/GUI/EditActorWindow.cs @@ -3,21 +3,21 @@ namespace Celeste64.Mod.Editor; -public class TestWindow : EditorWindow +public class EditActorWindow() : EditorWindow("EditActor") { - protected override string Title => "Test"; + // TODO: Properly display selected name + protected override string Title => EditorWorld.Current.Selected is { } selected + ? $"Edit Actor - {selected}" + : "Edit Actor - Nothing selected"; protected override void RenderWindow(EditorWorld editor) { - ImGui.Text("Testing"); - - if (ImGui.Button("Add Spikes")) + // TODO: Add some actor picker + if (ImGui.Button("DEBUG: Add Spikes")) { editor.Definitions.Add(new SpikeBlock.Definition()); } - ImGui.Text($"Selected: {editor.Selected}"); - if (editor.Selected is { } selected) { var props = selected.GetType() @@ -51,5 +51,9 @@ protected override void RenderWindow(EditorWorld editor) } } } + else + { + ImGui.Text("Nothing selected"); + } } } diff --git a/Source/Mod/Editor/GUI/EditorMenuBar.cs b/Source/Mod/Editor/GUI/EditorMenuBar.cs index dfba0a24..055a23fa 100644 --- a/Source/Mod/Editor/GUI/EditorMenuBar.cs +++ b/Source/Mod/Editor/GUI/EditorMenuBar.cs @@ -71,6 +71,6 @@ public override void Render() ImGui.EndMainMenuBar(); if (changed) - (Game.Scene as EditorWorld)!.RefreshEnvironment(); + EditorWorld.Current.RefreshEnvironment(); } } diff --git a/Source/Mod/Editor/GUI/EditorWindow.cs b/Source/Mod/Editor/GUI/EditorWindow.cs index 675bb20b..4324d90e 100644 --- a/Source/Mod/Editor/GUI/EditorWindow.cs +++ b/Source/Mod/Editor/GUI/EditorWindow.cs @@ -2,15 +2,15 @@ namespace Celeste64.Mod.Editor; -public abstract class EditorWindow : ImGuiHandler +public abstract class EditorWindow(string id) : ImGuiHandler { - protected abstract string Title { get; } + protected virtual string Title => id; protected abstract void RenderWindow(EditorWorld editor); public sealed override void Render() { - ImGui.Begin(Title); - RenderWindow((Game.Scene as EditorWorld)!); + ImGui.Begin($"{Title}###{id}"); + RenderWindow(EditorWorld.Current); ImGui.End(); } } diff --git a/Source/Mod/Editor/GUI/EnvironmentSettings.cs b/Source/Mod/Editor/GUI/EnvironmentSettingsWindow.cs similarity index 93% rename from Source/Mod/Editor/GUI/EnvironmentSettings.cs rename to Source/Mod/Editor/GUI/EnvironmentSettingsWindow.cs index 0b5ba8c4..f855c5ca 100644 --- a/Source/Mod/Editor/GUI/EnvironmentSettings.cs +++ b/Source/Mod/Editor/GUI/EnvironmentSettingsWindow.cs @@ -2,9 +2,10 @@ namespace Celeste64.Mod.Editor; -public class EnvironmentSettings : EditorWindow +public class EnvironmentSettingsWindow() : EditorWindow("EnvironmentSettings") { protected override string Title => "Environment Settings"; + protected override void RenderWindow(EditorWorld editor) { if (editor.Map == null) From 29dbb611499756fc9e38dd7103f0a6171921be7a Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 20:09:05 +0100 Subject: [PATCH 55/97] Properly reload editor scene --- Source/Game.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Game.cs b/Source/Game.cs index 17c48aeb..0f794024 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -360,23 +360,23 @@ internal void ReloadAssets() if (IsMidTransition) return; - if (scene is World world) + if (scene is EditorWorld editor) { Goto(new Transition() { Mode = Transition.Modes.Replace, - Scene = () => new World(world.Entry), + Scene = () => new EditorWorld(editor.Entry), ToPause = true, ToBlack = new AngledWipe(), PerformAssetReload = true }); } - else if (scene is EditorWorld editor) + else if (scene is World world) { Goto(new Transition() { Mode = Transition.Modes.Replace, - Scene = () => new EditorWorld(editor.Entry), + Scene = () => new World(world.Entry), ToPause = true, ToBlack = new AngledWipe(), PerformAssetReload = true From fc5e2f549d0d900dd9aa8a4dcd5987c6431c3545 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 20:12:42 +0100 Subject: [PATCH 56/97] Add support for removing actors --- Source/Mod/Editor/EditorWorld.cs | 13 +++++++++++++ Source/Mod/Editor/GUI/EditActorWindow.cs | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index dbdb8c91..92f2c6bd 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -144,6 +144,19 @@ public override void Exited() { Game.ResolutionScale = previousScale; } + + public void RemoveDefinition(ActorDefinition definition) + { + Definitions.Remove(definition); + if (actorsFromDefinition.Remove(definition, out var actors)) + { + foreach (var actor in actors) + { + definitionFromActors.Remove(actor); + Destroy(actor); + } + } + } public override void Update() { diff --git a/Source/Mod/Editor/GUI/EditActorWindow.cs b/Source/Mod/Editor/GUI/EditActorWindow.cs index 2944d98e..a133f1a4 100644 --- a/Source/Mod/Editor/GUI/EditActorWindow.cs +++ b/Source/Mod/Editor/GUI/EditActorWindow.cs @@ -50,6 +50,12 @@ protected override void RenderWindow(EditorWorld editor) break; } } + + ImGui.NewLine(); + if (ImGui.Button("Remove Actor") || Input.Keyboard.Pressed(Keys.Delete)) + { + editor.RemoveDefinition(selected); + } } else { From d17f9252e6640820612ec32d6330f6a9ce73abda Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 20:18:14 +0100 Subject: [PATCH 57/97] Prevent snow from being selected --- Source/Mod/Editor/EditorWorld.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 92f2c6bd..2b69d248 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -524,6 +524,9 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R // TODO: Allow selecting decorations, since they're currently one giant object if (actor is Decoration or FloatingDecoration) continue; + // Snow is not edited as an actor, but rather through the environment settings + if (actor is Snow) + continue; if (actor is not Solid solid) { From e718b95a4b1ca21d0b1e552d6b4fb11840580739 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 14 Mar 2024 21:05:59 +0100 Subject: [PATCH 58/97] Add 720p resolution option --- Source/Data/Save.cs | 4 ++-- Source/Mod/Editor/EditorWorld.cs | 1 + Source/Mod/Editor/GUI/EditorMenuBar.cs | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/Data/Save.cs b/Source/Data/Save.cs index 5d7ff789..0b534aa4 100644 --- a/Source/Data/Save.cs +++ b/Source/Data/Save.cs @@ -124,8 +124,8 @@ public float RenderDistance set => renderDistance = Math.Clamp(value, MinRenderDistance, MaxRenderDistance); } - public enum Resolution { Game = 0, HD = 1, Native = 2 } - public Resolution ResolutionType { get; set; } = Resolution.HD; + public enum Resolution { Game = 0, Double = 1, HD = 2, Native = 3 } + public Resolution ResolutionType { get; set; } = Resolution.Double; } public static Save Instance = new(); diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 2b69d248..4d80ecce 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -51,6 +51,7 @@ internal void RefreshEnvironment() Game.ResolutionScale = Save.Instance.Editor.ResolutionType switch { Save.EditorSettings.Resolution.Game => 1.0f, + Save.EditorSettings.Resolution.Double => 2.0f, Save.EditorSettings.Resolution.HD => 3.0f, Save.EditorSettings.Resolution.Native => Math.Max(App.Width / (float)Game.DefaultWidth, App.Height / (float)Game.DefaultHeight), _ => throw new ArgumentOutOfRangeException(), diff --git a/Source/Mod/Editor/GUI/EditorMenuBar.cs b/Source/Mod/Editor/GUI/EditorMenuBar.cs index 055a23fa..5818090c 100644 --- a/Source/Mod/Editor/GUI/EditorMenuBar.cs +++ b/Source/Mod/Editor/GUI/EditorMenuBar.cs @@ -39,6 +39,7 @@ public override void Render() string[] displayStrings = [ "Game (640x360)", + "720p (1280x720)", "HD (1920x1080)", $"Native ({App.Width}x{App.Height})", ]; @@ -51,6 +52,11 @@ public override void Render() Save.Instance.Editor.ResolutionType = Save.EditorSettings.Resolution.Game; changed = true; } + if (ImGui.Selectable(displayStrings[(int)Save.EditorSettings.Resolution.Double], resolutionType == Save.EditorSettings.Resolution.Double)) + { + Save.Instance.Editor.ResolutionType = Save.EditorSettings.Resolution.Double; + changed = true; + } if (ImGui.Selectable(displayStrings[(int)Save.EditorSettings.Resolution.HD], resolutionType == Save.EditorSettings.Resolution.HD)) { Save.Instance.Editor.ResolutionType = Save.EditorSettings.Resolution.HD; From 263387600ced23c9546946d21681e39b84fa4f48 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Fri, 15 Mar 2024 13:29:14 +0100 Subject: [PATCH 59/97] Clean-up Batcher3D a bit --- Source/Mod/Helpers/Batcher3D.cs | 90 +++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/Source/Mod/Helpers/Batcher3D.cs b/Source/Mod/Helpers/Batcher3D.cs index fac31133..4e75ab3e 100644 --- a/Source/Mod/Helpers/Batcher3D.cs +++ b/Source/Mod/Helpers/Batcher3D.cs @@ -50,14 +50,12 @@ public struct Vertex(Vec3 position, Vec2 texcoord, Color color) : IVertex private readonly Material material = new(Assets.Shaders["Sprite"]); private bool dirty = false; - public void Line(Vec3 from, Vec3 to, Color color, float thickness = 0.1f) => Line(from, to, color, Matrix4x4.Identity, thickness); + public void Line(Vec3 from, Vec3 to, Color color, float thickness = 0.1f) => Line(from, to, color, Matrix.Identity, thickness); public void Line(Vec3 from, Vec3 to, Color color, Matrix transform, float thickness = 0.1f) { var normal = (to - from).Normalized(); - // The other vector for the cross product can't be parallel to the normal - var tangent = Math.Abs(Vec3.Dot(normal, Vec3.UnitX)) < 0.5f ? Vec3.Cross(normal, Vec3.UnitX) : Vec3.Cross(normal, Vec3.UnitY); - var bitangent = Vec3.Cross(normal, tangent); - + var (tangent, bitangent) = GetTangentVectors(normal); + tangent *= thickness; bitangent *= thickness; @@ -66,7 +64,7 @@ public void Line(Vec3 from, Vec3 to, Color color, Matrix transform, float thickn color, transform); } - public void Cube(Vec3 center, Color color, float thickness = 0.1f) => Cube(center, color, Matrix4x4.Identity, thickness); + public void Cube(Vec3 center, Color color, float thickness = 0.1f) => Cube(center, color, Matrix.Identity, thickness); public void Cube(Vec3 center, Color color, Matrix transform, float thickness = 0.1f) { Box(center + new Vec3(-thickness, -thickness, -thickness), @@ -81,25 +79,27 @@ public void Cube(Vec3 center, Color color, Matrix transform, float thickness = 0 ); } - public void Torus(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Torus(center, radius, resolution, color, Matrix4x4.Identity, thickness); + public void Torus(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Torus(center, radius, resolution, color, Matrix.Identity, thickness); public void Torus(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) { var points = new Vec3[resolution]; float angleStep = Calc.TAU / resolution; - float angle = 0.0f; - for (int i = 0; i < resolution; i++, angle += angleStep) + for (int i = 0; i < resolution; i++) { - points[i] = new Vec3(Calc.AngleToVector(angle, radius), 0.0f); + points[i] = new Vec3(Calc.AngleToVector(i * angleStep, radius), 0.0f); } - EnsureVertexCapacity(vertexCount + resolution * 4); // 4 vertices each - EnsureIndexCapacity(indexCount + resolution * 4 * 2 * 3); // 4 faces * 2 triangles * 3 vertices each + int vtxCount = resolution * 4; // 4 vertices each + int idxCount = resolution * 4 * 2 * 3; // 4 faces * 2 triangles * 3 vertices each + + EnsureVertexCapacity(vertexCount + vtxCount); + EnsureIndexCapacity(indexCount + idxCount); unsafe { - Span vertices = new((Vertex*)vertexPtr + vertexCount, resolution * 4); - Span indices = new((int*)indexPtr + indexCount, resolution * 4 * 2 * 3); + var vertices = new Span((Vertex*)vertexPtr + vertexCount, vtxCount); + var indices = new Span((int*)indexPtr + indexCount, idxCount); for (int i = 0; i < resolution; i++) { @@ -152,30 +152,32 @@ public void Torus(Vec3 center, float radius, int resolution, Color color, Matrix } } - vertexCount += resolution * 4; - indexCount += resolution * 4 * 2 * 3; + vertexCount += vtxCount; + indexCount += idxCount; dirty = true; } - public void Disk(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Disk(center, radius, resolution, color, Matrix4x4.Identity, thickness); + public void Disk(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Disk(center, radius, resolution, color, Matrix.Identity, thickness); public void Disk(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) { var points = new Vec3[resolution]; float angleStep = Calc.TAU / resolution; - float angle = 0.0f; - for (int i = 0; i < resolution; i++, angle += angleStep) + for (int i = 0; i < resolution; i++) { - points[i] = new Vec3(Calc.AngleToVector(angle, radius), 0.0f); + points[i] = new Vec3(Calc.AngleToVector(i * angleStep, radius), 0.0f); } + + int vtxCount = resolution * 2 + 2; // 2 vertices each + 2 in the center + int idxCount = resolution * 4 * 3; // 1 faces for outside + 2 triangles on top/bottom = 4 triangles * 3 vertices each - EnsureVertexCapacity(vertexCount + resolution * 2 + 2); // 2 vertices each + 2 in the center - EnsureIndexCapacity(indexCount + resolution * 4 * 3); // 1 faces for outside + 2 triangles on top/bottom = 4 triangles * 3 vertices each + EnsureVertexCapacity(vertexCount + vtxCount); + EnsureIndexCapacity(indexCount + idxCount); unsafe { - Span vertices = new((Vertex*)vertexPtr + vertexCount, resolution * 2 + 2); - Span indices = new((int*)indexPtr + indexCount, resolution * 4 * 3); + var vertices = new Span((Vertex*)vertexPtr + vertexCount, vtxCount); + var indices = new Span((int*)indexPtr + indexCount, idxCount); var up = new Vec3(0.0f, 0.0f, thickness); vertices[0].Pos = Vec3.Transform(center - up, transform); @@ -214,25 +216,28 @@ public void Disk(Vec3 center, float radius, int resolution, Color color, Matrix } } - vertexCount += resolution * 2 + 2; - indexCount += resolution * 4 * 3; + vertexCount += vtxCount; + indexCount += idxCount; dirty = true; } - public void Sphere(Vec3 center, float radius, int resolution, Color color) => Sphere(center, radius, resolution, color, Matrix4x4.Identity); + public void Sphere(Vec3 center, float radius, int resolution, Color color) => Sphere(center, radius, resolution, color, Matrix.Identity); public void Sphere(Vec3 center, float radius, int resolution, Color color, Matrix transform) { // Taken and adapted from Utils.CreateSphere() int stackCount = resolution; int sliceCount = resolution; - EnsureVertexCapacity(vertexCount + 2 + (stackCount - 1) * sliceCount); - EnsureIndexCapacity(indexCount + sliceCount * 6 + (stackCount - 2) * sliceCount * 6); + int vtxCount = 2 + (stackCount - 1) * sliceCount; + int idxCount = sliceCount * 6 + (stackCount - 2) * sliceCount * 6; + + EnsureVertexCapacity(vertexCount + vtxCount); + EnsureIndexCapacity(indexCount + idxCount); unsafe { - Span vertices = new((Vertex*)vertexPtr + vertexCount, 2 + (stackCount - 1) * sliceCount); - Span indices = new((int*)indexPtr + indexCount, sliceCount * 6 + (stackCount - 2) * sliceCount * 6); + var vertices = new Span((Vertex*)vertexPtr + vertexCount, vtxCount); + var indices = new Span((int*)indexPtr + indexCount, idxCount); int vtx = 0; int idx = 0; @@ -334,13 +339,16 @@ public void Box(Vec3 v0, Vec3 v1, Vec3 v2, Vec3 v3, Vec3 v4, Vec3 v5, Vec3 v6, Vec3 v7, Color color, Matrix transform) { - EnsureVertexCapacity(vertexCount + 8); - EnsureIndexCapacity(indexCount + 6 * 2 * 3); // 6 faces * 2 triangles * 3 vertices + const int vtxCount = 8; + const int idxCount = 6 * 2 * 3; // 6 faces * 2 triangles * 3 vertices + + EnsureVertexCapacity(vertexCount + vtxCount); + EnsureIndexCapacity(indexCount + idxCount); unsafe { - Span vertices = new((Vertex*)vertexPtr + vertexCount, 8); - Span indices = new((int*)indexPtr + indexCount, 36); + var vertices = new Span((Vertex*)vertexPtr + vertexCount, vtxCount); + var indices = new Span((int*)indexPtr + indexCount, idxCount); vertices[0].Pos = Vec3.Transform(v0, transform); vertices[1].Pos = Vec3.Transform(v1, transform); @@ -403,8 +411,8 @@ public void Box(Vec3 v0, Vec3 v1, Vec3 v2, Vec3 v3, indices[35] = vertexCount + 7; } - vertexCount += 8; - indexCount += 6 * 2 * 3; + vertexCount += vtxCount; + indexCount += idxCount; dirty = true; } @@ -457,6 +465,14 @@ public void Clear() vertexCount = 0; indexCount = 0; } + + private (Vec3 Tangent, Vec3 Bitangent) GetTangentVectors(Vec3 normal) + { + // The other vector for the cross product can't be parallel to the normal + var tangent = Math.Abs(Vec3.Dot(normal, Vec3.UnitX)) < 0.5f ? Vec3.Cross(normal, Vec3.UnitX) : Vec3.Cross(normal, Vec3.UnitY); + var bitangent = Vec3.Cross(normal, tangent); + return (tangent, bitangent); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private unsafe void EnsureVertexCapacity(int capacity) From c2842f62c7849061de068e6198ddadfb9a65e4da Mon Sep 17 00:00:00 2001 From: psyGamer Date: Fri, 15 Mar 2024 14:13:37 +0100 Subject: [PATCH 60/97] Add cone shape to Batcher3D --- Source/Mod/Helpers/Batcher3D.cs | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/Source/Mod/Helpers/Batcher3D.cs b/Source/Mod/Helpers/Batcher3D.cs index 4e75ab3e..3cd7b11f 100644 --- a/Source/Mod/Helpers/Batcher3D.cs +++ b/Source/Mod/Helpers/Batcher3D.cs @@ -29,6 +29,8 @@ public struct Vertex(Vec3 position, Vec2 texcoord, Color color) : IVertex public readonly VertexFormat Format => VertexFormat; } + + public enum Direction { X, Y, Z } ~Batcher3D() { @@ -220,6 +222,78 @@ public void Disk(Vec3 center, float radius, int resolution, Color color, Matrix indexCount += idxCount; dirty = true; } + + public void Cone(Vec3 position, Direction direction, float length, float radius, int resolution, Color color) => Cone(position, direction, length, radius, resolution, color, Matrix.Identity); + public void Cone(Vec3 position, Direction direction, float length, float radius, int resolution, Color color, Matrix transform) + { + var points = new Vec3[resolution]; + + float angleStep = Calc.TAU / resolution; + + for (int i = 0; i < resolution; i++) + { + var vec = Calc.AngleToVector(i * angleStep, radius); + points[i] = direction switch + { + Direction.X => new Vec3(0.0f, vec.X, vec.Y), + Direction.Y => new Vec3(vec.X, 0.0f, vec.Y), + Direction.Z => new Vec3(vec.X, vec.Y, 0.0f), + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + } + + int vtxCount = resolution * 2 + 1 + 1; // 2 vertices each + 1 in the base center + 1 at the tip + int idxCount = resolution * 2 * 3; // 2 triangles on top/bottom * 3 vertices each + + EnsureVertexCapacity(vertexCount + vtxCount); + EnsureIndexCapacity(indexCount + idxCount); + + unsafe + { + var vertices = new Span((Vertex*)vertexPtr + vertexCount, vtxCount); + var indices = new Span((int*)indexPtr + indexCount, idxCount); + + var dir = direction switch + { + Direction.X => Vec3.UnitX, + Direction.Y => Vec3.UnitY, + Direction.Z => Vec3.UnitZ, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + + // Base center + vertices[0].Pos = position; + vertices[0].Col = color; + // Tip + vertices[1].Pos = position + dir * length; + vertices[1].Col = color; + + for (int i = 0; i < resolution; i++) + { + vertices[i + 2].Pos = Vec3.Transform(position + points[i], transform); + vertices[i + 2].Col = color; + } + + for (int i = 0; i < resolution; i++) + { + int curr = i; + int prev = i == 0 ? resolution - 1 : i - 1; // Wrap around to the end + + // Bottom + indices[i * (2 * 3) + 0] = vertexCount + (prev + 2); + indices[i * (2 * 3) + 1] = vertexCount + (curr + 2); + indices[i * (2 * 3) + 2] = vertexCount + 0; + // Top + indices[i * (2 * 3) + 3] = vertexCount + (curr + 2); + indices[i * (2 * 3) + 4] = vertexCount + (prev + 2); + indices[i * (2 * 3) + 5] = vertexCount + 1; + } + } + + vertexCount += vtxCount; + indexCount += idxCount; + dirty = true; + } public void Sphere(Vec3 center, float radius, int resolution, Color color) => Sphere(center, radius, resolution, color, Matrix.Identity); public void Sphere(Vec3 center, float radius, int resolution, Color color, Matrix transform) From b87e1ba50b4f71f86aab6a6e5e6e1de7d6341181 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Fri, 15 Mar 2024 14:24:02 +0100 Subject: [PATCH 61/97] Draw movement gizmos --- Source/Mod/Editor/EditorWorld.cs | 8 ++++ Source/Mod/Editor/Gizmo/Gizmo.cs | 6 +++ Source/Mod/Editor/Gizmo/PositionGizmo.cs | 53 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 Source/Mod/Editor/Gizmo/Gizmo.cs create mode 100644 Source/Mod/Editor/Gizmo/PositionGizmo.cs diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 4d80ecce..9bbcec26 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -475,6 +475,14 @@ public override void Render(Target target) } batch3D.Render(ref state); batch3D.Clear(); + + // Render gizmos on-top + target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); + { + (new PositionGizmo()).Render2(ref state, batch3D); + } + batch3D.Render(ref state); + batch3D.Clear(); // ui { diff --git a/Source/Mod/Editor/Gizmo/Gizmo.cs b/Source/Mod/Editor/Gizmo/Gizmo.cs new file mode 100644 index 00000000..508cc4a6 --- /dev/null +++ b/Source/Mod/Editor/Gizmo/Gizmo.cs @@ -0,0 +1,6 @@ +namespace Celeste64.Mod.Editor; + +public abstract class Gizmo +{ + public abstract void Render(ref RenderState state); +} diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs new file mode 100644 index 00000000..d771da87 --- /dev/null +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -0,0 +1,53 @@ +namespace Celeste64.Mod.Editor; + +public class PositionGizmo : Gizmo +{ + public override void Render(ref RenderState state) + { + if (EditorWorld.Current.Selected is not { } selected) + return; + + + } + + public void Render2(ref RenderState state, Batcher3D batch3D) + { + if (EditorWorld.Current.Selected is not SpikeBlock.Definition selected) + return; + + const float minScale = 10.0f; + float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, selected.Position) / 50.0f); + + float axisLen = 2.5f * scale; + float axisRadius = axisLen / 35.0f; + float coneLen = axisLen / 2.5f; + float coneRadius = coneLen / 3.0f; + + // const byte selectedAlpha = 0x7f; + // const byte deselectedAlpha = 0x2f; + const byte selectedAlpha = 0xff; + const byte deselectedAlpha = 0xff; + + var xColorSelected = new Color(0xff9999, selectedAlpha); + var yColorSelected = new Color(0xbfffbf, selectedAlpha); + var zColorSelected = new Color(0x8080ff, selectedAlpha); + var xColorDeselected = new Color(0xbf0000, deselectedAlpha); + var yColorDeselected = new Color(0x00bf00, deselectedAlpha); + var zColorDeselected = new Color(0x0000bf, deselectedAlpha); + + var xColor = false ? xColorSelected : xColorDeselected; + var yColor = false ? yColorSelected : yColorDeselected; + var zColor = false ? zColorSelected : zColorDeselected; + + // X + batch3D.Line(selected.Position, selected.Position + Vec3.UnitX * axisLen, xColor, axisRadius); + batch3D.Cone(selected.Position + Vec3.UnitX * axisLen, Batcher3D.Direction.X, coneLen, coneRadius, 12, xColor); + // Y + batch3D.Line(selected.Position, selected.Position + Vec3.UnitY * axisLen, yColor, axisRadius); + batch3D.Cone(selected.Position + Vec3.UnitY * axisLen, Batcher3D.Direction.Y, coneLen, coneRadius, 12, yColor); + // Z + batch3D.Line(selected.Position, selected.Position + Vec3.UnitZ * axisLen, zColor, axisRadius); + batch3D.Cone(selected.Position + Vec3.UnitZ * axisLen, Batcher3D.Direction.Z, coneLen, coneRadius, 12, zColor); + } +} + From 6b25fcc913faab2ca7ea393e769475473d72e8a4 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Fri, 15 Mar 2024 14:45:21 +0100 Subject: [PATCH 62/97] Draw XYZ move cube --- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index d771da87..bd53832f 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -16,9 +16,11 @@ public void Render2(ref RenderState state, Batcher3D batch3D) return; const float minScale = 10.0f; - float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, selected.Position) / 50.0f); + float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, selected.Position) / 20.0f); - float axisLen = 2.5f * scale; + float cubeSize = 0.15f * scale; + float padding = 0.15f * scale; + float axisLen = 1.5f * scale; float axisRadius = axisLen / 35.0f; float coneLen = axisLen / 2.5f; float coneRadius = coneLen / 3.0f; @@ -40,14 +42,17 @@ public void Render2(ref RenderState state, Batcher3D batch3D) var zColor = false ? zColorSelected : zColorDeselected; // X - batch3D.Line(selected.Position, selected.Position + Vec3.UnitX * axisLen, xColor, axisRadius); + batch3D.Line(selected.Position + Vec3.UnitX * (cubeSize + padding), selected.Position + Vec3.UnitX * axisLen, xColor, axisRadius); batch3D.Cone(selected.Position + Vec3.UnitX * axisLen, Batcher3D.Direction.X, coneLen, coneRadius, 12, xColor); // Y - batch3D.Line(selected.Position, selected.Position + Vec3.UnitY * axisLen, yColor, axisRadius); + batch3D.Line(selected.Position + Vec3.UnitY * (cubeSize + padding), selected.Position + Vec3.UnitY * axisLen, yColor, axisRadius); batch3D.Cone(selected.Position + Vec3.UnitY * axisLen, Batcher3D.Direction.Y, coneLen, coneRadius, 12, yColor); // Z - batch3D.Line(selected.Position, selected.Position + Vec3.UnitZ * axisLen, zColor, axisRadius); + batch3D.Line(selected.Position + Vec3.UnitZ * (cubeSize + padding), selected.Position + Vec3.UnitZ * axisLen, zColor, axisRadius); batch3D.Cone(selected.Position + Vec3.UnitZ * axisLen, Batcher3D.Direction.Z, coneLen, coneRadius, 12, zColor); + + // XYZ + batch3D.Cube(selected.Position, Color.White, cubeSize); } } From bab4c1111b2f4f8d6fa70d23d1e6c748998a4c7a Mon Sep 17 00:00:00 2001 From: psyGamer Date: Fri, 15 Mar 2024 14:58:58 +0100 Subject: [PATCH 63/97] Draw XZ/YZ/XY move planes --- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 33 +++++++--- Source/Mod/Helpers/Batcher3D.cs | 81 ++++++++++++++++++++---- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index bd53832f..b9c4624a 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -19,6 +19,7 @@ public void Render2(ref RenderState state, Batcher3D batch3D) float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, selected.Position) / 20.0f); float cubeSize = 0.15f * scale; + float planeSize = 0.6f * scale; float padding = 0.15f * scale; float axisLen = 1.5f * scale; float axisRadius = axisLen / 35.0f; @@ -37,20 +38,34 @@ public void Render2(ref RenderState state, Batcher3D batch3D) var yColorDeselected = new Color(0x00bf00, deselectedAlpha); var zColorDeselected = new Color(0x0000bf, deselectedAlpha); - var xColor = false ? xColorSelected : xColorDeselected; - var yColor = false ? yColorSelected : yColorDeselected; - var zColor = false ? zColorSelected : zColorDeselected; + var xAxisColor = false ? xColorSelected : xColorDeselected; + var yAxisColor = false ? yColorSelected : yColorDeselected; + var zAxisColor = false ? zColorSelected : zColorDeselected; + + var xzPlaneColor = false ? yColorSelected : yColorDeselected; + var yzPlaneColor = false ? xColorSelected : xColorDeselected; + var xyPlaneColor = false ? zColorSelected : zColorDeselected; // X - batch3D.Line(selected.Position + Vec3.UnitX * (cubeSize + padding), selected.Position + Vec3.UnitX * axisLen, xColor, axisRadius); - batch3D.Cone(selected.Position + Vec3.UnitX * axisLen, Batcher3D.Direction.X, coneLen, coneRadius, 12, xColor); + batch3D.Line(selected.Position + Vec3.UnitX * (cubeSize + padding), selected.Position + Vec3.UnitX * axisLen, xAxisColor, axisRadius); + batch3D.Cone(selected.Position + Vec3.UnitX * axisLen, Batcher3D.Direction.X, coneLen, coneRadius, 12, xAxisColor); // Y - batch3D.Line(selected.Position + Vec3.UnitY * (cubeSize + padding), selected.Position + Vec3.UnitY * axisLen, yColor, axisRadius); - batch3D.Cone(selected.Position + Vec3.UnitY * axisLen, Batcher3D.Direction.Y, coneLen, coneRadius, 12, yColor); + batch3D.Line(selected.Position + Vec3.UnitY * (cubeSize + padding), selected.Position + Vec3.UnitY * axisLen, yAxisColor, axisRadius); + batch3D.Cone(selected.Position + Vec3.UnitY * axisLen, Batcher3D.Direction.Y, coneLen, coneRadius, 12, yAxisColor); // Z - batch3D.Line(selected.Position + Vec3.UnitZ * (cubeSize + padding), selected.Position + Vec3.UnitZ * axisLen, zColor, axisRadius); - batch3D.Cone(selected.Position + Vec3.UnitZ * axisLen, Batcher3D.Direction.Z, coneLen, coneRadius, 12, zColor); + batch3D.Line(selected.Position + Vec3.UnitZ * (cubeSize + padding), selected.Position + Vec3.UnitZ * axisLen, zAxisColor, axisRadius); + batch3D.Cone(selected.Position + Vec3.UnitZ * axisLen, Batcher3D.Direction.Z, coneLen, coneRadius, 12, zAxisColor); + // XZ + batch3D.Square(selected.Position + Vec3.UnitX * (cubeSize + axisLen / 2.0f) + Vec3.UnitZ * (cubeSize + axisLen / 2.0f), + Vec3.UnitY, xzPlaneColor, planeSize / 2.0f); + // YZ + batch3D.Square(selected.Position + Vec3.UnitY * (cubeSize + axisLen / 2.0f) + Vec3.UnitZ * (cubeSize + axisLen / 2.0f), + Vec3.UnitX, yzPlaneColor, planeSize / 2.0f); + // XY + batch3D.Square(selected.Position + Vec3.UnitX * (cubeSize + axisLen / 2.0f) + Vec3.UnitY * (cubeSize + axisLen / 2.0f), + Vec3.UnitZ, xyPlaneColor, planeSize / 2.0f); + // XYZ batch3D.Cube(selected.Position, Color.White, cubeSize); } diff --git a/Source/Mod/Helpers/Batcher3D.cs b/Source/Mod/Helpers/Batcher3D.cs index 3cd7b11f..5410b4d2 100644 --- a/Source/Mod/Helpers/Batcher3D.cs +++ b/Source/Mod/Helpers/Batcher3D.cs @@ -51,6 +51,21 @@ public enum Direction { X, Y, Z } private readonly Mesh mesh = new(); private readonly Material material = new(Assets.Shaders["Sprite"]); private bool dirty = false; + + public void Square(Vec3 center, Vec3 normal, Color color, float size = 0.1f) => Square(center, normal, color, Matrix.Identity, size); + public void Square(Vec3 center, Vec3 normal, Color color, Matrix transform, float size = 0.1f) + { + var (tangent, bitangent) = GetTangentVectors(normal); + + tangent *= size; + bitangent *= size; + + Quad(center - tangent - bitangent, + center + tangent - bitangent, + center - tangent + bitangent, + center + tangent + bitangent, + color, transform); + } public void Line(Vec3 from, Vec3 to, Color color, float thickness = 0.1f) => Line(from, to, color, Matrix.Identity, thickness); public void Line(Vec3 from, Vec3 to, Color color, Matrix transform, float thickness = 0.1f) @@ -66,21 +81,21 @@ public void Line(Vec3 from, Vec3 to, Color color, Matrix transform, float thickn color, transform); } - public void Cube(Vec3 center, Color color, float thickness = 0.1f) => Cube(center, color, Matrix.Identity, thickness); - public void Cube(Vec3 center, Color color, Matrix transform, float thickness = 0.1f) + public void Cube(Vec3 center, Color color, float size = 0.1f) => Cube(center, color, Matrix.Identity, size); + public void Cube(Vec3 center, Color color, Matrix transform, float size = 0.1f) { - Box(center + new Vec3(-thickness, -thickness, -thickness), - center + new Vec3(thickness, -thickness, -thickness), - center + new Vec3(-thickness, thickness, -thickness), - center + new Vec3(thickness, thickness, -thickness), - center + new Vec3(-thickness, -thickness, thickness), - center + new Vec3(thickness, -thickness, thickness), - center + new Vec3(-thickness, thickness, thickness), - center + new Vec3(thickness, thickness, thickness), + Box(center + new Vec3(-size, -size, -size), + center + new Vec3(size, -size, -size), + center + new Vec3(-size, size, -size), + center + new Vec3(size, size, -size), + center + new Vec3(-size, -size, size), + center + new Vec3(size, -size, size), + center + new Vec3(-size, size, size), + center + new Vec3(size, size, size), color, transform ); } - + public void Torus(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Torus(center, radius, resolution, color, Matrix.Identity, thickness); public void Torus(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) { @@ -383,6 +398,50 @@ public void Sphere(Vec3 center, float radius, int resolution, Color color, Matri dirty = true; } } + + /// + /// Renders a quad of a solid color. + /// + /// Top Left + /// Top Right + /// Bottom Left + /// Bottom Right + /// Box color + public void Quad(Vec3 v0, Vec3 v1, Vec3 v2, Vec3 v3, + Color color, Matrix transform) + { + const int vtxCount = 4; + const int idxCount = 2 * 3; // 2 triangles * 3 vertices + + EnsureVertexCapacity(vertexCount + vtxCount); + EnsureIndexCapacity(indexCount + idxCount); + + unsafe + { + var vertices = new Span((Vertex*)vertexPtr + vertexCount, vtxCount); + var indices = new Span((int*)indexPtr + indexCount, idxCount); + + vertices[0].Pos = Vec3.Transform(v0, transform); + vertices[1].Pos = Vec3.Transform(v1, transform); + vertices[2].Pos = Vec3.Transform(v2, transform); + vertices[3].Pos = Vec3.Transform(v3, transform); + vertices[0].Col = color; + vertices[1].Col = color; + vertices[2].Col = color; + vertices[3].Col = color; + + indices[0] = vertexCount + 0; + indices[1] = vertexCount + 2; + indices[2] = vertexCount + 1; + indices[3] = vertexCount + 2; + indices[4] = vertexCount + 3; + indices[5] = vertexCount + 1; + } + + vertexCount += vtxCount; + indexCount += idxCount; + dirty = true; + } public void Box(Vec3 min, Vec3 max, Color color) => Box(min, max, color, Matrix.Identity); public void Box(Vec3 min, Vec3 max, Color color, Matrix transform) => From f0f279e18e6e31e5b5ef59b7bfaf4a861d5b02a5 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Fri, 15 Mar 2024 16:38:49 +0100 Subject: [PATCH 64/97] Use better ray/OBB intersection method --- Source/Mod/Helpers/ModUtils.cs | 142 +++++++++------------------------ 1 file changed, 37 insertions(+), 105 deletions(-) diff --git a/Source/Mod/Helpers/ModUtils.cs b/Source/Mod/Helpers/ModUtils.cs index 6e936e4b..85f4ff1e 100644 --- a/Source/Mod/Helpers/ModUtils.cs +++ b/Source/Mod/Helpers/ModUtils.cs @@ -37,115 +37,47 @@ public static bool RayIntersectsBox(Vec3 origin, Vec3 direction, BoundingBox box return tEnter < tExit; } - // Intersection method from "Real-Time Rendering and Essential Mathematics for Games" - // Reference Implementation: https://github.com/opengl-tutorials/ogl/blob/15e57f6cccef388915e565d8322b8442049e1bd8/misc05_picking/misc05_picking_custom.cpp#L83-L197 + // "Box Intersection Generic" from https://iquilezles.org/articles/boxfunctions public static bool RayIntersectOBB(Vec3 origin, Vec3 direction, BoundingBox box, Matrix transform, out float t) { t = 0.0f; - float tMin = 0.0f, tMax = 100000.0f; + if (!Matrix.Invert(transform, out var inverse)) + return false; + + // The center of the bounding box needs to be at <0,0,0> + inverse *= Matrix.CreateTranslation(-box.Center); + + // convert from world to box space + var ro = Vec3.Transform(origin, inverse); + var rd = Vec3.TransformNormal(direction, inverse); + + var rad = box.Size / 2.0f; + + // ray-box intersection in box space + var m = Vec3.One / rd; + var s = new Vec3( + (rd.X<0.0f)?1.0f:-1.0f, + (rd.Y<0.0f)?1.0f:-1.0f, + (rd.Z<0.0f)?1.0f:-1.0f); + var t1 = m*(-ro + s*rad); + var t2 = m*(-ro - s*rad); + + float tN = Math.Max( Math.Max( t1.X, t1.Y ), t1.Z ); + float tF = Math.Min( Math.Min( t2.X, t2.Y ), t2.Z ); + + if( tN>tF || tF<0.0) + return false; + + // compute normal (in world space), face and UV + // currently not required + // if( t1.x>t1.y && t1.x>t1.z ) { oN=txi[0].xyz*s.x; oU=ro.yz+rd.yz*t1.x; oF=([1+int(s.x))/[2]; + // else if( t1.y>t1.z ) { oN=txi[1].xyz*s.y; oU=ro.zx+rd.zx*t1.y; oF=([5+int(s.y))/[2]; + // else { oN=txi[2].xyz*s.z; oU=ro.xy+rd.xy*t1.z; oF=([9+int(s.z))/[2]; + + // exit point currently not required + // oT = vec2(tN,tF); + t = tN; - var oobWorldPos = new Vec3(transform.M41, transform.M42, transform.M43); - var delta = oobWorldPos - origin; - - // Test intersection with the 2 planes perpendicular to the OBB's X axis - { - var xAxis = new Vec3(transform.M11, transform.M12, transform.M13); - float e = Vec3.Dot(xAxis, delta); - float f = Vec3.Dot(direction, xAxis); - - if (Math.Abs(f) > 0.001f) // Standard case - { - float t1 = (e + box.Min.X) / f; // Intersection with the "left" plane - float t2 = (e + box.Max.X) / f; // Intersection with the "right" plane - // t1 and t2 now contain distances between ray origin and ray-plane intersections - - // We want t1 to represent the nearest intersection, - // so if it's not the case, invert t1 and t2 - if (t1 > t2) - (t1, t2) = (t2, t1); - - // tMax is the nearest "far" intersection (amongst the X,Y and Z planes pairs) - if (t2 < tMax) - tMax = t2; - // tMin is the farthest "near" intersection (amongst the X,Y and Z planes pairs) - if (t1 > tMin) - tMin = t1; - - // And here's the trick : - // If "far" is closer than "near", then there is NO intersection. - // See the images in the tutorials for the visual explanation. - if (tMax < tMin) - return false; - } - else // Rare case: The ray is almost parallel to the planes, so they don't have any "intersection" - { - if (-e + box.Min.X > 0.0f || -e + box.Max.X < 0.0f) - return false; - } - } - - // Test intersection with the 2 planes perpendicular to the OBB's Y axis - // Exactly the same thing than above. - { - var yAxis = new Vec3(transform.M21, transform.M22, transform.M23); - float e = Vec3.Dot(yAxis, delta); - float f = Vec3.Dot(direction, yAxis); - - if (Math.Abs(f) > 0.001f) - { - float t1 = (e + box.Min.Y) / f; - float t2 = (e + box.Max.Y) / f; - - if (t1 > t2) - (t1, t2) = (t2, t1); - - if (t2 < tMax) - tMax = t2; - if (t1 > tMin) - tMin = t1; - - if (tMax < tMin) - return false; - } - else - { - if (-e + box.Min.Y > 0.0f || -e + box.Max.Y < 0.0f) - return false; - } - } - - - // Test intersection with the 2 planes perpendicular to the OBB's Z axis - // Exactly the same thing than above. - { - var zAxis = new Vec3(transform.M31, transform.M32, transform.M33); - float e = Vec3.Dot(zAxis, delta); - float f = Vec3.Dot(direction, zAxis); - - if (Math.Abs(f) > 0.001f) - { - float t1 = (e + box.Min.Z) / f; - float t2 = (e + box.Max.Z) / f; - - if (t1 > t2) - (t1, t2) = (t2, t1); - - if (t2 < tMax) - tMax = t2; - if (t1 > tMin) - tMin = t1; - - if (tMax < tMin) - return false; - } - else - { - if (-e + box.Min.Z > 0.0f || -e + box.Max.Z < 0.0f) - return false; - } - } - - t = tMin; return true; } } From bd492fae7bb07b9c7dbf1f88af74ad5e331bfbcc Mon Sep 17 00:00:00 2001 From: psyGamer Date: Fri, 15 Mar 2024 17:22:17 +0100 Subject: [PATCH 65/97] Add selection support for the position gizmo --- Source/Mod/Editor/EditorWorld.cs | 53 ++++++++-- Source/Mod/Editor/Gizmo/Gizmo.cs | 8 ++ Source/Mod/Editor/Gizmo/PositionGizmo.cs | 119 +++++++++++++++++------ Source/Mod/Helpers/Batcher3D.cs | 4 +- 4 files changed, 145 insertions(+), 39 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 9bbcec26..43ec6145 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -30,6 +30,9 @@ public class EditorWorld : World private Vec2 cameraRot = new(0, 0); private readonly Batcher3D batch3D = new(); + + // TODO: Temporary! + private PositionGizmo posGizmo = new(); internal EditorWorld(EntryInfo entry) : base(entry) { @@ -192,9 +195,11 @@ public override void Update() float moveSpeed = 250.0f; if (Input.Keyboard.Down(Keys.W)) - cameraPos += cameraForward * moveSpeed * Time.Delta; + // cameraPos += cameraForward * moveSpeed * Time.Delta; + cameraPos += Camera.Forward * moveSpeed * Time.Delta; if (Input.Keyboard.Down(Keys.S)) - cameraPos -= cameraForward * moveSpeed * Time.Delta; + // cameraPos -= cameraForward * moveSpeed * Time.Delta; + cameraPos -= Camera.Forward * moveSpeed * Time.Delta; if (Input.Keyboard.Down(Keys.A)) cameraPos += cameraRight * moveSpeed * Time.Delta; if (Input.Keyboard.Down(Keys.D)) @@ -244,10 +249,44 @@ public override void Update() var worldPos = Vec4.Transform(eyePos, inverseView); var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); - if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) - Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; - else - Selected = null; + // First check if we hit a gizmo + (BoundingBox Bounds, GizmoTarget Target)[] gizmoTargets = [ + (posGizmo.xAxisBounds, GizmoTarget.AxisX), + (posGizmo.yAxisBounds, GizmoTarget.AxisY), + (posGizmo.zAxisBounds, GizmoTarget.AxisZ), + + (posGizmo.xzPlaneBounds, GizmoTarget.PlaneXZ), + (posGizmo.yzPlaneBounds, GizmoTarget.PlaneYZ), + (posGizmo.xyPlaneBounds, GizmoTarget.PlaneXY), + + (posGizmo.xyzCubeBounds, GizmoTarget.CubeXYZ), + ]; + + bool foundGizmo = false; + float closestGizmo = float.PositiveInfinity; + foreach (var (bounds, target) in gizmoTargets) + { + if (!ModUtils.RayIntersectOBB(Camera.Position, direction, bounds, posGizmo.Transform, out float dist) || dist >= closestGizmo) + continue; + + posGizmo.Target = target; + foundGizmo = true; + closestGizmo = dist; + } + + if (!foundGizmo) + { + posGizmo.Target = GizmoTarget.None; + + // Then check for actors + if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) + Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; + // Otherwise we clicked into nothing + else + Selected = null; + } + + Log.Info(posGizmo.Target); } } @@ -479,7 +518,7 @@ public override void Render(Target target) // Render gizmos on-top target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); { - (new PositionGizmo()).Render2(ref state, batch3D); + posGizmo.Render2(ref state, batch3D); } batch3D.Render(ref state); batch3D.Clear(); diff --git a/Source/Mod/Editor/Gizmo/Gizmo.cs b/Source/Mod/Editor/Gizmo/Gizmo.cs index 508cc4a6..f4ecd55d 100644 --- a/Source/Mod/Editor/Gizmo/Gizmo.cs +++ b/Source/Mod/Editor/Gizmo/Gizmo.cs @@ -4,3 +4,11 @@ public abstract class Gizmo { public abstract void Render(ref RenderState state); } + +public enum GizmoTarget +{ + None, + AxisX, AxisY, AxisZ, + PlaneXZ, PlaneYZ, PlaneXY, + CubeXYZ, +} diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index b9c4624a..27a53d4b 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -10,22 +10,77 @@ public override void Render(ref RenderState state) } + public GizmoTarget Target; + + static float cubeSize = 0.15f; + static float planeSize = 0.6f; + static float padding = 0.15f; + static float axisLen = 1.5f; + static float axisRadius => axisLen / 35.0f; + static float coneLen => axisLen / 2.5f; + static float coneRadius => coneLen / 3.0f; + + static float boundsPadding => 0.1f; + + // Axis + static float axisBoundsLengthMin => cubeSize + padding; + static float axisBoundsLengthMax => axisLen + coneLen * 0.9f; + static float axisBoundsRadiusMin => -axisRadius - boundsPadding; + static float axisBoundsRadiusMax => axisRadius + boundsPadding; + + public BoundingBox xAxisBounds => new( + new Vec3(axisBoundsLengthMin, axisBoundsRadiusMin, axisBoundsRadiusMin), + new Vec3(axisBoundsLengthMax, axisBoundsRadiusMax, axisBoundsRadiusMax)); + + public BoundingBox yAxisBounds => new( + new Vec3(axisBoundsRadiusMin, axisBoundsLengthMin, axisBoundsRadiusMin), + new Vec3(axisBoundsRadiusMax, axisBoundsLengthMax, axisBoundsRadiusMax)); + + public BoundingBox zAxisBounds => new( + new Vec3(axisBoundsRadiusMin, axisBoundsRadiusMin, axisBoundsLengthMin), + new Vec3(axisBoundsRadiusMax, axisBoundsRadiusMax, axisBoundsLengthMax)); + + // Planes + static float planeBoundsMin => cubeSize + axisLen / 2.0f - planeSize / 2.0f - boundsPadding; + static float planeBoundsMax => cubeSize + axisLen / 2.0f + planeSize / 2.0f + boundsPadding; + + public BoundingBox xzPlaneBounds => new( + new Vec3(planeBoundsMin, 0.0f, planeBoundsMin), + new Vec3(planeBoundsMax, 0.0f, planeBoundsMax)); + + public BoundingBox yzPlaneBounds => new( + new Vec3(0.0f, planeBoundsMin, planeBoundsMin), + new Vec3(0.0f, planeBoundsMax, planeBoundsMax)); + + public BoundingBox xyPlaneBounds => new( + new Vec3(planeBoundsMin, planeBoundsMin, 0.0f), + new Vec3(planeBoundsMax, planeBoundsMax, 0.0f)); + + // Cube + public BoundingBox xyzCubeBounds => new( + -new Vec3(cubeSize + boundsPadding), + new Vec3(cubeSize + boundsPadding)); + + public Matrix Transform + { + get + { + if (EditorWorld.Current.Selected is not SpikeBlock.Definition selected) + return Matrix.Identity; + + const float minScale = 10.0f; + float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, selected.Position) / 20.0f); + + return Matrix.CreateScale(scale) * + Matrix.CreateTranslation(selected.Position); + } + } + public void Render2(ref RenderState state, Batcher3D batch3D) { if (EditorWorld.Current.Selected is not SpikeBlock.Definition selected) return; - const float minScale = 10.0f; - float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, selected.Position) / 20.0f); - - float cubeSize = 0.15f * scale; - float planeSize = 0.6f * scale; - float padding = 0.15f * scale; - float axisLen = 1.5f * scale; - float axisRadius = axisLen / 35.0f; - float coneLen = axisLen / 2.5f; - float coneRadius = coneLen / 3.0f; - // const byte selectedAlpha = 0x7f; // const byte deselectedAlpha = 0x2f; const byte selectedAlpha = 0xff; @@ -34,40 +89,44 @@ public void Render2(ref RenderState state, Batcher3D batch3D) var xColorSelected = new Color(0xff9999, selectedAlpha); var yColorSelected = new Color(0xbfffbf, selectedAlpha); var zColorSelected = new Color(0x8080ff, selectedAlpha); + var cubeColorSelected = new Color(0xffffff, selectedAlpha); var xColorDeselected = new Color(0xbf0000, deselectedAlpha); var yColorDeselected = new Color(0x00bf00, deselectedAlpha); var zColorDeselected = new Color(0x0000bf, deselectedAlpha); + var cubeColorDeselected = new Color(0xbfbfbf, deselectedAlpha); + + var xAxisColor = Target == GizmoTarget.AxisX ? xColorSelected : xColorDeselected; + var yAxisColor = Target == GizmoTarget.AxisY ? yColorSelected : yColorDeselected; + var zAxisColor = Target == GizmoTarget.AxisZ ? zColorSelected : zColorDeselected; - var xAxisColor = false ? xColorSelected : xColorDeselected; - var yAxisColor = false ? yColorSelected : yColorDeselected; - var zAxisColor = false ? zColorSelected : zColorDeselected; + var xzPlaneColor = Target == GizmoTarget.PlaneXZ ? yColorSelected : yColorDeselected; + var yzPlaneColor = Target == GizmoTarget.PlaneYZ ? xColorSelected : xColorDeselected; + var xyPlaneColor = Target == GizmoTarget.PlaneXY ? zColorSelected : zColorDeselected; - var xzPlaneColor = false ? yColorSelected : yColorDeselected; - var yzPlaneColor = false ? xColorSelected : xColorDeselected; - var xyPlaneColor = false ? zColorSelected : zColorDeselected; + var xyzCubeColor = Target == GizmoTarget.CubeXYZ ? cubeColorSelected : cubeColorDeselected; // X - batch3D.Line(selected.Position + Vec3.UnitX * (cubeSize + padding), selected.Position + Vec3.UnitX * axisLen, xAxisColor, axisRadius); - batch3D.Cone(selected.Position + Vec3.UnitX * axisLen, Batcher3D.Direction.X, coneLen, coneRadius, 12, xAxisColor); + batch3D.Line(Vec3.UnitX * (cubeSize + padding), Vec3.UnitX * axisLen, xAxisColor, Transform, axisRadius); + batch3D.Cone(Vec3.UnitX * axisLen, Batcher3D.Direction.X, coneLen, coneRadius, 12, xAxisColor, Transform); // Y - batch3D.Line(selected.Position + Vec3.UnitY * (cubeSize + padding), selected.Position + Vec3.UnitY * axisLen, yAxisColor, axisRadius); - batch3D.Cone(selected.Position + Vec3.UnitY * axisLen, Batcher3D.Direction.Y, coneLen, coneRadius, 12, yAxisColor); + batch3D.Line(Vec3.UnitY * (cubeSize + padding), Vec3.UnitY * axisLen, yAxisColor, Transform, axisRadius); + batch3D.Cone(Vec3.UnitY * axisLen, Batcher3D.Direction.Y, coneLen, coneRadius, 12, yAxisColor, Transform); // Z - batch3D.Line(selected.Position + Vec3.UnitZ * (cubeSize + padding), selected.Position + Vec3.UnitZ * axisLen, zAxisColor, axisRadius); - batch3D.Cone(selected.Position + Vec3.UnitZ * axisLen, Batcher3D.Direction.Z, coneLen, coneRadius, 12, zAxisColor); + batch3D.Line(Vec3.UnitZ * (cubeSize + padding), Vec3.UnitZ * axisLen, zAxisColor, Transform, axisRadius); + batch3D.Cone(Vec3.UnitZ * axisLen, Batcher3D.Direction.Z, coneLen, coneRadius, 12, zAxisColor, Transform); // XZ - batch3D.Square(selected.Position + Vec3.UnitX * (cubeSize + axisLen / 2.0f) + Vec3.UnitZ * (cubeSize + axisLen / 2.0f), - Vec3.UnitY, xzPlaneColor, planeSize / 2.0f); + batch3D.Square(Vec3.UnitX * (cubeSize + axisLen / 2.0f) + Vec3.UnitZ * (cubeSize + axisLen / 2.0f), + Vec3.UnitY, xzPlaneColor, Transform, planeSize / 2.0f); // YZ - batch3D.Square(selected.Position + Vec3.UnitY * (cubeSize + axisLen / 2.0f) + Vec3.UnitZ * (cubeSize + axisLen / 2.0f), - Vec3.UnitX, yzPlaneColor, planeSize / 2.0f); + batch3D.Square(Vec3.UnitY * (cubeSize + axisLen / 2.0f) + Vec3.UnitZ * (cubeSize + axisLen / 2.0f), + Vec3.UnitX, yzPlaneColor, Transform, planeSize / 2.0f); // XY - batch3D.Square(selected.Position + Vec3.UnitX * (cubeSize + axisLen / 2.0f) + Vec3.UnitY * (cubeSize + axisLen / 2.0f), - Vec3.UnitZ, xyPlaneColor, planeSize / 2.0f); + batch3D.Square(Vec3.UnitX * (cubeSize + axisLen / 2.0f) + Vec3.UnitY * (cubeSize + axisLen / 2.0f), + Vec3.UnitZ, xyPlaneColor, Transform, planeSize / 2.0f); // XYZ - batch3D.Cube(selected.Position, Color.White, cubeSize); + batch3D.Cube(Vec3.Zero, xyzCubeColor, Transform, cubeSize); } } diff --git a/Source/Mod/Helpers/Batcher3D.cs b/Source/Mod/Helpers/Batcher3D.cs index 5410b4d2..de36600b 100644 --- a/Source/Mod/Helpers/Batcher3D.cs +++ b/Source/Mod/Helpers/Batcher3D.cs @@ -277,10 +277,10 @@ public void Cone(Vec3 position, Direction direction, float length, float radius, }; // Base center - vertices[0].Pos = position; + vertices[0].Pos = Vec3.Transform(position, transform); vertices[0].Col = color; // Tip - vertices[1].Pos = position + dir * length; + vertices[1].Pos = Vec3.Transform(position + dir * length, transform); vertices[1].Col = color; for (int i = 0; i < resolution; i++) From 1fc6d68c13b985a2f9dc4d10ff3d715f4b458121 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Fri, 15 Mar 2024 19:08:22 +0100 Subject: [PATCH 66/97] Add dragging support to position gizmo --- Source/Mod/Editor/EditorWorld.cs | 105 ++++++++++++++++++++++++++++++- Source/Mod/Helpers/ModUtils.cs | 27 ++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 43ec6145..7278ee7d 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -33,6 +33,8 @@ public class EditorWorld : World // TODO: Temporary! private PositionGizmo posGizmo = new(); + private Vec2 dragStart; + private Vec3 dragStartPosition; internal EditorWorld(EntryInfo entry) : base(entry) { @@ -274,7 +276,15 @@ public override void Update() closestGizmo = dist; } - if (!foundGizmo) + if (foundGizmo) + { + if (Selected is SpikeBlock.Definition def) + { + dragStart = Input.Mouse.Position; + dragStartPosition = def.Position; + } + } + else { posGizmo.Target = GizmoTarget.None; @@ -285,8 +295,99 @@ public override void Update() else Selected = null; } + } + } + + // Drag position gizmo + if (Input.Mouse.LeftDown && !ImGuiManager.WantCaptureMouse) + { + if (Camera.Target != null && + Matrix.Invert(Camera.Projection, out var inverseProj) && + Matrix.Invert(Camera.View, out var inverseView)) + { + // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios + var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); + var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); + // Convert into normalized-device-coordinates + var ndcPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; + // Flip Y, since up is negative in NDC coords + ndcPos.Y *= -1.0f; + var clipPos = new Vec4(ndcPos, -1.0f, 1.0f); + var eyePos = Vec4.Transform(clipPos, inverseProj); + // We only care about XY, so we set ZW to "forward" + eyePos.Z = -1.0f; + eyePos.W = 0.0f; + var worldPos = Vec4.Transform(eyePos, inverseView); + var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); + + var axisMatrix = posGizmo.Transform * Camera.ViewProjection; + var screenXAxis = Vec3.TransformNormal(Vec3.UnitX, axisMatrix).XY(); + var screenYAxis = Vec3.TransformNormal(Vec3.UnitY, axisMatrix).XY(); + var screenZAxis = Vec3.TransformNormal(Vec3.UnitZ, axisMatrix).XY(); + // Flip Y, since down is positive in screen coords + screenXAxis.Y *= -1.0f; + screenYAxis.Y *= -1.0f; + screenZAxis.Y *= -1.0f; + + // Linear scalar for the movement. Chosen on what felt best. + const float dotScale = 1.0f / 50.0f; + float dotX = Vec2.Dot(Input.Mouse.Position - dragStart, screenXAxis) * dotScale; + float dotY = Vec2.Dot(Input.Mouse.Position - dragStart, screenYAxis) * dotScale; + float dotZ = Vec2.Dot(Input.Mouse.Position - dragStart, screenZAxis) * dotScale; + + Vec3 newPosition = Vec3.Zero; + if (Selected is SpikeBlock.Definition def) + newPosition = def.Position; + + var xzPlaneDelta = Vec3.Transform(posGizmo.xzPlaneBounds.Center, posGizmo.Transform) - newPosition; + var yzPlaneDelta = Vec3.Transform(posGizmo.yzPlaneBounds.Center, posGizmo.Transform) - newPosition; + var xyPlaneDelta = Vec3.Transform(posGizmo.xyPlaneBounds.Center, posGizmo.Transform) - newPosition; + + var cameraPlaneNormal = (Camera.Position - dragStartPosition).Normalized(); + var cameraPlane = new Plane(cameraPlaneNormal, Vec3.Dot(cameraPlaneNormal, dragStartPosition)); + + switch (posGizmo.Target) + { + case GizmoTarget.AxisX: + newPosition = dragStartPosition + Vec3.UnitX * dotX; + break; + case GizmoTarget.AxisY: + newPosition = dragStartPosition + Vec3.UnitY * dotY; + break; + case GizmoTarget.AxisZ: + newPosition = dragStartPosition + Vec3.UnitZ * dotZ; + break; + + case GizmoTarget.PlaneXZ: + float tY = (dragStartPosition.Y - Camera.Position.Y) / direction.Y; + newPosition = Camera.Position + direction * tY - xzPlaneDelta; + break; + case GizmoTarget.PlaneYZ: + float tX = (dragStartPosition.X - Camera.Position.X) / direction.X; + newPosition = Camera.Position + direction * tX - yzPlaneDelta; + break; + case GizmoTarget.PlaneXY: + float tZ = (dragStartPosition.Z - Camera.Position.Z) / direction.Z; + newPosition = Camera.Position + direction * tZ - xyPlaneDelta; + break; + + case GizmoTarget.CubeXYZ: + if (ModUtils.RayIntersectsPlane(Camera.Position, direction, cameraPlane, out var hit)) + { + newPosition = hit; + } + break; + + case GizmoTarget.None: + default: + break; + } - Log.Info(posGizmo.Target); + if (Selected is SpikeBlock.Definition def2) + { + def2.Position = newPosition; + def2.Dirty = true; + } } } diff --git a/Source/Mod/Helpers/ModUtils.cs b/Source/Mod/Helpers/ModUtils.cs index 85f4ff1e..4cfbec51 100644 --- a/Source/Mod/Helpers/ModUtils.cs +++ b/Source/Mod/Helpers/ModUtils.cs @@ -80,4 +80,31 @@ public static bool RayIntersectOBB(Vec3 origin, Vec3 direction, BoundingBox box, return true; } + + // From "GamePhysicsCookbook" + // See: https://github.com/gszauer/GamePhysicsCookbook/blob/a0b8ee0c39fed6d4b90bb6d2195004dfcf5a1115/Code/Geometry3D.cpp#L769-L796 + public static bool RayIntersectsPlane(Vec3 origin, Vec3 direction, Plane plane, out Vec3 hitPoint) + { + hitPoint = default; + + float nd = Vec3.Dot(direction, plane.Normal); + float pn = Vec3.Dot(origin, plane.Normal); + + // nd must be negative, and not 0 + // if nd is positive, the ray and plane normals + // point in the same direction. No intersection. + if (nd >= 0.0f) + return false; + + float t = (plane.D - pn) / nd; + + // t must be positive + if (t >= 0.0f) + { + hitPoint = origin + direction * t; + return true; + } + + return false; + } } From 16071bfc067c388dd2105baaac134bc260249b8a Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 09:47:08 +0100 Subject: [PATCH 67/97] Refactor out gizmo logic into the gizmo itself --- Source/Mod/Editor/EditorWorld.cs | 102 +--------- Source/Mod/Editor/Gizmo/Gizmo.cs | 2 +- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 228 ++++++++++++++++------- 3 files changed, 164 insertions(+), 168 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 7278ee7d..b34bcb92 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -252,31 +252,7 @@ public override void Update() var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); // First check if we hit a gizmo - (BoundingBox Bounds, GizmoTarget Target)[] gizmoTargets = [ - (posGizmo.xAxisBounds, GizmoTarget.AxisX), - (posGizmo.yAxisBounds, GizmoTarget.AxisY), - (posGizmo.zAxisBounds, GizmoTarget.AxisZ), - - (posGizmo.xzPlaneBounds, GizmoTarget.PlaneXZ), - (posGizmo.yzPlaneBounds, GizmoTarget.PlaneYZ), - (posGizmo.xyPlaneBounds, GizmoTarget.PlaneXY), - - (posGizmo.xyzCubeBounds, GizmoTarget.CubeXYZ), - ]; - - bool foundGizmo = false; - float closestGizmo = float.PositiveInfinity; - foreach (var (bounds, target) in gizmoTargets) - { - if (!ModUtils.RayIntersectOBB(Camera.Position, direction, bounds, posGizmo.Transform, out float dist) || dist >= closestGizmo) - continue; - - posGizmo.Target = target; - foundGizmo = true; - closestGizmo = dist; - } - - if (foundGizmo) + if (posGizmo.RaycastCheck(Camera.Position, direction)) { if (Selected is SpikeBlock.Definition def) { @@ -284,14 +260,11 @@ public override void Update() dragStartPosition = def.Position; } } + // Then check for actors else { - posGizmo.Target = GizmoTarget.None; - - // Then check for actors if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; - // Otherwise we clicked into nothing else Selected = null; } @@ -320,74 +293,7 @@ public override void Update() var worldPos = Vec4.Transform(eyePos, inverseView); var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); - var axisMatrix = posGizmo.Transform * Camera.ViewProjection; - var screenXAxis = Vec3.TransformNormal(Vec3.UnitX, axisMatrix).XY(); - var screenYAxis = Vec3.TransformNormal(Vec3.UnitY, axisMatrix).XY(); - var screenZAxis = Vec3.TransformNormal(Vec3.UnitZ, axisMatrix).XY(); - // Flip Y, since down is positive in screen coords - screenXAxis.Y *= -1.0f; - screenYAxis.Y *= -1.0f; - screenZAxis.Y *= -1.0f; - - // Linear scalar for the movement. Chosen on what felt best. - const float dotScale = 1.0f / 50.0f; - float dotX = Vec2.Dot(Input.Mouse.Position - dragStart, screenXAxis) * dotScale; - float dotY = Vec2.Dot(Input.Mouse.Position - dragStart, screenYAxis) * dotScale; - float dotZ = Vec2.Dot(Input.Mouse.Position - dragStart, screenZAxis) * dotScale; - - Vec3 newPosition = Vec3.Zero; - if (Selected is SpikeBlock.Definition def) - newPosition = def.Position; - - var xzPlaneDelta = Vec3.Transform(posGizmo.xzPlaneBounds.Center, posGizmo.Transform) - newPosition; - var yzPlaneDelta = Vec3.Transform(posGizmo.yzPlaneBounds.Center, posGizmo.Transform) - newPosition; - var xyPlaneDelta = Vec3.Transform(posGizmo.xyPlaneBounds.Center, posGizmo.Transform) - newPosition; - - var cameraPlaneNormal = (Camera.Position - dragStartPosition).Normalized(); - var cameraPlane = new Plane(cameraPlaneNormal, Vec3.Dot(cameraPlaneNormal, dragStartPosition)); - - switch (posGizmo.Target) - { - case GizmoTarget.AxisX: - newPosition = dragStartPosition + Vec3.UnitX * dotX; - break; - case GizmoTarget.AxisY: - newPosition = dragStartPosition + Vec3.UnitY * dotY; - break; - case GizmoTarget.AxisZ: - newPosition = dragStartPosition + Vec3.UnitZ * dotZ; - break; - - case GizmoTarget.PlaneXZ: - float tY = (dragStartPosition.Y - Camera.Position.Y) / direction.Y; - newPosition = Camera.Position + direction * tY - xzPlaneDelta; - break; - case GizmoTarget.PlaneYZ: - float tX = (dragStartPosition.X - Camera.Position.X) / direction.X; - newPosition = Camera.Position + direction * tX - yzPlaneDelta; - break; - case GizmoTarget.PlaneXY: - float tZ = (dragStartPosition.Z - Camera.Position.Z) / direction.Z; - newPosition = Camera.Position + direction * tZ - xyPlaneDelta; - break; - - case GizmoTarget.CubeXYZ: - if (ModUtils.RayIntersectsPlane(Camera.Position, direction, cameraPlane, out var hit)) - { - newPosition = hit; - } - break; - - case GizmoTarget.None: - default: - break; - } - - if (Selected is SpikeBlock.Definition def2) - { - def2.Position = newPosition; - def2.Dirty = true; - } + posGizmo.Drag(this, Input.Mouse.Position - dragStart, direction, dragStartPosition); } } @@ -619,7 +525,7 @@ public override void Render(Target target) // Render gizmos on-top target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); { - posGizmo.Render2(ref state, batch3D); + posGizmo.Render(batch3D); } batch3D.Render(ref state); batch3D.Clear(); diff --git a/Source/Mod/Editor/Gizmo/Gizmo.cs b/Source/Mod/Editor/Gizmo/Gizmo.cs index f4ecd55d..663d4444 100644 --- a/Source/Mod/Editor/Gizmo/Gizmo.cs +++ b/Source/Mod/Editor/Gizmo/Gizmo.cs @@ -2,7 +2,7 @@ namespace Celeste64.Mod.Editor; public abstract class Gizmo { - public abstract void Render(ref RenderState state); + public abstract void Render(Batcher3D batch3D); } public enum GizmoTarget diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index 27a53d4b..57217277 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -2,66 +2,58 @@ namespace Celeste64.Mod.Editor; public class PositionGizmo : Gizmo { - public override void Render(ref RenderState state) - { - if (EditorWorld.Current.Selected is not { } selected) - return; - - - } - - public GizmoTarget Target; + private GizmoTarget target; - static float cubeSize = 0.15f; - static float planeSize = 0.6f; - static float padding = 0.15f; - static float axisLen = 1.5f; - static float axisRadius => axisLen / 35.0f; - static float coneLen => axisLen / 2.5f; - static float coneRadius => coneLen / 3.0f; + private const float CubeSize = 0.15f; + private const float PlaneSize = 0.6f; + private const float Padding = 0.15f; + private const float AxisLen = 1.5f; + private const float AxisRadius = AxisLen / 35.0f; + private const float ConeLen = AxisLen / 2.5f; + private const float ConeRadius = ConeLen / 3.0f; - static float boundsPadding => 0.1f; + private const float BoundsPadding = 0.1f; // Axis - static float axisBoundsLengthMin => cubeSize + padding; - static float axisBoundsLengthMax => axisLen + coneLen * 0.9f; - static float axisBoundsRadiusMin => -axisRadius - boundsPadding; - static float axisBoundsRadiusMax => axisRadius + boundsPadding; + private const float AxisBoundsLengthMin = CubeSize + Padding; + private const float AxisBoundsLengthMax = AxisLen + ConeLen * 0.9f; + private const float AxisBoundsRadiusMin = -AxisRadius - BoundsPadding; + private const float AxisBoundsRadiusMax = AxisRadius + BoundsPadding; - public BoundingBox xAxisBounds => new( - new Vec3(axisBoundsLengthMin, axisBoundsRadiusMin, axisBoundsRadiusMin), - new Vec3(axisBoundsLengthMax, axisBoundsRadiusMax, axisBoundsRadiusMax)); + private static readonly BoundingBox XAxisBounds = new( + new Vec3(AxisBoundsLengthMin, AxisBoundsRadiusMin, AxisBoundsRadiusMin), + new Vec3(AxisBoundsLengthMax, AxisBoundsRadiusMax, AxisBoundsRadiusMax)); - public BoundingBox yAxisBounds => new( - new Vec3(axisBoundsRadiusMin, axisBoundsLengthMin, axisBoundsRadiusMin), - new Vec3(axisBoundsRadiusMax, axisBoundsLengthMax, axisBoundsRadiusMax)); + private static readonly BoundingBox YAxisBounds = new( + new Vec3(AxisBoundsRadiusMin, AxisBoundsLengthMin, AxisBoundsRadiusMin), + new Vec3(AxisBoundsRadiusMax, AxisBoundsLengthMax, AxisBoundsRadiusMax)); - public BoundingBox zAxisBounds => new( - new Vec3(axisBoundsRadiusMin, axisBoundsRadiusMin, axisBoundsLengthMin), - new Vec3(axisBoundsRadiusMax, axisBoundsRadiusMax, axisBoundsLengthMax)); + private static readonly BoundingBox ZAxisBounds = new( + new Vec3(AxisBoundsRadiusMin, AxisBoundsRadiusMin, AxisBoundsLengthMin), + new Vec3(AxisBoundsRadiusMax, AxisBoundsRadiusMax, AxisBoundsLengthMax)); // Planes - static float planeBoundsMin => cubeSize + axisLen / 2.0f - planeSize / 2.0f - boundsPadding; - static float planeBoundsMax => cubeSize + axisLen / 2.0f + planeSize / 2.0f + boundsPadding; + private const float PlaneBoundsMin = CubeSize + AxisLen / 2.0f - PlaneSize / 2.0f - BoundsPadding; + private const float PlaneBoundsMax = CubeSize + AxisLen / 2.0f + PlaneSize / 2.0f + BoundsPadding; - public BoundingBox xzPlaneBounds => new( - new Vec3(planeBoundsMin, 0.0f, planeBoundsMin), - new Vec3(planeBoundsMax, 0.0f, planeBoundsMax)); + private static readonly BoundingBox XZPlaneBounds = new( + new Vec3(PlaneBoundsMin, 0.0f, PlaneBoundsMin), + new Vec3(PlaneBoundsMax, 0.0f, PlaneBoundsMax)); - public BoundingBox yzPlaneBounds => new( - new Vec3(0.0f, planeBoundsMin, planeBoundsMin), - new Vec3(0.0f, planeBoundsMax, planeBoundsMax)); + private static readonly BoundingBox YZPlaneBounds = new( + new Vec3(0.0f, PlaneBoundsMin, PlaneBoundsMin), + new Vec3(0.0f, PlaneBoundsMax, PlaneBoundsMax)); - public BoundingBox xyPlaneBounds => new( - new Vec3(planeBoundsMin, planeBoundsMin, 0.0f), - new Vec3(planeBoundsMax, planeBoundsMax, 0.0f)); + private static readonly BoundingBox XYPlaneBounds = new( + new Vec3(PlaneBoundsMin, PlaneBoundsMin, 0.0f), + new Vec3(PlaneBoundsMax, PlaneBoundsMax, 0.0f)); // Cube - public BoundingBox xyzCubeBounds => new( - -new Vec3(cubeSize + boundsPadding), - new Vec3(cubeSize + boundsPadding)); + private static readonly BoundingBox XYZCubeBounds = new( + -new Vec3(CubeSize + BoundsPadding), + new Vec3(CubeSize + BoundsPadding)); - public Matrix Transform + public static Matrix Transform { get { @@ -75,14 +67,12 @@ public Matrix Transform Matrix.CreateTranslation(selected.Position); } } - - public void Render2(ref RenderState state, Batcher3D batch3D) + + public override void Render(Batcher3D batch3D) { if (EditorWorld.Current.Selected is not SpikeBlock.Definition selected) return; - // const byte selectedAlpha = 0x7f; - // const byte deselectedAlpha = 0x2f; const byte selectedAlpha = 0xff; const byte deselectedAlpha = 0xff; @@ -95,38 +85,138 @@ public void Render2(ref RenderState state, Batcher3D batch3D) var zColorDeselected = new Color(0x0000bf, deselectedAlpha); var cubeColorDeselected = new Color(0xbfbfbf, deselectedAlpha); - var xAxisColor = Target == GizmoTarget.AxisX ? xColorSelected : xColorDeselected; - var yAxisColor = Target == GizmoTarget.AxisY ? yColorSelected : yColorDeselected; - var zAxisColor = Target == GizmoTarget.AxisZ ? zColorSelected : zColorDeselected; + var xAxisColor = target == GizmoTarget.AxisX ? xColorSelected : xColorDeselected; + var yAxisColor = target == GizmoTarget.AxisY ? yColorSelected : yColorDeselected; + var zAxisColor = target == GizmoTarget.AxisZ ? zColorSelected : zColorDeselected; - var xzPlaneColor = Target == GizmoTarget.PlaneXZ ? yColorSelected : yColorDeselected; - var yzPlaneColor = Target == GizmoTarget.PlaneYZ ? xColorSelected : xColorDeselected; - var xyPlaneColor = Target == GizmoTarget.PlaneXY ? zColorSelected : zColorDeselected; + var xzPlaneColor = target == GizmoTarget.PlaneXZ ? yColorSelected : yColorDeselected; + var yzPlaneColor = target == GizmoTarget.PlaneYZ ? xColorSelected : xColorDeselected; + var xyPlaneColor = target == GizmoTarget.PlaneXY ? zColorSelected : zColorDeselected; - var xyzCubeColor = Target == GizmoTarget.CubeXYZ ? cubeColorSelected : cubeColorDeselected; + var xyzCubeColor = target == GizmoTarget.CubeXYZ ? cubeColorSelected : cubeColorDeselected; // X - batch3D.Line(Vec3.UnitX * (cubeSize + padding), Vec3.UnitX * axisLen, xAxisColor, Transform, axisRadius); - batch3D.Cone(Vec3.UnitX * axisLen, Batcher3D.Direction.X, coneLen, coneRadius, 12, xAxisColor, Transform); + batch3D.Line(Vec3.UnitX * (CubeSize + Padding), Vec3.UnitX * AxisLen, xAxisColor, Transform, AxisRadius); + batch3D.Cone(Vec3.UnitX * AxisLen, Batcher3D.Direction.X, ConeLen, ConeRadius, 12, xAxisColor, Transform); // Y - batch3D.Line(Vec3.UnitY * (cubeSize + padding), Vec3.UnitY * axisLen, yAxisColor, Transform, axisRadius); - batch3D.Cone(Vec3.UnitY * axisLen, Batcher3D.Direction.Y, coneLen, coneRadius, 12, yAxisColor, Transform); + batch3D.Line(Vec3.UnitY * (CubeSize + Padding), Vec3.UnitY * AxisLen, yAxisColor, Transform, AxisRadius); + batch3D.Cone(Vec3.UnitY * AxisLen, Batcher3D.Direction.Y, ConeLen, ConeRadius, 12, yAxisColor, Transform); // Z - batch3D.Line(Vec3.UnitZ * (cubeSize + padding), Vec3.UnitZ * axisLen, zAxisColor, Transform, axisRadius); - batch3D.Cone(Vec3.UnitZ * axisLen, Batcher3D.Direction.Z, coneLen, coneRadius, 12, zAxisColor, Transform); + batch3D.Line(Vec3.UnitZ * (CubeSize + Padding), Vec3.UnitZ * AxisLen, zAxisColor, Transform, AxisRadius); + batch3D.Cone(Vec3.UnitZ * AxisLen, Batcher3D.Direction.Z, ConeLen, ConeRadius, 12, zAxisColor, Transform); // XZ - batch3D.Square(Vec3.UnitX * (cubeSize + axisLen / 2.0f) + Vec3.UnitZ * (cubeSize + axisLen / 2.0f), - Vec3.UnitY, xzPlaneColor, Transform, planeSize / 2.0f); + batch3D.Square(Vec3.UnitX * (CubeSize + AxisLen / 2.0f) + Vec3.UnitZ * (CubeSize + AxisLen / 2.0f), + Vec3.UnitY, xzPlaneColor, Transform, PlaneSize / 2.0f); // YZ - batch3D.Square(Vec3.UnitY * (cubeSize + axisLen / 2.0f) + Vec3.UnitZ * (cubeSize + axisLen / 2.0f), - Vec3.UnitX, yzPlaneColor, Transform, planeSize / 2.0f); + batch3D.Square(Vec3.UnitY * (CubeSize + AxisLen / 2.0f) + Vec3.UnitZ * (CubeSize + AxisLen / 2.0f), + Vec3.UnitX, yzPlaneColor, Transform, PlaneSize / 2.0f); // XY - batch3D.Square(Vec3.UnitX * (cubeSize + axisLen / 2.0f) + Vec3.UnitY * (cubeSize + axisLen / 2.0f), - Vec3.UnitZ, xyPlaneColor, Transform, planeSize / 2.0f); + batch3D.Square(Vec3.UnitX * (CubeSize + AxisLen / 2.0f) + Vec3.UnitY * (CubeSize + AxisLen / 2.0f), + Vec3.UnitZ, xyPlaneColor, Transform, PlaneSize / 2.0f); // XYZ - batch3D.Cube(Vec3.Zero, xyzCubeColor, Transform, cubeSize); + batch3D.Cube(Vec3.Zero, xyzCubeColor, Transform, CubeSize); + } + + private static readonly (BoundingBox Bounds, GizmoTarget Target)[] GizmoTargets = [ + (XAxisBounds, GizmoTarget.AxisX), + (YAxisBounds, GizmoTarget.AxisY), + (ZAxisBounds, GizmoTarget.AxisZ), + + (XZPlaneBounds, GizmoTarget.PlaneXZ), + (YZPlaneBounds, GizmoTarget.PlaneYZ), + (XYPlaneBounds, GizmoTarget.PlaneXY), + + (XYZCubeBounds, GizmoTarget.CubeXYZ), + ]; + public bool RaycastCheck(Vec3 origin, Vec3 direction) + { + float closestGizmo = float.PositiveInfinity; + + target = GizmoTarget.None; + foreach (var (checkBounds, checkTarget) in GizmoTargets) + { + if (!ModUtils.RayIntersectOBB(origin, direction, checkBounds, Transform, out float dist) || dist >= closestGizmo) + continue; + + target = checkTarget; + closestGizmo = dist; + } + + return target != GizmoTarget.None; + } + + public void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 objectStartingPosition) + { + var axisMatrix = Transform * editor.Camera.ViewProjection; + var screenXAxis = Vec3.TransformNormal(Vec3.UnitX, axisMatrix).XY(); + var screenYAxis = Vec3.TransformNormal(Vec3.UnitY, axisMatrix).XY(); + var screenZAxis = Vec3.TransformNormal(Vec3.UnitZ, axisMatrix).XY(); + // Flip Y, since down is positive in screen coords + screenXAxis.Y *= -1.0f; + screenYAxis.Y *= -1.0f; + screenZAxis.Y *= -1.0f; + + // Linear scalar for the movement. Chosen on what felt best. + const float dotScale = 1.0f / 50.0f; + float dotX = Vec2.Dot(mouseDelta, screenXAxis) * dotScale; + float dotY = Vec2.Dot(mouseDelta, screenYAxis) * dotScale; + float dotZ = Vec2.Dot(mouseDelta, screenZAxis) * dotScale; + + Vec3 newPosition = Vec3.Zero; + if (editor.Selected is SpikeBlock.Definition def) + newPosition = def.Position; + + var xzPlaneDelta = Vec3.Transform(XZPlaneBounds.Center, Transform) - newPosition; + var yzPlaneDelta = Vec3.Transform(YZPlaneBounds.Center, Transform) - newPosition; + var xyPlaneDelta = Vec3.Transform(XYPlaneBounds.Center, Transform) - newPosition; + + var cameraPlaneNormal = (editor.Camera.Position - objectStartingPosition).Normalized(); + var cameraPlane = new Plane(cameraPlaneNormal, Vec3.Dot(cameraPlaneNormal, objectStartingPosition)); + + switch (target) + { + case GizmoTarget.AxisX: + newPosition = objectStartingPosition + Vec3.UnitX * dotX; + break; + case GizmoTarget.AxisY: + newPosition = objectStartingPosition + Vec3.UnitY * dotY; + break; + case GizmoTarget.AxisZ: + newPosition = objectStartingPosition + Vec3.UnitZ * dotZ; + break; + + case GizmoTarget.PlaneXZ: + float tY = (objectStartingPosition.Y - editor.Camera.Position.Y) / mouseRay.Y; + newPosition = editor.Camera.Position + mouseRay * tY - xzPlaneDelta; + break; + case GizmoTarget.PlaneYZ: + float tX = (objectStartingPosition.X - editor.Camera.Position.X) / mouseRay.X; + newPosition = editor.Camera.Position + mouseRay * tX - yzPlaneDelta; + break; + case GizmoTarget.PlaneXY: + float tZ = (objectStartingPosition.Z - editor.Camera.Position.Z) / mouseRay.Z; + newPosition = editor.Camera.Position + mouseRay * tZ - xyPlaneDelta; + break; + + case GizmoTarget.CubeXYZ: + if (ModUtils.RayIntersectsPlane(editor.Camera.Position, mouseRay, cameraPlane, out var hit)) + { + newPosition = hit; + } + break; + + case GizmoTarget.None: + default: + break; + } + + if (editor.Selected is SpikeBlock.Definition def2) + { + def2.Position = newPosition; + def2.Dirty = true; + } } } From 3f9c3054043a6d9046df6df6bd4a025cc0967fda Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 10:25:37 +0100 Subject: [PATCH 68/97] Improve gizmo dragging experience --- Source/Mod/Editor/EditorWorld.cs | 72 +++++++++++++------------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index b34bcb92..d87f1c52 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -230,30 +230,37 @@ public override void Update() Camera.LookAt = cameraPos + forward; // Shoot ray cast for selection - if (Input.Mouse.LeftPressed && !ImGuiManager.WantCaptureMouse) + if (!ImGuiManager.WantCaptureMouse && + Camera.Target != null && + Matrix.Invert(Camera.Projection, out var inverseProj1) && + Matrix.Invert(Camera.View, out var inverseView1)) { - if (Camera.Target != null && - Matrix.Invert(Camera.Projection, out var inverseProj) && - Matrix.Invert(Camera.View, out var inverseView)) + // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios + var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); + var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); + // Convert into normalized-device-coordinates + var ndcPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; + // Flip Y, since up is negative in NDC coords + ndcPos.Y *= -1.0f; + var clipPos = new Vec4(ndcPos, -1.0f, 1.0f); + var eyePos = Vec4.Transform(clipPos, inverseProj1); + // We only care about XY, so we set ZW to "forward" + eyePos.Z = -1.0f; + eyePos.W = 0.0f; + var worldPos = Vec4.Transform(eyePos, inverseView1); + var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); + + // Notice when mouse is hovering over. + // While dragging, don't update the gizmo since we might go out of the gizmo's bounds. + bool isDragging = Input.Mouse.LeftDown && !Input.Mouse.LeftPressed; + bool hitGizmo = isDragging || posGizmo.RaycastCheck(Camera.Position, direction); + + if (Input.Mouse.LeftPressed) { - // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios - var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); - var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); - // Convert into normalized-device-coordinates - var ndcPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; - // Flip Y, since up is negative in NDC coords - ndcPos.Y *= -1.0f; - var clipPos = new Vec4(ndcPos, -1.0f, 1.0f); - var eyePos = Vec4.Transform(clipPos, inverseProj); - // We only care about XY, so we set ZW to "forward" - eyePos.Z = -1.0f; - eyePos.W = 0.0f; - var worldPos = Vec4.Transform(eyePos, inverseView); - var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); - // First check if we hit a gizmo - if (posGizmo.RaycastCheck(Camera.Position, direction)) + if (hitGizmo) { + // Start dragging if (Selected is SpikeBlock.Definition def) { dragStart = Input.Mouse.Position; @@ -269,30 +276,9 @@ public override void Update() Selected = null; } } - } - - // Drag position gizmo - if (Input.Mouse.LeftDown && !ImGuiManager.WantCaptureMouse) - { - if (Camera.Target != null && - Matrix.Invert(Camera.Projection, out var inverseProj) && - Matrix.Invert(Camera.View, out var inverseView)) + // Continue dragging + else if (Input.Mouse.LeftDown) { - // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios - var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); - var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); - // Convert into normalized-device-coordinates - var ndcPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; - // Flip Y, since up is negative in NDC coords - ndcPos.Y *= -1.0f; - var clipPos = new Vec4(ndcPos, -1.0f, 1.0f); - var eyePos = Vec4.Transform(clipPos, inverseProj); - // We only care about XY, so we set ZW to "forward" - eyePos.Z = -1.0f; - eyePos.W = 0.0f; - var worldPos = Vec4.Transform(eyePos, inverseView); - var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); - posGizmo.Drag(this, Input.Mouse.Position - dragStart, direction, dragStartPosition); } } From 9a3672d93806c427ba71b0b7353fe2b5850412bf Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 10:29:24 +0100 Subject: [PATCH 69/97] Improve position gizmo colors --- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 85 ++++++++++++++++++------ 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index 57217277..d4071048 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -73,27 +73,70 @@ public override void Render(Batcher3D batch3D) if (EditorWorld.Current.Selected is not SpikeBlock.Definition selected) return; - const byte selectedAlpha = 0xff; - const byte deselectedAlpha = 0xff; - - var xColorSelected = new Color(0xff9999, selectedAlpha); - var yColorSelected = new Color(0xbfffbf, selectedAlpha); - var zColorSelected = new Color(0x8080ff, selectedAlpha); - var cubeColorSelected = new Color(0xffffff, selectedAlpha); - var xColorDeselected = new Color(0xbf0000, deselectedAlpha); - var yColorDeselected = new Color(0x00bf00, deselectedAlpha); - var zColorDeselected = new Color(0x0000bf, deselectedAlpha); - var cubeColorDeselected = new Color(0xbfbfbf, deselectedAlpha); - - var xAxisColor = target == GizmoTarget.AxisX ? xColorSelected : xColorDeselected; - var yAxisColor = target == GizmoTarget.AxisY ? yColorSelected : yColorDeselected; - var zAxisColor = target == GizmoTarget.AxisZ ? zColorSelected : zColorDeselected; - - var xzPlaneColor = target == GizmoTarget.PlaneXZ ? yColorSelected : yColorDeselected; - var yzPlaneColor = target == GizmoTarget.PlaneYZ ? xColorSelected : xColorDeselected; - var xyPlaneColor = target == GizmoTarget.PlaneXY ? zColorSelected : zColorDeselected; - - var xyzCubeColor = target == GizmoTarget.CubeXYZ ? cubeColorSelected : cubeColorDeselected; + const byte normalAlpha = 0xff; + const byte hoverAlpha = 0xff; + const byte dragAlpha = 0xff; + + const byte mainLightnessNormal = 0xcc; + const byte otherLightnessNormal = 0x00; + + const byte mainLightnessHover = 0xff; + const byte otherLightnessHover = 0x44; + + const byte mainLightnessDrag = 0xff; + const byte otherLightnessDrag = 0xb0; + + // var xColorNormal = new Color(0xcc0000, normalAlpha); + // var xColorHover = new Color(0xff3333, hoverAlpha); + // var xColorDrag = new Color(0xff9999, dragAlpha); + // + // var yColorNormal = new Color(0x00cc00, normalAlpha); + // var yColorHover = new Color(0x33ff33, hoverAlpha); + // var yColorDrag = new Color(0x99ff99, dragAlpha); + // + // var zColorNormal = new Color(0x0000cc, normalAlpha); + // var zColorHover = new Color(0x3333ff, hoverAlpha); + // var zColorDrag = new Color(0x9999ff, dragAlpha); + + var xColorNormal = new Color(mainLightnessNormal, otherLightnessNormal, otherLightnessNormal, normalAlpha); + var xColorHover = new Color(mainLightnessHover, otherLightnessHover, otherLightnessHover, hoverAlpha); + var xColorDrag = new Color(mainLightnessDrag, otherLightnessDrag, otherLightnessDrag, dragAlpha); + + var yColorNormal = new Color(otherLightnessNormal, mainLightnessNormal, otherLightnessNormal, normalAlpha); + var yColorHover = new Color(otherLightnessHover, mainLightnessHover, otherLightnessHover, hoverAlpha); + var yColorDrag = new Color(otherLightnessDrag, mainLightnessDrag, otherLightnessDrag, dragAlpha); + + var zColorNormal = new Color(otherLightnessNormal, otherLightnessNormal, mainLightnessNormal, normalAlpha); + var zColorHover = new Color(otherLightnessHover, otherLightnessHover, mainLightnessHover, hoverAlpha); + var zColorDrag = new Color(otherLightnessDrag, otherLightnessDrag, mainLightnessDrag, dragAlpha); + + var xyzColorNormal = new Color(mainLightnessNormal, mainLightnessNormal, mainLightnessNormal, normalAlpha); + var xyzColorHover = new Color(mainLightnessHover, mainLightnessHover, mainLightnessHover, hoverAlpha); + var xyzColorDrag = new Color(mainLightnessDrag, mainLightnessDrag, mainLightnessDrag, dragAlpha); + + var xAxisColor = target == GizmoTarget.AxisX + ? Input.Mouse.LeftDown ? xColorDrag : xColorHover + : xColorNormal; + var yAxisColor = target == GizmoTarget.AxisY + ? Input.Mouse.LeftDown ? yColorDrag : yColorHover + : yColorNormal; + var zAxisColor = target == GizmoTarget.AxisZ + ? Input.Mouse.LeftDown ? zColorDrag : zColorHover + : zColorNormal; + + var xzPlaneColor = target == GizmoTarget.PlaneXZ + ? Input.Mouse.LeftDown ? yColorDrag : yColorHover + : yColorNormal; + var yzPlaneColor = target == GizmoTarget.PlaneYZ + ? Input.Mouse.LeftDown ? xColorDrag : xColorHover + : xColorNormal; + var xyPlaneColor = target == GizmoTarget.PlaneXY + ? Input.Mouse.LeftDown ? zColorDrag : zColorHover + : zColorNormal; + + var xyzCubeColor = target == GizmoTarget.CubeXYZ + ? Input.Mouse.LeftDown ? xyzColorDrag : xyzColorHover + : xyzColorNormal; // X batch3D.Line(Vec3.UnitX * (CubeSize + Padding), Vec3.UnitX * AxisLen, xAxisColor, Transform, AxisRadius); From 65c07948dd6b0101676c0f9ce1f270e30d5f0f83 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 10:58:40 +0100 Subject: [PATCH 70/97] Choose much better colors for the axis --- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 45 +++++++----------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index d4071048..62967a7b 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -77,42 +77,21 @@ public override void Render(Batcher3D batch3D) const byte hoverAlpha = 0xff; const byte dragAlpha = 0xff; - const byte mainLightnessNormal = 0xcc; - const byte otherLightnessNormal = 0x00; + var xColorNormal = new Color(0xde1100, normalAlpha); + var xColorHover = new Color(0xff6450, hoverAlpha); + var xColorDrag = new Color(0xff9989, dragAlpha); - const byte mainLightnessHover = 0xff; - const byte otherLightnessHover = 0x44; - - const byte mainLightnessDrag = 0xff; - const byte otherLightnessDrag = 0xb0; - - // var xColorNormal = new Color(0xcc0000, normalAlpha); - // var xColorHover = new Color(0xff3333, hoverAlpha); - // var xColorDrag = new Color(0xff9999, dragAlpha); - // - // var yColorNormal = new Color(0x00cc00, normalAlpha); - // var yColorHover = new Color(0x33ff33, hoverAlpha); - // var yColorDrag = new Color(0x99ff99, dragAlpha); - // - // var zColorNormal = new Color(0x0000cc, normalAlpha); - // var zColorHover = new Color(0x3333ff, hoverAlpha); - // var zColorDrag = new Color(0x9999ff, dragAlpha); - - var xColorNormal = new Color(mainLightnessNormal, otherLightnessNormal, otherLightnessNormal, normalAlpha); - var xColorHover = new Color(mainLightnessHover, otherLightnessHover, otherLightnessHover, hoverAlpha); - var xColorDrag = new Color(mainLightnessDrag, otherLightnessDrag, otherLightnessDrag, dragAlpha); - - var yColorNormal = new Color(otherLightnessNormal, mainLightnessNormal, otherLightnessNormal, normalAlpha); - var yColorHover = new Color(otherLightnessHover, mainLightnessHover, otherLightnessHover, hoverAlpha); - var yColorDrag = new Color(otherLightnessDrag, mainLightnessDrag, otherLightnessDrag, dragAlpha); + var yColorNormal = new Color(0x4aed00, normalAlpha); + var yColorHover = new Color(0x83ff66, hoverAlpha); + var yColorDrag = new Color(0xccffbe, dragAlpha); - var zColorNormal = new Color(otherLightnessNormal, otherLightnessNormal, mainLightnessNormal, normalAlpha); - var zColorHover = new Color(otherLightnessHover, otherLightnessHover, mainLightnessHover, hoverAlpha); - var zColorDrag = new Color(otherLightnessDrag, otherLightnessDrag, mainLightnessDrag, dragAlpha); + var zColorNormal = new Color(0x0d00f3, normalAlpha); + var zColorHover = new Color(0x3064ff, hoverAlpha); + var zColorDrag = new Color(0x6693ff, dragAlpha); - var xyzColorNormal = new Color(mainLightnessNormal, mainLightnessNormal, mainLightnessNormal, normalAlpha); - var xyzColorHover = new Color(mainLightnessHover, mainLightnessHover, mainLightnessHover, hoverAlpha); - var xyzColorDrag = new Color(mainLightnessDrag, mainLightnessDrag, mainLightnessDrag, dragAlpha); + var xyzColorNormal = new Color(0xc7c7c7, normalAlpha); + var xyzColorHover = new Color(0xe2e2e2, hoverAlpha); + var xyzColorDrag = new Color(0xffffff, dragAlpha); var xAxisColor = target == GizmoTarget.AxisX ? Input.Mouse.LeftDown ? xColorDrag : xColorHover From 7a33aa251f3698840e75a46b53c3820845573f35 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 11:06:02 +0100 Subject: [PATCH 71/97] Code formatting --- Source/Data/Save.cs | 8 +- Source/Mod/Editor/EditorWorld.cs | 36 +++--- Source/Mod/Editor/GUI/EditActorWindow.cs | 2 +- Source/Mod/Editor/GUI/EditorMenuBar.cs | 22 ++-- .../Editor/GUI/EnvironmentSettingsWindow.cs | 18 +-- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 106 +++++++++--------- Source/Mod/Helpers/Batcher3D.cs | 42 +++---- Source/Mod/Helpers/ModUtils.cs | 32 +++--- Source/Mod/ImGui/ImGuiManager.cs | 4 +- Source/Scenes/World.cs | 4 +- 10 files changed, 137 insertions(+), 137 deletions(-) diff --git a/Source/Data/Save.cs b/Source/Data/Save.cs index 0b534aa4..d0093ebe 100644 --- a/Source/Data/Save.cs +++ b/Source/Data/Save.cs @@ -104,13 +104,13 @@ public bool SettingsGetBool(string name, bool defaultValue = false) public bool SettingsSetBool(string name, bool value = false) => SettingsBoolData[name] = value; } - + public class EditorSettings { // Settings public bool PlayMusic { get; set; } = false; public bool PlayAmbience { get; set; } = false; - + // View public bool RenderSnow { get; set; } = false; public bool RenderSkybox { get; set; } = true; @@ -123,7 +123,7 @@ public float RenderDistance get => renderDistance; set => renderDistance = Math.Clamp(value, MinRenderDistance, MaxRenderDistance); } - + public enum Resolution { Game = 0, Double = 1, HD = 2, Native = 3 } public Resolution ResolutionType { get; set; } = Resolution.Double; } @@ -194,7 +194,7 @@ public enum Resolution { Game = 0, Double = 1, HD = 2, Native = 3 } /// Fuji Custom - Whether The debug menu should be enabled /// public bool EnableDebugMenu { get; set; } = false; - + /// /// Fuji Custom - Settings for the in-game editor /// diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index d87f1c52..0583b1bc 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -8,12 +8,12 @@ public class EditorWorld : World private const float EditorResolutionScale = 3.0f; internal readonly ImGuiHandler[] Handlers = [ - new EditorMenuBar(), - + new EditorMenuBar(), + new EditActorWindow(), new EnvironmentSettingsWindow(), ]; - + public static EditorWorld Current => (Game.Scene as EditorWorld)!; public List Definitions => Map is FujiMap fujiMap ? fujiMap.Definitions : []; @@ -30,7 +30,7 @@ public class EditorWorld : World private Vec2 cameraRot = new(0, 0); private readonly Batcher3D batch3D = new(); - + // TODO: Temporary! private PositionGizmo posGizmo = new(); private Vec2 dragStart; @@ -40,16 +40,16 @@ internal EditorWorld(EntryInfo entry) : base(entry) { Camera.NearPlane = 0.1f; // Allow getting closer to objects Camera.FOVMultiplier = 1.25f; // Higher FOV feels better in the editor - + // Store previous game resolution to restore it when exiting previousScale = Game.ResolutionScale; - + // Load environment RefreshEnvironment(); // Map gets implicitly loaded, since our Definitions are taken directly from it } - + internal void RefreshEnvironment() { Camera.FarPlane = Save.Instance.Editor.RenderDistance; @@ -61,12 +61,12 @@ internal void RefreshEnvironment() Save.EditorSettings.Resolution.Native => Math.Max(App.Width / (float)Game.DefaultWidth, App.Height / (float)Game.DefaultHeight), _ => throw new ArgumentOutOfRangeException(), }; - + if (Map == null) return; - + // Taken from World constructor with added cleanup of previously created stuff - + if (Get() is { } snow) Destroy(snow); if (Map.SnowAmount > 0 && Save.Instance.Editor.RenderSnow) @@ -123,7 +123,7 @@ internal void RefreshEnvironment() Game.Instance.Ambience = Audio.Play(Ambience); if (!string.IsNullOrWhiteSpace(AmbienceWav)) Game.Instance.AmbienceWav = Audio.PlayMusic(AmbienceWav); - + skyboxes.Clear(); if (!string.IsNullOrEmpty(Map.Skybox) && Save.Instance.Editor.RenderSkybox) { @@ -150,7 +150,7 @@ public override void Exited() { Game.ResolutionScale = previousScale; } - + public void RemoveDefinition(ActorDefinition definition) { Definitions.Remove(definition); @@ -231,9 +231,9 @@ public override void Update() // Shoot ray cast for selection if (!ImGuiManager.WantCaptureMouse && - Camera.Target != null && - Matrix.Invert(Camera.Projection, out var inverseProj1) && - Matrix.Invert(Camera.View, out var inverseView1)) + Camera.Target != null && + Matrix.Invert(Camera.Projection, out var inverseProj1) && + Matrix.Invert(Camera.View, out var inverseView1)) { // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); @@ -252,9 +252,9 @@ public override void Update() // Notice when mouse is hovering over. // While dragging, don't update the gizmo since we might go out of the gizmo's bounds. - bool isDragging = Input.Mouse.LeftDown && !Input.Mouse.LeftPressed; + bool isDragging = Input.Mouse.LeftDown && !Input.Mouse.LeftPressed; bool hitGizmo = isDragging || posGizmo.RaycastCheck(Camera.Position, direction); - + if (Input.Mouse.LeftPressed) { // First check if we hit a gizmo @@ -507,7 +507,7 @@ public override void Render(Target target) } batch3D.Render(ref state); batch3D.Clear(); - + // Render gizmos on-top target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); { diff --git a/Source/Mod/Editor/GUI/EditActorWindow.cs b/Source/Mod/Editor/GUI/EditActorWindow.cs index a133f1a4..128b7dd8 100644 --- a/Source/Mod/Editor/GUI/EditActorWindow.cs +++ b/Source/Mod/Editor/GUI/EditActorWindow.cs @@ -50,7 +50,7 @@ protected override void RenderWindow(EditorWorld editor) break; } } - + ImGui.NewLine(); if (ImGui.Button("Remove Actor") || Input.Keyboard.Pressed(Keys.Delete)) { diff --git a/Source/Mod/Editor/GUI/EditorMenuBar.cs b/Source/Mod/Editor/GUI/EditorMenuBar.cs index 5818090c..f5e126a9 100644 --- a/Source/Mod/Editor/GUI/EditorMenuBar.cs +++ b/Source/Mod/Editor/GUI/EditorMenuBar.cs @@ -9,30 +9,30 @@ public override void Render() bool changed = false; ImGui.BeginMainMenuBar(); - + if (ImGui.BeginMenu("Settings")) { bool music = Save.Instance.Editor.PlayMusic; changed |= ImGui.Checkbox("Player Music", ref music); Save.Instance.Editor.PlayMusic = music; - + bool ambience = Save.Instance.Editor.PlayAmbience; changed |= ImGui.Checkbox("Play Ambience", ref ambience); Save.Instance.Editor.PlayAmbience = ambience; - + ImGui.EndMenu(); } - + if (ImGui.BeginMenu("View")) { bool snow = Save.Instance.Editor.RenderSnow; changed |= ImGui.Checkbox("Show Snow", ref snow); Save.Instance.Editor.RenderSnow = snow; - + bool skybox = Save.Instance.Editor.RenderSkybox; changed |= ImGui.Checkbox("Show Skybox", ref skybox); Save.Instance.Editor.RenderSkybox = skybox; - + float renderDistance = Save.Instance.Editor.RenderDistance; changed |= ImGui.DragFloat("Render Distance", ref renderDistance, v_speed: 10.0f, v_min: Save.EditorSettings.MinRenderDistance, v_max: Save.EditorSettings.MaxRenderDistance); Save.Instance.Editor.RenderDistance = renderDistance; @@ -43,7 +43,7 @@ public override void Render() "HD (1920x1080)", $"Native ({App.Width}x{App.Height})", ]; - + var resolutionType = Save.Instance.Editor.ResolutionType; if (ImGui.BeginCombo("Resolution", displayStrings[(int)resolutionType])) { @@ -67,15 +67,15 @@ public override void Render() Save.Instance.Editor.ResolutionType = Save.EditorSettings.Resolution.Native; changed = true; } - + ImGui.EndCombo(); } - + ImGui.EndMenu(); } - + ImGui.EndMainMenuBar(); - + if (changed) EditorWorld.Current.RefreshEnvironment(); } diff --git a/Source/Mod/Editor/GUI/EnvironmentSettingsWindow.cs b/Source/Mod/Editor/GUI/EnvironmentSettingsWindow.cs index f855c5ca..f53e4208 100644 --- a/Source/Mod/Editor/GUI/EnvironmentSettingsWindow.cs +++ b/Source/Mod/Editor/GUI/EnvironmentSettingsWindow.cs @@ -5,38 +5,38 @@ namespace Celeste64.Mod.Editor; public class EnvironmentSettingsWindow() : EditorWindow("EnvironmentSettings") { protected override string Title => "Environment Settings"; - + protected override void RenderWindow(EditorWorld editor) { if (editor.Map == null) return; - + bool changed = false; - + // AFAIK just passing a large value works fine for C# strings const int bufferSize = 32767; - + // TODO: Add an asset picker?? string skybox = editor.Map.Skybox ?? string.Empty; changed |= ImGui.InputText("Skybox", ref skybox, bufferSize); editor.Map.Skybox = skybox; - + float snowAmount = editor.Map.SnowAmount; changed |= ImGui.DragFloat("Snow Amount", ref snowAmount, v_speed: 0.1f, v_min: 0.0f); editor.Map.SnowAmount = snowAmount; - + var snowWind = editor.Map.SnowWind; changed |= ImGui.DragFloat3("Snow Wind", ref snowWind, v_speed: 0.1f); editor.Map.SnowWind = snowWind; - + string music = editor.Map.Music ?? string.Empty; changed |= ImGui.InputText("Music", ref music, bufferSize); editor.Map.Music = music; - + string ambience = editor.Map.Ambience ?? string.Empty; changed |= ImGui.InputText("Ambience", ref ambience, bufferSize); editor.Map.Ambience = ambience; - + if (changed) editor.RefreshEnvironment(); } diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index 62967a7b..109baccc 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -3,7 +3,7 @@ namespace Celeste64.Mod.Editor; public class PositionGizmo : Gizmo { private GizmoTarget target; - + private const float CubeSize = 0.15f; private const float PlaneSize = 0.6f; private const float Padding = 0.15f; @@ -11,7 +11,7 @@ public class PositionGizmo : Gizmo private const float AxisRadius = AxisLen / 35.0f; private const float ConeLen = AxisLen / 2.5f; private const float ConeRadius = ConeLen / 3.0f; - + private const float BoundsPadding = 0.1f; // Axis @@ -19,104 +19,104 @@ public class PositionGizmo : Gizmo private const float AxisBoundsLengthMax = AxisLen + ConeLen * 0.9f; private const float AxisBoundsRadiusMin = -AxisRadius - BoundsPadding; private const float AxisBoundsRadiusMax = AxisRadius + BoundsPadding; - + private static readonly BoundingBox XAxisBounds = new( new Vec3(AxisBoundsLengthMin, AxisBoundsRadiusMin, AxisBoundsRadiusMin), new Vec3(AxisBoundsLengthMax, AxisBoundsRadiusMax, AxisBoundsRadiusMax)); - + private static readonly BoundingBox YAxisBounds = new( new Vec3(AxisBoundsRadiusMin, AxisBoundsLengthMin, AxisBoundsRadiusMin), new Vec3(AxisBoundsRadiusMax, AxisBoundsLengthMax, AxisBoundsRadiusMax)); - + private static readonly BoundingBox ZAxisBounds = new( new Vec3(AxisBoundsRadiusMin, AxisBoundsRadiusMin, AxisBoundsLengthMin), new Vec3(AxisBoundsRadiusMax, AxisBoundsRadiusMax, AxisBoundsLengthMax)); - + // Planes private const float PlaneBoundsMin = CubeSize + AxisLen / 2.0f - PlaneSize / 2.0f - BoundsPadding; private const float PlaneBoundsMax = CubeSize + AxisLen / 2.0f + PlaneSize / 2.0f + BoundsPadding; - + private static readonly BoundingBox XZPlaneBounds = new( new Vec3(PlaneBoundsMin, 0.0f, PlaneBoundsMin), new Vec3(PlaneBoundsMax, 0.0f, PlaneBoundsMax)); - + private static readonly BoundingBox YZPlaneBounds = new( new Vec3(0.0f, PlaneBoundsMin, PlaneBoundsMin), new Vec3(0.0f, PlaneBoundsMax, PlaneBoundsMax)); - + private static readonly BoundingBox XYPlaneBounds = new( new Vec3(PlaneBoundsMin, PlaneBoundsMin, 0.0f), new Vec3(PlaneBoundsMax, PlaneBoundsMax, 0.0f)); - + // Cube private static readonly BoundingBox XYZCubeBounds = new( -new Vec3(CubeSize + BoundsPadding), new Vec3(CubeSize + BoundsPadding)); - + public static Matrix Transform { get { if (EditorWorld.Current.Selected is not SpikeBlock.Definition selected) return Matrix.Identity; - + const float minScale = 10.0f; float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, selected.Position) / 20.0f); - - return Matrix.CreateScale(scale) * + + return Matrix.CreateScale(scale) * Matrix.CreateTranslation(selected.Position); } } - + public override void Render(Batcher3D batch3D) { if (EditorWorld.Current.Selected is not SpikeBlock.Definition selected) return; - + const byte normalAlpha = 0xff; const byte hoverAlpha = 0xff; const byte dragAlpha = 0xff; - + var xColorNormal = new Color(0xde1100, normalAlpha); var xColorHover = new Color(0xff6450, hoverAlpha); var xColorDrag = new Color(0xff9989, dragAlpha); - + var yColorNormal = new Color(0x4aed00, normalAlpha); var yColorHover = new Color(0x83ff66, hoverAlpha); var yColorDrag = new Color(0xccffbe, dragAlpha); - + var zColorNormal = new Color(0x0d00f3, normalAlpha); var zColorHover = new Color(0x3064ff, hoverAlpha); var zColorDrag = new Color(0x6693ff, dragAlpha); - + var xyzColorNormal = new Color(0xc7c7c7, normalAlpha); var xyzColorHover = new Color(0xe2e2e2, hoverAlpha); var xyzColorDrag = new Color(0xffffff, dragAlpha); - - var xAxisColor = target == GizmoTarget.AxisX - ? Input.Mouse.LeftDown ? xColorDrag : xColorHover + + var xAxisColor = target == GizmoTarget.AxisX + ? Input.Mouse.LeftDown ? xColorDrag : xColorHover : xColorNormal; var yAxisColor = target == GizmoTarget.AxisY - ? Input.Mouse.LeftDown ? yColorDrag : yColorHover + ? Input.Mouse.LeftDown ? yColorDrag : yColorHover : yColorNormal; - var zAxisColor = target == GizmoTarget.AxisZ - ? Input.Mouse.LeftDown ? zColorDrag : zColorHover + var zAxisColor = target == GizmoTarget.AxisZ + ? Input.Mouse.LeftDown ? zColorDrag : zColorHover : zColorNormal; - + var xzPlaneColor = target == GizmoTarget.PlaneXZ - ? Input.Mouse.LeftDown ? yColorDrag : yColorHover + ? Input.Mouse.LeftDown ? yColorDrag : yColorHover : yColorNormal; var yzPlaneColor = target == GizmoTarget.PlaneYZ - ? Input.Mouse.LeftDown ? xColorDrag : xColorHover + ? Input.Mouse.LeftDown ? xColorDrag : xColorHover : xColorNormal; var xyPlaneColor = target == GizmoTarget.PlaneXY - ? Input.Mouse.LeftDown ? zColorDrag : zColorHover + ? Input.Mouse.LeftDown ? zColorDrag : zColorHover : zColorNormal; - + var xyzCubeColor = target == GizmoTarget.CubeXYZ - ? Input.Mouse.LeftDown ? xyzColorDrag : xyzColorHover + ? Input.Mouse.LeftDown ? xyzColorDrag : xyzColorHover : xyzColorNormal; - + // X batch3D.Line(Vec3.UnitX * (CubeSize + Padding), Vec3.UnitX * AxisLen, xAxisColor, Transform, AxisRadius); batch3D.Cone(Vec3.UnitX * AxisLen, Batcher3D.Direction.X, ConeLen, ConeRadius, 12, xAxisColor, Transform); @@ -126,49 +126,49 @@ public override void Render(Batcher3D batch3D) // Z batch3D.Line(Vec3.UnitZ * (CubeSize + Padding), Vec3.UnitZ * AxisLen, zAxisColor, Transform, AxisRadius); batch3D.Cone(Vec3.UnitZ * AxisLen, Batcher3D.Direction.Z, ConeLen, ConeRadius, 12, zAxisColor, Transform); - + // XZ - batch3D.Square(Vec3.UnitX * (CubeSize + AxisLen / 2.0f) + Vec3.UnitZ * (CubeSize + AxisLen / 2.0f), + batch3D.Square(Vec3.UnitX * (CubeSize + AxisLen / 2.0f) + Vec3.UnitZ * (CubeSize + AxisLen / 2.0f), Vec3.UnitY, xzPlaneColor, Transform, PlaneSize / 2.0f); // YZ - batch3D.Square(Vec3.UnitY * (CubeSize + AxisLen / 2.0f) + Vec3.UnitZ * (CubeSize + AxisLen / 2.0f), - Vec3.UnitX, yzPlaneColor, Transform, PlaneSize / 2.0f); + batch3D.Square(Vec3.UnitY * (CubeSize + AxisLen / 2.0f) + Vec3.UnitZ * (CubeSize + AxisLen / 2.0f), + Vec3.UnitX, yzPlaneColor, Transform, PlaneSize / 2.0f); // XY - batch3D.Square(Vec3.UnitX * (CubeSize + AxisLen / 2.0f) + Vec3.UnitY * (CubeSize + AxisLen / 2.0f), - Vec3.UnitZ, xyPlaneColor, Transform, PlaneSize / 2.0f); + batch3D.Square(Vec3.UnitX * (CubeSize + AxisLen / 2.0f) + Vec3.UnitY * (CubeSize + AxisLen / 2.0f), + Vec3.UnitZ, xyPlaneColor, Transform, PlaneSize / 2.0f); // XYZ batch3D.Cube(Vec3.Zero, xyzCubeColor, Transform, CubeSize); } - + private static readonly (BoundingBox Bounds, GizmoTarget Target)[] GizmoTargets = [ (XAxisBounds, GizmoTarget.AxisX), (YAxisBounds, GizmoTarget.AxisY), (ZAxisBounds, GizmoTarget.AxisZ), - + (XZPlaneBounds, GizmoTarget.PlaneXZ), (YZPlaneBounds, GizmoTarget.PlaneYZ), (XYPlaneBounds, GizmoTarget.PlaneXY), - + (XYZCubeBounds, GizmoTarget.CubeXYZ), ]; public bool RaycastCheck(Vec3 origin, Vec3 direction) { float closestGizmo = float.PositiveInfinity; - + target = GizmoTarget.None; foreach (var (checkBounds, checkTarget) in GizmoTargets) { - if (!ModUtils.RayIntersectOBB(origin, direction, checkBounds, Transform, out float dist) || dist >= closestGizmo) + if (!ModUtils.RayIntersectOBB(origin, direction, checkBounds, Transform, out float dist) || dist >= closestGizmo) continue; target = checkTarget; closestGizmo = dist; } - + return target != GizmoTarget.None; } - + public void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 objectStartingPosition) { var axisMatrix = Transform * editor.Camera.ViewProjection; @@ -179,24 +179,24 @@ public void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 object screenXAxis.Y *= -1.0f; screenYAxis.Y *= -1.0f; screenZAxis.Y *= -1.0f; - + // Linear scalar for the movement. Chosen on what felt best. const float dotScale = 1.0f / 50.0f; float dotX = Vec2.Dot(mouseDelta, screenXAxis) * dotScale; float dotY = Vec2.Dot(mouseDelta, screenYAxis) * dotScale; float dotZ = Vec2.Dot(mouseDelta, screenZAxis) * dotScale; - + Vec3 newPosition = Vec3.Zero; if (editor.Selected is SpikeBlock.Definition def) newPosition = def.Position; - + var xzPlaneDelta = Vec3.Transform(XZPlaneBounds.Center, Transform) - newPosition; var yzPlaneDelta = Vec3.Transform(YZPlaneBounds.Center, Transform) - newPosition; var xyPlaneDelta = Vec3.Transform(XYPlaneBounds.Center, Transform) - newPosition; - + var cameraPlaneNormal = (editor.Camera.Position - objectStartingPosition).Normalized(); var cameraPlane = new Plane(cameraPlaneNormal, Vec3.Dot(cameraPlaneNormal, objectStartingPosition)); - + switch (target) { case GizmoTarget.AxisX: @@ -208,7 +208,7 @@ public void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 object case GizmoTarget.AxisZ: newPosition = objectStartingPosition + Vec3.UnitZ * dotZ; break; - + case GizmoTarget.PlaneXZ: float tY = (objectStartingPosition.Y - editor.Camera.Position.Y) / mouseRay.Y; newPosition = editor.Camera.Position + mouseRay * tY - xzPlaneDelta; @@ -233,7 +233,7 @@ public void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 object default: break; } - + if (editor.Selected is SpikeBlock.Definition def2) { def2.Position = newPosition; diff --git a/Source/Mod/Helpers/Batcher3D.cs b/Source/Mod/Helpers/Batcher3D.cs index de36600b..91743e04 100644 --- a/Source/Mod/Helpers/Batcher3D.cs +++ b/Source/Mod/Helpers/Batcher3D.cs @@ -29,7 +29,7 @@ public struct Vertex(Vec3 position, Vec2 texcoord, Color color) : IVertex public readonly VertexFormat Format => VertexFormat; } - + public enum Direction { X, Y, Z } ~Batcher3D() @@ -51,19 +51,19 @@ public enum Direction { X, Y, Z } private readonly Mesh mesh = new(); private readonly Material material = new(Assets.Shaders["Sprite"]); private bool dirty = false; - + public void Square(Vec3 center, Vec3 normal, Color color, float size = 0.1f) => Square(center, normal, color, Matrix.Identity, size); public void Square(Vec3 center, Vec3 normal, Color color, Matrix transform, float size = 0.1f) { var (tangent, bitangent) = GetTangentVectors(normal); - + tangent *= size; bitangent *= size; - + Quad(center - tangent - bitangent, center + tangent - bitangent, center - tangent + bitangent, - center + tangent + bitangent, + center + tangent + bitangent, color, transform); } @@ -72,7 +72,7 @@ public void Line(Vec3 from, Vec3 to, Color color, Matrix transform, float thickn { var normal = (to - from).Normalized(); var (tangent, bitangent) = GetTangentVectors(normal); - + tangent *= thickness; bitangent *= thickness; @@ -95,7 +95,7 @@ public void Cube(Vec3 center, Color color, Matrix transform, float size = 0.1f) color, transform ); } - + public void Torus(Vec3 center, float radius, int resolution, Color color, float thickness = 0.1f) => Torus(center, radius, resolution, color, Matrix.Identity, thickness); public void Torus(Vec3 center, float radius, int resolution, Color color, Matrix transform, float thickness = 0.1f) { @@ -109,7 +109,7 @@ public void Torus(Vec3 center, float radius, int resolution, Color color, Matrix int vtxCount = resolution * 4; // 4 vertices each int idxCount = resolution * 4 * 2 * 3; // 4 faces * 2 triangles * 3 vertices each - + EnsureVertexCapacity(vertexCount + vtxCount); EnsureIndexCapacity(indexCount + idxCount); @@ -184,7 +184,7 @@ public void Disk(Vec3 center, float radius, int resolution, Color color, Matrix { points[i] = new Vec3(Calc.AngleToVector(i * angleStep, radius), 0.0f); } - + int vtxCount = resolution * 2 + 2; // 2 vertices each + 2 in the center int idxCount = resolution * 4 * 3; // 1 faces for outside + 2 triangles on top/bottom = 4 triangles * 3 vertices each @@ -237,14 +237,14 @@ public void Disk(Vec3 center, float radius, int resolution, Color color, Matrix indexCount += idxCount; dirty = true; } - + public void Cone(Vec3 position, Direction direction, float length, float radius, int resolution, Color color) => Cone(position, direction, length, radius, resolution, color, Matrix.Identity); public void Cone(Vec3 position, Direction direction, float length, float radius, int resolution, Color color, Matrix transform) { var points = new Vec3[resolution]; float angleStep = Calc.TAU / resolution; - + for (int i = 0; i < resolution; i++) { var vec = Calc.AngleToVector(i * angleStep, radius); @@ -256,13 +256,13 @@ public void Cone(Vec3 position, Direction direction, float length, float radius, _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) }; } - + int vtxCount = resolution * 2 + 1 + 1; // 2 vertices each + 1 in the base center + 1 at the tip int idxCount = resolution * 2 * 3; // 2 triangles on top/bottom * 3 vertices each - + EnsureVertexCapacity(vertexCount + vtxCount); EnsureIndexCapacity(indexCount + idxCount); - + unsafe { var vertices = new Span((Vertex*)vertexPtr + vertexCount, vtxCount); @@ -293,7 +293,7 @@ public void Cone(Vec3 position, Direction direction, float length, float radius, { int curr = i; int prev = i == 0 ? resolution - 1 : i - 1; // Wrap around to the end - + // Bottom indices[i * (2 * 3) + 0] = vertexCount + (prev + 2); indices[i * (2 * 3) + 1] = vertexCount + (curr + 2); @@ -304,7 +304,7 @@ public void Cone(Vec3 position, Direction direction, float length, float radius, indices[i * (2 * 3) + 5] = vertexCount + 1; } } - + vertexCount += vtxCount; indexCount += idxCount; dirty = true; @@ -319,7 +319,7 @@ public void Sphere(Vec3 center, float radius, int resolution, Color color, Matri int vtxCount = 2 + (stackCount - 1) * sliceCount; int idxCount = sliceCount * 6 + (stackCount - 2) * sliceCount * 6; - + EnsureVertexCapacity(vertexCount + vtxCount); EnsureIndexCapacity(indexCount + idxCount); @@ -398,7 +398,7 @@ public void Sphere(Vec3 center, float radius, int resolution, Color color, Matri dirty = true; } } - + /// /// Renders a quad of a solid color. /// @@ -412,7 +412,7 @@ public void Quad(Vec3 v0, Vec3 v1, Vec3 v2, Vec3 v3, { const int vtxCount = 4; const int idxCount = 2 * 3; // 2 triangles * 3 vertices - + EnsureVertexCapacity(vertexCount + vtxCount); EnsureIndexCapacity(indexCount + idxCount); @@ -474,7 +474,7 @@ public void Box(Vec3 v0, Vec3 v1, Vec3 v2, Vec3 v3, { const int vtxCount = 8; const int idxCount = 6 * 2 * 3; // 6 faces * 2 triangles * 3 vertices - + EnsureVertexCapacity(vertexCount + vtxCount); EnsureIndexCapacity(indexCount + idxCount); @@ -598,7 +598,7 @@ public void Clear() vertexCount = 0; indexCount = 0; } - + private (Vec3 Tangent, Vec3 Bitangent) GetTangentVectors(Vec3 normal) { // The other vector for the cross product can't be parallel to the normal diff --git a/Source/Mod/Helpers/ModUtils.cs b/Source/Mod/Helpers/ModUtils.cs index 4cfbec51..b81139c0 100644 --- a/Source/Mod/Helpers/ModUtils.cs +++ b/Source/Mod/Helpers/ModUtils.cs @@ -43,29 +43,29 @@ public static bool RayIntersectOBB(Vec3 origin, Vec3 direction, BoundingBox box, t = 0.0f; if (!Matrix.Invert(transform, out var inverse)) return false; - + // The center of the bounding box needs to be at <0,0,0> inverse *= Matrix.CreateTranslation(-box.Center); - + // convert from world to box space var ro = Vec3.Transform(origin, inverse); var rd = Vec3.TransformNormal(direction, inverse); var rad = box.Size / 2.0f; - + // ray-box intersection in box space var m = Vec3.One / rd; var s = new Vec3( - (rd.X<0.0f)?1.0f:-1.0f, - (rd.Y<0.0f)?1.0f:-1.0f, - (rd.Z<0.0f)?1.0f:-1.0f); - var t1 = m*(-ro + s*rad); - var t2 = m*(-ro - s*rad); - - float tN = Math.Max( Math.Max( t1.X, t1.Y ), t1.Z ); - float tF = Math.Min( Math.Min( t2.X, t2.Y ), t2.Z ); - - if( tN>tF || tF<0.0) + (rd.X < 0.0f) ? 1.0f : -1.0f, + (rd.Y < 0.0f) ? 1.0f : -1.0f, + (rd.Z < 0.0f) ? 1.0f : -1.0f); + var t1 = m * (-ro + s * rad); + var t2 = m * (-ro - s * rad); + + float tN = Math.Max(Math.Max(t1.X, t1.Y), t1.Z); + float tF = Math.Min(Math.Min(t2.X, t2.Y), t2.Z); + + if (tN > tF || tF < 0.0) return false; // compute normal (in world space), face and UV @@ -80,13 +80,13 @@ public static bool RayIntersectOBB(Vec3 origin, Vec3 direction, BoundingBox box, return true; } - + // From "GamePhysicsCookbook" // See: https://github.com/gszauer/GamePhysicsCookbook/blob/a0b8ee0c39fed6d4b90bb6d2195004dfcf5a1115/Code/Geometry3D.cpp#L769-L796 public static bool RayIntersectsPlane(Vec3 origin, Vec3 direction, Plane plane, out Vec3 hitPoint) { hitPoint = default; - + float nd = Vec3.Dot(direction, plane.Normal); float pn = Vec3.Dot(origin, plane.Normal); @@ -99,7 +99,7 @@ public static bool RayIntersectsPlane(Vec3 origin, Vec3 direction, Plane plane, float t = (plane.D - pn) / nd; // t must be positive - if (t >= 0.0f) + if (t >= 0.0f) { hitPoint = origin + direction * t; return true; diff --git a/Source/Mod/ImGui/ImGuiManager.cs b/Source/Mod/ImGui/ImGuiManager.cs index 47fd69e2..a93acc66 100644 --- a/Source/Mod/ImGui/ImGuiManager.cs +++ b/Source/Mod/ImGui/ImGuiManager.cs @@ -30,7 +30,7 @@ internal void UpdateHandlers() // Reset so that ImGui itself actually receives the inputs WantCaptureKeyboard = false; WantCaptureMouse = false; - + renderer.Update(); if (debugMenu.Active) @@ -48,7 +48,7 @@ internal void UpdateHandlers() { if (handler.Active) handler.Update(); } - + var io = ImGui.GetIO(); WantCaptureKeyboard = io.WantCaptureKeyboard; WantCaptureMouse = io.WantCaptureMouse; diff --git a/Source/Scenes/World.cs b/Source/Scenes/World.cs index 46c93416..8dc17603 100644 --- a/Source/Scenes/World.cs +++ b/Source/Scenes/World.cs @@ -208,7 +208,7 @@ public World(EntryInfo entry) AmbienceWav = ""; Ambience = $"event:/sfx/ambience/{map.Ambience}"; } - + if (!string.IsNullOrEmpty(map.Skybox)) { // single skybox @@ -224,7 +224,7 @@ public World(EntryInfo entry) } } } - + if (Type == WorldType.Game) // The editor handles loading itself { ModManager.Instance.OnPreMapLoaded(this, map); From 8885c5773fb9e423598031ee29843ea3c1cf4fbd Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 11:41:13 +0100 Subject: [PATCH 72/97] Apply outline to gizmos --- Source/Mod/Editor/EditorWorld.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 0583b1bc..f6f68d0b 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -515,6 +515,7 @@ public override void Render(Target target) } batch3D.Render(ref state); batch3D.Clear(); + ApplyPostEffects(); // ui { From c105b761ac8cc73930bff617f3e49491f0df8a11 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 11:47:37 +0100 Subject: [PATCH 73/97] Fix definitions not being added sometimes --- Source/Mod/Editor/EditorWorld.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index f6f68d0b..0c22e9f0 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -48,6 +48,11 @@ internal EditorWorld(EntryInfo entry) : base(entry) RefreshEnvironment(); // Map gets implicitly loaded, since our Definitions are taken directly from it + // However mark all definitions as dirty to ensure they will get added + foreach (var def in Definitions) + { + def.Dirty = true; + } } internal void RefreshEnvironment() From 32d158ad977c032f5a3c81de4cdca494bb4c3e6a Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 12:40:30 +0100 Subject: [PATCH 74/97] Generalize PositionGizmo to any property --- Source/Actors/SpikeBlock.cs | 1 + Source/Mod/Editor/EditorWorld.cs | 45 ++++++++++++++++--- Source/Mod/Editor/Gizmo/Gizmo.cs | 4 ++ Source/Mod/Editor/Gizmo/PositionGizmo.cs | 32 ++++++------- .../Serialization/SpecialPropertyAttribute.cs | 12 +++++ 5 files changed, 68 insertions(+), 26 deletions(-) create mode 100644 Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs diff --git a/Source/Actors/SpikeBlock.cs b/Source/Actors/SpikeBlock.cs index a44c5a1f..1863a08b 100644 --- a/Source/Actors/SpikeBlock.cs +++ b/Source/Actors/SpikeBlock.cs @@ -6,6 +6,7 @@ public class SpikeBlock() : Attacher(typeof(Definition)), IHaveModels { public class Definition : ActorDefinition { + [SpecialProperty(SpecialPropertyType.PositionXYZ)] public Vec3 Position { get; set; } = Vec3.Zero; public Vec3 Rotation { get; set; } = Vec3.Zero; public Vec3 Size { get; set; } = new Vec3(50.0f, 10.0f, 100.0f); diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 0c22e9f0..98f4efda 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -1,12 +1,11 @@ using Celeste64.Mod.Helpers; using System.Collections.ObjectModel; +using System.Reflection; namespace Celeste64.Mod.Editor; public class EditorWorld : World { - private const float EditorResolutionScale = 3.0f; - internal readonly ImGuiHandler[] Handlers = [ new EditorMenuBar(), @@ -20,7 +19,38 @@ public class EditorWorld : World public ReadOnlyDictionary ActorsFromDefinition => actorsFromDefinition.AsReadOnly(); public ReadOnlyDictionary DefinitionFromActors => definitionFromActors.AsReadOnly(); - public ActorDefinition? Selected { internal set; get; } = null; + private ActorDefinition? selectedDefinition = null; + public ActorDefinition? Selected + { + private set + { + selectedDefinition = value; + gizmo = null; + + if (selectedDefinition is null) + return; + + var positionProp = selectedDefinition + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(prop => + !prop.HasAttr() && + prop.GetCustomAttribute() is { Value: SpecialPropertyType.PositionXYZ }); + + if (positionProp is null || positionProp.GetGetMethod() is not { } getMethod || positionProp.GetSetMethod() is not { } setMethod) + return; + + gizmo = new PositionGizmo( + () => (Vec3)getMethod.Invoke(selectedDefinition, [])!, + newValue => + { + setMethod.Invoke(selectedDefinition, [newValue]); + selectedDefinition.Dirty = true; + }); + } + get => selectedDefinition; + } + public Actor[] SelectedActors => Selected is not null && ActorsFromDefinition.TryGetValue(Selected, out var actors) ? actors : []; private readonly Dictionary actorsFromDefinition = new(); @@ -32,7 +62,7 @@ public class EditorWorld : World private readonly Batcher3D batch3D = new(); // TODO: Temporary! - private PositionGizmo posGizmo = new(); + private Gizmo? gizmo; private Vec2 dragStart; private Vec3 dragStartPosition; @@ -147,6 +177,7 @@ internal void RefreshEnvironment() } private float previousScale = 1.0f; + public override void Entered() { // Game.ResolutionScale = Save.; @@ -258,7 +289,7 @@ public override void Update() // Notice when mouse is hovering over. // While dragging, don't update the gizmo since we might go out of the gizmo's bounds. bool isDragging = Input.Mouse.LeftDown && !Input.Mouse.LeftPressed; - bool hitGizmo = isDragging || posGizmo.RaycastCheck(Camera.Position, direction); + bool hitGizmo = isDragging || (gizmo?.RaycastCheck(Camera.Position, direction) ?? false); if (Input.Mouse.LeftPressed) { @@ -284,7 +315,7 @@ public override void Update() // Continue dragging else if (Input.Mouse.LeftDown) { - posGizmo.Drag(this, Input.Mouse.Position - dragStart, direction, dragStartPosition); + gizmo?.Drag(this, Input.Mouse.Position - dragStart, direction, dragStartPosition); } } @@ -516,7 +547,7 @@ public override void Render(Target target) // Render gizmos on-top target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); { - posGizmo.Render(batch3D); + gizmo?.Render(batch3D); } batch3D.Render(ref state); batch3D.Clear(); diff --git a/Source/Mod/Editor/Gizmo/Gizmo.cs b/Source/Mod/Editor/Gizmo/Gizmo.cs index 663d4444..df96d676 100644 --- a/Source/Mod/Editor/Gizmo/Gizmo.cs +++ b/Source/Mod/Editor/Gizmo/Gizmo.cs @@ -2,7 +2,11 @@ namespace Celeste64.Mod.Editor; public abstract class Gizmo { + public abstract Matrix Transform { get; } + public abstract void Render(Batcher3D batch3D); + public abstract bool RaycastCheck(Vec3 origin, Vec3 direction); + public abstract void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 objectStartingPosition); } public enum GizmoTarget diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index 109baccc..071eaa1f 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -1,7 +1,10 @@ namespace Celeste64.Mod.Editor; -public class PositionGizmo : Gizmo +public class PositionGizmo(PositionGizmo.GetPositionDelegate getPosition, PositionGizmo.SetPositionDelegate setPosition) : Gizmo { + public delegate Vec3 GetPositionDelegate(); + public delegate void SetPositionDelegate(Vec3 value); + private GizmoTarget target; private const float CubeSize = 0.15f; @@ -53,26 +56,22 @@ public class PositionGizmo : Gizmo -new Vec3(CubeSize + BoundsPadding), new Vec3(CubeSize + BoundsPadding)); - public static Matrix Transform + public override Matrix Transform { get { - if (EditorWorld.Current.Selected is not SpikeBlock.Definition selected) - return Matrix.Identity; + var position = getPosition(); const float minScale = 10.0f; - float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, selected.Position) / 20.0f); + float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, position) / 20.0f); return Matrix.CreateScale(scale) * - Matrix.CreateTranslation(selected.Position); + Matrix.CreateTranslation(position); } } public override void Render(Batcher3D batch3D) { - if (EditorWorld.Current.Selected is not SpikeBlock.Definition selected) - return; - const byte normalAlpha = 0xff; const byte hoverAlpha = 0xff; const byte dragAlpha = 0xff; @@ -152,7 +151,8 @@ private static readonly (BoundingBox Bounds, GizmoTarget Target)[] GizmoTargets (XYZCubeBounds, GizmoTarget.CubeXYZ), ]; - public bool RaycastCheck(Vec3 origin, Vec3 direction) + + public override bool RaycastCheck(Vec3 origin, Vec3 direction) { float closestGizmo = float.PositiveInfinity; @@ -169,7 +169,7 @@ public bool RaycastCheck(Vec3 origin, Vec3 direction) return target != GizmoTarget.None; } - public void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 objectStartingPosition) + public override void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 objectStartingPosition) { var axisMatrix = Transform * editor.Camera.ViewProjection; var screenXAxis = Vec3.TransformNormal(Vec3.UnitX, axisMatrix).XY(); @@ -186,9 +186,7 @@ public void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 object float dotY = Vec2.Dot(mouseDelta, screenYAxis) * dotScale; float dotZ = Vec2.Dot(mouseDelta, screenZAxis) * dotScale; - Vec3 newPosition = Vec3.Zero; - if (editor.Selected is SpikeBlock.Definition def) - newPosition = def.Position; + Vec3 newPosition = getPosition(); var xzPlaneDelta = Vec3.Transform(XZPlaneBounds.Center, Transform) - newPosition; var yzPlaneDelta = Vec3.Transform(YZPlaneBounds.Center, Transform) - newPosition; @@ -234,11 +232,7 @@ public void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 object break; } - if (editor.Selected is SpikeBlock.Definition def2) - { - def2.Position = newPosition; - def2.Dirty = true; - } + setPosition(newPosition); } } diff --git a/Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs b/Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs new file mode 100644 index 00000000..3551085c --- /dev/null +++ b/Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs @@ -0,0 +1,12 @@ +namespace Celeste64.Mod.Editor; + +[AttributeUsage(AttributeTargets.Property)] +public class SpecialPropertyAttribute(SpecialPropertyType value) : Attribute +{ + public readonly SpecialPropertyType Value = value; +} + +public enum SpecialPropertyType +{ + PositionXYZ, +} From 502835c958d0ee6af90b2e79c2b9f2c701fd751e Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 12:42:40 +0100 Subject: [PATCH 75/97] Tweak names of attributes for definition properties --- Source/Mod/Editor/EditorWorld.cs | 2 +- Source/Mod/Editor/FujiMap.cs | 8 ++++---- Source/Mod/Editor/GUI/EditActorWindow.cs | 4 ++-- ...tyCustomAttribute.cs => CustomPropertyAttribute.cs} | 2 +- ...tyIgnoreAttribute.cs => IgnorePropertyAttribute.cs} | 2 +- .../Editor/Serialization/SpecialPropertyAttribute.cs | 10 +++++----- 6 files changed, 14 insertions(+), 14 deletions(-) rename Source/Mod/Editor/Serialization/{PropertyCustomAttribute.cs => CustomPropertyAttribute.cs} (95%) rename Source/Mod/Editor/Serialization/{PropertyIgnoreAttribute.cs => IgnorePropertyAttribute.cs} (60%) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 98f4efda..32c4296d 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -34,7 +34,7 @@ private set .GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .FirstOrDefault(prop => - !prop.HasAttr() && + !prop.HasAttr() && prop.GetCustomAttribute() is { Value: SpecialPropertyType.PositionXYZ }); if (positionProp is null || positionProp.GetGetMethod() is not { } getMethod || positionProp.GetSetMethod() is not { } setMethod) diff --git a/Source/Mod/Editor/FujiMap.cs b/Source/Mod/Editor/FujiMap.cs index 11d8cb04..8362229f 100644 --- a/Source/Mod/Editor/FujiMap.cs +++ b/Source/Mod/Editor/FujiMap.cs @@ -62,11 +62,11 @@ public FujiMap(string name, string virtPath, Stream stream, string? fullPath) var props = defType .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(prop => !prop.HasAttr()); + .Where(prop => !prop.HasAttr()); foreach (var prop in props) { - if (prop.GetCustomAttribute() is { } custom) + if (prop.GetCustomAttribute() is { } custom) { prop.SetValue(def, custom.Deserialize(reader)); continue; @@ -162,11 +162,11 @@ public void SaveToFile() var props = def.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) - .Where(prop => !prop.HasAttr()); + .Where(prop => !prop.HasAttr()); foreach (var prop in props) { - if (prop.GetCustomAttribute() is { } custom) + if (prop.GetCustomAttribute() is { } custom) { custom.Serialize(prop.GetValue(def)!, writer); continue; diff --git a/Source/Mod/Editor/GUI/EditActorWindow.cs b/Source/Mod/Editor/GUI/EditActorWindow.cs index 128b7dd8..70b81946 100644 --- a/Source/Mod/Editor/GUI/EditActorWindow.cs +++ b/Source/Mod/Editor/GUI/EditActorWindow.cs @@ -22,11 +22,11 @@ protected override void RenderWindow(EditorWorld editor) { var props = selected.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(prop => !prop.HasAttr()); + .Where(prop => !prop.HasAttr()); foreach (var prop in props) { - if (prop.GetCustomAttribute() is { } custom) + if (prop.GetCustomAttribute() is { } custom) { var obj = prop.GetValue(selected)!; custom.RenderGui(ref obj); diff --git a/Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs b/Source/Mod/Editor/Serialization/CustomPropertyAttribute.cs similarity index 95% rename from Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs rename to Source/Mod/Editor/Serialization/CustomPropertyAttribute.cs index c4b6cf93..2acfb3ca 100644 --- a/Source/Mod/Editor/Serialization/PropertyCustomAttribute.cs +++ b/Source/Mod/Editor/Serialization/CustomPropertyAttribute.cs @@ -10,7 +10,7 @@ public interface ICustomProperty } [AttributeUsage(AttributeTargets.Property)] -public class PropertyCustomAttribute(Type type) : Attribute +public class CustomPropertyAttribute(Type type) : Attribute { private readonly MethodInfo m_Serialize = type.GetMethod(nameof(ICustomProperty.Serialize), BindingFlags.Public | BindingFlags.Static) ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); diff --git a/Source/Mod/Editor/Serialization/PropertyIgnoreAttribute.cs b/Source/Mod/Editor/Serialization/IgnorePropertyAttribute.cs similarity index 60% rename from Source/Mod/Editor/Serialization/PropertyIgnoreAttribute.cs rename to Source/Mod/Editor/Serialization/IgnorePropertyAttribute.cs index a747b6e6..be933880 100644 --- a/Source/Mod/Editor/Serialization/PropertyIgnoreAttribute.cs +++ b/Source/Mod/Editor/Serialization/IgnorePropertyAttribute.cs @@ -1,4 +1,4 @@ namespace Celeste64.Mod.Editor; [AttributeUsage(AttributeTargets.Property)] -public class PropertyIgnoreAttribute : Attribute; +public class IgnorePropertyAttribute : Attribute; diff --git a/Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs b/Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs index 3551085c..18327152 100644 --- a/Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs +++ b/Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs @@ -1,12 +1,12 @@ namespace Celeste64.Mod.Editor; -[AttributeUsage(AttributeTargets.Property)] -public class SpecialPropertyAttribute(SpecialPropertyType value) : Attribute +public enum SpecialPropertyType { - public readonly SpecialPropertyType Value = value; + PositionXYZ, } -public enum SpecialPropertyType +[AttributeUsage(AttributeTargets.Property)] +public class SpecialPropertyAttribute(SpecialPropertyType value) : Attribute { - PositionXYZ, + public readonly SpecialPropertyType Value = value; } From 0519ffbf9ca16b65506caabba8ac2e53d26d7d27 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 13:54:55 +0100 Subject: [PATCH 76/97] Remove requirement of SpikeBlock.Definition for dragging --- Source/Mod/Editor/EditorWorld.cs | 10 +++------- Source/Mod/Editor/Gizmo/Gizmo.cs | 4 +++- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 24 +++++++++++++++--------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 32c4296d..7d4211ba 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -64,7 +64,6 @@ private set // TODO: Temporary! private Gizmo? gizmo; private Vec2 dragStart; - private Vec3 dragStartPosition; internal EditorWorld(EntryInfo entry) : base(entry) { @@ -297,11 +296,8 @@ public override void Update() if (hitGizmo) { // Start dragging - if (Selected is SpikeBlock.Definition def) - { - dragStart = Input.Mouse.Position; - dragStartPosition = def.Position; - } + dragStart = Input.Mouse.Position; + gizmo?.DragStart(); } // Then check for actors else @@ -315,7 +311,7 @@ public override void Update() // Continue dragging else if (Input.Mouse.LeftDown) { - gizmo?.Drag(this, Input.Mouse.Position - dragStart, direction, dragStartPosition); + gizmo?.Drag(this, Input.Mouse.Position - dragStart, direction); } } diff --git a/Source/Mod/Editor/Gizmo/Gizmo.cs b/Source/Mod/Editor/Gizmo/Gizmo.cs index df96d676..921db9c4 100644 --- a/Source/Mod/Editor/Gizmo/Gizmo.cs +++ b/Source/Mod/Editor/Gizmo/Gizmo.cs @@ -6,7 +6,9 @@ public abstract class Gizmo public abstract void Render(Batcher3D batch3D); public abstract bool RaycastCheck(Vec3 origin, Vec3 direction); - public abstract void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 objectStartingPosition); + + public abstract void DragStart(); + public abstract void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay); } public enum GizmoTarget diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index 071eaa1f..e3eeebe2 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -6,6 +6,7 @@ public class PositionGizmo(PositionGizmo.GetPositionDelegate getPosition, Positi public delegate void SetPositionDelegate(Vec3 value); private GizmoTarget target; + private Vec3 beforeDragPosition = Vec3.Zero; private const float CubeSize = 0.15f; private const float PlaneSize = 0.6f; @@ -168,8 +169,13 @@ public override bool RaycastCheck(Vec3 origin, Vec3 direction) return target != GizmoTarget.None; } + + public override void DragStart() + { + beforeDragPosition = getPosition(); + } - public override void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Vec3 objectStartingPosition) + public override void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay) { var axisMatrix = Transform * editor.Camera.ViewProjection; var screenXAxis = Vec3.TransformNormal(Vec3.UnitX, axisMatrix).XY(); @@ -192,31 +198,31 @@ public override void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay, Ve var yzPlaneDelta = Vec3.Transform(YZPlaneBounds.Center, Transform) - newPosition; var xyPlaneDelta = Vec3.Transform(XYPlaneBounds.Center, Transform) - newPosition; - var cameraPlaneNormal = (editor.Camera.Position - objectStartingPosition).Normalized(); - var cameraPlane = new Plane(cameraPlaneNormal, Vec3.Dot(cameraPlaneNormal, objectStartingPosition)); + var cameraPlaneNormal = (editor.Camera.Position - beforeDragPosition).Normalized(); + var cameraPlane = new Plane(cameraPlaneNormal, Vec3.Dot(cameraPlaneNormal, beforeDragPosition)); switch (target) { case GizmoTarget.AxisX: - newPosition = objectStartingPosition + Vec3.UnitX * dotX; + newPosition = beforeDragPosition + Vec3.UnitX * dotX; break; case GizmoTarget.AxisY: - newPosition = objectStartingPosition + Vec3.UnitY * dotY; + newPosition = beforeDragPosition + Vec3.UnitY * dotY; break; case GizmoTarget.AxisZ: - newPosition = objectStartingPosition + Vec3.UnitZ * dotZ; + newPosition = beforeDragPosition + Vec3.UnitZ * dotZ; break; case GizmoTarget.PlaneXZ: - float tY = (objectStartingPosition.Y - editor.Camera.Position.Y) / mouseRay.Y; + float tY = (beforeDragPosition.Y - editor.Camera.Position.Y) / mouseRay.Y; newPosition = editor.Camera.Position + mouseRay * tY - xzPlaneDelta; break; case GizmoTarget.PlaneYZ: - float tX = (objectStartingPosition.X - editor.Camera.Position.X) / mouseRay.X; + float tX = (beforeDragPosition.X - editor.Camera.Position.X) / mouseRay.X; newPosition = editor.Camera.Position + mouseRay * tX - yzPlaneDelta; break; case GizmoTarget.PlaneXY: - float tZ = (objectStartingPosition.Z - editor.Camera.Position.Z) / mouseRay.Z; + float tZ = (beforeDragPosition.Z - editor.Camera.Position.Z) / mouseRay.Z; newPosition = editor.Camera.Position + mouseRay * tZ - xyPlaneDelta; break; From 8a89d5dc1ac5973dd3ca58eb6c93f7cf605890b1 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 14:04:34 +0100 Subject: [PATCH 77/97] Fix RenderGui of custom properties --- Source/Mod/Editor/GUI/EditActorWindow.cs | 9 ++++++--- .../Mod/Editor/Serialization/CustomPropertyAttribute.cs | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Source/Mod/Editor/GUI/EditActorWindow.cs b/Source/Mod/Editor/GUI/EditActorWindow.cs index 70b81946..54fb3769 100644 --- a/Source/Mod/Editor/GUI/EditActorWindow.cs +++ b/Source/Mod/Editor/GUI/EditActorWindow.cs @@ -28,9 +28,12 @@ protected override void RenderWindow(EditorWorld editor) { if (prop.GetCustomAttribute() is { } custom) { - var obj = prop.GetValue(selected)!; - custom.RenderGui(ref obj); - prop.SetValue(selected, obj); + object obj = prop.GetValue(selected)!; + if (custom.RenderGui(ref obj)) + { + prop.SetValue(selected, obj); + selected.Dirty = true; + } continue; } diff --git a/Source/Mod/Editor/Serialization/CustomPropertyAttribute.cs b/Source/Mod/Editor/Serialization/CustomPropertyAttribute.cs index 2acfb3ca..5d76b4b8 100644 --- a/Source/Mod/Editor/Serialization/CustomPropertyAttribute.cs +++ b/Source/Mod/Editor/Serialization/CustomPropertyAttribute.cs @@ -6,7 +6,7 @@ public interface ICustomProperty { public static abstract void Serialize(T value, BinaryWriter writer); public static abstract T Deserialize(BinaryReader reader); - public static abstract void RenderGui(ref T value); + public static abstract bool RenderGui(ref T value); } [AttributeUsage(AttributeTargets.Property)] @@ -18,11 +18,11 @@ public class CustomPropertyAttribute(Type type) : Attribute private readonly MethodInfo m_Deserialize = type.GetMethod(nameof(ICustomProperty.Deserialize), BindingFlags.Public | BindingFlags.Static) ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); - private readonly MethodInfo m_RenderGui = type.GetMethod(nameof(ICustomProperty.Deserialize), BindingFlags.Public | BindingFlags.Static) + private readonly MethodInfo m_RenderGui = type.GetMethod(nameof(ICustomProperty.RenderGui), BindingFlags.Public | BindingFlags.Static) ?? throw new Exception($"Custom property definition {type} does not inherit from ICustomProperty"); internal void Serialize(object value, BinaryWriter writer) => m_Serialize.Invoke(null, [value, writer]); internal object Deserialize(BinaryReader reader) => m_Deserialize.Invoke(null, [reader])!; - internal void RenderGui(ref object value) => m_RenderGui.Invoke(null, [value]); + internal bool RenderGui(ref object value) => (bool)m_RenderGui.Invoke(null, [value])!; } From f7449e6943b313c373c2533d1bb946e7bb8a8f02 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 14:05:46 +0100 Subject: [PATCH 78/97] Add basic ActorDefinition for solids --- Source/Actors/Solid.cs | 161 +++++++++++++++++++++++ Source/Mod/Editor/GUI/EditActorWindow.cs | 4 + 2 files changed, 165 insertions(+) diff --git a/Source/Actors/Solid.cs b/Source/Actors/Solid.cs index 2394a0f7..c6038fa2 100644 --- a/Source/Actors/Solid.cs +++ b/Source/Actors/Solid.cs @@ -1,7 +1,168 @@ +using Celeste64.Mod; +using Celeste64.Mod.Editor; +using ImGuiNET; +using System.Runtime.InteropServices; + namespace Celeste64; public class Solid : Actor, IHaveModels { + public class Definition : ActorDefinition + { + [SpecialProperty(SpecialPropertyType.PositionXYZ)] + public Vec3 Position { get; set; } + + static float size => 10.0f; + + [CustomProperty(typeof(VerticesProperty))] + public List> Faces { get; set; } = [ + [new Vec3(-size, 0.0f, size), new Vec3(size, 0.0f, size), new Vec3(size, 0.0f, -size), new Vec3(-size, 0.0f, -size)] + ]; + + public override Actor[] Load(World.WorldType type) + { + // Calculate bounds + var bounds = new BoundingBox(); + foreach (var face in Faces) + { + var faceMin = face.Aggregate(Vec3.Min); + var faceMax = face.Aggregate(Vec3.Max); + bounds = new BoundingBox(Vec3.Min(bounds.Min, faceMin), Vec3.Max(bounds.Max, faceMax)); + } + + // Generate visual / collision mesh + var colliderVertices = new List(); + var colliderFaces = new List(); + + var meshVertices = new List(); + var meshIndices = new List(); + + foreach (var face in Faces) + { + int vertexIndex = colliderVertices.Count; + var plane = Plane.CreateFromVertices(face[0], face[1], face[2]); + + colliderFaces.Add(new Face + { + Plane = plane, + VertexStart = vertexIndex, + VertexCount = face.Count + }); + + // Triangulate the mesh + for (int i = 0; i < face.Count - 2; i++) + { + meshIndices.Add(vertexIndex + 0); + meshIndices.Add(vertexIndex + i + 1); + meshIndices.Add(vertexIndex + i + 2); + } + + // The center of the bounding box should always be <0, 0, 0> + colliderVertices.AddRange(face.Select(vertex => vertex - bounds.Center)); + meshVertices.AddRange(face.Select(vertex => new Vertex( + position: vertex - bounds.Center, + texcoord: Vec2.Zero, + color: Vec3.One, + normal: plane.Normal))); + } + + var solid = new Solid(); + solid.Model.Mesh.SetVertices(CollectionsMarshal.AsSpan(meshVertices)); + solid.Model.Mesh.SetIndices(CollectionsMarshal.AsSpan(meshIndices)); + solid.Model.Materials.Add(new DefaultMaterial(Assets.Textures["wall"])); + solid.Model.Parts.Add(new SimpleModel.Part(0, 0, meshIndices.Count)); + + solid.LocalBounds = new BoundingBox( + colliderVertices.Aggregate(Vec3.Min), + colliderVertices.Aggregate(Vec3.Max) + ); + solid.LocalVertices = colliderVertices.ToArray(); + solid.LocalFaces = colliderFaces.ToArray(); + solid.Position = Position + bounds.Center; + + return [solid]; + } + + public class VerticesProperty : ICustomProperty>> + { + public static void Serialize(List> value, BinaryWriter writer) + { + writer.Write(value.Count); + foreach (var vertices in value) + { + writer.Write(vertices.Count); + foreach (var vertex in vertices) + { + writer.Write(vertex); + } + } + } + + public static List> Deserialize(BinaryReader reader) + { + var value = new List>(capacity: reader.ReadInt32()); + for (int i = 0; i < value.Capacity; i++) + { + var vertices = new List(capacity: reader.ReadInt32()); + for (int j = 0; j < vertices.Capacity; j++) + { + vertices[j] = reader.ReadVec3(); + } + value[i] = vertices; + } + + return value; + } + + public static bool RenderGui(ref List> value) + { + bool changed = false; + + ImGui.Text("Geometry:"); + for (int i = 0; i < value.Count; i++) + { + ImGui.SeparatorText($"Face {i + 1}"); + for (int j = 0; j < value[i].Count; j++) + { + var v = value[i][j]; + changed |= ImGui.DragFloat3($"{j + 1}##{i}-{j}", ref v); + value[i][j] = v; + + if (value[i].Count > 3 && ImGui.Button($"Remove Vertex##{i}-{j}")) + { + value[i].RemoveAt(j); + changed = true; + } + } + + ImGui.Separator(); + + if (ImGui.Button($"Add Vertex##{i}")) + { + value[i].Add(Vec3.Zero); + changed = true; + } + + if (ImGui.Button($"Remove Face##{i}")) + { + value.RemoveAt(i); + changed = true; + } + } + + ImGui.Separator(); + + if (ImGui.Button("Add Face")) + { + value.Add([Vec3.Zero, Vec3.Zero, Vec3.Zero]); + changed = true; + } + + return changed; + } + } + } + /// /// If we're currently solid /// diff --git a/Source/Mod/Editor/GUI/EditActorWindow.cs b/Source/Mod/Editor/GUI/EditActorWindow.cs index 54fb3769..99c5c51e 100644 --- a/Source/Mod/Editor/GUI/EditActorWindow.cs +++ b/Source/Mod/Editor/GUI/EditActorWindow.cs @@ -17,6 +17,10 @@ protected override void RenderWindow(EditorWorld editor) { editor.Definitions.Add(new SpikeBlock.Definition()); } + if (ImGui.Button("DEBUG: Add Solid")) + { + editor.Definitions.Add(new Solid.Definition()); + } if (editor.Selected is { } selected) { From 5cf5292684f488e01e2728003f126464501f4fc7 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sat, 16 Mar 2024 23:04:39 +0100 Subject: [PATCH 79/97] Fix CVE Severity 9.8 critical in map parser --- Source/Mod/Editor/FujiMap.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/Mod/Editor/FujiMap.cs b/Source/Mod/Editor/FujiMap.cs index 8362229f..dcdd24a3 100644 --- a/Source/Mod/Editor/FujiMap.cs +++ b/Source/Mod/Editor/FujiMap.cs @@ -55,7 +55,14 @@ public FujiMap(string name, string virtPath, Stream stream, string? fullPath) { // Get the definition data type, by the full name var fullName = reader.ReadString(); - var defType = Assembly.GetExecutingAssembly().GetType(fullName)!; + var defType = Assembly.GetExecutingAssembly().GetType(fullName); + if (defType is null || !defType.IsAssignableTo(typeof(ActorDefinition))) + { + isMalformed = true; + readExceptionMessage = $"The definition type {fullName} is invalid"; + return; + } + var def = Activator.CreateInstance(defType); Log.Info($"Reading def: {def}"); From 0e746f7c2472318c830a4a79541cc56ead765e0e Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 17 Mar 2024 11:18:16 +0100 Subject: [PATCH 80/97] Add custom SelectionTypes for definitions --- Source/Actors/Solid.cs | 59 +++++++- .../ActorDefinition.cs | 6 +- .../CustomPropertyAttribute.cs | 0 .../IgnorePropertyAttribute.cs | 0 .../SpecialPropertyAttribute.cs | 0 Source/Mod/Editor/EditorWorld.cs | 137 +++++++++++------- .../Mod/Editor/Selection/SelectionTarget.cs | 9 ++ Source/Mod/Editor/Selection/SelectionType.cs | 6 + 8 files changed, 160 insertions(+), 57 deletions(-) rename Source/Mod/Editor/{Serialization => Definition}/ActorDefinition.cs (51%) rename Source/Mod/Editor/{Serialization => Definition}/CustomPropertyAttribute.cs (100%) rename Source/Mod/Editor/{Serialization => Definition}/IgnorePropertyAttribute.cs (100%) rename Source/Mod/Editor/{Serialization => Definition}/SpecialPropertyAttribute.cs (100%) create mode 100644 Source/Mod/Editor/Selection/SelectionTarget.cs create mode 100644 Source/Mod/Editor/Selection/SelectionType.cs diff --git a/Source/Actors/Solid.cs b/Source/Actors/Solid.cs index c6038fa2..ccddb57c 100644 --- a/Source/Actors/Solid.cs +++ b/Source/Actors/Solid.cs @@ -13,12 +13,18 @@ public class Definition : ActorDefinition public Vec3 Position { get; set; } static float size => 10.0f; - [CustomProperty(typeof(VerticesProperty))] public List> Faces { get; set; } = [ [new Vec3(-size, 0.0f, size), new Vec3(size, 0.0f, size), new Vec3(size, 0.0f, -size), new Vec3(-size, 0.0f, -size)] ]; + public Definition() + { + SelectionTypes = [ + new VertexSelectionType(this), + ]; + } + public override Actor[] Load(World.WorldType type) { // Calculate bounds @@ -100,15 +106,17 @@ public static void Serialize(List> value, BinaryWriter writer) public static List> Deserialize(BinaryReader reader) { - var value = new List>(capacity: reader.ReadInt32()); - for (int i = 0; i < value.Capacity; i++) + int faceCount = reader.ReadInt32(); + var value = new List>(capacity: faceCount); + for (int i = 0; i < faceCount; i++) { - var vertices = new List(capacity: reader.ReadInt32()); + int vertexCount = reader.ReadInt32(); + var vertices = new List(capacity: vertexCount); for (int j = 0; j < vertices.Capacity; j++) { - vertices[j] = reader.ReadVec3(); + vertices.Add(reader.ReadVec3()); } - value[i] = vertices; + value.Add(vertices); } return value; @@ -163,6 +171,45 @@ public static bool RenderGui(ref List> value) } } + public class VertexSelectionType : SelectionType + { + private readonly List targets = []; + public override IEnumerable Targets => targets; + + public VertexSelectionType(Solid.Definition def) + { + def.OnUpdated += () => + { + targets.Clear(); + + var transform = Matrix.CreateTranslation(def.Position); + const float selectionRadius = 1.0f; + + // TODO: Which implementation is better for performance? Probably the foreach one? + targets.AddRange(def.Faces + .SelectMany(face => face) + .Select(vertex => new SelectionTarget + { + Transform = transform, + Bounds = new BoundingBox(vertex, selectionRadius * 2.0f), + OnSelected = () => Log.Info($"Selected vertex {vertex}"), + })); + + // foreach (var face in def.Faces) + // { + // foreach (var vertex in face) + // { + // targets.Add(new SelectionTarget() + // { + // Transform = transform, + // Bounds = new BoundingBox(vertex, selectionRadius * 2.0f) + // }); + // } + // } + }; + } + } + /// /// If we're currently solid /// diff --git a/Source/Mod/Editor/Serialization/ActorDefinition.cs b/Source/Mod/Editor/Definition/ActorDefinition.cs similarity index 51% rename from Source/Mod/Editor/Serialization/ActorDefinition.cs rename to Source/Mod/Editor/Definition/ActorDefinition.cs index cd35f048..6376ebc5 100644 --- a/Source/Mod/Editor/Serialization/ActorDefinition.cs +++ b/Source/Mod/Editor/Definition/ActorDefinition.cs @@ -2,7 +2,11 @@ namespace Celeste64.Mod.Editor; public abstract class ActorDefinition { + public event Action OnUpdated = () => {}; + internal void Updated() => OnUpdated(); + public bool Dirty = true; - + public SelectionType[] SelectionTypes { get; init; } = []; + public abstract Actor[] Load(World.WorldType type); } diff --git a/Source/Mod/Editor/Serialization/CustomPropertyAttribute.cs b/Source/Mod/Editor/Definition/CustomPropertyAttribute.cs similarity index 100% rename from Source/Mod/Editor/Serialization/CustomPropertyAttribute.cs rename to Source/Mod/Editor/Definition/CustomPropertyAttribute.cs diff --git a/Source/Mod/Editor/Serialization/IgnorePropertyAttribute.cs b/Source/Mod/Editor/Definition/IgnorePropertyAttribute.cs similarity index 100% rename from Source/Mod/Editor/Serialization/IgnorePropertyAttribute.cs rename to Source/Mod/Editor/Definition/IgnorePropertyAttribute.cs diff --git a/Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs b/Source/Mod/Editor/Definition/SpecialPropertyAttribute.cs similarity index 100% rename from Source/Mod/Editor/Serialization/SpecialPropertyAttribute.cs rename to Source/Mod/Editor/Definition/SpecialPropertyAttribute.cs diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 7d4211ba..c09ea199 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -19,6 +19,8 @@ public class EditorWorld : World public ReadOnlyDictionary ActorsFromDefinition => actorsFromDefinition.AsReadOnly(); public ReadOnlyDictionary DefinitionFromActors => definitionFromActors.AsReadOnly(); + public event Action OnSelectionChanged = def => {}; + private ActorDefinition? selectedDefinition = null; public ActorDefinition? Selected { @@ -26,10 +28,12 @@ private set { selectedDefinition = value; gizmo = null; + + OnSelectionChanged(value); if (selectedDefinition is null) return; - + var positionProp = selectedDefinition .GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) @@ -265,55 +269,7 @@ public override void Update() Camera.LookAt = cameraPos + forward; // Shoot ray cast for selection - if (!ImGuiManager.WantCaptureMouse && - Camera.Target != null && - Matrix.Invert(Camera.Projection, out var inverseProj1) && - Matrix.Invert(Camera.View, out var inverseView1)) - { - // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios - var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); - var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); - // Convert into normalized-device-coordinates - var ndcPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; - // Flip Y, since up is negative in NDC coords - ndcPos.Y *= -1.0f; - var clipPos = new Vec4(ndcPos, -1.0f, 1.0f); - var eyePos = Vec4.Transform(clipPos, inverseProj1); - // We only care about XY, so we set ZW to "forward" - eyePos.Z = -1.0f; - eyePos.W = 0.0f; - var worldPos = Vec4.Transform(eyePos, inverseView1); - var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); - - // Notice when mouse is hovering over. - // While dragging, don't update the gizmo since we might go out of the gizmo's bounds. - bool isDragging = Input.Mouse.LeftDown && !Input.Mouse.LeftPressed; - bool hitGizmo = isDragging || (gizmo?.RaycastCheck(Camera.Position, direction) ?? false); - - if (Input.Mouse.LeftPressed) - { - // First check if we hit a gizmo - if (hitGizmo) - { - // Start dragging - dragStart = Input.Mouse.Position; - gizmo?.DragStart(); - } - // Then check for actors - else - { - if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) - Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; - else - Selected = null; - } - } - // Continue dragging - else if (Input.Mouse.LeftDown) - { - gizmo?.Drag(this, Input.Mouse.Position - dragStart, direction); - } - } + SelectionRaycast(); // Update actors of definitions foreach (var def in Definitions.Where(def => def.Dirty)) @@ -337,6 +293,7 @@ public override void Update() } def.Dirty = false; + def.Updated(); } // Don't call base.Update, since we don't want the actors to update @@ -350,6 +307,86 @@ public override void Update() ResolveChanges(); } + private void SelectionRaycast() + { + if (ImGuiManager.WantCaptureMouse || + Camera.Target is null || + !Matrix.Invert(Camera.Projection, out var inverseProj) || + !Matrix.Invert(Camera.View, out var inverseView)) + { + return; + } + + // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios + var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); + var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); + // Convert into normalized-device-coordinates + var ndcPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; + // Flip Y, since up is negative in NDC coords + ndcPos.Y *= -1.0f; + var clipPos = new Vec4(ndcPos, -1.0f, 1.0f); + var eyePos = Vec4.Transform(clipPos, inverseProj); + // We only care about XY, so we set ZW to "forward" + eyePos.Z = -1.0f; + eyePos.W = 0.0f; + var worldPos = Vec4.Transform(eyePos, inverseView); + var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); + + // Check for selection target first + if (Input.Mouse.LeftPressed && Selected is not null && Selected.SelectionTypes.Length > 0) + { + // TODO: Allow for selection different types + var selType = Selected.SelectionTypes[0]; + + SelectionTarget? closest = null; + float closestDist = float.PositiveInfinity; + + foreach (var target in selType.Targets) + { + if (!ModUtils.RayIntersectOBB(Camera.Position, direction, target.Bounds, target.Transform, out float dist) || dist >= closestDist) + continue; + + closest = target; + closestDist = dist; + } + + if (closest is not null) + { + closest.OnSelected(); + return; + } + } + + // Notice when mouse is hovering over. + // While dragging, don't update the gizmo since we might go out of the gizmo's bounds. + bool isDragging = Input.Mouse.LeftDown && !Input.Mouse.LeftPressed; + bool hitGizmo = isDragging || (gizmo?.RaycastCheck(Camera.Position, direction) ?? false); + + if (Input.Mouse.LeftPressed) + { + // First check if we hit a gizmo + if (hitGizmo) + { + // Start dragging + dragStart = Input.Mouse.Position; + gizmo?.DragStart(); + } + // Then check for actors + else + { + if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) + Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; + else + Selected = null; + } + } + // Continue dragging + else if (Input.Mouse.LeftDown) + { + gizmo?.Drag(this, Input.Mouse.Position - dragStart, direction); + } + } + public override void Render(Target target) { // We copy and modify World.Render, since that's easier diff --git a/Source/Mod/Editor/Selection/SelectionTarget.cs b/Source/Mod/Editor/Selection/SelectionTarget.cs new file mode 100644 index 00000000..eeff6916 --- /dev/null +++ b/Source/Mod/Editor/Selection/SelectionTarget.cs @@ -0,0 +1,9 @@ +namespace Celeste64.Mod.Editor; + +public class SelectionTarget +{ + public required Matrix Transform; + public required BoundingBox Bounds; + + public required Action OnSelected; +} diff --git a/Source/Mod/Editor/Selection/SelectionType.cs b/Source/Mod/Editor/Selection/SelectionType.cs new file mode 100644 index 00000000..86560d5c --- /dev/null +++ b/Source/Mod/Editor/Selection/SelectionType.cs @@ -0,0 +1,6 @@ +namespace Celeste64.Mod.Editor; + +public abstract class SelectionType +{ + public abstract IEnumerable Targets { get; } +} From a8277fb1321be2037f38fe0f0a5cdebf6061e45d Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 17 Mar 2024 11:19:56 +0100 Subject: [PATCH 81/97] Blacklist SelectionTypes in (de)serialization --- Source/Mod/Editor/FujiMap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Mod/Editor/FujiMap.cs b/Source/Mod/Editor/FujiMap.cs index dcdd24a3..4e3914f3 100644 --- a/Source/Mod/Editor/FujiMap.cs +++ b/Source/Mod/Editor/FujiMap.cs @@ -69,7 +69,7 @@ public FujiMap(string name, string virtPath, Stream stream, string? fullPath) var props = defType .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(prop => !prop.HasAttr()); + .Where(prop => !prop.HasAttr() && prop.Name != nameof(ActorDefinition.SelectionTypes)); foreach (var prop in props) { @@ -169,7 +169,7 @@ public void SaveToFile() var props = def.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic) - .Where(prop => !prop.HasAttr()); + .Where(prop => !prop.HasAttr() && prop.Name != nameof(ActorDefinition.SelectionTypes)); foreach (var prop in props) { From ecfe0bf0b5fe8aefbb9c0e5d85754cb20aafd252 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 17 Mar 2024 12:02:38 +0100 Subject: [PATCH 82/97] Add hover and drag support to SelectionTargets --- Source/Actors/Solid.cs | 2 ++ Source/Mod/Editor/EditorWorld.cs | 27 ++++++++++++++----- .../Mod/Editor/Selection/SelectionTarget.cs | 4 ++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/Source/Actors/Solid.cs b/Source/Actors/Solid.cs index ccddb57c..1fb778de 100644 --- a/Source/Actors/Solid.cs +++ b/Source/Actors/Solid.cs @@ -192,7 +192,9 @@ public VertexSelectionType(Solid.Definition def) { Transform = transform, Bounds = new BoundingBox(vertex, selectionRadius * 2.0f), + OnHovered = () => Log.Info($"Hovered vertex {vertex}"), OnSelected = () => Log.Info($"Selected vertex {vertex}"), + OnDragged = (mouseDelta, mouseRay) => Log.Info($"Dragged vertex {vertex} ({mouseDelta}, {mouseRay})"), })); // foreach (var face in def.Faces) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index c09ea199..1a06b070 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -67,7 +67,8 @@ private set // TODO: Temporary! private Gizmo? gizmo; - private Vec2 dragStart; + private SelectionTarget? dragTarget = null; + private Vec2 dragMouseStart = Vec2.Zero; internal EditorWorld(EntryInfo entry) : base(entry) { @@ -332,8 +333,15 @@ Camera.Target is null || var worldPos = Vec4.Transform(eyePos, inverseView); var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); - // Check for selection target first - if (Input.Mouse.LeftPressed && Selected is not null && Selected.SelectionTypes.Length > 0) + // Continue/Stop dragging + if (Input.Mouse.LeftDown && dragTarget is not null) + { + dragTarget.OnDragged?.Invoke(Input.Mouse.Position - dragMouseStart, direction); + return; + } + dragTarget = null; + + if (Selected is not null && Selected.SelectionTypes.Length > 0) { // TODO: Allow for selection different types var selType = Selected.SelectionTypes[0]; @@ -352,7 +360,14 @@ Camera.Target is null || if (closest is not null) { - closest.OnSelected(); + closest.OnHovered?.Invoke(); + if (Input.Mouse.LeftPressed) + { + closest.OnSelected?.Invoke(); + + dragTarget = closest; + dragMouseStart = Input.Mouse.Position; + } return; } } @@ -368,7 +383,7 @@ Camera.Target is null || if (hitGizmo) { // Start dragging - dragStart = Input.Mouse.Position; + gizmo?.DragStart(); } // Then check for actors @@ -383,7 +398,7 @@ Camera.Target is null || // Continue dragging else if (Input.Mouse.LeftDown) { - gizmo?.Drag(this, Input.Mouse.Position - dragStart, direction); + gizmo?.Drag(this, Input.Mouse.Position - dragMouseStart, direction); } } diff --git a/Source/Mod/Editor/Selection/SelectionTarget.cs b/Source/Mod/Editor/Selection/SelectionTarget.cs index eeff6916..4dfa3f48 100644 --- a/Source/Mod/Editor/Selection/SelectionTarget.cs +++ b/Source/Mod/Editor/Selection/SelectionTarget.cs @@ -5,5 +5,7 @@ public class SelectionTarget public required Matrix Transform; public required BoundingBox Bounds; - public required Action OnSelected; + public Action? OnHovered = null; + public Action? OnSelected = null; + public Action? OnDragged = null; } From 1fae7432fefe970f9ed7aab2a98585265e2ca2db Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 17 Mar 2024 12:15:59 +0100 Subject: [PATCH 83/97] Refactor gizmos to use SelectionTargets --- Source/Mod/Editor/EditorWorld.cs | 21 +++---- Source/Mod/Editor/Gizmo/Gizmo.cs | 2 +- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 74 +++++++++++++++++++++++- 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 1a06b070..7d5c66d6 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -345,11 +345,12 @@ Camera.Target is null || { // TODO: Allow for selection different types var selType = Selected.SelectionTypes[0]; + var targets = selType.Targets.Concat((gizmo as PositionGizmo)?.Targets ?? []); SelectionTarget? closest = null; float closestDist = float.PositiveInfinity; - foreach (var target in selType.Targets) + foreach (var target in targets) { if (!ModUtils.RayIntersectOBB(Camera.Position, direction, target.Bounds, target.Transform, out float dist) || dist >= closestDist) continue; @@ -375,30 +376,30 @@ Camera.Target is null || // Notice when mouse is hovering over. // While dragging, don't update the gizmo since we might go out of the gizmo's bounds. bool isDragging = Input.Mouse.LeftDown && !Input.Mouse.LeftPressed; - bool hitGizmo = isDragging || (gizmo?.RaycastCheck(Camera.Position, direction) ?? false); + // bool hitGizmo = isDragging || (gizmo?.RaycastCheck(Camera.Position, direction) ?? false); if (Input.Mouse.LeftPressed) { // First check if we hit a gizmo - if (hitGizmo) - { + // if (hitGizmo) + // { // Start dragging - gizmo?.DragStart(); - } + // gizmo?.DragStart(); + // } // Then check for actors - else - { + // else + // { if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; else Selected = null; - } + // } } // Continue dragging else if (Input.Mouse.LeftDown) { - gizmo?.Drag(this, Input.Mouse.Position - dragMouseStart, direction); + // gizmo?.Drag(this, Input.Mouse.Position - dragMouseStart, direction); } } diff --git a/Source/Mod/Editor/Gizmo/Gizmo.cs b/Source/Mod/Editor/Gizmo/Gizmo.cs index 921db9c4..0b9ef775 100644 --- a/Source/Mod/Editor/Gizmo/Gizmo.cs +++ b/Source/Mod/Editor/Gizmo/Gizmo.cs @@ -8,7 +8,7 @@ public abstract class Gizmo public abstract bool RaycastCheck(Vec3 origin, Vec3 direction); public abstract void DragStart(); - public abstract void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay); + public abstract void Drag(Vec2 mouseDelta, Vec3 mouseRay); } public enum GizmoTarget diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index e3eeebe2..10552ed6 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -57,6 +57,7 @@ public class PositionGizmo(PositionGizmo.GetPositionDelegate getPosition, Positi -new Vec3(CubeSize + BoundsPadding), new Vec3(CubeSize + BoundsPadding)); + // TODO: Please cache these properties public override Matrix Transform { get @@ -70,6 +71,75 @@ public override Matrix Transform Matrix.CreateTranslation(position); } } + + public IEnumerable Targets => + [ + // X Axis + new SelectionTarget + { + Transform = Transform, + Bounds = XAxisBounds, + OnSelected = DragStart, + OnHovered = () => target = GizmoTarget.AxisX, + OnDragged = Drag + }, + // Y Axis + new SelectionTarget + { + Transform = Transform, + Bounds = YAxisBounds, + OnSelected = DragStart, + OnHovered = () => target = GizmoTarget.AxisY, + OnDragged = Drag + }, + // Z Axis + new SelectionTarget + { + Transform = Transform, + Bounds = ZAxisBounds, + OnSelected = DragStart, + OnHovered = () => target = GizmoTarget.AxisZ, + OnDragged = Drag + }, + + // XZ Plane + new SelectionTarget + { + Transform = Transform, + Bounds = XZPlaneBounds, + OnSelected = DragStart, + OnHovered = () => target = GizmoTarget.PlaneXZ, + OnDragged = Drag + }, + // YZ Plane + new SelectionTarget + { + Transform = Transform, + Bounds = YZPlaneBounds, + OnSelected = DragStart, + OnHovered = () => target = GizmoTarget.PlaneYZ, + OnDragged = Drag + }, + // XY Plane + new SelectionTarget + { + Transform = Transform, + Bounds = XYPlaneBounds, + OnSelected = DragStart, + OnHovered = () => target = GizmoTarget.PlaneXY, + OnDragged = Drag + }, + + // XYZ Cube + new SelectionTarget + { + Transform = Transform, + Bounds = XYZCubeBounds, + OnSelected = DragStart, + OnHovered = () => target = GizmoTarget.CubeXYZ, + OnDragged = Drag + }, + ]; public override void Render(Batcher3D batch3D) { @@ -175,8 +245,10 @@ public override void DragStart() beforeDragPosition = getPosition(); } - public override void Drag(EditorWorld editor, Vec2 mouseDelta, Vec3 mouseRay) + public override void Drag(Vec2 mouseDelta, Vec3 mouseRay) { + var editor = EditorWorld.Current; + var axisMatrix = Transform * editor.Camera.ViewProjection; var screenXAxis = Vec3.TransformNormal(Vec3.UnitX, axisMatrix).XY(); var screenYAxis = Vec3.TransformNormal(Vec3.UnitY, axisMatrix).XY(); From 936119b589bd2a50614a6f3278c84420e399487e Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 17 Mar 2024 12:52:23 +0100 Subject: [PATCH 84/97] Fully refactor gizmos for SelectionTargets --- Source/Actors/Solid.cs | 4 +- Source/Mod/Editor/EditorWorld.cs | 34 +++- Source/Mod/Editor/Gizmo/Gizmo.cs | 14 +- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 160 +++++++----------- .../Mod/Editor/Selection/SelectionTarget.cs | 32 +++- 5 files changed, 114 insertions(+), 130 deletions(-) diff --git a/Source/Actors/Solid.cs b/Source/Actors/Solid.cs index 1fb778de..88508742 100644 --- a/Source/Actors/Solid.cs +++ b/Source/Actors/Solid.cs @@ -188,10 +188,8 @@ public VertexSelectionType(Solid.Definition def) // TODO: Which implementation is better for performance? Probably the foreach one? targets.AddRange(def.Faces .SelectMany(face => face) - .Select(vertex => new SelectionTarget + .Select(vertex => new SimpleSelectionTarget(transform, new BoundingBox(vertex, selectionRadius * 2.0f)) { - Transform = transform, - Bounds = new BoundingBox(vertex, selectionRadius * 2.0f), OnHovered = () => Log.Info($"Hovered vertex {vertex}"), OnSelected = () => Log.Info($"Selected vertex {vertex}"), OnDragged = (mouseDelta, mouseRay) => Log.Info($"Dragged vertex {vertex} ({mouseDelta}, {mouseRay})"), diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 7d5c66d6..3de12fca 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -336,21 +336,38 @@ Camera.Target is null || // Continue/Stop dragging if (Input.Mouse.LeftDown && dragTarget is not null) { - dragTarget.OnDragged?.Invoke(Input.Mouse.Position - dragMouseStart, direction); + dragTarget.Dragged(Input.Mouse.Position - dragMouseStart, direction); return; } + + if (dragTarget != null) + { + dragTarget.IsDragged = false; + } dragTarget = null; + // Collect all active selection targets + List selectionTargets = []; if (Selected is not null && Selected.SelectionTypes.Length > 0) { - // TODO: Allow for selection different types + // TODO: Allow for selecting different types var selType = Selected.SelectionTypes[0]; - var targets = selType.Targets.Concat((gizmo as PositionGizmo)?.Targets ?? []); - + selectionTargets.AddRange(selType.Targets); + } + if (gizmo is not null) + { + selectionTargets.AddRange(gizmo.SelectionTargets); + } + + // Un-hover everything + foreach (var target in selectionTargets) + target.IsHovered = false; + + { SelectionTarget? closest = null; float closestDist = float.PositiveInfinity; - foreach (var target in targets) + foreach (var target in selectionTargets) { if (!ModUtils.RayIntersectOBB(Camera.Position, direction, target.Bounds, target.Transform, out float dist) || dist >= closestDist) continue; @@ -361,12 +378,15 @@ Camera.Target is null || if (closest is not null) { - closest.OnHovered?.Invoke(); + closest.Hovered(); + closest.IsHovered = true; + if (Input.Mouse.LeftPressed) { - closest.OnSelected?.Invoke(); + closest.Selected(); dragTarget = closest; + dragTarget.IsDragged = true; dragMouseStart = Input.Mouse.Position; } return; diff --git a/Source/Mod/Editor/Gizmo/Gizmo.cs b/Source/Mod/Editor/Gizmo/Gizmo.cs index 0b9ef775..4181e6d2 100644 --- a/Source/Mod/Editor/Gizmo/Gizmo.cs +++ b/Source/Mod/Editor/Gizmo/Gizmo.cs @@ -2,19 +2,7 @@ namespace Celeste64.Mod.Editor; public abstract class Gizmo { - public abstract Matrix Transform { get; } + public abstract IEnumerable SelectionTargets { get; } public abstract void Render(Batcher3D batch3D); - public abstract bool RaycastCheck(Vec3 origin, Vec3 direction); - - public abstract void DragStart(); - public abstract void Drag(Vec2 mouseDelta, Vec3 mouseRay); -} - -public enum GizmoTarget -{ - None, - AxisX, AxisY, AxisZ, - PlaneXZ, PlaneYZ, PlaneXY, - CubeXYZ, } diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index 10552ed6..ae4e1630 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -1,10 +1,32 @@ namespace Celeste64.Mod.Editor; -public class PositionGizmo(PositionGizmo.GetPositionDelegate getPosition, PositionGizmo.SetPositionDelegate setPosition) : Gizmo +public enum GizmoTarget { + None, + AxisX, AxisY, AxisZ, + PlaneXZ, PlaneYZ, PlaneXY, + CubeXYZ, +} + +public class PositionGizmo : Gizmo +{ + private class PositionGizmoSelectionTarget(PositionGizmo gizmo, GizmoTarget target, BoundingBox bounds) : SelectionTarget + { + public readonly GizmoTarget Target = target; + + public override Matrix Transform => gizmo.Transform; + public override BoundingBox Bounds { get; } = bounds; + + public override void Selected() => gizmo.DragStart(); + public override void Dragged(Vec2 mouseDelta, Vec3 mouseRay) => gizmo.Drag(mouseDelta, mouseRay); + } + public delegate Vec3 GetPositionDelegate(); public delegate void SetPositionDelegate(Vec3 value); - + + private readonly GetPositionDelegate getPosition; + private readonly SetPositionDelegate setPosition; + private GizmoTarget target; private Vec3 beforeDragPosition = Vec3.Zero; @@ -58,7 +80,7 @@ public class PositionGizmo(PositionGizmo.GetPositionDelegate getPosition, Positi new Vec3(CubeSize + BoundsPadding)); // TODO: Please cache these properties - public override Matrix Transform + public Matrix Transform { get { @@ -72,77 +94,40 @@ public override Matrix Transform } } - public IEnumerable Targets => - [ - // X Axis - new SelectionTarget - { - Transform = Transform, - Bounds = XAxisBounds, - OnSelected = DragStart, - OnHovered = () => target = GizmoTarget.AxisX, - OnDragged = Drag - }, - // Y Axis - new SelectionTarget - { - Transform = Transform, - Bounds = YAxisBounds, - OnSelected = DragStart, - OnHovered = () => target = GizmoTarget.AxisY, - OnDragged = Drag - }, - // Z Axis - new SelectionTarget - { - Transform = Transform, - Bounds = ZAxisBounds, - OnSelected = DragStart, - OnHovered = () => target = GizmoTarget.AxisZ, - OnDragged = Drag - }, - - // XZ Plane - new SelectionTarget - { - Transform = Transform, - Bounds = XZPlaneBounds, - OnSelected = DragStart, - OnHovered = () => target = GizmoTarget.PlaneXZ, - OnDragged = Drag - }, - // YZ Plane - new SelectionTarget - { - Transform = Transform, - Bounds = YZPlaneBounds, - OnSelected = DragStart, - OnHovered = () => target = GizmoTarget.PlaneYZ, - OnDragged = Drag - }, - // XY Plane - new SelectionTarget - { - Transform = Transform, - Bounds = XYPlaneBounds, - OnSelected = DragStart, - OnHovered = () => target = GizmoTarget.PlaneXY, - OnDragged = Drag - }, + private readonly PositionGizmoSelectionTarget[] selectionTargets; + public override IEnumerable SelectionTargets => selectionTargets; + + public PositionGizmo(GetPositionDelegate getPosition, SetPositionDelegate setPosition) + { + this.getPosition = getPosition; + this.setPosition = setPosition; - // XYZ Cube - new SelectionTarget - { - Transform = Transform, - Bounds = XYZCubeBounds, - OnSelected = DragStart, - OnHovered = () => target = GizmoTarget.CubeXYZ, - OnDragged = Drag - }, - ]; - + selectionTargets = [ + new PositionGizmoSelectionTarget(this, GizmoTarget.AxisX, XAxisBounds), + new PositionGizmoSelectionTarget(this, GizmoTarget.AxisY, YAxisBounds), + new PositionGizmoSelectionTarget(this, GizmoTarget.AxisZ, ZAxisBounds), + + new PositionGizmoSelectionTarget(this, GizmoTarget.PlaneXZ, XZPlaneBounds), + new PositionGizmoSelectionTarget(this, GizmoTarget.PlaneYZ, YZPlaneBounds), + new PositionGizmoSelectionTarget(this, GizmoTarget.PlaneXY, XYPlaneBounds), + + new PositionGizmoSelectionTarget(this, GizmoTarget.CubeXYZ, XYZCubeBounds), + ]; + } + public override void Render(Batcher3D batch3D) { + // Check which part is targeted + target = GizmoTarget.None; + foreach (var selectionTarget in selectionTargets) + { + if (selectionTarget.IsHovered || selectionTarget.IsDragged) + { + target = selectionTarget.Target; + break; + } + } + const byte normalAlpha = 0xff; const byte hoverAlpha = 0xff; const byte dragAlpha = 0xff; @@ -211,41 +196,12 @@ public override void Render(Batcher3D batch3D) batch3D.Cube(Vec3.Zero, xyzCubeColor, Transform, CubeSize); } - private static readonly (BoundingBox Bounds, GizmoTarget Target)[] GizmoTargets = [ - (XAxisBounds, GizmoTarget.AxisX), - (YAxisBounds, GizmoTarget.AxisY), - (ZAxisBounds, GizmoTarget.AxisZ), - - (XZPlaneBounds, GizmoTarget.PlaneXZ), - (YZPlaneBounds, GizmoTarget.PlaneYZ), - (XYPlaneBounds, GizmoTarget.PlaneXY), - - (XYZCubeBounds, GizmoTarget.CubeXYZ), - ]; - - public override bool RaycastCheck(Vec3 origin, Vec3 direction) - { - float closestGizmo = float.PositiveInfinity; - - target = GizmoTarget.None; - foreach (var (checkBounds, checkTarget) in GizmoTargets) - { - if (!ModUtils.RayIntersectOBB(origin, direction, checkBounds, Transform, out float dist) || dist >= closestGizmo) - continue; - - target = checkTarget; - closestGizmo = dist; - } - - return target != GizmoTarget.None; - } - - public override void DragStart() + private void DragStart() { beforeDragPosition = getPosition(); } - public override void Drag(Vec2 mouseDelta, Vec3 mouseRay) + private void Drag(Vec2 mouseDelta, Vec3 mouseRay) { var editor = EditorWorld.Current; diff --git a/Source/Mod/Editor/Selection/SelectionTarget.cs b/Source/Mod/Editor/Selection/SelectionTarget.cs index 4dfa3f48..b7c67510 100644 --- a/Source/Mod/Editor/Selection/SelectionTarget.cs +++ b/Source/Mod/Editor/Selection/SelectionTarget.cs @@ -1,11 +1,33 @@ namespace Celeste64.Mod.Editor; -public class SelectionTarget +public abstract class SelectionTarget { - public required Matrix Transform; - public required BoundingBox Bounds; + public abstract Matrix Transform { get; } + public abstract BoundingBox Bounds { get; } + + public bool IsHovered { get; internal set; } = false; + public bool IsDragged { get; internal set; } = false; + + public virtual void Update() { } + public virtual void Render(ref RenderState state, Batcher3D batch3D) { } + + public virtual void Hovered() { } + public virtual void Selected() { } + public virtual void Dragged(Vec2 mouseDelta, Vec3 mouseRay) { } +} + +// TODO: Is this a good name? It provides Actions so you don't need to create a subclass for every type +public class SimpleSelectionTarget(Matrix transform, BoundingBox bounds) : SelectionTarget +{ + public override Matrix Transform { get; } = transform; + public override BoundingBox Bounds { get; } = bounds; public Action? OnHovered = null; - public Action? OnSelected = null; - public Action? OnDragged = null; + public Action? OnSelected = null; + public Action? OnDragged = null; + + public override void Hovered() => OnHovered?.Invoke(); + public override void Selected() => OnSelected?.Invoke(); + public override void Dragged(Vec2 mouseDelta, Vec3 mouseRay) => OnDragged?.Invoke(mouseDelta, mouseRay); } + From 33dc92d3804e6dfabf44840bc6a819e938501700 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 17 Mar 2024 13:33:54 +0100 Subject: [PATCH 85/97] Add vertex position editing --- Source/Actors/Solid.cs | 60 +++++++++++++------- Source/Mod/Editor/EditorWorld.cs | 13 +++++ Source/Mod/Editor/Selection/SelectionType.cs | 1 + 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/Source/Actors/Solid.cs b/Source/Actors/Solid.cs index 88508742..1e412908 100644 --- a/Source/Actors/Solid.cs +++ b/Source/Actors/Solid.cs @@ -174,38 +174,54 @@ public static bool RenderGui(ref List> value) public class VertexSelectionType : SelectionType { private readonly List targets = []; - public override IEnumerable Targets => targets; + // TODO: Support other gizmos depending on current tool. Maybe with some restriction on which tools are allowed tho + private PositionGizmo? vertexGizmo = null; + public override IEnumerable Targets => targets; + public override IEnumerable Gizmos => vertexGizmo is null ? [] : [vertexGizmo]; + public VertexSelectionType(Solid.Definition def) { + // TODO: This line crashes on initial load, since the editor is still null there + EditorWorld.Current.OnBeforeSelection += () => vertexGizmo = null; + def.OnUpdated += () => { targets.Clear(); + // TODO: Detect when geometry changed? + // vertexGizmo = null; var transform = Matrix.CreateTranslation(def.Position); const float selectionRadius = 1.0f; - - // TODO: Which implementation is better for performance? Probably the foreach one? - targets.AddRange(def.Faces - .SelectMany(face => face) - .Select(vertex => new SimpleSelectionTarget(transform, new BoundingBox(vertex, selectionRadius * 2.0f)) + + for (int i = 0; i < def.Faces.Count; i++) + { + var face = def.Faces[i]; + for (int j = 0; j < face.Count; j++) { - OnHovered = () => Log.Info($"Hovered vertex {vertex}"), - OnSelected = () => Log.Info($"Selected vertex {vertex}"), - OnDragged = (mouseDelta, mouseRay) => Log.Info($"Dragged vertex {vertex} ({mouseDelta}, {mouseRay})"), - })); - - // foreach (var face in def.Faces) - // { - // foreach (var vertex in face) - // { - // targets.Add(new SelectionTarget() - // { - // Transform = transform, - // Bounds = new BoundingBox(vertex, selectionRadius * 2.0f) - // }); - // } - // } + var vertex = face[j]; + + int faceIdx = i; + int vertexIdx = j; + + targets.Add(new SimpleSelectionTarget(transform, new BoundingBox(vertex, selectionRadius * 2.0f)) + { + // OnHovered = () => Log.Info($"Hovered vertex {vertex}"), + OnSelected = () => + { + Log.Info($"Selected vertex {vertex}"); + vertexGizmo = new PositionGizmo( + () => def.Faces[faceIdx][vertexIdx] + def.Position, + v => + { + def.Faces[faceIdx][vertexIdx] = v - def.Position; + def.Dirty = true; + }); + }, + // OnDragged = (mouseDelta, mouseRay) => Log.Info($"Dragged vertex {vertex} ({mouseDelta}, {mouseRay})"), + }); + } + } }; } } diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 3de12fca..ceb7e209 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -20,6 +20,7 @@ public class EditorWorld : World public ReadOnlyDictionary DefinitionFromActors => definitionFromActors.AsReadOnly(); public event Action OnSelectionChanged = def => {}; + public event Action OnBeforeSelection = () => {}; private ActorDefinition? selectedDefinition = null; public ActorDefinition? Selected @@ -345,6 +346,8 @@ Camera.Target is null || dragTarget.IsDragged = false; } dragTarget = null; + + OnBeforeSelection.Invoke(); // Collect all active selection targets List selectionTargets = []; @@ -353,6 +356,7 @@ Camera.Target is null || // TODO: Allow for selecting different types var selType = Selected.SelectionTypes[0]; selectionTargets.AddRange(selType.Targets); + selectionTargets.AddRange(selType.Gizmos.SelectMany(static gizmo => gizmo.SelectionTargets)); } if (gizmo is not null) { @@ -617,6 +621,15 @@ public override void Render(Target target) target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); { gizmo?.Render(batch3D); + if (Selected is not null && Selected.SelectionTypes.Length > 0) + { + // TODO: Allow for selecting different types + var selType = Selected.SelectionTypes[0]; + foreach (var g in selType.Gizmos) + { + g.Render(batch3D); + } + } } batch3D.Render(ref state); batch3D.Clear(); diff --git a/Source/Mod/Editor/Selection/SelectionType.cs b/Source/Mod/Editor/Selection/SelectionType.cs index 86560d5c..18e287b7 100644 --- a/Source/Mod/Editor/Selection/SelectionType.cs +++ b/Source/Mod/Editor/Selection/SelectionType.cs @@ -3,4 +3,5 @@ namespace Celeste64.Mod.Editor; public abstract class SelectionType { public abstract IEnumerable Targets { get; } + public abstract IEnumerable Gizmos { get; } } From 0dd00a1a8d4f77e4aea125b2292719d74d977374 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 17 Mar 2024 17:09:32 +0100 Subject: [PATCH 86/97] Allow editing vertices of adjacent faces --- Source/Actors/Solid.cs | 129 ++++-------------- Source/Mod/Editor/EditorWorld.cs | 4 +- Source/Mod/Editor/FujiMap.cs | 226 +++++++++++++++++-------------- 3 files changed, 155 insertions(+), 204 deletions(-) diff --git a/Source/Actors/Solid.cs b/Source/Actors/Solid.cs index 1e412908..ab0217e7 100644 --- a/Source/Actors/Solid.cs +++ b/Source/Actors/Solid.cs @@ -13,9 +13,19 @@ public class Definition : ActorDefinition public Vec3 Position { get; set; } static float size => 10.0f; - [CustomProperty(typeof(VerticesProperty))] - public List> Faces { get; set; } = [ - [new Vec3(-size, 0.0f, size), new Vec3(size, 0.0f, size), new Vec3(size, 0.0f, -size), new Vec3(-size, 0.0f, -size)] + + public List Vertices { get; set; } = [ + new Vec3(-size, size, size), new Vec3(size, size, size), new Vec3(size, size, -size), new Vec3(-size, size, -size), + new Vec3(-size, -size, size), new Vec3(size, -size, size), new Vec3(size, -size, -size), new Vec3(-size, -size, -size) + ]; + + public List> Faces { get; set; } = [ + [0, 1, 2, 3], // Front + [7, 6, 5, 4], // Back + [4, 5, 1, 0], // Top + [3, 2, 6, 7], // Bottom + [1, 5, 6, 2], // Left + [4, 0, 3, 7], // Right ]; public Definition() @@ -31,8 +41,8 @@ public override Actor[] Load(World.WorldType type) var bounds = new BoundingBox(); foreach (var face in Faces) { - var faceMin = face.Aggregate(Vec3.Min); - var faceMax = face.Aggregate(Vec3.Max); + var faceMin = face.Select(idx => Vertices[idx]).Aggregate(Vec3.Min); + var faceMax = face.Select(idx => Vertices[idx]).Aggregate(Vec3.Max); bounds = new BoundingBox(Vec3.Min(bounds.Min, faceMin), Vec3.Max(bounds.Max, faceMax)); } @@ -46,7 +56,7 @@ public override Actor[] Load(World.WorldType type) foreach (var face in Faces) { int vertexIndex = colliderVertices.Count; - var plane = Plane.CreateFromVertices(face[0], face[1], face[2]); + var plane = Plane.CreateFromVertices(Vertices[face[0]], Vertices[face[1]], Vertices[face[2]]); colliderFaces.Add(new Face { @@ -64,9 +74,9 @@ public override Actor[] Load(World.WorldType type) } // The center of the bounding box should always be <0, 0, 0> - colliderVertices.AddRange(face.Select(vertex => vertex - bounds.Center)); - meshVertices.AddRange(face.Select(vertex => new Vertex( - position: vertex - bounds.Center, + colliderVertices.AddRange(face.Select(idx => Vertices[idx] - bounds.Center)); + meshVertices.AddRange(face.Select(idx => new Vertex( + position: Vertices[idx] - bounds.Center, texcoord: Vec2.Zero, color: Vec3.One, normal: plane.Normal))); @@ -88,87 +98,6 @@ public override Actor[] Load(World.WorldType type) return [solid]; } - - public class VerticesProperty : ICustomProperty>> - { - public static void Serialize(List> value, BinaryWriter writer) - { - writer.Write(value.Count); - foreach (var vertices in value) - { - writer.Write(vertices.Count); - foreach (var vertex in vertices) - { - writer.Write(vertex); - } - } - } - - public static List> Deserialize(BinaryReader reader) - { - int faceCount = reader.ReadInt32(); - var value = new List>(capacity: faceCount); - for (int i = 0; i < faceCount; i++) - { - int vertexCount = reader.ReadInt32(); - var vertices = new List(capacity: vertexCount); - for (int j = 0; j < vertices.Capacity; j++) - { - vertices.Add(reader.ReadVec3()); - } - value.Add(vertices); - } - - return value; - } - - public static bool RenderGui(ref List> value) - { - bool changed = false; - - ImGui.Text("Geometry:"); - for (int i = 0; i < value.Count; i++) - { - ImGui.SeparatorText($"Face {i + 1}"); - for (int j = 0; j < value[i].Count; j++) - { - var v = value[i][j]; - changed |= ImGui.DragFloat3($"{j + 1}##{i}-{j}", ref v); - value[i][j] = v; - - if (value[i].Count > 3 && ImGui.Button($"Remove Vertex##{i}-{j}")) - { - value[i].RemoveAt(j); - changed = true; - } - } - - ImGui.Separator(); - - if (ImGui.Button($"Add Vertex##{i}")) - { - value[i].Add(Vec3.Zero); - changed = true; - } - - if (ImGui.Button($"Remove Face##{i}")) - { - value.RemoveAt(i); - changed = true; - } - } - - ImGui.Separator(); - - if (ImGui.Button("Add Face")) - { - value.Add([Vec3.Zero, Vec3.Zero, Vec3.Zero]); - changed = true; - } - - return changed; - } - } } public class VertexSelectionType : SelectionType @@ -183,7 +112,7 @@ public class VertexSelectionType : SelectionType public VertexSelectionType(Solid.Definition def) { // TODO: This line crashes on initial load, since the editor is still null there - EditorWorld.Current.OnBeforeSelection += () => vertexGizmo = null; + // EditorWorld.Current.OnBeforeSelection += () => vertexGizmo = null; def.OnUpdated += () => { @@ -194,27 +123,21 @@ public VertexSelectionType(Solid.Definition def) var transform = Matrix.CreateTranslation(def.Position); const float selectionRadius = 1.0f; - for (int i = 0; i < def.Faces.Count; i++) + foreach (var face in def.Faces) { - var face = def.Faces[i]; - for (int j = 0; j < face.Count; j++) + foreach (int idx in face) { - var vertex = face[j]; - - int faceIdx = i; - int vertexIdx = j; - - targets.Add(new SimpleSelectionTarget(transform, new BoundingBox(vertex, selectionRadius * 2.0f)) + targets.Add(new SimpleSelectionTarget(transform, new BoundingBox(def.Vertices[idx], selectionRadius * 2.0f)) { // OnHovered = () => Log.Info($"Hovered vertex {vertex}"), OnSelected = () => { - Log.Info($"Selected vertex {vertex}"); + Log.Info($"Selected vertex {idx} {def.Vertices[idx]}"); vertexGizmo = new PositionGizmo( - () => def.Faces[faceIdx][vertexIdx] + def.Position, + () => def.Vertices[idx] + def.Position, v => { - def.Faces[faceIdx][vertexIdx] = v - def.Position; + def.Vertices[idx] = v - def.Position; def.Dirty = true; }); }, diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index ceb7e209..27d37850 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -347,8 +347,6 @@ Camera.Target is null || } dragTarget = null; - OnBeforeSelection.Invoke(); - // Collect all active selection targets List selectionTargets = []; if (Selected is not null && Selected.SelectionTypes.Length > 0) @@ -387,6 +385,7 @@ Camera.Target is null || if (Input.Mouse.LeftPressed) { + OnBeforeSelection.Invoke(); closest.Selected(); dragTarget = closest; @@ -414,6 +413,7 @@ Camera.Target is null || // Then check for actors // else // { + OnBeforeSelection.Invoke(); if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; else diff --git a/Source/Mod/Editor/FujiMap.cs b/Source/Mod/Editor/FujiMap.cs index 4e3914f3..954f09be 100644 --- a/Source/Mod/Editor/FujiMap.cs +++ b/Source/Mod/Editor/FujiMap.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Reflection; using System.Reflection.Emit; @@ -78,43 +79,11 @@ public FujiMap(string name, string virtPath, Stream stream, string? fullPath) prop.SetValue(def, custom.Deserialize(reader)); continue; } - - // Primitives - if (prop.PropertyType == typeof(bool)) - prop.SetValue(def, reader.ReadBoolean()); - else if (prop.PropertyType == typeof(byte)) - prop.SetValue(def, reader.ReadByte()); - else if (prop.PropertyType == typeof(byte[])) - prop.SetValue(def, reader.ReadBytes(reader.Read7BitEncodedInt())); - else if (prop.PropertyType == typeof(char)) - prop.SetValue(def, reader.ReadChar()); - else if (prop.PropertyType == typeof(char[])) - prop.SetValue(def, reader.ReadChars(reader.Read7BitEncodedInt())); - else if (prop.PropertyType == typeof(decimal)) - prop.SetValue(def, reader.ReadDecimal()); - else if (prop.PropertyType == typeof(double)) - prop.SetValue(def, reader.ReadDouble()); - else if (prop.PropertyType == typeof(float)) - prop.SetValue(def, reader.ReadSingle()); - else if (prop.PropertyType == typeof(int)) - prop.SetValue(def, reader.ReadInt32()); - else if (prop.PropertyType == typeof(long)) - prop.SetValue(def, reader.ReadInt64()); - else if (prop.PropertyType == typeof(sbyte)) - prop.SetValue(def, reader.ReadSByte()); - else if (prop.PropertyType == typeof(short)) - prop.SetValue(def, reader.ReadInt16()); - else if (prop.PropertyType == typeof(Half)) - prop.SetValue(def, reader.ReadHalf()); - else if (prop.PropertyType == typeof(string)) - prop.SetValue(def, reader.ReadString()); - // Special support - else if (prop.PropertyType == typeof(Vec2)) - prop.SetValue(def, reader.ReadVec2()); - else if (prop.PropertyType == typeof(Vec3)) - prop.SetValue(def, reader.ReadVec3()); - else if (prop.PropertyType == typeof(Color)) - prop.SetValue(def, reader.ReadColor()); + + if (DeserializeObject(prop.PropertyType, reader) is not { } obj) + throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def}' cannot be deserialized"); + + prop.SetValue(def, obj); Log.Info($" - {prop.Name}: {prop.GetValue(def)}"); } @@ -179,68 +148,8 @@ public void SaveToFile() continue; } - switch (prop.GetValue(def)) - { - // Primitives - case bool v: - writer.Write(v); - break; - case byte v: - writer.Write(v); - break; - case byte[] v: - writer.Write7BitEncodedInt(v.Length); - writer.Write(v); - break; - case char v: - writer.Write(v); - break; - case char[] v: - writer.Write7BitEncodedInt(v.Length); - writer.Write(v); - break; - case decimal v: - writer.Write(v); - break; - case double v: - writer.Write(v); - break; - case float v: - writer.Write(v); - break; - case int v: - writer.Write(v); - break; - case long v: - writer.Write(v); - break; - case sbyte v: - writer.Write(v); - break; - case short v: - writer.Write(v); - break; - case Half v: - writer.Write(v); - break; - case string v: - writer.Write(v); - break; - - // Special support - case Vec2 v: - writer.Write(v); - break; - case Vec3 v: - writer.Write(v); - break; - case Color v: - writer.Write(v); - break; - - default: - throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def}' cannot be serialized"); - } + if (!SerializeObject(prop.GetValue(def), writer)) + throw new Exception($"Property '{prop.Name}' of type {prop.PropertyType} from definition '{def}' cannot be serialized"); Log.Info($" * {prop.Name}: {prop.GetValue(def)}"); } @@ -259,4 +168,123 @@ public override void Load(World world) } world.Add(new Player { Position = new Vec3(0, 0, 100) }); } + + private bool SerializeObject(object? obj, BinaryWriter writer) + { + switch (obj) + { + // Primitives + case bool v: + writer.Write(v); + break; + case byte v: + writer.Write(v); + break; + case char v: + writer.Write(v); + break; + case decimal v: + writer.Write(v); + break; + case double v: + writer.Write(v); + break; + case float v: + writer.Write(v); + break; + case int v: + writer.Write(v); + break; + case long v: + writer.Write(v); + break; + case sbyte v: + writer.Write(v); + break; + case short v: + writer.Write(v); + break; + case Half v: + writer.Write(v); + break; + case string v: + writer.Write(v); + break; + + // Special support + case Vec2 v: + writer.Write(v); + break; + case Vec3 v: + writer.Write(v); + break; + case Color v: + writer.Write(v); + break; + + // Collections + case IList v: + writer.Write7BitEncodedInt(v.Count); + foreach (var item in v) + SerializeObject(item, writer); + break; + + default: + return false; + } + + return true; + } + + private object? DeserializeObject(Type type, BinaryReader reader) + { + // Primitives + if (type == typeof(bool)) + return reader.ReadBoolean(); + if (type == typeof(byte)) + return reader.ReadByte(); + if (type == typeof(byte[])) + return reader.ReadBytes(reader.Read7BitEncodedInt()); + if (type == typeof(char)) + return reader.ReadChar(); + if (type == typeof(char[])) + return reader.ReadChars(reader.Read7BitEncodedInt()); + if (type == typeof(decimal)) + return reader.ReadDecimal(); + if (type == typeof(double)) + return reader.ReadDouble(); + if (type == typeof(float)) + return reader.ReadSingle(); + if (type == typeof(int)) + return reader.ReadInt32(); + if (type == typeof(long)) + return reader.ReadInt64(); + if (type == typeof(sbyte)) + return reader.ReadSByte(); + if (type == typeof(short)) + return reader.ReadInt16(); + if (type == typeof(Half)) + return reader.ReadHalf(); + if (type == typeof(string)) + return reader.ReadString(); + // Special support + if (type == typeof(Vec2)) + return reader.ReadVec2(); + if (type == typeof(Vec3)) + return reader.ReadVec3(); + if (type == typeof(Color)) + return reader.ReadColor(); + // Collections + if (type.IsAssignableTo(typeof(IList)) && type.IsGenericType) + { + var itemType = type.GenericTypeArguments[0]; + var list = (IList)Activator.CreateInstance(type)!; + int count = reader.Read7BitEncodedInt(); + for (int i = 0; i < count; i++) + list.Add(DeserializeObject(itemType, reader)); + return list; + } + + return null; + } } From 357b6c78c93a467dc6a621fa9600a9ff0f5c9ebc Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 17 Mar 2024 17:29:13 +0100 Subject: [PATCH 87/97] Improve selection logic --- Source/Actors/Solid.cs | 12 ++- Source/Mod/Editor/EditorWorld.cs | 105 ++++++++++--------- Source/Mod/Editor/GUI/EditActorWindow.cs | 4 +- Source/Mod/Editor/Selection/SelectionType.cs | 2 + 4 files changed, 66 insertions(+), 57 deletions(-) diff --git a/Source/Actors/Solid.cs b/Source/Actors/Solid.cs index ab0217e7..d33eea03 100644 --- a/Source/Actors/Solid.cs +++ b/Source/Actors/Solid.cs @@ -100,7 +100,7 @@ public override Actor[] Load(World.WorldType type) } } - public class VertexSelectionType : SelectionType + public class VertexSelectionType(Solid.Definition def) : SelectionType { private readonly List targets = []; // TODO: Support other gizmos depending on current tool. Maybe with some restriction on which tools are allowed tho @@ -109,10 +109,14 @@ public class VertexSelectionType : SelectionType public override IEnumerable Targets => targets; public override IEnumerable Gizmos => vertexGizmo is null ? [] : [vertexGizmo]; - public VertexSelectionType(Solid.Definition def) + public override void Awake() { - // TODO: This line crashes on initial load, since the editor is still null there - // EditorWorld.Current.OnBeforeSelection += () => vertexGizmo = null; + EditorWorld.Current.OnTargetSelected += target => + { + // Deselect if something else was selected + if (target is null || !targets.Contains(target) && !(vertexGizmo?.SelectionTargets.Contains(target) ?? false)) + vertexGizmo = null; + }; def.OnUpdated += () => { diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 27d37850..87351d4a 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -15,22 +15,22 @@ public class EditorWorld : World public static EditorWorld Current => (Game.Scene as EditorWorld)!; - public List Definitions => Map is FujiMap fujiMap ? fujiMap.Definitions : []; + public IReadOnlyList Definitions => Map is FujiMap fujiMap ? fujiMap.Definitions : []; public ReadOnlyDictionary ActorsFromDefinition => actorsFromDefinition.AsReadOnly(); public ReadOnlyDictionary DefinitionFromActors => definitionFromActors.AsReadOnly(); - public event Action OnSelectionChanged = def => {}; - public event Action OnBeforeSelection = () => {}; + public event Action OnTargetSelected = target => {}; private ActorDefinition? selectedDefinition = null; public ActorDefinition? Selected { private set { + if (selectedDefinition == value) + return; + selectedDefinition = value; gizmo = null; - - OnSelectionChanged(value); if (selectedDefinition is null) return; @@ -83,12 +83,42 @@ internal EditorWorld(EntryInfo entry) : base(entry) RefreshEnvironment(); // Map gets implicitly loaded, since our Definitions are taken directly from it - // However mark all definitions as dirty to ensure they will get added foreach (var def in Definitions) { + // Mark all definitions as dirty to ensure they will get added def.Dirty = true; } } + + public void AddDefinition(ActorDefinition definition) + { + if (Map is not FujiMap fujiMap) + return; + + fujiMap.Definitions.Add(definition); + + // Make sure it'll get loaded + definition.Dirty = true; + + foreach (var selectionType in definition.SelectionTypes) + selectionType.Awake(); + } + + public void RemoveDefinition(ActorDefinition definition) + { + if (Map is not FujiMap fujiMap) + return; + + fujiMap.Definitions.Remove(definition); + if (actorsFromDefinition.Remove(definition, out var actors)) + { + foreach (var actor in actors) + { + definitionFromActors.Remove(actor); + Destroy(actor); + } + } + } internal void RefreshEnvironment() { @@ -185,26 +215,18 @@ internal void RefreshEnvironment() public override void Entered() { - // Game.ResolutionScale = Save.; + // Awake all SelectionTypes of definitions which already exist + foreach (var def in Definitions) + { + foreach (var selectionType in def.SelectionTypes) + selectionType.Awake(); + } } public override void Exited() { Game.ResolutionScale = previousScale; } - public void RemoveDefinition(ActorDefinition definition) - { - Definitions.Remove(definition); - if (actorsFromDefinition.Remove(definition, out var actors)) - { - foreach (var actor in actors) - { - definitionFromActors.Remove(actor); - Destroy(actor); - } - } - } - public override void Update() { // Toggle to in-game @@ -365,6 +387,7 @@ Camera.Target is null || foreach (var target in selectionTargets) target.IsHovered = false; + // Handle SelectionTargets { SelectionTarget? closest = null; float closestDist = float.PositiveInfinity; @@ -385,7 +408,7 @@ Camera.Target is null || if (Input.Mouse.LeftPressed) { - OnBeforeSelection.Invoke(); + OnTargetSelected(closest); closest.Selected(); dragTarget = closest; @@ -394,36 +417,20 @@ Camera.Target is null || } return; } + + if (Input.Mouse.LeftPressed) + { + // No SelectionTarget was hit + OnTargetSelected(null); + } } - // Notice when mouse is hovering over. - // While dragging, don't update the gizmo since we might go out of the gizmo's bounds. - bool isDragging = Input.Mouse.LeftDown && !Input.Mouse.LeftPressed; - // bool hitGizmo = isDragging || (gizmo?.RaycastCheck(Camera.Position, direction) ?? false); - if (Input.Mouse.LeftPressed) { - // First check if we hit a gizmo - // if (hitGizmo) - // { - // Start dragging - - // gizmo?.DragStart(); - // } - // Then check for actors - // else - // { - OnBeforeSelection.Invoke(); - if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) - Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; - else - Selected = null; - // } - } - // Continue dragging - else if (Input.Mouse.LeftDown) - { - // gizmo?.Drag(this, Input.Mouse.Position - dragMouseStart, direction); + if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) + Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; + else + Selected = null; } } @@ -677,10 +684,6 @@ public bool ActorRayCast(in Vec3 point, in Vec3 direction, float distance, out R if (!actor.WorldBounds.Intersects(box)) continue; - // Don't re-select an actor - if (SelectedActors.Contains(actor)) - continue; - // TODO: Allow selecting decorations, since they're currently one giant object if (actor is Decoration or FloatingDecoration) continue; diff --git a/Source/Mod/Editor/GUI/EditActorWindow.cs b/Source/Mod/Editor/GUI/EditActorWindow.cs index 99c5c51e..2fffbfea 100644 --- a/Source/Mod/Editor/GUI/EditActorWindow.cs +++ b/Source/Mod/Editor/GUI/EditActorWindow.cs @@ -15,11 +15,11 @@ protected override void RenderWindow(EditorWorld editor) // TODO: Add some actor picker if (ImGui.Button("DEBUG: Add Spikes")) { - editor.Definitions.Add(new SpikeBlock.Definition()); + editor.AddDefinition(new SpikeBlock.Definition()); } if (ImGui.Button("DEBUG: Add Solid")) { - editor.Definitions.Add(new Solid.Definition()); + editor.AddDefinition(new Solid.Definition()); } if (editor.Selected is { } selected) diff --git a/Source/Mod/Editor/Selection/SelectionType.cs b/Source/Mod/Editor/Selection/SelectionType.cs index 18e287b7..c025b147 100644 --- a/Source/Mod/Editor/Selection/SelectionType.cs +++ b/Source/Mod/Editor/Selection/SelectionType.cs @@ -4,4 +4,6 @@ public abstract class SelectionType { public abstract IEnumerable Targets { get; } public abstract IEnumerable Gizmos { get; } + + public virtual void Awake() { } } From 37c5b2af31985ba3c54a57e88e90eb7d8c5dd053 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 17 Mar 2024 17:32:30 +0100 Subject: [PATCH 88/97] Use smaller gizmos for vertex editing --- Source/Actors/Solid.cs | 3 ++- Source/Mod/Editor/EditorWorld.cs | 3 ++- Source/Mod/Editor/Gizmo/PositionGizmo.cs | 8 +++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Source/Actors/Solid.cs b/Source/Actors/Solid.cs index d33eea03..2e6748d1 100644 --- a/Source/Actors/Solid.cs +++ b/Source/Actors/Solid.cs @@ -143,7 +143,8 @@ public override void Awake() { def.Vertices[idx] = v - def.Position; def.Dirty = true; - }); + }, + scale: 0.5f); }, // OnDragged = (mouseDelta, mouseRay) => Log.Info($"Dragged vertex {vertex} ({mouseDelta}, {mouseRay})"), }); diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 87351d4a..ee66f044 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -51,7 +51,8 @@ private set { setMethod.Invoke(selectedDefinition, [newValue]); selectedDefinition.Dirty = true; - }); + }, + scale: 1.0f); } get => selectedDefinition; } diff --git a/Source/Mod/Editor/Gizmo/PositionGizmo.cs b/Source/Mod/Editor/Gizmo/PositionGizmo.cs index ae4e1630..5a5adbec 100644 --- a/Source/Mod/Editor/Gizmo/PositionGizmo.cs +++ b/Source/Mod/Editor/Gizmo/PositionGizmo.cs @@ -26,6 +26,7 @@ private class PositionGizmoSelectionTarget(PositionGizmo gizmo, GizmoTarget targ private readonly GetPositionDelegate getPosition; private readonly SetPositionDelegate setPosition; + private readonly float scale; private GizmoTarget target; private Vec3 beforeDragPosition = Vec3.Zero; @@ -87,9 +88,9 @@ public Matrix Transform var position = getPosition(); const float minScale = 10.0f; - float scale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, position) / 20.0f); + float distanceScale = Math.Max(minScale, Vec3.Distance(EditorWorld.Current.Camera.Position, position) / 20.0f); - return Matrix.CreateScale(scale) * + return Matrix.CreateScale(distanceScale * scale) * Matrix.CreateTranslation(position); } } @@ -97,10 +98,11 @@ public Matrix Transform private readonly PositionGizmoSelectionTarget[] selectionTargets; public override IEnumerable SelectionTargets => selectionTargets; - public PositionGizmo(GetPositionDelegate getPosition, SetPositionDelegate setPosition) + public PositionGizmo(GetPositionDelegate getPosition, SetPositionDelegate setPosition, float scale) { this.getPosition = getPosition; this.setPosition = setPosition; + this.scale = scale; selectionTargets = [ new PositionGizmoSelectionTarget(this, GizmoTarget.AxisX, XAxisBounds), From 60988954ef46f556447945e9a8147efbec655fe7 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 4 Apr 2024 11:48:59 +0200 Subject: [PATCH 89/97] Generalize GeometryDefinition --- Source/Actors/Solid.cs | 81 +---------------- .../Editor/Definition/GeometryDefinition.cs | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+), 78 deletions(-) create mode 100644 Source/Mod/Editor/Definition/GeometryDefinition.cs diff --git a/Source/Actors/Solid.cs b/Source/Actors/Solid.cs index 0fa390d5..b7bcf7c2 100644 --- a/Source/Actors/Solid.cs +++ b/Source/Actors/Solid.cs @@ -7,34 +7,13 @@ namespace Celeste64; public class Solid : Actor, IHaveModels { - public class Definition : ActorDefinition + public class Definition : GeometryDefinition { + protected override Matrix Transform => Matrix.CreateTranslation(Position); + [SpecialProperty(SpecialPropertyType.PositionXYZ)] public Vec3 Position { get; set; } - static float size => 10.0f; - - public List Vertices { get; set; } = [ - new Vec3(-size, size, size), new Vec3(size, size, size), new Vec3(size, size, -size), new Vec3(-size, size, -size), - new Vec3(-size, -size, size), new Vec3(size, -size, size), new Vec3(size, -size, -size), new Vec3(-size, -size, -size) - ]; - - public List> Faces { get; set; } = [ - [0, 1, 2, 3], // Front - [7, 6, 5, 4], // Back - [4, 5, 1, 0], // Top - [3, 2, 6, 7], // Bottom - [1, 5, 6, 2], // Left - [4, 0, 3, 7], // Right - ]; - - public Definition() - { - SelectionTypes = [ - new VertexSelectionType(this), - ]; - } - public override Actor[] Load(World.WorldType type) { // Calculate bounds @@ -100,60 +79,6 @@ public override Actor[] Load(World.WorldType type) } } - public class VertexSelectionType(Solid.Definition def) : SelectionType - { - private readonly List targets = []; - // TODO: Support other gizmos depending on current tool. Maybe with some restriction on which tools are allowed tho - private PositionGizmo? vertexGizmo = null; - - public override IEnumerable Targets => targets; - public override IEnumerable Gizmos => vertexGizmo is null ? [] : [vertexGizmo]; - - public override void Awake() - { - EditorWorld.Current.OnTargetSelected += target => - { - // Deselect if something else was selected - if (target is null || !targets.Contains(target) && !(vertexGizmo?.SelectionTargets.Contains(target) ?? false)) - vertexGizmo = null; - }; - - def.OnUpdated += () => - { - targets.Clear(); - // TODO: Detect when geometry changed? - // vertexGizmo = null; - - var transform = Matrix.CreateTranslation(def.Position); - const float selectionRadius = 1.0f; - - foreach (var face in def.Faces) - { - foreach (int idx in face) - { - targets.Add(new SimpleSelectionTarget(transform, new BoundingBox(def.Vertices[idx], selectionRadius * 2.0f)) - { - // OnHovered = () => Log.Info($"Hovered vertex {vertex}"), - OnSelected = () => - { - Log.Info($"Selected vertex {idx} {def.Vertices[idx]}"); - vertexGizmo = new PositionGizmo( - () => def.Vertices[idx] + def.Position, - v => - { - def.Vertices[idx] = v - def.Position; - def.Dirty = true; - }, - scale: 0.5f); - }, - // OnDragged = (mouseDelta, mouseRay) => Log.Info($"Dragged vertex {vertex} ({mouseDelta}, {mouseRay})"), - }); - } - } - }; - } - } - /// /// If we're currently solid /// diff --git a/Source/Mod/Editor/Definition/GeometryDefinition.cs b/Source/Mod/Editor/Definition/GeometryDefinition.cs new file mode 100644 index 00000000..b59d7f07 --- /dev/null +++ b/Source/Mod/Editor/Definition/GeometryDefinition.cs @@ -0,0 +1,87 @@ +namespace Celeste64.Mod.Editor; + +public abstract class GeometryDefinition : ActorDefinition +{ + [IgnoreProperty] + protected abstract Matrix Transform { get; } + + static float size => 10.0f; + + public List Vertices { get; set; } = [ + new Vec3(-size, size, size), new Vec3(size, size, size), new Vec3(size, size, -size), new Vec3(-size, size, -size), + new Vec3(-size, -size, size), new Vec3(size, -size, size), new Vec3(size, -size, -size), new Vec3(-size, -size, -size) + ]; + + public List> Faces { get; set; } = [ + [0, 1, 2, 3], // Front + [7, 6, 5, 4], // Back + [4, 5, 1, 0], // Top + [3, 2, 6, 7], // Bottom + [1, 5, 6, 2], // Left + [4, 0, 3, 7], // Right + ]; + + public GeometryDefinition() + { + SelectionTypes = [ + new VertexSelectionType(this), + ]; + } + + public class VertexSelectionType(GeometryDefinition def) : SelectionType + { + private readonly List targets = []; + // TODO: Support other gizmos depending on current tool. Maybe with some restriction on which tools are allowed tho + private PositionGizmo? vertexGizmo = null; + + public override IEnumerable Targets => targets; + public override IEnumerable Gizmos => vertexGizmo is null ? [] : [vertexGizmo]; + + public override void Awake() + { + EditorWorld.Current.OnTargetSelected += target => + { + // Deselect if something else was selected + if (target is null || !targets.Contains(target) && !(vertexGizmo?.SelectionTargets.Contains(target) ?? false)) + vertexGizmo = null; + }; + + def.OnUpdated += () => + { + targets.Clear(); + // TODO: Detect when geometry changed? + // vertexGizmo = null; + + var transform = def.Transform; + if (!Matrix.Invert(transform, out var inverseTransform)) + return; + + const float selectionRadius = 1.0f; + + foreach (var face in def.Faces) + { + foreach (int idx in face) + { + targets.Add(new SimpleSelectionTarget(transform, new BoundingBox(def.Vertices[idx], selectionRadius * 2.0f)) + { + // OnHovered = () => Log.Info($"Hovered vertex {vertex}"), + OnSelected = () => + { + Log.Info($"Selected vertex {idx} {def.Vertices[idx]}"); + vertexGizmo = new PositionGizmo( + () => Vec3.Transform(def.Vertices[idx], transform), + v => + { + def.Vertices[idx] = Vec3.Transform(v, inverseTransform); + def.Dirty = true; + }, + scale: 0.5f); + }, + // OnDragged = (mouseDelta, mouseRay) => Log.Info($"Dragged vertex {vertex} ({mouseDelta}, {mouseRay})"), + }); + } + } + }; + } + } +} From faeba70c2863ec38daf14fd4c806b713d422e28d Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 4 Apr 2024 12:24:35 +0200 Subject: [PATCH 90/97] Setup window for actor selection --- Source/Mod/Editor/EditorWorld.cs | 1 + Source/Mod/Editor/GUI/ActorSelectionWindow.cs | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 Source/Mod/Editor/GUI/ActorSelectionWindow.cs diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index be0798f3..ae8c0946 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -9,6 +9,7 @@ public class EditorWorld : World internal readonly ImGuiHandler[] Handlers = [ new EditorMenuBar(), + new ActorSelectionWindow(), new EditActorWindow(), new EnvironmentSettingsWindow(), ]; diff --git a/Source/Mod/Editor/GUI/ActorSelectionWindow.cs b/Source/Mod/Editor/GUI/ActorSelectionWindow.cs new file mode 100644 index 00000000..84b351b2 --- /dev/null +++ b/Source/Mod/Editor/GUI/ActorSelectionWindow.cs @@ -0,0 +1,28 @@ +using ImGuiNET; + +namespace Celeste64.Mod.Editor; + +public class ActorSelectionWindow() : EditorWindow("ActorSelect") +{ + protected override string Title => "Select Actor"; + + // TODO: Detect these definitions somehow + private List definitionTypes = [typeof(SpikeBlock.Definition), typeof(Solid.Definition)]; + + private int currentDefinition = 0; + + protected override void RenderWindow(EditorWorld editor) + { + for (int i = 0; i < definitionTypes.Count; i++) + { + bool isSelected = currentDefinition == i; + + // TODO: 1) Cache this? 2) Somehow get a good human-readable name + if (ImGui.Selectable(definitionTypes[i].FullName, isSelected)) + currentDefinition = i; + + if (isSelected) + ImGui.SetItemDefaultFocus(); + } + } +} From 105624d4197eb8ab9826b0f4a663501985cf7ce5 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 4 Apr 2024 12:25:16 +0200 Subject: [PATCH 91/97] Add debug key to enable ImGui demo window --- Source/Mod/ImGui/DemoWindowHandler.cs | 11 +++++++++++ Source/Mod/ImGui/ImGuiManager.cs | 6 ++++++ 2 files changed, 17 insertions(+) create mode 100644 Source/Mod/ImGui/DemoWindowHandler.cs diff --git a/Source/Mod/ImGui/DemoWindowHandler.cs b/Source/Mod/ImGui/DemoWindowHandler.cs new file mode 100644 index 00000000..2d9d28b7 --- /dev/null +++ b/Source/Mod/ImGui/DemoWindowHandler.cs @@ -0,0 +1,11 @@ +using ImGuiNET; + +namespace Celeste64.Mod; + +public class DemoWindowHandler : ImGuiHandler +{ + public override void Render() + { + ImGui.ShowDemoWindow(); + } +} diff --git a/Source/Mod/ImGui/ImGuiManager.cs b/Source/Mod/ImGui/ImGuiManager.cs index a93acc66..eb26115a 100644 --- a/Source/Mod/ImGui/ImGuiManager.cs +++ b/Source/Mod/ImGui/ImGuiManager.cs @@ -17,6 +17,7 @@ public class ImGuiManager private readonly ImGuiRenderer renderer; private static FujiDebugMenu debugMenu = new FujiDebugMenu(); + private static DemoWindowHandler demoWindow = new DemoWindowHandler() { Visible = false }; private static IEnumerable Handlers => ModManager.Instance.EnabledMods.SelectMany(mod => mod.ImGuiHandlers); internal ImGuiManager() @@ -35,6 +36,9 @@ internal void UpdateHandlers() if (debugMenu.Active) debugMenu.Update(); + + if (Input.Keyboard.Pressed(Keys.F2)) + demoWindow.Visible = !demoWindow.Visible; if (Game.Scene is EditorWorld editor) { @@ -60,6 +64,8 @@ internal void RenderHandlers() if (debugMenu.Visible) debugMenu.Render(); + if (demoWindow.Visible) + demoWindow.Render(); if (Game.Scene is EditorWorld editor) { From baaac0d27d65cec1f56b36ff57675659bdead387 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 4 Apr 2024 13:00:17 +0200 Subject: [PATCH 92/97] Setup an initial tool system --- Source/Mod/Editor/EditorWorld.cs | 60 +++++++++++------------- Source/Mod/Editor/Tool/MoveTool.cs | 42 +++++++++++++++++ Source/Mod/Editor/Tool/PlaceActorTool.cs | 7 +++ Source/Mod/Editor/Tool/Tool.cs | 9 ++++ 4 files changed, 86 insertions(+), 32 deletions(-) create mode 100644 Source/Mod/Editor/Tool/MoveTool.cs create mode 100644 Source/Mod/Editor/Tool/PlaceActorTool.cs create mode 100644 Source/Mod/Editor/Tool/Tool.cs diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index ae8c0946..56c10f4d 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -10,9 +10,15 @@ public class EditorWorld : World new EditorMenuBar(), new ActorSelectionWindow(), + new EditActorWindow(), new EnvironmentSettingsWindow(), ]; + + internal readonly Tool[] Tools = [ + new MoveTool(), + new PlaceActorTool(), + ]; public static EditorWorld Current => (Game.Scene as EditorWorld)!; @@ -20,6 +26,7 @@ public class EditorWorld : World public ReadOnlyDictionary ActorsFromDefinition => actorsFromDefinition.AsReadOnly(); public ReadOnlyDictionary DefinitionFromActors => definitionFromActors.AsReadOnly(); + public event Action? OnDefinitionSelected; public event Action OnTargetSelected = target => {}; private ActorDefinition? selectedDefinition = null; @@ -29,31 +36,9 @@ private set { if (selectedDefinition == value) return; - - selectedDefinition = value; - gizmo = null; - if (selectedDefinition is null) - return; - - var positionProp = selectedDefinition - .GetType() - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .FirstOrDefault(prop => - !prop.HasAttr() && - prop.GetCustomAttribute() is { Value: SpecialPropertyType.PositionXYZ }); - - if (positionProp is null || positionProp.GetGetMethod() is not { } getMethod || positionProp.GetSetMethod() is not { } setMethod) - return; - - gizmo = new PositionGizmo( - () => (Vec3)getMethod.Invoke(selectedDefinition, [])!, - newValue => - { - setMethod.Invoke(selectedDefinition, [newValue]); - selectedDefinition.Dirty = true; - }, - scale: 1.0f); + selectedDefinition = value; + OnDefinitionSelected?.Invoke(value); } get => selectedDefinition; } @@ -63,13 +48,13 @@ private set private readonly Dictionary actorsFromDefinition = new(); private readonly Dictionary definitionFromActors = new(); + internal Tool CurrentTool; + private Vec3 cameraPos = new(0, -10, 0); private Vec2 cameraRot = new(0, 0); private readonly Batcher3D batch3D = new(); - // TODO: Temporary! - private Gizmo? gizmo; private SelectionTarget? dragTarget = null; private Vec2 dragMouseStart = Vec2.Zero; @@ -90,6 +75,10 @@ internal EditorWorld(EntryInfo entry) : base(entry) // Mark all definitions as dirty to ensure they will get added def.Dirty = true; } + + // Select default tool + // TODO: Maybe save the last selected tool from the previous session? + CurrentTool = Tools[0]; } public void AddDefinition(ActorDefinition definition) @@ -223,6 +212,12 @@ public override void Entered() foreach (var selectionType in def.SelectionTypes) selectionType.Awake(); } + + // Awake all tools + foreach (var tool in Tools) + { + tool.Awake(this); + } } public override void Exited() { @@ -380,10 +375,7 @@ Camera.Target is null || selectionTargets.AddRange(selType.Targets); selectionTargets.AddRange(selType.Gizmos.SelectMany(static gizmo => gizmo.SelectionTargets)); } - if (gizmo is not null) - { - selectionTargets.AddRange(gizmo.SelectionTargets); - } + selectionTargets.AddRange(CurrentTool.Gizmos.SelectMany(static gizmo => gizmo.SelectionTargets)); // Un-hover everything foreach (var target in selectionTargets) @@ -629,7 +621,6 @@ public override void Render(Target target) // Render gizmos on-top target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); { - gizmo?.Render(batch3D); if (Selected is not null && Selected.SelectionTypes.Length > 0) { // TODO: Allow for selecting different types @@ -638,7 +629,12 @@ public override void Render(Target target) { g.Render(batch3D); } - } + } + + foreach (var gizmo in CurrentTool.Gizmos) + { + gizmo.Render(batch3D); + } } batch3D.Render(ref state); batch3D.Clear(); diff --git a/Source/Mod/Editor/Tool/MoveTool.cs b/Source/Mod/Editor/Tool/MoveTool.cs new file mode 100644 index 00000000..cfedb2f2 --- /dev/null +++ b/Source/Mod/Editor/Tool/MoveTool.cs @@ -0,0 +1,42 @@ +using System.Reflection; + +namespace Celeste64.Mod.Editor; + +public class MoveTool : Tool +{ + public override string Name => "Move"; + public override Gizmo[] Gizmos => gizmo == null ? [] : [gizmo]; + + private Gizmo? gizmo; + + public override void Awake(EditorWorld editor) + { + editor.OnDefinitionSelected += def => + { + if (def is null) + { + gizmo = null; + return; + } + + var positionProp = def + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(prop => + !prop.HasAttr() && + prop.GetCustomAttribute() is { Value: SpecialPropertyType.PositionXYZ }); + + if (positionProp is null || positionProp.GetGetMethod() is not { } getMethod || positionProp.GetSetMethod() is not { } setMethod) + return; + + gizmo = new PositionGizmo( + () => (Vec3)getMethod.Invoke(def, [])!, + newValue => + { + setMethod.Invoke(def, [newValue]); + def.Dirty = true; + }, + scale: 1.0f); + }; + } +} diff --git a/Source/Mod/Editor/Tool/PlaceActorTool.cs b/Source/Mod/Editor/Tool/PlaceActorTool.cs new file mode 100644 index 00000000..1a37228b --- /dev/null +++ b/Source/Mod/Editor/Tool/PlaceActorTool.cs @@ -0,0 +1,7 @@ +namespace Celeste64.Mod.Editor; + +public class PlaceActorTool : Tool +{ + public override string Name => "Place Actor"; + public override Gizmo[] Gizmos => []; +} diff --git a/Source/Mod/Editor/Tool/Tool.cs b/Source/Mod/Editor/Tool/Tool.cs new file mode 100644 index 00000000..f0b958e5 --- /dev/null +++ b/Source/Mod/Editor/Tool/Tool.cs @@ -0,0 +1,9 @@ +namespace Celeste64.Mod.Editor; + +public abstract class Tool +{ + public abstract string Name { get; } + public abstract Gizmo[] Gizmos { get; } + + public virtual void Awake(EditorWorld editor) { } +} From f7b68aae612ee2e23bcfa88498fc995ceffc6f27 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 4 Apr 2024 13:00:36 +0200 Subject: [PATCH 93/97] Add a tool selection window --- Source/Mod/Editor/EditorWorld.cs | 1 + Source/Mod/Editor/GUI/ToolSelectionWindow.cs | 22 ++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 Source/Mod/Editor/GUI/ToolSelectionWindow.cs diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 56c10f4d..a0a2fb6e 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -9,6 +9,7 @@ public class EditorWorld : World internal readonly ImGuiHandler[] Handlers = [ new EditorMenuBar(), + new ToolSelectionWindow(), new ActorSelectionWindow(), new EditActorWindow(), diff --git a/Source/Mod/Editor/GUI/ToolSelectionWindow.cs b/Source/Mod/Editor/GUI/ToolSelectionWindow.cs new file mode 100644 index 00000000..f423938f --- /dev/null +++ b/Source/Mod/Editor/GUI/ToolSelectionWindow.cs @@ -0,0 +1,22 @@ +using ImGuiNET; + +namespace Celeste64.Mod.Editor; + +public class ToolSelectionWindow() : EditorWindow("ToolSelect") +{ + protected override string Title => "Select Tool"; + + protected override void RenderWindow(EditorWorld editor) + { + foreach (var tool in editor.Tools) + { + bool isSelected = editor.CurrentTool == tool; + + if (ImGui.Selectable(tool.Name, isSelected)) + editor.CurrentTool = tool; + + if (isSelected) + ImGui.SetItemDefaultFocus(); + } + } +} From adbcd6529fab6059c89e3b2f99b5107f2b045ef2 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 4 Apr 2024 13:06:02 +0200 Subject: [PATCH 94/97] Only enable selection for certain tools --- Source/Mod/Editor/EditorWorld.cs | 158 ++++++++++++----------- Source/Mod/Editor/Tool/MoveTool.cs | 1 + Source/Mod/Editor/Tool/PlaceActorTool.cs | 1 + Source/Mod/Editor/Tool/Tool.cs | 1 + 4 files changed, 84 insertions(+), 77 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index a0a2fb6e..cfbae59c 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -291,7 +291,8 @@ public override void Update() Camera.LookAt = cameraPos + forward; // Shoot ray cast for selection - SelectionRaycast(); + if (CurrentTool.EnableSelection) + SelectionRaycast(); // Update actors of definitions foreach (var def in Definitions.Where(def => def.Dirty)) @@ -541,83 +542,86 @@ public override void Render(Target target) const float selectedBoundsInflate = 0.25f; // Render selected actors bounding box - foreach (var selected in SelectedActors) - { - var matrix = selected.Matrix; - var bounds = selected.LocalBounds.Inflate(selectedBoundsInflate); - - batch3D.Box(bounds.Min, bounds.Max, selectedLocalBoundsFillColor, matrix); - } - batch3D.Render(ref state); - batch3D.Clear(); - - // Render outline on-top of everything else - target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); - foreach (var selected in SelectedActors) - { - // Scale thickness based on distance - var lineThickness = Vec3.Distance(Camera.Position, selected.WorldBounds.Center) * 0.001f; - - // Transformed local bounds - var matrix = selected.Matrix; - var bounds = selected.LocalBounds.Inflate(selectedBoundsInflate); - var v000 = bounds.Min; - var v100 = bounds.Min with { X = bounds.Max.X }; - var v010 = bounds.Min with { Y = bounds.Max.Y }; - var v001 = bounds.Min with { Z = bounds.Max.Z }; - var v011 = bounds.Max with { X = bounds.Min.X }; - var v101 = bounds.Max with { Y = bounds.Min.Y }; - var v110 = bounds.Max with { Z = bounds.Min.Z }; - var v111 = bounds.Max; - - batch3D.Line(v000, v100, selectedLocalBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v000, v010, selectedLocalBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v000, v001, selectedLocalBoundsOutlineColor, matrix, lineThickness); - - batch3D.Line(v111, v011, selectedLocalBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v111, v101, selectedLocalBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v111, v110, selectedLocalBoundsOutlineColor, matrix, lineThickness); - - batch3D.Line(v010, v011, selectedLocalBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v010, v110, selectedLocalBoundsOutlineColor, matrix, lineThickness); - - batch3D.Line(v101, v100, selectedLocalBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v101, v001, selectedLocalBoundsOutlineColor, matrix, lineThickness); - - batch3D.Line(v100, v110, selectedLocalBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v001, v011, selectedLocalBoundsOutlineColor, matrix, lineThickness); - - // World bounds - matrix = Matrix.Identity; - bounds = selected.WorldBounds.Inflate(selectedBoundsInflate); - v000 = bounds.Min; - v100 = bounds.Min with { X = bounds.Max.X }; - v010 = bounds.Min with { Y = bounds.Max.Y }; - v001 = bounds.Min with { Z = bounds.Max.Z }; - v011 = bounds.Max with { X = bounds.Min.X }; - v101 = bounds.Max with { Y = bounds.Min.Y }; - v110 = bounds.Max with { Z = bounds.Min.Z }; - v111 = bounds.Max; - - batch3D.Line(v000, v100, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v000, v010, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v000, v001, selectedWorldBoundsOutlineColor, matrix, lineThickness); - - batch3D.Line(v111, v011, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v111, v101, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v111, v110, selectedWorldBoundsOutlineColor, matrix, lineThickness); - - batch3D.Line(v010, v011, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v010, v110, selectedWorldBoundsOutlineColor, matrix, lineThickness); - - batch3D.Line(v101, v100, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v101, v001, selectedWorldBoundsOutlineColor, matrix, lineThickness); - - batch3D.Line(v100, v110, selectedWorldBoundsOutlineColor, matrix, lineThickness); - batch3D.Line(v001, v011, selectedWorldBoundsOutlineColor, matrix, lineThickness); + if (CurrentTool.EnableSelection) + { + foreach (var selected in SelectedActors) + { + var matrix = selected.Matrix; + var bounds = selected.LocalBounds.Inflate(selectedBoundsInflate); + + batch3D.Box(bounds.Min, bounds.Max, selectedLocalBoundsFillColor, matrix); + } + batch3D.Render(ref state); + batch3D.Clear(); + + // Render outline on-top of everything else + target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); + foreach (var selected in SelectedActors) + { + // Scale thickness based on distance + var lineThickness = Vec3.Distance(Camera.Position, selected.WorldBounds.Center) * 0.001f; + + // Transformed local bounds + var matrix = selected.Matrix; + var bounds = selected.LocalBounds.Inflate(selectedBoundsInflate); + var v000 = bounds.Min; + var v100 = bounds.Min with { X = bounds.Max.X }; + var v010 = bounds.Min with { Y = bounds.Max.Y }; + var v001 = bounds.Min with { Z = bounds.Max.Z }; + var v011 = bounds.Max with { X = bounds.Min.X }; + var v101 = bounds.Max with { Y = bounds.Min.Y }; + var v110 = bounds.Max with { Z = bounds.Min.Z }; + var v111 = bounds.Max; + + batch3D.Line(v000, v100, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v000, v010, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v000, v001, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v111, v011, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v111, v101, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v111, v110, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v010, v011, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v010, v110, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v101, v100, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v101, v001, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v100, v110, selectedLocalBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v001, v011, selectedLocalBoundsOutlineColor, matrix, lineThickness); + + // World bounds + matrix = Matrix.Identity; + bounds = selected.WorldBounds.Inflate(selectedBoundsInflate); + v000 = bounds.Min; + v100 = bounds.Min with { X = bounds.Max.X }; + v010 = bounds.Min with { Y = bounds.Max.Y }; + v001 = bounds.Min with { Z = bounds.Max.Z }; + v011 = bounds.Max with { X = bounds.Min.X }; + v101 = bounds.Max with { Y = bounds.Min.Y }; + v110 = bounds.Max with { Z = bounds.Min.Z }; + v111 = bounds.Max; + + batch3D.Line(v000, v100, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v000, v010, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v000, v001, selectedWorldBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v111, v011, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v111, v101, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v111, v110, selectedWorldBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v010, v011, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v010, v110, selectedWorldBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v101, v100, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v101, v001, selectedWorldBoundsOutlineColor, matrix, lineThickness); + + batch3D.Line(v100, v110, selectedWorldBoundsOutlineColor, matrix, lineThickness); + batch3D.Line(v001, v011, selectedWorldBoundsOutlineColor, matrix, lineThickness); + } + batch3D.Render(ref state); + batch3D.Clear(); } - batch3D.Render(ref state); - batch3D.Clear(); // Render gizmos on-top target.Clear(Color.Black, 1.0f, 0, ClearMask.Depth); diff --git a/Source/Mod/Editor/Tool/MoveTool.cs b/Source/Mod/Editor/Tool/MoveTool.cs index cfedb2f2..0146d778 100644 --- a/Source/Mod/Editor/Tool/MoveTool.cs +++ b/Source/Mod/Editor/Tool/MoveTool.cs @@ -6,6 +6,7 @@ public class MoveTool : Tool { public override string Name => "Move"; public override Gizmo[] Gizmos => gizmo == null ? [] : [gizmo]; + public override bool EnableSelection => true; private Gizmo? gizmo; diff --git a/Source/Mod/Editor/Tool/PlaceActorTool.cs b/Source/Mod/Editor/Tool/PlaceActorTool.cs index 1a37228b..d1e5bd9d 100644 --- a/Source/Mod/Editor/Tool/PlaceActorTool.cs +++ b/Source/Mod/Editor/Tool/PlaceActorTool.cs @@ -4,4 +4,5 @@ public class PlaceActorTool : Tool { public override string Name => "Place Actor"; public override Gizmo[] Gizmos => []; + public override bool EnableSelection => false; } diff --git a/Source/Mod/Editor/Tool/Tool.cs b/Source/Mod/Editor/Tool/Tool.cs index f0b958e5..ea736a26 100644 --- a/Source/Mod/Editor/Tool/Tool.cs +++ b/Source/Mod/Editor/Tool/Tool.cs @@ -4,6 +4,7 @@ public abstract class Tool { public abstract string Name { get; } public abstract Gizmo[] Gizmos { get; } + public abstract bool EnableSelection { get; } public virtual void Awake(EditorWorld editor) { } } From 4aebe50d299b5d854dd3ac60dcbf8a34361517bf Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 4 Apr 2024 13:53:51 +0200 Subject: [PATCH 95/97] Implement basic actor placement --- Source/Mod/Editor/EditorWorld.cs | 67 +++++++++------- Source/Mod/Editor/GUI/ActorSelectionWindow.cs | 17 ++-- Source/Mod/Editor/Tool/PlaceActorTool.cs | 78 +++++++++++++++++++ Source/Mod/Editor/Tool/Tool.cs | 6 ++ 4 files changed, 132 insertions(+), 36 deletions(-) diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index cfbae59c..64d8bf31 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -49,12 +49,24 @@ private set private readonly Dictionary actorsFromDefinition = new(); private readonly Dictionary definitionFromActors = new(); - internal Tool CurrentTool; - + private Tool? currentTool; + public Tool CurrentTool + { + get => currentTool!; + internal set + { + currentTool?.OnDeselectTool(this); + currentTool = value; + currentTool.OnSelectTool(this); + } + } + private Vec3 cameraPos = new(0, -10, 0); private Vec2 cameraRot = new(0, 0); private readonly Batcher3D batch3D = new(); + + public Vec3 MouseRay { get; private set; } private SelectionTarget? dragTarget = null; private Vec2 dragMouseStart = Vec2.Zero; @@ -290,10 +302,33 @@ public override void Update() Camera.Position = cameraPos; Camera.LookAt = cameraPos + forward; + // Calculate mouse ray from camera + if (Camera.Target is not null && + Matrix.Invert(Camera.Projection, out var inverseProj) && + Matrix.Invert(Camera.View, out var inverseView)) + { + // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios + var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); + var imageRelativeDir = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); + // Convert into normalized-device-coordinates + var ndcDir = imageRelativeDir / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; + // Flip Y, since up is negative in NDC coords + ndcDir.Y *= -1.0f; + var clipDir = new Vec4(ndcDir, -1.0f, 1.0f); + var eyeDir = Vec4.Transform(clipDir, inverseProj); + // We only care about XY, so we set ZW to "forward" + eyeDir.Z = -1.0f; + eyeDir.W = 0.0f; + var worldDir = Vec4.Transform(eyeDir, inverseView); + MouseRay = new Vec3(worldDir.X, worldDir.Y, worldDir.Z).Normalized(); + } + // Shoot ray cast for selection if (CurrentTool.EnableSelection) SelectionRaycast(); + CurrentTool.Update(this); + // Update actors of definitions foreach (var def in Definitions.Where(def => def.Dirty)) { @@ -332,33 +367,13 @@ public override void Update() private void SelectionRaycast() { - if (ImGuiManager.WantCaptureMouse || - Camera.Target is null || - !Matrix.Invert(Camera.Projection, out var inverseProj) || - !Matrix.Invert(Camera.View, out var inverseView)) - { + if (ImGuiManager.WantCaptureMouse) return; - } - // The top-left of the image might not be the top-left of the window, when using non 16:9 aspect ratios - var scale = Math.Min(App.WidthInPixels / (float)Camera.Target.Width, App.HeightInPixels / (float)Camera.Target.Height); - var imageRelativePos = Input.Mouse.Position - (App.SizeInPixels / 2 - Camera.Target.Bounds.Size / 2 * scale); - // Convert into normalized-device-coordinates - var ndcPos = imageRelativePos / (Camera.Target.Bounds.Size / 2 * scale) - Vec2.One; - // Flip Y, since up is negative in NDC coords - ndcPos.Y *= -1.0f; - var clipPos = new Vec4(ndcPos, -1.0f, 1.0f); - var eyePos = Vec4.Transform(clipPos, inverseProj); - // We only care about XY, so we set ZW to "forward" - eyePos.Z = -1.0f; - eyePos.W = 0.0f; - var worldPos = Vec4.Transform(eyePos, inverseView); - var direction = new Vec3(worldPos.X, worldPos.Y, worldPos.Z).Normalized(); - // Continue/Stop dragging if (Input.Mouse.LeftDown && dragTarget is not null) { - dragTarget.Dragged(Input.Mouse.Position - dragMouseStart, direction); + dragTarget.Dragged(Input.Mouse.Position - dragMouseStart, MouseRay); return; } @@ -390,7 +405,7 @@ Camera.Target is null || foreach (var target in selectionTargets) { - if (!ModUtils.RayIntersectOBB(Camera.Position, direction, target.Bounds, target.Transform, out float dist) || dist >= closestDist) + if (!ModUtils.RayIntersectOBB(Camera.Position, MouseRay, target.Bounds, target.Transform, out float dist) || dist >= closestDist) continue; closest = target; @@ -423,7 +438,7 @@ Camera.Target is null || if (Input.Mouse.LeftPressed) { - if (ActorRayCast(Camera.Position, direction, 10000.0f, out var hit, ignoreBackfaces: false)) + if (ActorRayCast(Camera.Position, MouseRay, 10000.0f, out var hit, ignoreBackfaces: false)) Selected = hit.Actor is not null && definitionFromActors.TryGetValue(hit.Actor, out var def) ? def : null; else Selected = null; diff --git a/Source/Mod/Editor/GUI/ActorSelectionWindow.cs b/Source/Mod/Editor/GUI/ActorSelectionWindow.cs index 84b351b2..6b6857b3 100644 --- a/Source/Mod/Editor/GUI/ActorSelectionWindow.cs +++ b/Source/Mod/Editor/GUI/ActorSelectionWindow.cs @@ -6,20 +6,17 @@ public class ActorSelectionWindow() : EditorWindow("ActorSelect") { protected override string Title => "Select Actor"; - // TODO: Detect these definitions somehow - private List definitionTypes = [typeof(SpikeBlock.Definition), typeof(Solid.Definition)]; - - private int currentDefinition = 0; - protected override void RenderWindow(EditorWorld editor) { - for (int i = 0; i < definitionTypes.Count; i++) + if (editor.CurrentTool is not PlaceActorTool placeActorTool) + return; + + foreach (var def in placeActorTool.Definitions) { - bool isSelected = currentDefinition == i; + bool isSelected = placeActorTool.CurrentDefinition == def; - // TODO: 1) Cache this? 2) Somehow get a good human-readable name - if (ImGui.Selectable(definitionTypes[i].FullName, isSelected)) - currentDefinition = i; + if (ImGui.Selectable(def.FullName, isSelected)) + placeActorTool.CurrentDefinition = def; if (isSelected) ImGui.SetItemDefaultFocus(); diff --git a/Source/Mod/Editor/Tool/PlaceActorTool.cs b/Source/Mod/Editor/Tool/PlaceActorTool.cs index d1e5bd9d..1cc4537b 100644 --- a/Source/Mod/Editor/Tool/PlaceActorTool.cs +++ b/Source/Mod/Editor/Tool/PlaceActorTool.cs @@ -1,3 +1,5 @@ +using System.Reflection; + namespace Celeste64.Mod.Editor; public class PlaceActorTool : Tool @@ -5,4 +7,80 @@ public class PlaceActorTool : Tool public override string Name => "Place Actor"; public override Gizmo[] Gizmos => []; public override bool EnableSelection => false; + + // TODO: Detect these definitions somehow + internal readonly List Definitions = [typeof(SpikeBlock.Definition), typeof(Solid.Definition)]; + + private Type currentDefinition = null!; // Indirectly initialized in constructor + internal Type CurrentDefinition + { + get => currentDefinition; + set + { + if (currentDefinition == value) + return; + + currentDefinition = value; + definitionToPlace = (ActorDefinition)Activator.CreateInstance(value)!; + + var prop = value + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(prop => + !prop.HasAttr() && + prop.GetCustomAttribute() is { Value: SpecialPropertyType.PositionXYZ }); + + if (prop is null || prop.GetGetMethod() is not { } getMethod || prop.GetSetMethod() is not { } setMethod) + positionProp = null; + else + positionProp = prop; + } + } + + private Actor[] actorsToPlace = []; + private ActorDefinition? definitionToPlace = null; + private PropertyInfo? positionProp; + + public PlaceActorTool() + { + CurrentDefinition = Definitions[0]; + } + + public override void OnDeselectTool(EditorWorld editor) + { + foreach (var actor in actorsToPlace) + { + editor.Destroy(actor); + } + actorsToPlace = []; + } + + public override void Update(EditorWorld editor) + { + if (positionProp is null || definitionToPlace is null) + return; + + // TODO: Choose the placement position more intelligently: configurable distance, place along geometry, etc. + positionProp.SetValue(definitionToPlace, editor.Camera.Position + editor.MouseRay * 250.0f); + + foreach (var actor in actorsToPlace) + { + editor.Destroy(actor); + } + + if (!ImGuiManager.WantCaptureMouse && Input.Mouse.LeftPressed) + { + // Place the definition. + editor.AddDefinition(definitionToPlace); + // Generate a new definition + actors + definitionToPlace = (ActorDefinition)Activator.CreateInstance(currentDefinition)!; + actorsToPlace = []; + return; + } + + actorsToPlace = definitionToPlace.Load(World.WorldType.Editor); + foreach (var actor in actorsToPlace) + { + editor.Add(actor); + } + } } diff --git a/Source/Mod/Editor/Tool/Tool.cs b/Source/Mod/Editor/Tool/Tool.cs index ea736a26..6223d856 100644 --- a/Source/Mod/Editor/Tool/Tool.cs +++ b/Source/Mod/Editor/Tool/Tool.cs @@ -7,4 +7,10 @@ public abstract class Tool public abstract bool EnableSelection { get; } public virtual void Awake(EditorWorld editor) { } + + public virtual void OnSelectTool(EditorWorld editor) { } + public virtual void OnDeselectTool(EditorWorld editor) { } + + public virtual void Update(EditorWorld editor) { } + public virtual void Render(EditorWorld editor) { } // TODO: Implement in EditorWorld } From 4bf43a9686ba39209d5c82b1c85e634f701f04b6 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 4 Apr 2024 13:54:53 +0200 Subject: [PATCH 96/97] Add toggle for playing animations --- Source/Mod/Data/PersistedData/EditorSettings_V01.cs | 1 + Source/Mod/Editor/EditorWorld.cs | 3 +++ Source/Mod/Editor/GUI/EditorMenuBar.cs | 6 +++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Mod/Data/PersistedData/EditorSettings_V01.cs b/Source/Mod/Data/PersistedData/EditorSettings_V01.cs index d309af62..1389788e 100644 --- a/Source/Mod/Data/PersistedData/EditorSettings_V01.cs +++ b/Source/Mod/Data/PersistedData/EditorSettings_V01.cs @@ -14,6 +14,7 @@ public sealed class EditorSettings_V01 : PersistedData // View public bool RenderSnow { get; set; } = false; public bool RenderSkybox { get; set; } = true; + public bool PlayAnimations { get; set; } = true; public const float MinRenderDistance = 500.0f; public const float MaxRenderDistance = 5000.0f; diff --git a/Source/Mod/Editor/EditorWorld.cs b/Source/Mod/Editor/EditorWorld.cs index 64d8bf31..6c34730d 100644 --- a/Source/Mod/Editor/EditorWorld.cs +++ b/Source/Mod/Editor/EditorWorld.cs @@ -361,6 +361,9 @@ public override void Update() if (Input.Keyboard.Pressed(Keys.F1)) DebugDraw = !DebugDraw; + if (Settings.Editor.PlayAnimations) + GeneralTimer += Time.Delta; + // add / remove actors ResolveChanges(); } diff --git a/Source/Mod/Editor/GUI/EditorMenuBar.cs b/Source/Mod/Editor/GUI/EditorMenuBar.cs index 981f00d3..2642ed03 100644 --- a/Source/Mod/Editor/GUI/EditorMenuBar.cs +++ b/Source/Mod/Editor/GUI/EditorMenuBar.cs @@ -28,10 +28,14 @@ public override void Render() bool snow = Settings.Editor.RenderSnow; changed |= ImGui.Checkbox("Show Snow", ref snow); Settings.Editor.RenderSnow = snow; - + bool skybox = Settings.Editor.RenderSkybox; changed |= ImGui.Checkbox("Show Skybox", ref skybox); Settings.Editor.RenderSkybox = skybox; + + bool anim = Settings.Editor.PlayAnimations; + changed |= ImGui.Checkbox("Play Animations", ref anim); + Settings.Editor.PlayAnimations = anim; float renderDistance = Settings.Editor.RenderDistance; changed |= ImGui.DragFloat("Render Distance", ref renderDistance, v_speed: 10.0f, v_min: EditorSettings_V01.MinRenderDistance, v_max: EditorSettings_V01.MaxRenderDistance); From df4043c874a92e3c6421f66fce7c6b283c98f7f2 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Thu, 4 Apr 2024 13:55:16 +0200 Subject: [PATCH 97/97] Access top-scene in crash handler more safely --- Source/Game.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Game.cs b/Source/Game.cs index 90dd3b80..241f8a0c 100644 --- a/Source/Game.cs +++ b/Source/Game.cs @@ -186,7 +186,7 @@ public void UnsafelySetScene(Scene next) private void HandleError(Exception e) { - if (scenes.Peek() is GameErrorMessage) + if (Scene is GameErrorMessage) { throw e; // If we're already on the error message screen, accept our fate: it's a fatal crash! }