XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;

namespace SoulHarvest.Common;

/// <summary>
/// Client-only irregular fragments cut directly from any sickle texture.
/// Each cached mask owns a disjoint Voronoi region, so the pieces reconstruct the
/// original weapon without introducing a second set of artwork.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class DeathSickleAssemblyTextureSystem : ModSystem
{
    private const int VariantCount = 3;
    private const int FragmentCount = ReaperDefinitions.AssemblyFragmentCount;

    private sealed class Fragment(Texture2D texture, Texture2D seamTexture,
        Vector2 centroid)
    {
        internal readonly Texture2D Texture = texture;
        internal readonly Texture2D SeamTexture = seamTexture;
        internal readonly Vector2 Centroid = centroid;
    }

    private sealed class FragmentSet(Fragment[] fragments)
    {
        internal readonly Fragment[] Fragments = fragments;
    }

    private static Dictionary<Texture2D, FragmentSet[]>? fragmentSetsByTexture;

#if DEBUG
    internal static int CachedSourceCount => fragmentSetsByTexture?.Count ?? 0;
    internal static int LastDrawFragmentCount { get; private set; }
    internal static int LastDrawPortalCount { get; private set; }
    internal static int LastVisiblePortalCount { get; private set; }
    internal static int MaxMovingFragmentCount { get; private set; }
    internal static int MaxVisiblePortalCount { get; private set; }
    internal static bool SawStaggeredPortalBatch { get; private set; }
    internal static bool SawSimultaneousPortalBatch { get; private set; }
    internal static bool SawFinalSeamFlash { get; private set; }
    internal static bool SawPartialPortalAperture { get; private set; }
    internal static bool SawSimultaneousFragmentReturn { get; private set; }

    internal static void ResetDebugMetrics()
    {
        LastDrawFragmentCount = 0;
        LastDrawPortalCount = 0;
        LastVisiblePortalCount = 0;
        MaxMovingFragmentCount = 0;
        MaxVisiblePortalCount = 0;
        SawStaggeredPortalBatch = false;
        SawSimultaneousPortalBatch = false;
        SawFinalSeamFlash = false;
        SawPartialPortalAperture = false;
        SawSimultaneousFragmentReturn = false;
    }
#endif

    public override void Unload()
    {
        Dictionary<Texture2D, FragmentSet[]>? oldCache = fragmentSetsByTexture;
        fragmentSetsByTexture = null;
#if DEBUG
        ResetDebugMetrics();
#endif
        if (oldCache is null || Main.dedServ)
            return;

        Main.QueueMainThreadAction(() =>
        {
            foreach (FragmentSet[] sets in oldCache.Values)
            {
                foreach (FragmentSet set in sets)
                {
                    foreach (Fragment fragment in set.Fragments)
                    {
                        fragment.Texture.Dispose();
                        fragment.SeamTexture.Dispose();
                    }
                }
            }
        });
    }

    internal static void Draw(
        Texture2D source,
        Vector2 grip,
        float rotation,
        Vector2 anchor,
        float scale,
        SpriteEffects effects,
        Color lightColor,
        int elapsedFrames,
        int totalFrames,
        int actionSeed,
        bool largeDeathLayout)
    {
        if (Main.dedServ)
            return;

        elapsedFrames = Math.Clamp(elapsedFrames, 0, Math.Max(1, totalFrames));
        totalFrames = Math.Max(1, totalFrames);
        float progress = elapsedFrames / (float)totalFrames;
        Dictionary<Texture2D, FragmentSet[]> cache = fragmentSetsByTexture ??= [];
        if (!cache.TryGetValue(source, out FragmentSet[]? sets))
        {
            sets = CreateFragmentSets(source);
            cache[source] = sets;
        }
        if (sets.Length == 0)
        {
            Main.EntitySpriteDraw(source, grip - Main.screenPosition, null,
                lightColor * progress, rotation, anchor, scale, effects);
            return;
        }

        int variant = PositiveModulo(actionSeed, sets.Length);
        Fragment[] fragments = sets[variant].Fragments;
        Span<int> sequence = stackalloc int[fragments.Length];
        BuildHeadToTailSequence(fragments, anchor, effects, sequence);
        int portalCloseFrames = ReaperDefinitions
            .GetAssemblyPortalCloseFrames(totalFrames);
        float finalizationProgress = MathHelper.Clamp((elapsedFrames
            - (totalFrames - portalCloseFrames))
            / (float)portalCloseFrames, 0f, 1f);
        float seamFlash = (float)Math.Sin(finalizationProgress * MathHelper.Pi);
        seamFlash *= seamFlash;
        float seamBaseGlow = 1f - SmoothStep(finalizationProgress);
#if DEBUG
        LastDrawFragmentCount = fragments.Length;
        int movingFragmentCount = 0;
        SawFinalSeamFlash |= finalizationProgress > 0.2f
            && finalizationProgress < 0.8f && seamFlash > 0.65f;
#endif

        for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
            sequenceIndex++)
        {
            int index = sequence[sequenceIndex];
            Fragment fragment = fragments[index];
            float localProgress = GetFragmentTravelProgress(elapsedFrames,
                totalFrames, sequenceIndex, fragments.Length);
#if DEBUG
            if (localProgress > 0.0001f && localProgress < 0.9999f)
                movingFragmentCount++;
#endif
            float eased = SmoothStep(localProgress);
            float remaining = 1f - eased;
            float twist = (Hash01(actionSeed, index, 193) * 2f - 1f)
                * MathHelper.Lerp(1.15f, 2.55f,
                    Hash01(actionSeed, index, 389));
            float drawRotation = rotation + twist * remaining;

            Vector2 localCentroid = GetLocalCentroid(fragment.Centroid,
                anchor, effects);
            Vector2 finalCentroid = grip
                + localCentroid.RotatedBy(rotation) * scale;
            Vector2 portalCenter = GetRiftWorldCenter(grip, rotation, anchor,
                scale, effects, fragments, index, actionSeed,
                largeDeathLayout);
            Vector2 desiredCentroid = Vector2.Lerp(portalCenter,
                finalCentroid, eased);

            float opacity = SmoothStep(MathHelper.Clamp(localProgress * 2.8f,
                0f, 1f));
            float fragmentScale = scale * MathHelper.Lerp(
                0.72f + Hash01(actionSeed, index, 521) * 0.18f,
                1f, eased);
            Vector2 drawPosition = desiredCentroid
                - localCentroid.RotatedBy(drawRotation) * fragmentScale;
            Main.EntitySpriteDraw(fragment.Texture,
                drawPosition - Main.screenPosition, null,
                lightColor * opacity, drawRotation, anchor, fragmentScale,
                effects);

            // The seam mask contains only pixels bordering another fragment;
            // it deliberately excludes the weapon's exterior silhouette. Keep
            // those cut edges incandescent while assembling. Once the final
            // fragment arrives, every seam emits one strong sealing flash and
            // then extinguishes before gameplay commits the completed weapon.
            float seamPulse = 0.84f + (float)Math.Sin(
                Main.GlobalTimeWrappedHourly * 8.5f + actionSeed * 0.19f
                    + index * 0.73f) * 0.16f;
            float seamStrength = seamBaseGlow * 0.82f + seamFlash * 1.65f;
            float seamOpacity = opacity * seamStrength * seamPulse;
            if (seamOpacity > 0.001f)
            {
                float haloScale = fragmentScale * 1.035f;
                Vector2 haloPosition = desiredCentroid
                    - localCentroid.RotatedBy(drawRotation) * haloScale;
                Main.EntitySpriteDraw(fragment.SeamTexture,
                    haloPosition - Main.screenPosition, null,
                    new Color(232, 8, 45, 0) * (seamOpacity * 0.72f),
                    drawRotation, anchor, haloScale, effects);
                Main.EntitySpriteDraw(fragment.SeamTexture,
                    drawPosition - Main.screenPosition, null,
                    new Color(255, 211, 198, 225) * (seamOpacity * 0.92f),
                    drawRotation, anchor, fragmentScale, effects);
            }
        }
#if DEBUG
        MaxMovingFragmentCount = Math.Max(MaxMovingFragmentCount,
            movingFragmentCount);
#endif

        // Render the slit after the pieces so its void core masks the crossing
        // edge. This gives every fragment a clear behind-the-rift/in-front-of-
        // the-rift transition instead of looking as if it spawned beside it.
        DrawRifts(grip, rotation, anchor, scale, effects, fragments, sequence,
            elapsedFrames, totalFrames, actionSeed, largeDeathLayout);
    }

    internal static void DrawCancellation(
        Texture2D source,
        Vector2 grip,
        float rotation,
        Vector2 anchor,
        float scale,
        SpriteEffects effects,
        Color lightColor,
        int assemblyElapsedFrames,
        int cancellationElapsedFrames,
        int totalAssemblyFrames,
        int actionSeed,
        bool largeDeathLayout)
    {
        if (Main.dedServ)
            return;

        totalAssemblyFrames = Math.Max(1, totalAssemblyFrames);
        assemblyElapsedFrames = Math.Clamp(assemblyElapsedFrames, 0,
            totalAssemblyFrames);
        cancellationElapsedFrames = Math.Clamp(cancellationElapsedFrames, 0,
            ReaperDefinitions.AssemblyCancelFrames);
        Dictionary<Texture2D, FragmentSet[]> cache = fragmentSetsByTexture ??= [];
        if (!cache.TryGetValue(source, out FragmentSet[]? sets))
        {
            sets = CreateFragmentSets(source);
            cache[source] = sets;
        }
        if (sets.Length == 0)
            return;

        int variant = PositiveModulo(actionSeed, sets.Length);
        Fragment[] fragments = sets[variant].Fragments;
        Span<int> sequence = stackalloc int[fragments.Length];
        BuildHeadToTailSequence(fragments, anchor, effects, sequence);
        float returnProgress = SmoothStep(MathHelper.Clamp(
            cancellationElapsedFrames
                / (float)ReaperDefinitions.AssemblyFragmentReturnFrames,
            0f, 1f));
#if DEBUG
        LastDrawFragmentCount = fragments.Length;
        int returningFragmentCount = 0;
#endif

        for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
            sequenceIndex++)
        {
            int index = sequence[sequenceIndex];
            Fragment fragment = fragments[index];
            float assembledProgress = GetFragmentTravelProgress(
                assemblyElapsedFrames, totalAssemblyFrames, sequenceIndex,
                fragments.Length);
            float localProgress = assembledProgress * (1f - returnProgress);
#if DEBUG
            if (assembledProgress > 0.0001f && returnProgress > 0.0001f
                && returnProgress < 0.9999f)
            {
                returningFragmentCount++;
            }
#endif
            if (localProgress <= 0.0001f)
                continue;

            float eased = SmoothStep(localProgress);
            float remaining = 1f - eased;
            float twist = (Hash01(actionSeed, index, 193) * 2f - 1f)
                * MathHelper.Lerp(1.15f, 2.55f,
                    Hash01(actionSeed, index, 389));
            float drawRotation = rotation + twist * remaining;
            Vector2 localCentroid = GetLocalCentroid(fragment.Centroid,
                anchor, effects);
            Vector2 finalCentroid = grip
                + localCentroid.RotatedBy(rotation) * scale;
            Vector2 portalCenter = GetRiftWorldCenter(grip, rotation, anchor,
                scale, effects, fragments, index, actionSeed,
                largeDeathLayout);
            Vector2 desiredCentroid = Vector2.Lerp(portalCenter,
                finalCentroid, eased);
            float opacity = SmoothStep(MathHelper.Clamp(localProgress * 2.8f,
                0f, 1f));
            float fragmentScale = scale * MathHelper.Lerp(
                0.72f + Hash01(actionSeed, index, 521) * 0.18f,
                1f, eased);
            Vector2 drawPosition = desiredCentroid
                - localCentroid.RotatedBy(drawRotation) * fragmentScale;
            Main.EntitySpriteDraw(fragment.Texture,
                drawPosition - Main.screenPosition, null,
                lightColor * opacity, drawRotation, anchor, fragmentScale,
                effects);

            float seamPulse = 0.84f + (float)Math.Sin(
                Main.GlobalTimeWrappedHourly * 8.5f + actionSeed * 0.19f
                    + index * 0.73f) * 0.16f;
            float seamOpacity = opacity * seamPulse
                * (1f - returnProgress) * 0.82f;
            if (seamOpacity <= 0.001f)
                continue;
            float haloScale = fragmentScale * 1.035f;
            Vector2 haloPosition = desiredCentroid
                - localCentroid.RotatedBy(drawRotation) * haloScale;
            Main.EntitySpriteDraw(fragment.SeamTexture,
                haloPosition - Main.screenPosition, null,
                new Color(232, 8, 45, 0) * (seamOpacity * 0.72f),
                drawRotation, anchor, haloScale, effects);
            Main.EntitySpriteDraw(fragment.SeamTexture,
                drawPosition - Main.screenPosition, null,
                new Color(255, 211, 198, 225) * (seamOpacity * 0.92f),
                drawRotation, anchor, fragmentScale, effects);
        }
