From 275405c16fec4575eeec6fe3013321a9871779b2 Mon Sep 17 00:00:00 2001 From: horizon-wings <127271516+horizon-wings@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:11:35 -0700 Subject: [PATCH] Fix non-even decal rendering (#624) Fix decal rendering when decal dimensions are non-even --- Celeste.Mod.mm/Patches/Decal.cs | 227 +++++++++++++++++++++++++++++--- 1 file changed, 210 insertions(+), 17 deletions(-) diff --git a/Celeste.Mod.mm/Patches/Decal.cs b/Celeste.Mod.mm/Patches/Decal.cs index 818b28c9a..b2c19a4a6 100644 --- a/Celeste.Mod.mm/Patches/Decal.cs +++ b/Celeste.Mod.mm/Patches/Decal.cs @@ -84,7 +84,6 @@ public patch_CoreSwapImage(MTexture hot, MTexture cold) : base(active: false, vi } [MonoModIgnore] - [PatchDecalImageRender] public extern override void Render(); } @@ -110,8 +109,11 @@ public override void Update() { } public override void Render() { - if (activeTextures.Count > 0) - activeTextures[(int) frame % activeTextures.Count].DrawCentered(Decal.Position, Decal.Color, Decal.scale, Decal.Rotation); + if (activeTextures.Count > 0) { + MTexture texture = activeTextures[(int) frame % activeTextures.Count]; + Vector2 offset = new Vector2(texture.Center.X * Decal.scale.X % 1, texture.Center.Y * Decal.scale.X % 1); + texture.DrawCentered(Decal.Position + offset, Decal.Color, Decal.scale, Decal.Rotation); + } } } @@ -152,15 +154,15 @@ public void ctor(string texture, Vector2 position, Vector2 scale, int depth, flo [MonoModIgnore] [MonoModPublic] - [PatchMirrorMaskRotation] + [PatchMirrorMaskRender] public extern void MakeMirror(string path, bool keepOffsetsClose); [MonoModIgnore] - [PatchMirrorMaskRotation] + [PatchMirrorMaskRender] private extern void MakeMirror(string path, Vector2 offset); [MonoModIgnore] - [PatchMirrorMaskRotation] + [PatchMirrorMaskRender] private extern void MakeMirrorSpecialCase(string path, Vector2 offset); [MonoModIgnore] @@ -348,16 +350,16 @@ namespace MonoMod { class PatchDecalUpdateAttribute : Attribute { } /// - /// Allow decal images to be rotated. + /// Allow decal images to be rotated and correct rendering of images with non-even dimensions. /// [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchDecalImageRender))] class PatchDecalImageRenderAttribute : Attribute { } /// - /// Allow mirror masks to be rotated. + /// Allow mirror masks to be rotated and correct rendering of masks with non-even dimensions. /// - [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchMirrorMaskRotation))] - class PatchMirrorMaskRotationAttribute : Attribute { } + [MonoModCustomMethodAttribute(nameof(MonoModRules.PatchMirrorMaskRender))] + class PatchMirrorMaskRenderAttribute : Attribute { } static partial class MonoModRules { @@ -380,21 +382,120 @@ public static void PatchDecalUpdate(ILContext context, CustomAttribute attrib) { public static void PatchDecalImageRender(ILContext context, CustomAttribute attrib) { TypeDefinition t_Decal = MonoModRule.Modder.FindType("Celeste.Decal").Resolve(); TypeDefinition t_MTexture = MonoModRule.Modder.FindType("Monocle.MTexture").Resolve(); + TypeDefinition t_Vector2 = MonoModRule.Modder.FindType("Microsoft.Xna.Framework.Vector2").Resolve(); + TypeDefinition t_Matrix = MonoModRule.Modder.FindType("Microsoft.Xna.Framework.Matrix").Resolve(); + // This is not allowed to be a TypeDefinition for ?? reasons + TypeReference t_float = MonoModRule.Modder.Module.ImportReference(MonoModRule.Modder.FindType("System.Single").Resolve()); + FieldReference f_Decal_Rotation = t_Decal.FindField("Rotation"); FieldReference f_Decal_Color = t_Decal.FindField("Color"); - + FieldReference f_Decal_scale = t_Decal.FindField("scale"); + + MethodReference m_get_Center = t_MTexture.FindProperty("Center").GetMethod; + + MethodReference m_Vector2_ctor = MonoModRule.Modder.Module.ImportReference(t_Vector2.FindMethod("System.Void .ctor(System.Single,System.Single)")); + MethodReference m_Vector2_Transform = MonoModRule.Modder.Module.ImportReference(t_Vector2.FindMethod("Microsoft.Xna.Framework.Vector2 Transform(Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Matrix)")); + FieldReference f_Vector2_X = MonoModRule.Modder.Module.ImportReference(t_Vector2.FindField("X")); + FieldReference f_Vector2_Y = MonoModRule.Modder.Module.ImportReference(t_Vector2.FindField("Y")); + + MethodReference m_Matrix_CreateRotationZ = MonoModRule.Modder.Module.ImportReference(t_Matrix.FindMethod("Microsoft.Xna.Framework.Matrix CreateRotationZ(System.Single)")); + + // These methods vary based on what class we're patching MethodReference m_get_Decal = null; MethodReference m_Draw_old = null; + // Create some extra locals to make the math easier + VariableDefinition v_vector = new VariableDefinition(MonoModRule.Modder.Module.ImportReference(t_Vector2)); + VariableDefinition v_X = new VariableDefinition(t_float); + VariableDefinition v_Y = new VariableDefinition(t_float); + context.Body.Variables.Add(v_vector); + context.Body.Variables.Add(v_X); + context.Body.Variables.Add(v_Y); + ILCursor cursor = new ILCursor(context); + // Part one: fix rendering for decals with non-integer centers (i.e. non-even dimensions) + + // Jump to just after the decal texture is put on the stack. + // Because the various decal components have different methods of finding their texture, + // it's not possible to match the instruction that loads the texture itself. + // Instead, we can just look for the instructions that put the decal position on the stack, + // and put our cursor just before that, since the decal texture immediately precedes position + // in the arguments being made to MTexture.Draw(). + // Also, we collect the MethodReference for the Decal getter for this component type. + cursor.GotoNext(MoveType.Before, + instr => instr.MatchLdarg(0), + instr => instr.MatchCallvirt(out m_get_Decal), + instr => instr.MatchLdfld("Monocle.Entity", "Position") + ); + + // Duplicate the texture reference on the stack so we leave a copy to be used as an argument to Draw(). + // Then get MTexture.Center, multiply it by a rotation matrix based on decal rotation, and store it in our Vector2 local. + cursor.Emit(OpCodes.Dup); + cursor.Emit(OpCodes.Call, m_get_Center); + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Callvirt, m_get_Decal); + cursor.Emit(OpCodes.Ldfld, f_Decal_Rotation); + cursor.Emit(OpCodes.Ldc_R4, (float) Math.PI / 180); + cursor.Emit(OpCodes.Mul); + cursor.Emit(OpCodes.Call, m_Matrix_CreateRotationZ); + cursor.Emit(OpCodes.Call, m_Vector2_Transform); + cursor.Emit(OpCodes.Stloc_S, v_vector); + + // Get our stored Vector2 back, then get the X component as a float, and store the non-integer component + // of that float in our first float local. This will be used to offset the "real" position when the decal + // renders if the decal's Center is a non-integer. (If the Center is an integer, the offset will be 0.) + cursor.Emit(OpCodes.Ldloc_S, v_vector); + cursor.Emit(OpCodes.Ldfld, f_Vector2_X); + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Callvirt, m_get_Decal); + cursor.Emit(OpCodes.Ldfld, f_Decal_scale); + cursor.Emit(OpCodes.Ldfld, f_Vector2_X); + cursor.Emit(OpCodes.Mul); + cursor.Emit(OpCodes.Ldc_R4, 1f); + cursor.Emit(OpCodes.Rem); + cursor.Emit(OpCodes.Stloc_S, v_X); + + // Same thing but for the Y. + cursor.Emit(OpCodes.Ldloc_S, v_vector); + cursor.Emit(OpCodes.Ldfld, f_Vector2_Y); + cursor.Emit(OpCodes.Ldarg_0); + cursor.Emit(OpCodes.Callvirt, m_get_Decal); + cursor.Emit(OpCodes.Ldfld, f_Decal_scale); + cursor.Emit(OpCodes.Ldfld, f_Vector2_Y); + cursor.Emit(OpCodes.Mul); + cursor.Emit(OpCodes.Ldc_R4, 1f); + cursor.Emit(OpCodes.Rem); + cursor.Emit(OpCodes.Stloc_S, v_Y); + + // Now jump after the position has been put on the stack, so we can store and then modify it. + cursor.GotoNext(MoveType.After, + instr => instr.MatchLdfld("Monocle.Entity", "Position")); + cursor.Emit(OpCodes.Stloc_S, v_vector); + + // Get our stored position back, get the position's X component, and add it to our calculated offset. + cursor.Emit(OpCodes.Ldloc_S, v_vector); + cursor.Emit(OpCodes.Ldfld, f_Vector2_X); + cursor.Emit(OpCodes.Ldloc_S, v_X); + cursor.Emit(OpCodes.Add); + + // And again for Y. + cursor.Emit(OpCodes.Ldloc_S, v_vector); + cursor.Emit(OpCodes.Ldfld, f_Vector2_Y); + cursor.Emit(OpCodes.Ldloc_S, v_Y); + cursor.Emit(OpCodes.Add); + + // Use our offsetted position values to create a new "adjusted" position, which will be used instead. + cursor.Emit(OpCodes.Newobj, m_Vector2_ctor); + + // Part two: inject decal rotation + // move to just after the decal's scale is obtained, but just before the draw call. - // also get references to the Decal getter and to the draw call itself + // also get a reference to the draw call itself cursor.GotoNext(MoveType.After, - instr => instr.MatchCallvirt(out m_get_Decal), - instr => instr.MatchLdfld("Celeste.Decal", "scale"), - instr => instr.MatchCallvirt(out m_Draw_old)); + instr => instr.MatchLdfld("Celeste.Decal", "scale"), + instr => instr.MatchCallvirt(out m_Draw_old)); cursor.Index--; // find an appropriate draw method; it should have the same signature, but also take a float for the rotation as its last argument @@ -420,10 +521,28 @@ public static void PatchDecalImageRender(ILContext context, CustomAttribute attr cursor.Remove(); } - public static void PatchMirrorMaskRotation(ILContext context, CustomAttribute attrib) { + public static void PatchMirrorMaskRender(ILContext context, CustomAttribute attrib) { + TypeDefinition t_Decal = MonoModRule.Modder.FindType("Celeste.Decal").Resolve(); TypeDefinition t_MTexture = MonoModRule.Modder.FindType("Monocle.MTexture").Resolve(); - FieldDefinition f_Decal_Rotation = context.Method.DeclaringType.FindField("Rotation"); + TypeDefinition t_Vector2 = MonoModRule.Modder.FindType("Microsoft.Xna.Framework.Vector2").Resolve(); + TypeDefinition t_Entity = MonoModRule.Modder.FindType("Monocle.Entity").Resolve(); + TypeDefinition t_Matrix = MonoModRule.Modder.FindType("Microsoft.Xna.Framework.Matrix").Resolve(); + + FieldReference f_Decal_Rotation = t_Decal.FindField("Rotation"); + FieldReference f_Decal_scale = t_Decal.FindField("scale"); + + FieldReference f_Entity_Position = t_Entity.FindField("Position"); + + MethodReference m_get_Center = t_MTexture.FindProperty("Center").GetMethod; MethodDefinition m_DrawCentered = t_MTexture.FindMethod("System.Void DrawCentered(Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color,Microsoft.Xna.Framework.Vector2,System.Single)"); + + MethodReference m_Vector2_ctor = MonoModRule.Modder.Module.ImportReference(t_Vector2.FindMethod("System.Void .ctor(System.Single,System.Single)")); + MethodReference m_Vector2_Transform = MonoModRule.Modder.Module.ImportReference(t_Vector2.FindMethod("Microsoft.Xna.Framework.Vector2 Transform(Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Matrix)")); + FieldReference f_Vector2_X = MonoModRule.Modder.Module.ImportReference(t_Vector2.FindField("X")); + FieldReference f_Vector2_Y = MonoModRule.Modder.Module.ImportReference(t_Vector2.FindField("Y")); + + MethodReference m_Matrix_CreateRotationZ = MonoModRule.Modder.Module.ImportReference(t_Matrix.FindMethod("Microsoft.Xna.Framework.Matrix CreateRotationZ(System.Single)")); + MethodReference r_OnRender = null; ILCursor cursor = new ILCursor(context); @@ -436,8 +555,82 @@ public static void PatchMirrorMaskRotation(ILContext context, CustomAttribute at // Some of the patched methods use closure locals so search for those FieldDefinition f_locals = m_OnRender.DeclaringType.FindField("CS$<>8__locals1"); FieldReference f_this = null; + FieldReference f_mask = null; ILCursor cursor = new ILCursor(il); + + // Copies the IL strategy from PatchDecalImageRotationAndCentering, modified to avoid adding locals + + cursor.GotoNext(MoveType.After, + instr => instr.MatchLdfld(out f_mask), + instr => instr.MatchLdarg(0) + ); + + cursor.GotoNext(MoveType.After, + instr => instr.MatchLdfld(out f_this), + instr => instr.MatchLdfld("Monocle.Entity", "Position") + ); + + cursor.Emit(OpCodes.Ldfld, f_Vector2_X); + cursor.Emit(OpCodes.Ldarg_0); + if (f_locals != null) + cursor.Emit(OpCodes.Ldfld, f_locals); + cursor.Emit(OpCodes.Ldfld, f_mask); + cursor.Emit(OpCodes.Call, m_get_Center); + cursor.Emit(OpCodes.Ldarg_0); + if (f_locals != null) + cursor.Emit(OpCodes.Ldfld, f_locals); + cursor.Emit(OpCodes.Ldfld, f_this); + cursor.Emit(OpCodes.Ldfld, f_Decal_Rotation); + cursor.Emit(OpCodes.Ldc_R4, (float) Math.PI / 180); + cursor.Emit(OpCodes.Mul); + cursor.Emit(OpCodes.Call, m_Matrix_CreateRotationZ); + cursor.Emit(OpCodes.Call, m_Vector2_Transform); + cursor.Emit(OpCodes.Ldfld, f_Vector2_X); + cursor.Emit(OpCodes.Ldarg_0); + if (f_locals != null) + cursor.Emit(OpCodes.Ldfld, f_locals); + cursor.Emit(OpCodes.Ldfld, f_this); + cursor.Emit(OpCodes.Ldfld, f_Decal_scale); + cursor.Emit(OpCodes.Ldfld, f_Vector2_X); + cursor.Emit(OpCodes.Mul); + cursor.Emit(OpCodes.Ldc_R4, 1f); + cursor.Emit(OpCodes.Rem); + cursor.Emit(OpCodes.Add); + + cursor.Emit(OpCodes.Ldarg_0); + if (f_locals != null) + cursor.Emit(OpCodes.Ldfld, f_locals); + cursor.Emit(OpCodes.Ldfld, f_this); + cursor.Emit(OpCodes.Ldfld, f_Entity_Position); + cursor.Emit(OpCodes.Ldfld, f_Vector2_Y); + cursor.Emit(OpCodes.Ldarg_0); + if (f_locals != null) + cursor.Emit(OpCodes.Ldfld, f_locals); + cursor.Emit(OpCodes.Ldfld, f_mask); + cursor.Emit(OpCodes.Call, m_get_Center); + cursor.Emit(OpCodes.Ldarg_0); + if (f_locals != null) + cursor.Emit(OpCodes.Ldfld, f_locals); + cursor.Emit(OpCodes.Ldfld, f_this); + cursor.Emit(OpCodes.Ldfld, f_Decal_Rotation); + cursor.Emit(OpCodes.Ldc_R4, (float) Math.PI / 180); + cursor.Emit(OpCodes.Mul); + cursor.Emit(OpCodes.Call, m_Matrix_CreateRotationZ); + cursor.Emit(OpCodes.Call, m_Vector2_Transform); + cursor.Emit(OpCodes.Ldfld, f_Vector2_Y); + cursor.Emit(OpCodes.Ldarg_0); + if (f_locals != null) + cursor.Emit(OpCodes.Ldfld, f_locals); + cursor.Emit(OpCodes.Ldfld, f_this); + cursor.Emit(OpCodes.Ldfld, f_Decal_scale); + cursor.Emit(OpCodes.Ldfld, f_Vector2_X); + cursor.Emit(OpCodes.Mul); + cursor.Emit(OpCodes.Ldc_R4, 1f); + cursor.Emit(OpCodes.Rem); + cursor.Emit(OpCodes.Add); + + cursor.Emit(OpCodes.Newobj, m_Vector2_ctor); // Grab the function's decal reference and move to just before the draw call cursor.GotoNext(MoveType.After,