From 2a832928079bc508cb5d22c5f6a0464279aebfd3 Mon Sep 17 00:00:00 2001 From: MayNL Date: Tue, 16 Jun 2026 16:26:38 -0400 Subject: [PATCH] Improved skin paper doll draw function --- PckStudio.Core/Extensions/SkinExtensions.cs | 427 ++++++++++++-------- 1 file changed, 261 insertions(+), 166 deletions(-) diff --git a/PckStudio.Core/Extensions/SkinExtensions.cs b/PckStudio.Core/Extensions/SkinExtensions.cs index f544f7ec..0decd60d 100644 --- a/PckStudio.Core/Extensions/SkinExtensions.cs +++ b/PckStudio.Core/Extensions/SkinExtensions.cs @@ -7,11 +7,7 @@ using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; -using System.Drawing.Imaging; using System.Linq; -using System.Numerics; -using System.Text; -using System.Threading.Tasks; namespace PckStudio.Core.Extensions { @@ -95,6 +91,7 @@ namespace PckStudio.Core.Extensions gfx.InterpolationMode = InterpolationMode.NearestNeighbor; gfx.PixelOffsetMode = PixelOffsetMode.HighQuality; gfx.SmoothingMode = SmoothingMode.None; + gfx.CompositingMode = CompositingMode.SourceOver; SkinBOX baseHeadBox = new SkinBOX("HEAD", new(-4, -8, -4), new(8), new(0, 0)); SkinBOX baseTorsoBox = new SkinBOX("BODY", new(-4, 0, -2), new(8, 12, 4), new(16, 16)); @@ -165,17 +162,29 @@ namespace PckStudio.Core.Extensions baseBoxes.Add(new SkinBOX("PANTS1", new(-2, 0, -2), new(4, 12, 4), new(0, 48), scale: 0.25f)); } - skinBoxes.AddRange(baseBoxes); + HashSet baseBoxSet = new(baseBoxes); + + baseBoxes.Clear(); + + skinBoxes.AddRange(baseBoxSet); skinBoxes.AddRange(skin.Model.AdditionalBoxes); - skinBoxes = skinBoxes - .OrderByDescending(box => - isStatueOfLiberty && box.Type == "ARM0" - ? -box.Pos.Z - : box.Pos.Z - ) - .ThenBy(box => box.Scale) - .ToList(); + skinBoxes.Sort((first, second) => + { + float aZPos = isStatueOfLiberty && first.Type == "ARM0" + ? -first.Pos.Z + : first.Pos.Z; + + float bZPos = isStatueOfLiberty && second.Type == "ARM0" + ? -second.Pos.Z + : second.Pos.Z; + + int result = bZPos.CompareTo(aZPos); + + return result != 0 + ? result + : first.Scale.CompareTo(second.Scale); + }); float canvasWidth = 0; float canvasHeight = 0; @@ -190,178 +199,264 @@ namespace PckStudio.Core.Extensions float defaultDrawOffsetX = paperDoll.Width / 2f; // draw at center of the canvas float defaultDrawOffsetY = paperDoll.Height / 2f - torsoCenterY * pixelScale; // draw from center of the torso box + var drawFunctions = new List<( + Image BackFace, + Image FrontFace, + RectangleF DrawRect, + bool StatueOfLibertyArm + )>(); + + var partOffsets = skin.Model.PartOffsets + .ToDictionary(x => x.Type, x => x.Value); + foreach (SkinBOX box in skinBoxes) { - try + float boxWidth = box.Size.X; + float boxHeight = box.Size.Y; + float boxDepth = box.Size.Z; + + if (boxWidth <= 0 || boxHeight <= 0) // if either of these are 0, skip because drawing is impossible + continue; + + bool isStatueOfLibertyArm = isStatueOfLiberty && (box.Type == "ARM0" || box.Type == "SLEEVE0" || box.Type == "ARMARMOR0"); + + // this math is basically to ensure the face is stretched if the texture is improper + Rectangle faceRect = new Rectangle( + isStatueOfLibertyArm // get back of ARM 0 box if Statue of Liberty + ? (int)((box.UV.X + boxWidth + 2 * boxDepth) * textureScaleX) // depth + width + depth = offset for back of box + : (int)((box.UV.X + boxDepth) * textureScaleX), + (int)((box.UV.Y + boxDepth) * textureScaleY), + (int)(boxWidth * textureScaleX), + (int)(boxHeight * textureScaleY) + ); + + // get back of the face to display behind just incase transparent parts exist on the front layer + Rectangle backFaceRect = new Rectangle( + !isStatueOfLibertyArm // get front of ARM 0 box if Statue of Liberty + ? (int)((box.UV.X + boxWidth + 2 * boxDepth) * textureScaleX) // depth + width + depth = offset for back of box + : (int)((box.UV.X + boxDepth) * textureScaleX), + (int)((box.UV.Y + boxDepth) * textureScaleY), + (int)(boxWidth * textureScaleX), + (int)(boxHeight * textureScaleY) + ); + + Image boxFace = skin.Texture.GetArea(faceRect); + Image backBoxFace = skin.Texture.GetArea(backFaceRect); + + if (box.Mirror) { - float boxWidth = box.Size.X; - float boxHeight = box.Size.Y; - float boxDepth = box.Size.Z; + boxFace.RotateFlip(RotateFlipType.RotateNoneFlipX); + } + else + { + backBoxFace.RotateFlip(RotateFlipType.RotateNoneFlipX); + } - if (boxWidth <= 0 || boxHeight <= 0) // if either of these are 0, skip because drawing is impossible - continue; + if (isStatueOfLibertyArm) + { + boxFace.RotateFlip(RotateFlipType.RotateNoneFlipY); + backBoxFace.RotateFlip(RotateFlipType.RotateNoneFlipY); + } - bool isStatueOfLibertyArm = isStatueOfLiberty && (box.Type == "ARM0" || box.Type == "SLEEVE0" || box.Type == "ARMARMOR0"); + // these values are added to later + float drawOffsetX = defaultDrawOffsetX; // draw at center of the canvas + float drawOffsetY = defaultDrawOffsetY; - // this math is basically to ensure the face is stretched if the texture is improper - Rectangle faceRect = new Rectangle( - isStatueOfLibertyArm // get back of ARM 0 box if Statue of Liberty - ? (int)((box.UV.X + boxWidth + 2 * boxDepth) * textureScaleX) // depth + width + depth = offset for back of box - : (int)((box.UV.X + boxDepth) * textureScaleX), - (int)((box.UV.Y + boxDepth) * textureScaleY), - (int)(boxWidth * textureScaleX), - (int)(boxHeight * textureScaleY) - ); + // offset by 4 pixels so feet align with where the head should be for the bust crop. This might not be perfect, but it gets the job done for the 2 "vanilla" upside-down skins + if (isDinnerbone) + drawOffsetY -= 4 * pixelScale; - Image boxFace = skin.Texture.GetArea(faceRect); - if (box.Mirror) boxFace.RotateFlip(RotateFlipType.RotateNoneFlipX); - if (isStatueOfLibertyArm) + float armXOffset = GameConstants.SkinLeftArmTranslation.X * pixelScale; + float armYOffset = GameConstants.SkinLeftArmTranslation.Y * pixelScale; + float legXOffset = GameConstants.SkinLeftLegTranslation.X * pixelScale; + float legYOffset = GameConstants.SkinLeftLegTranslation.Y * pixelScale; + + float offset = 0; + + switch (box.Type) + { + case "HEAD": + case "HEADWEAR": + partOffsets.TryGetValue("HEAD", out offset); + break; + + case "BODY": + case "WAIST": + case "JACKET": + case "BELT": + case "BODYARMOR": + partOffsets.TryGetValue("BODY", out offset); + break; + + case "ARM0": + case "SLEEVE0": + case "ARMARMOR0": + partOffsets.TryGetValue("ARM0", out offset); + drawOffsetX -= armXOffset; + drawOffsetY += armYOffset; + break; + + case "ARM1": + case "SLEEVE1": + case "ARMARMOR1": + partOffsets.TryGetValue("ARM1", out offset); + drawOffsetX += armXOffset; + drawOffsetY += armYOffset; + break; + + case "LEG0": + case "PANTS0": + case "LEGGING0": + case "SOCK0": + case "BOOT0": + partOffsets.TryGetValue("BOOT0", out offset); + drawOffsetX -= legXOffset; + drawOffsetY += legYOffset; + break; + + case "LEG1": + case "PANTS1": + case "LEGGING1": + case "SOCK1": + case "BOOT1": + partOffsets.TryGetValue("BOOT1", out offset); + drawOffsetX += legXOffset; + drawOffsetY += legYOffset; + break; + } + + drawOffsetY += offset * pixelScale; + + float boxPosY = box.Pos.Y; + + if (isStatueOfLibertyArm) + { + boxPosY = -(box.Pos.Y + box.Size.Y); + } + + float drawX = box.Pos.X * pixelScale; + float drawY = boxPosY * pixelScale; + + float drawWidth = boxWidth * pixelScale; + float drawHeight = boxHeight * pixelScale; + + // handle Box scale + float boxInflate = + // if XMLVersion 3 or a base box; then return box scale + (xmlVersion == 3 || baseBoxSet.Contains(box)) ? box.Scale + // if not; then check that it is an overlay box + : (xmlVersion > 0 && xmlVersion < 3 && SkinBOX.OverlayTypes.Contains(box.Type)) + // if so, return hardcoded box inflate value + ? (box.Type == "HEADWEAR" ? 0.5f : 0.25f) + : 0f; // finally, if not caught anywhere else, return no scale + + float drawInflate = boxInflate * pixelScale * 1.5f; // no idea why 1.5 seems to fix this :,) + float halfInflation = drawInflate * 0.5f; + + float finalDrawX = drawOffsetX + drawX - halfInflation; + float finalDrawY = drawOffsetY + drawY - halfInflation; + + float finalDrawWidth = drawWidth + drawInflate; + float finalDrawHeight = drawHeight + drawInflate; + + RectangleF boxRect = new RectangleF(finalDrawX, finalDrawY, finalDrawWidth, finalDrawHeight); + + if (isStatueOfLiberty && box.Type == "ARM0") + { + float pivotX = defaultDrawOffsetX + GameConstants.SkinRightArmPivot.X * pixelScale; + float pivotY = defaultDrawOffsetY + GameConstants.SkinRightArmPivot.Y * pixelScale; + + const float Angle = -8f * (float)Math.PI / 180f; + + float cos = (float)Math.Cos(Angle); + float sin = (float)Math.Sin(Angle); + + // used to calculate if the bounds of the arm go beyond the current bounds of the image so that the entire skin is shown + PointF[] armBounds = { - boxFace.RotateFlip(RotateFlipType.RotateNoneFlipY); - } - - float drawOffsetX = defaultDrawOffsetX; // draw at center of the canvas - float drawOffsetY = defaultDrawOffsetY; - - // offset by 4 pixels so feet align with where the head should be for the bust crop. This might not be perfect, but it gets the job done for the 2 "vanilla" upside-down skins - if (isDinnerbone) - drawOffsetY -= 4 * pixelScale; - - float armXOffset = GameConstants.SkinLeftArmTranslation.X * pixelScale; - float armYOffset = GameConstants.SkinLeftArmTranslation.Y * pixelScale; - float legXOffset = GameConstants.SkinLeftLegTranslation.X * pixelScale; - float legYOffset = GameConstants.SkinLeftLegTranslation.Y * pixelScale; - - switch (box.Type) - { - case "HEAD": - case "HEADWEAR": - drawOffsetY += skin.Model.PartOffsets.Find(x => x.Type == "HEAD").Value * pixelScale; - break; - case "BODY": - case "WAIST": - case "JACKET": - case "BELT": - case "BODYARMOR": - drawOffsetY += skin.Model.PartOffsets.Find(x => x.Type == "BODY").Value * pixelScale; - break; - case "ARM0": - case "SLEEVE0": - case "ARMARMOR0": - drawOffsetX -= armXOffset; - drawOffsetY += armYOffset + skin.Model.PartOffsets.Find(x => x.Type == "ARM0").Value * pixelScale; - break; - case "ARM1": - case "SLEEVE1": - case "ARMARMOR1": - drawOffsetX += armXOffset; - drawOffsetY += armYOffset + skin.Model.PartOffsets.Find(x => x.Type == "ARM1").Value * pixelScale; - break; - case "LEG0": - case "PANTS0": - case "LEGGING0": - case "SOCK0": - case "BOOT0": - drawOffsetX -= legXOffset; - drawOffsetY += legYOffset + skin.Model.PartOffsets.Find(x => x.Type == "LEG0").Value * pixelScale; - break; - case "LEG1": - case "PANTS1": - case "LEGGING1": - case "SOCK1": - case "BOOT1": - drawOffsetX += legXOffset; - drawOffsetY += legYOffset + skin.Model.PartOffsets.Find(x => x.Type == "LEG1").Value * pixelScale; - break; - } - - float boxPosY = box.Pos.Y; - - if (isStatueOfLibertyArm) - { - boxPosY = -(box.Pos.Y + box.Size.Y); - } - - float drawX = box.Pos.X * pixelScale; - float drawY = boxPosY * pixelScale; - - float drawWidth = boxWidth * pixelScale; - float drawHeight = boxHeight * pixelScale; - - // handle Box scale - float boxInflate = - // if XMLVersion 3 or a base box; then return box scale - (xmlVersion == 3 || baseBoxes.Contains(box)) ? box.Scale - // if not; then check that it is an overlay box - : (xmlVersion > 0 && xmlVersion < 3 && SkinBOX.OverlayTypes.Contains(box.Type)) - // if so, return hardcoded box inflate value - ? (box.Type == "HEADWEAR" ? 0.5f : 0.25f) - : 0f; // finally, if not caught anywhere else, return no scale - - float drawInflate = boxInflate * pixelScale * 1.5f; // no idea why 1.5 seems to fix this :,) - float halfInflation = drawInflate * 0.5f; - - float finalDrawX = drawOffsetX + drawX - halfInflation; - float finalDrawY = drawOffsetY + drawY - halfInflation; - - float finalDrawWidth = drawWidth + drawInflate; - float finalDrawHeight = drawHeight + drawInflate; - - RectangleF boxRect = new RectangleF(finalDrawX, finalDrawY, finalDrawWidth, finalDrawHeight); - - if (isStatueOfLiberty && box.Type == "ARM0") - { - float pivotX = defaultDrawOffsetX + GameConstants.SkinRightArmPivot.X * pixelScale; - float pivotY = defaultDrawOffsetY + GameConstants.SkinRightArmPivot.Y * pixelScale; - - float angle = -8f * (float)Math.PI / 180f; - - // used to calculate if the bounds of the arm go beyond the current bounds of the image so that the entire skin is shown - PointF[] armBounds = - { new PointF(finalDrawX, finalDrawY), new PointF(finalDrawX + finalDrawWidth, finalDrawY), new PointF(finalDrawX, finalDrawY + finalDrawHeight), new PointF(finalDrawX + finalDrawWidth, finalDrawY + finalDrawHeight) }; - foreach (var bound in armBounds) - { - float finalX = bound.X - pivotX; - float finalY = bound.Y - pivotY; - - float rotateX = pivotX + (finalX * (float)Math.Cos(angle) - finalY * (float)Math.Sin(angle)); - float rotateY = pivotY + (finalX * (float)Math.Sin(angle) + finalY * (float)Math.Cos(angle)); - - canvasMinX = Math.Min(canvasMinX, rotateX); - canvasMinY = Math.Min(canvasMinY, rotateY); - canvasMaxX = Math.Max(canvasMaxX, rotateX); - canvasMaxY = Math.Max(canvasMaxY, rotateY); - } - - var state = gfx.Save(); - - gfx.TranslateTransform(pivotX, pivotY); - gfx.RotateTransform(-8f); - gfx.TranslateTransform(-pivotX, -pivotY); - - gfx.DrawImage(boxFace, boxRect); - - gfx.Restore(state); - } - else + foreach (var bound in armBounds) { - canvasMinX = Math.Min(canvasMinX, finalDrawX); - canvasMinY = Math.Min(canvasMinY, finalDrawY); - canvasMaxX = Math.Max(canvasMaxX, finalDrawX + finalDrawWidth); - canvasMaxY = Math.Max(canvasMaxY, finalDrawY + finalDrawHeight); + float finalX = bound.X - pivotX; + float finalY = bound.Y - pivotY; - gfx.DrawImage(boxFace, boxRect); + float rotateX = pivotX + (finalX * cos - finalY * sin); + float rotateY = pivotY + (finalX * sin + finalY * cos); + + canvasMinX = Math.Min(canvasMinX, rotateX); + canvasMinY = Math.Min(canvasMinY, rotateY); + canvasMaxX = Math.Max(canvasMaxX, rotateX); + canvasMaxY = Math.Max(canvasMaxY, rotateY); } } - catch (Exception ex) + else { - Console.WriteLine(ex.Message); + canvasMinX = Math.Min(canvasMinX, finalDrawX); + canvasMinY = Math.Min(canvasMinY, finalDrawY); + canvasMaxX = Math.Max(canvasMaxX, finalDrawX + finalDrawWidth); + canvasMaxY = Math.Max(canvasMaxY, finalDrawY + finalDrawHeight); + } + + drawFunctions.Add(( + backBoxFace, + boxFace, + boxRect, + isStatueOfLiberty && box.Type == "ARM0" + )); + } + + // draw back faces first + for (int i = drawFunctions.Count - 1; i >= 0; i--) + { + var function = drawFunctions[i]; + + if (function.StatueOfLibertyArm) + { + float pivotX = defaultDrawOffsetX + GameConstants.SkinRightArmPivot.X * pixelScale; + float pivotY = defaultDrawOffsetY + GameConstants.SkinRightArmPivot.Y * pixelScale; + + var state = gfx.Save(); + + gfx.TranslateTransform(pivotX, pivotY); + gfx.RotateTransform(-8f); + gfx.TranslateTransform(-pivotX, -pivotY); + + gfx.DrawImage(function.BackFace, function.DrawRect); + + gfx.Restore(state); + } + else + { + gfx.DrawImage(function.BackFace, function.DrawRect); + } + } + + // then draw all fronts overtop + foreach (var function in drawFunctions) + { + if (function.StatueOfLibertyArm) + { + float pivotX = defaultDrawOffsetX + GameConstants.SkinRightArmPivot.X * pixelScale; + float pivotY = defaultDrawOffsetY + GameConstants.SkinRightArmPivot.Y * pixelScale; + + var state = gfx.Save(); + + gfx.TranslateTransform(pivotX, pivotY); + gfx.RotateTransform(-8f); + gfx.TranslateTransform(-pivotX, -pivotY); + + gfx.DrawImage(function.FrontFace, function.DrawRect); + + gfx.Restore(state); + } + else + { + gfx.DrawImage(function.FrontFace, function.DrawRect); } }