#if DEBUG
        SawSimultaneousFragmentReturn |= returningFragmentCount > 1;
#endif

        DrawCancellationRifts(grip, rotation, anchor, scale, effects,
            fragments, sequence, assemblyElapsedFrames,
            cancellationElapsedFrames, totalAssemblyFrames, actionSeed,
            largeDeathLayout);
    }

    private static void DrawCancellationRifts(Vector2 grip, float rotation,
        Vector2 anchor, float scale, SpriteEffects effects,
        Fragment[] fragments, ReadOnlySpan<int> sequence,
        int assemblyElapsedFrames, int cancellationElapsedFrames,
        int totalAssemblyFrames, int actionSeed, bool largeDeathLayout)
    {
#if DEBUG
        LastDrawPortalCount = fragments.Length;
        LastVisiblePortalCount = 0;
#endif
        float closeProgress = MathHelper.Clamp((cancellationElapsedFrames
            - ReaperDefinitions.AssemblyFragmentReturnFrames)
            / (float)ReaperDefinitions.AssemblyPortalCloseFrames, 0f, 1f);
        float closing = 1f - SmoothStep(closeProgress);
        for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
            sequenceIndex++)
        {
            int fragmentIndex = sequence[sequenceIndex];
            float ageAtCancel = assemblyElapsedFrames - GetPortalStartFrame(
                totalAssemblyFrames, sequenceIndex, fragments.Length);
            float openingAtCancel = SmoothStep(MathHelper.Clamp(ageAtCancel
                / ReaperDefinitions.AssemblyPortalExpandFrames, 0f, 1f));
            float aperture = openingAtCancel * closing;
            if (aperture <= 0.001f)
                continue;
#if DEBUG
            LastVisiblePortalCount++;
            SawPartialPortalAperture |= aperture > 0.12f && aperture < 0.88f;
#endif
            Vector2 center = GetRiftWorldCenter(grip, rotation, anchor, scale,
                effects, fragments, fragmentIndex, actionSeed,
                largeDeathLayout);
            Vector2 travelDirection = -GetRiftOffset(actionSeed,
                fragmentIndex, largeDeathLayout);
            float portalScale = MathHelper.Lerp(0.54f, 0.76f,
                Hash01(actionSeed, fragmentIndex, 977))
                * (largeDeathLayout ? 3.1f : 1f);
            DeathDomainPortalRenderer.Draw(center, travelDirection, portalScale,
                aperture, actionSeed * 31 + fragmentIndex * 977);
        }
#if DEBUG
        MaxVisiblePortalCount = Math.Max(MaxVisiblePortalCount,
            LastVisiblePortalCount);
#endif
    }

    private static void DrawRifts(Vector2 grip, float rotation, Vector2 anchor,
        float scale, SpriteEffects effects, Fragment[] fragments,
        ReadOnlySpan<int> sequence, int elapsedFrames, int totalFrames,
        int actionSeed, bool largeDeathLayout)
    {
#if DEBUG
        LastDrawPortalCount = fragments.Length;
        LastVisiblePortalCount = 0;
#endif
        // Every real fragment owns one rift. Sub-max levels preserve the readable
        // serial procession. At the cap every rift shares frame zero, letting all
        // fragments cross together while retaining the same per-fragment origin.
        int portalCloseFrames = ReaperDefinitions
            .GetAssemblyPortalCloseFrames(totalFrames);
        for (int sequenceIndex = 0; sequenceIndex < sequence.Length;
            sequenceIndex++)
        {
            int fragmentIndex = sequence[sequenceIndex];
            float age = elapsedFrames - GetPortalStartFrame(totalFrames,
                sequenceIndex, fragments.Length);
            float opening = SmoothStep(MathHelper.Clamp(age
                / ReaperDefinitions.AssemblyPortalExpandFrames, 0f, 1f));
            float closing = SmoothStep(MathHelper.Clamp((totalFrames
                - elapsedFrames)
                / (float)portalCloseFrames,
                0f, 1f));
            float aperture = Math.Min(opening, closing);
            if (aperture <= 0.001f)
                continue;
#if DEBUG
            LastVisiblePortalCount++;
            SawPartialPortalAperture |= aperture > 0.12f && aperture < 0.88f;
#endif

            Vector2 center = GetRiftWorldCenter(grip, rotation, anchor, scale,
                effects, fragments, fragmentIndex, actionSeed,
                largeDeathLayout);
            Vector2 travelDirection = -GetRiftOffset(actionSeed,
                fragmentIndex, largeDeathLayout);
            float portalScale = MathHelper.Lerp(0.54f, 0.76f,
                Hash01(actionSeed, fragmentIndex, 977))
                * (largeDeathLayout ? 3.1f : 1f);
            DeathDomainPortalRenderer.Draw(center, travelDirection, portalScale,
                aperture, actionSeed * 31 + fragmentIndex * 977);
        }
#if DEBUG
        MaxVisiblePortalCount = Math.Max(MaxVisiblePortalCount,
            LastVisiblePortalCount);
        SawStaggeredPortalBatch |= LastVisiblePortalCount > 0
            && LastVisiblePortalCount < fragments.Length;
        SawSimultaneousPortalBatch |= ReaperDefinitions
                .UsesSimultaneousAssembly(totalFrames)
            && LastVisiblePortalCount == fragments.Length;
#endif
    }

    private static float GetFragmentTravelProgress(float elapsedFrames,
        int totalFrames, int sequenceIndex, int fragmentCount)
    {
        float start = GetFragmentTravelStartFrame(totalFrames, sequenceIndex,
            fragmentCount);
        return MathHelper.Clamp((elapsedFrames - start)
            / ReaperDefinitions.AssemblyFragmentTravelFrames, 0f, 1f);
    }

    private static float GetFragmentTravelStartFrame(int totalFrames,
        int sequenceIndex, int fragmentCount)
    {
        if (ReaperDefinitions.UsesSimultaneousAssembly(totalFrames))
            return ReaperDefinitions.AssemblyPortalExpandFrames;

        float interval = GetPortalInterval(totalFrames, fragmentCount);
        // Portal N's fragment departs only when portal N+1 begins opening. The
        // last fragment waits the same interval in lieu of a successor portal.
        return (sequenceIndex + 1) * interval;
    }

    private static float GetPortalStartFrame(int totalFrames,
        int sequenceIndex, int fragmentCount)
    {
        if (ReaperDefinitions.UsesSimultaneousAssembly(totalFrames))
            return 0f;

        return sequenceIndex * GetPortalInterval(totalFrames, fragmentCount);
    }

    private static float GetPortalInterval(int totalFrames, int fragmentCount)
    {
        return Math.Max(ReaperDefinitions.AssemblyPortalExpandFrames,
            (totalFrames - ReaperDefinitions.AssemblyFragmentTravelFrames
                - ReaperDefinitions.AssemblyPortalCloseFrames)
            / (float)Math.Max(1, fragmentCount));
    }

    private static void BuildHeadToTailSequence(Fragment[] fragments,
        Vector2 anchor, SpriteEffects effects, Span<int> sequence)
    {
        for (int index = 0; index < sequence.Length; index++)
        {
            sequence[index] = index;
            float distance = GetHandleDistanceSquared(fragments[index],
                anchor, effects);
            int position = index;
            while (position > 0)
            {
                int previousIndex = sequence[position - 1];
                float previousDistance = GetHandleDistanceSquared(
                    fragments[previousIndex], anchor, effects);
                if (previousDistance > distance
                    || previousDistance == distance
                        && previousIndex < index)
                {
                    break;
                }

                sequence[position] = previousIndex;
                position--;
            }
            sequence[position] = index;
        }
    }

    private static float GetHandleDistanceSquared(Fragment fragment,
        Vector2 anchor, SpriteEffects effects)
    {
        return GetLocalCentroid(fragment.Centroid, anchor, effects)
            .LengthSquared();
    }

    private static Vector2 GetRiftWorldCenter(Vector2 grip, float rotation,
        Vector2 anchor, float scale, SpriteEffects effects,
        Fragment[] fragments, int fragmentIndex, int actionSeed,
        bool largeDeathLayout)
    {
        fragmentIndex = Math.Clamp(fragmentIndex, 0,
            Math.Max(0, fragments.Length - 1));
        Vector2 centroid = fragments[fragmentIndex].Centroid;
        Vector2 localCentroid = GetLocalCentroid(centroid, anchor, effects);
        return grip + localCentroid.RotatedBy(rotation) * scale
            + GetRiftOffset(actionSeed, fragmentIndex, largeDeathLayout);
    }

    private static Vector2 GetRiftOffset(int actionSeed, int fragmentIndex,
        bool largeDeathLayout)
    {
        const float GoldenAngle = 2.3999632f;
        float baseAngle = Hash01(actionSeed, 0, 1459) * MathHelper.TwoPi;
        float jitter = (Hash01(actionSeed, fragmentIndex, 1601) - 0.5f)
            * 0.24f;
        float angle = baseAngle + fragmentIndex * GoldenAngle + jitter;
        float radius = 132f + fragmentIndex % 4 * 47f
            + Hash01(actionSeed, fragmentIndex, 1747) * 20f;
        if (largeDeathLayout)
            radius *= 1.5f;
        Vector2 offset = angle.ToRotationVector2() * radius;
        offset.Y *= 0.72f;
        return offset;
    }

    private static Vector2 GetLocalCentroid(Vector2 centroid, Vector2 anchor,
        SpriteEffects effects)
    {
        Vector2 local = centroid - anchor;
        if ((effects & SpriteEffects.FlipHorizontally) != 0)
            local.X = -local.X;
        if ((effects & SpriteEffects.FlipVertically) != 0)
            local.Y = -local.Y;
        return local;
    }

    private static FragmentSet[] CreateFragmentSets(Texture2D source)
    {
        Color[] sourcePixels = new Color[source.Width * source.Height];
        source.GetData(sourcePixels);
        List<int> opaquePixels = [];
        for (int index = 0; index < sourcePixels.Length; index++)
        {
            if (sourcePixels[index].A >= 12)
                opaquePixels.Add(index);
        }
        if (opaquePixels.Count == 0)
            return [];

        FragmentSet[] sets = new FragmentSet[VariantCount];
        for (int variant = 0; variant < sets.Length; variant++)
            sets[variant] = CreateFragmentSet(source, sourcePixels,
                opaquePixels, variant);
        return sets;
    }

    private static FragmentSet CreateFragmentSet(Texture2D source,
        Color[] sourcePixels, List<int> opaquePixels, int variant)
    {
        Vector2[] seeds = new Vector2[FragmentCount];
        uint random = unchecked(0xA341316Cu + (uint)variant * 0x9E3779B9u);
        for (int index = 0; index < seeds.Length; index++)
        {
            random = XorShift(random);
            int opaqueIndex = opaquePixels[(int)(random
                % (uint)opaquePixels.Count)];
            seeds[index] = new Vector2(opaqueIndex % source.Width,
                opaqueIndex / source.Width);
        }

        Color[][] masks = new Color[FragmentCount][];
        Color[][] seamMasks = new Color[FragmentCount][];
        int[] owners = new int[sourcePixels.Length];
        Array.Fill(owners, -1);
        Vector2[] centroidTotals = new Vector2[FragmentCount];
        int[] counts = new int[FragmentCount];
        for (int index = 0; index < masks.Length; index++)
        {
            masks[index] = new Color[sourcePixels.Length];
            seamMasks[index] = new Color[sourcePixels.Length];
        }

        foreach (int pixelIndex in opaquePixels)
        {
            int x = pixelIndex % source.Width;
            int y = pixelIndex / source.Width;
            int closest = 0;
            float closestDistance = float.MaxValue;
            for (int seed = 0; seed < seeds.Length; seed++)
            {
                float dx = x - seeds[seed].X;
                float dy = (y - seeds[seed].Y) * 0.82f;
                float noise = Hash01(variant * 7919 + x, y,
                    seed * 3571 + 1879) * 105f;
                float weight = 0.86f + Hash01(variant, seed, 2029) * 0.28f;
                float distance = (dx * dx + dy * dy) * weight + noise;
                if (distance >= closestDistance)
                    continue;
                closestDistance = distance;
                closest = seed;
            }

            masks[closest][pixelIndex] = sourcePixels[pixelIndex];
            owners[pixelIndex] = closest;
            centroidTotals[closest] += new Vector2(x + 0.5f, y + 0.5f);
            counts[closest]++;
        }

        foreach (int pixelIndex in opaquePixels)
        {
            int owner = owners[pixelIndex];
            int x = pixelIndex % source.Width;
            int y = pixelIndex / source.Width;
            if (HasForeignOpaqueNeighbor(owners, source.Width,
                source.Height, x, y, owner))
            {
                seamMasks[owner][pixelIndex] = Color.White;
            }
        }

        List<Fragment> fragments = [];
        for (int index = 0; index < masks.Length; index++)
        {
            if (counts[index] == 0)
                continue;
            Texture2D texture = new(Main.instance.GraphicsDevice, source.Width,
                source.Height, false, SurfaceFormat.Color);
            texture.SetData(masks[index]);
            Texture2D seamTexture = new(Main.instance.GraphicsDevice,
                source.Width, source.Height, false, SurfaceFormat.Color);
            seamTexture.SetData(seamMasks[index]);
            fragments.Add(new Fragment(texture, seamTexture,
                centroidTotals[index] / counts[index]));
        }
        return new FragmentSet([.. fragments]);
    }

    private static bool HasForeignOpaqueNeighbor(int[] owners, int width,
        int height, int x, int y, int owner)
    {
        return IsForeignOwner(owners, width, height, x - 1, y, owner)
            || IsForeignOwner(owners, width, height, x + 1, y, owner)
            || IsForeignOwner(owners, width, height, x, y - 1, owner)
            || IsForeignOwner(owners, width, height, x, y + 1, owner);
    }

    private static bool IsForeignOwner(int[] owners, int width, int height,
        int x, int y, int owner)
    {
        if (x < 0 || x >= width || y < 0 || y >= height)
            return false;
        int neighbor = owners[y * width + x];
        return neighbor >= 0 && neighbor != owner;
    }

    private static uint XorShift(uint value)
    {
        value ^= value << 13;
        value ^= value >> 17;
        value ^= value << 5;
        return value == 0 ? 0x6D2B79F5u : value;
    }

    private static float Hash01(int a, int b, int c)
    {
        uint value = unchecked((uint)a * 0x9E3779B9u
            ^ (uint)b * 0x85EBCA6Bu ^ (uint)c * 0xC2B2AE35u);
        value ^= value >> 16;
        value *= 0x7FEB352Du;
        value ^= value >> 15;
        value *= 0x846CA68Bu;
        value ^= value >> 16;
        return (value & 0x00FFFFFFu) / 16777215f;
    }

    private static int PositiveModulo(int value, int modulus)
    {
        int result = value % modulus;
        return result < 0 ? result + modulus : result;
    }

    private static float SmoothStep(float value)
    {
        value = MathHelper.Clamp(value, 0f, 1f);
        return value * value * (3f - 2f * value);
    }
}