using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>
/// Clips the stationary Death Domain backdrop into an arbitrary blade-tip ribbon.
/// World-space texture coordinates keep the scenery fixed while the red rim is
/// drawn later by the owning projectile.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class DeathDomainTrailVisualSystem : ModSystem
{
private const int MaximumPoints = 72;
private static readonly Dictionary<long, TrailDrawState> drawStates = [];
private static readonly VertexPositionColorTexture[] vertices =
new VertexPositionColorTexture[MaximumPoints * 2];
private static VertexPositionColorTexture[] mergedOutlineVertices =
new VertexPositionColorTexture[4096];
private static VertexPositionColorTexture[][] mergedBackdropVertices =
[
new VertexPositionColorTexture[4096],
new VertexPositionColorTexture[4096],
new VertexPositionColorTexture[4096]
];
private static readonly short[] indices = CreateIndices();
private static BasicEffect? effect;
private readonly record struct TrailDrawState(
int Owner,
IReadOnlyList<Vector2> Points,
float Width,
float Opacity,
bool MergeOverlappingRims,
float Fracture,
bool TaperEnds,
bool NoiseFadeEnds,
int Seed,
Vector4 Bounds,
ulong UpdateTick);
internal static void Record(int owner, int identity,
IReadOnlyList<Vector2> points, float width, float opacity,
bool mergeOverlappingRims = false, float fracture = 0f,
bool taperEnds = true, bool noiseFadeEnds = false)
{
if (Main.dedServ || points.Count < 2 || width <= 0.5f
|| opacity <= 0.001f)
{
return;
}
long key = ((long)owner << 32) | (uint)identity;
if (drawStates.TryGetValue(key, out TrailDrawState existing)
&& existing.MergeOverlappingRims == mergeOverlappingRims
&& existing.TaperEnds == taperEnds
&& existing.NoiseFadeEnds == noiseFadeEnds
&& ReferenceEquals(existing.Points, points))
{
// Persistent rifts submit the same immutable path every update. Only
// opacity and freshness change, so keep their cached world bounds.
drawStates[key] = existing with
{
Opacity = opacity,
Fracture = MathHelper.Clamp(fracture, 0f, 1f),
UpdateTick = Main.GameUpdateCount
};
return;
}
drawStates[key] = new TrailDrawState(owner, points, width, opacity,
mergeOverlappingRims, MathHelper.Clamp(fracture, 0f, 1f),
taperEnds, noiseFadeEnds,
unchecked(identity * 397 ^ owner * 7919),
CalculateTrailBounds(points, width * 1.72f),
Main.GameUpdateCount);
}
public override void PostUpdateEverything()
{
if (drawStates.Count == 0)
return;
List<long>? stale = null;
foreach ((long key, TrailDrawState state) in drawStates)
{
if (Main.GameUpdateCount <= state.UpdateTick + 1)
continue;
stale ??= [];
stale.Add(key);
}
if (stale is null)
return;
foreach (long key in stale)
drawStates.Remove(key);
}
public override void OnWorldUnload() => ClearVisualState();
public override void Unload()
{
ClearVisualState();
BasicEffect? oldEffect = effect;
effect = null;
if (oldEffect is not null && !Main.dedServ)
Main.QueueMainThreadAction(oldEffect.Dispose);
}
private static void ClearVisualState()
{
drawStates.Clear();
}
public override void PostDrawTiles()
{
if (Main.gameMenu || drawStates.Count == 0
|| Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
{
return;
}
effect ??= new BasicEffect(graphicsDevice)
{
TextureEnabled = true,
VertexColorEnabled = true,
LightingEnabled = false,
FogEnabled = false
};
effect.World = Main.GameViewMatrix.TransformationMatrix;
effect.View = Matrix.Identity;
effect.Projection = Matrix.CreateOrthographicOffCenter(
0f, Main.screenWidth, Main.screenHeight, 0f, 0f, 1f);
graphicsDevice.BlendState = BlendState.AlphaBlend;
graphicsDevice.DepthStencilState = DepthStencilState.None;
graphicsDevice.RasterizerState = RasterizerState.CullNone;
graphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
DrawMergedTrails(graphicsDevice);
foreach (TrailDrawState state in drawStates.Values)
{
if (Main.GameUpdateCount > state.UpdateTick + 1)
continue;
if (state.MergeOverlappingRims)
continue;
if (!IsOnScreen(state.Bounds))
continue;
int pointCount = Math.Min(MaximumPoints, state.Points.Count);
if (pointCount < 2)
continue;
DrawSolidRibbon(graphicsDevice, state, state.Width * 1.72f,
new Color(174, 0, 44, 0) * (state.Opacity * 0.24f));
DrawSolidRibbon(graphicsDevice, state, state.Width * 1.40f,
new Color(244, 12, 66, 235) * state.Opacity);
DrawSolidRibbon(graphicsDevice, state, state.Width * 1.17f,
new Color(255, 187, 178, 235)
* (state.Opacity * 0.88f));
effect.TextureEnabled = true;
for (int layer = 0; layer < 3; layer++)
{
BuildVertices(state, state.Width,
Color.White * (state.Opacity
* DeathDomainBackdropTextureSystem
.GetLayerOpacity(layer)), layer);
effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserIndexedPrimitives(
PrimitiveType.TriangleList, vertices, 0, pointCount * 2,
indices, 0, (pointCount - 1) * 2);
}
}
}
}
private static void DrawMergedTrails(GraphicsDevice graphicsDevice)
{
if (effect is null)
return;
// Render every rim first and the opaque, world-aligned domain ribbons
// afterwards. The common backdrop naturally covers rims inside an
// overlap, producing one continuous surface without any pairwise trail
// intersection tests. Work therefore stays linear as rifts accumulate.
DrawMergedOutlineBatch(graphicsDevice, 0.70f, 0.86f,
new Color(174, 0, 44, 0), 0.24f);
DrawMergedOutlineBatch(graphicsDevice, 0.585f, 0.70f,
new Color(244, 12, 66, 235), 1f);
DrawMergedOutlineBatch(graphicsDevice, 0.50f, 0.585f,
new Color(255, 187, 178, 235), 0.88f);
int backdropVertexCount = BuildMergedBackdropBatch();
if (backdropVertexCount < 3)
return;
effect.TextureEnabled = true;
for (int layer = 0; layer < 3; layer++)
{
effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
mergedBackdropVertices[layer], 0,
backdropVertexCount / 3);
}
}
}
private static void DrawMergedOutlineBatch(GraphicsDevice graphicsDevice,
float innerWidthRatio, float outerWidthRatio, Color baseColor,
float opacityMultiplier)
{
if (effect is null)
return;
int vertexIndex = 0;
foreach (TrailDrawState state in drawStates.Values)
{
if (!state.MergeOverlappingRims
|| Main.GameUpdateCount > state.UpdateTick + 1
|| !IsOnScreen(state.Bounds))
{
continue;
}
int pointCount = Math.Min(MaximumPoints, state.Points.Count);
float innerHalfWidth = state.Width * innerWidthRatio;
float outerHalfWidth = state.Width * outerWidthRatio;
Color color = baseColor * (state.Opacity * opacityMultiplier);
for (int segment = 0; segment < pointCount - 1; segment++)
{
if (!IsSegmentVisible(state, segment, pointCount))
continue;
Vector2 start = state.Points[segment];
Vector2 end = state.Points[segment + 1];
Vector2 startNormal = GetTrailNormal(state.Points, segment,
pointCount);
Vector2 endNormal = GetTrailNormal(state.Points, segment + 1,
pointCount);
float startTaper = GetTrailTaper(state, segment
/ Math.Max(1f, pointCount - 1f));
float endTaper = GetTrailTaper(state, (segment + 1f)
/ Math.Max(1f, pointCount - 1f));
float startProgress = segment
/ Math.Max(1f, pointCount - 1f);
float endProgress = (segment + 1f)
/ Math.Max(1f, pointCount - 1f);
for (int sideIndex = 0; sideIndex < 2; sideIndex++)
{
float side = sideIndex == 0 ? -1f : 1f;
Color startColor = color * GetEndNoiseOpacity(state,
startProgress, side);
Color endColor = color * GetEndNoiseOpacity(state,
endProgress, side);
Vector2 innerStart = start
+ startNormal * side * innerHalfWidth * startTaper;
Vector2 outerStart = start
+ startNormal * side * outerHalfWidth * startTaper;
Vector2 innerEnd = end
+ endNormal * side * innerHalfWidth * endTaper;
Vector2 outerEnd = end
+ endNormal * side * outerHalfWidth * endTaper;
EnsureBatchCapacity(ref mergedOutlineVertices,
vertexIndex + 6);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
outerStart, startColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
innerStart, startColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
outerEnd, endColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
outerEnd, endColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
innerStart, startColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
innerEnd, endColor, Vector2.Zero);
}
}
}
if (vertexIndex < 3)
return;
effect.TextureEnabled = false;
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
mergedOutlineVertices, 0, vertexIndex / 3);
}
}
private static int BuildMergedBackdropBatch()
{
int vertexIndex = 0;
foreach (TrailDrawState state in drawStates.Values)
{
if (!state.MergeOverlappingRims
|| Main.GameUpdateCount > state.UpdateTick + 1
|| !IsOnScreen(state.Bounds))
{
continue;
}
int pointCount = Math.Min(MaximumPoints, state.Points.Count);
for (int segment = 0; segment < pointCount - 1; segment++)
{
if (!IsSegmentVisible(state, segment, pointCount))
continue;
Vector2 start = state.Points[segment];
Vector2 end = state.Points[segment + 1];
Vector2 startNormal = GetTrailNormal(state.Points, segment,
pointCount);
Vector2 endNormal = GetTrailNormal(state.Points, segment + 1,
pointCount);
float startHalfWidth = state.Width
* GetTrailTaper(state,
segment / Math.Max(1f, pointCount - 1f))
* 0.5f;
float endHalfWidth = state.Width
* GetTrailTaper(state, (segment + 1f)
/ Math.Max(1f, pointCount - 1f)) * 0.5f;
Vector2 leftStart = start - startNormal * startHalfWidth;
Vector2 rightStart = start + startNormal * startHalfWidth;
Vector2 leftEnd = end - endNormal * endHalfWidth;
Vector2 rightEnd = end + endNormal * endHalfWidth;
int bandCount = state.NoiseFadeEnds ? 6 : 1;
int segmentVertexCount = bandCount * 6;
for (int layer = 0; layer < 3; layer++)
EnsureBatchCapacity(ref mergedBackdropVertices[layer],
vertexIndex + segmentVertexCount);
Vector2 anchor = GetBackgroundAnchor(state);
float startProgress = segment
/ Math.Max(1f, pointCount - 1f);
float endProgress = (segment + 1f)
/ Math.Max(1f, pointCount - 1f);
for (int layer = 0; layer < 3; layer++)
{
int layerVertexIndex = vertexIndex;
Color baseColor = Color.White * (state.Opacity
* DeathDomainBackdropTextureSystem
.GetLayerOpacity(layer));
for (int band = 0; band < bandCount; band++)
{
float acrossStart = band / (float)bandCount;
float acrossEnd = (band + 1f) / bandCount;
float lateralStart = MathHelper.Lerp(-1f, 1f,
acrossStart);
float lateralEnd = MathHelper.Lerp(-1f, 1f,
acrossEnd);
Vector2 bandStartLeft = Vector2.Lerp(leftStart,
rightStart, acrossStart);
Vector2 bandStartRight = Vector2.Lerp(leftStart,
rightStart, acrossEnd);
Vector2 bandEndLeft = Vector2.Lerp(leftEnd, rightEnd,
acrossStart);
Vector2 bandEndRight = Vector2.Lerp(leftEnd, rightEnd,
acrossEnd);
Color startLeftColor = baseColor
* GetEndNoiseOpacity(state, startProgress,
lateralStart);
Color startRightColor = baseColor
* GetEndNoiseOpacity(state, startProgress,
lateralEnd);
Color endLeftColor = baseColor
* GetEndNoiseOpacity(state, endProgress,
lateralStart);
Color endRightColor = baseColor
* GetEndNoiseOpacity(state, endProgress,
lateralEnd);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandStartLeft, anchor,
startLeftColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandStartRight, anchor,
startRightColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandEndLeft, anchor,
endLeftColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandEndLeft, anchor,
endLeftColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandStartRight, anchor,
startRightColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandEndRight, anchor,
endRightColor);
}
}
vertexIndex += segmentVertexCount;
}
}
return vertexIndex;
}
private static void WriteBackdropBatchVertex(int layer, int owner,
ref int vertexIndex, Vector2 world, Vector2 anchor, Color color)
{
Vector2 uv = DeathDomainBackdropTextureSystem.GetMatchingDomainUv(
layer, world, owner, anchor);
WriteBatchVertex(mergedBackdropVertices[layer], ref vertexIndex, world,
color, uv);
}
private static void WriteBatchVertex(VertexPositionColorTexture[] target,
ref int vertexIndex, Vector2 world, Color color, Vector2 uv)
{
target[vertexIndex++] = new VertexPositionColorTexture(
new Vector3(world - Main.screenPosition, 0f), color, uv);
}
private static void EnsureBatchCapacity(
ref VertexPositionColorTexture[] target, int required)
{
if (target.Length >= required)
return;
int capacity = Math.Max(required, Math.Max(4096, target.Length * 2));
Array.Resize(ref target, capacity);
}
private static Vector2 GetTrailNormal(IReadOnlyList<Vector2> points,
int index, int pointCount)
{
Vector2 previous = points[Math.Max(0, index - 1)];
Vector2 next = points[Math.Min(pointCount - 1, index + 1)];
return (next - previous).SafeNormalize(Vector2.UnitX)
.RotatedBy(MathHelper.PiOver2);
}
private static float GetTrailTaper(TrailDrawState state, float progress)
=> state.TaperEnds
? (float)Math.Pow(Math.Max(0f,
Math.Sin(MathHelper.Clamp(progress, 0f, 1f)
* MathHelper.Pi)), 0.34f)
: 1f;
private static float GetEndNoiseOpacity(TrailDrawState state,
float progress, float lateral)
{
if (!state.NoiseFadeEnds)
return 1f;
progress = MathHelper.Clamp(progress, 0f, 1f);
const float fadeSpan = 0.28f;
float edgeDistance = Math.Min(progress, 1f - progress);
if (edgeDistance >= fadeSpan)
return 1f;
float fade = edgeDistance / fadeSpan;
int alongCell = (int)Math.Floor(progress * 53f);
int lateralCell = (int)Math.Floor((lateral + 1f) * 17f);
uint hash = unchecked((uint)(state.Seed * 747796405
+ alongCell * 2891336453 + lateralCell * 1181783497));
hash ^= hash >> 16;
hash *= 0x7FEB352Du;
hash ^= hash >> 15;
float noise = (hash & 0x00FFFFFFu) / 16777215f;
float dissolve = (fade - 0.06f) * 1.18f
+ (noise - 0.5f) * 0.4f;
return Smooth01(MathHelper.Clamp(dissolve, 0f, 1f));
}
private static bool IsSegmentVisible(TrailDrawState state, int segment,
int pointCount)
{
if (state.Fracture <= 0.001f)
return true;
uint hash = unchecked((uint)(state.Seed * 747796405
+ segment * 2891336453));
hash ^= hash >> 16;
hash *= 0x7FEB352Du;
hash ^= hash >> 15;
float random = (hash & 0x00FFFFFFu) / 16777215f;
float along = (segment + 0.5f) / Math.Max(1f, pointCount - 1f);
float ripple = 0.5f + 0.5f * (float)Math.Sin(
along * MathHelper.TwoPi * 5f + state.Seed * 0.017f);
float breakThreshold = 0.10f + random * 0.68f + ripple * 0.16f;
return Smooth01(state.Fracture) < breakThreshold;
}
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
private static Vector4 CalculateTrailBounds(
IReadOnlyList<Vector2> points, float width)
{
Vector2 minimum = points[0];
Vector2 maximum = points[0];
int count = Math.Min(MaximumPoints, points.Count);
for (int index = 1; index < count; index++)
{
minimum = Vector2.Min(minimum, points[index]);
maximum = Vector2.Max(maximum, points[index]);
}
float padding = width * 0.5f + 4f;
return new Vector4(minimum.X - padding, minimum.Y - padding,
maximum.X + padding, maximum.Y + padding);
}
private static bool IsOnScreen(Vector4 bounds)
{
const float padding = 96f;
float left = Main.screenPosition.X - padding;
float top = Main.screenPosition.Y - padding;
float right = Main.screenPosition.X + Main.screenWidth + padding;
float bottom = Main.screenPosition.Y + Main.screenHeight + padding;
return bounds.Z >= left && bounds.W >= top
&& bounds.X <= right && bounds.Y <= bottom;
}
private static Vector2 GetBackgroundAnchor(TrailDrawState state)
=> new((state.Bounds.X + state.Bounds.Z) * 0.5f,
(state.Bounds.Y + state.Bounds.W) * 0.5f);
private static void DrawSolidRibbon(GraphicsDevice graphicsDevice,
TrailDrawState state, float width, Color color)
{
if (effect is null)
return;
int pointCount = BuildVertices(state, width, color);
effect.TextureEnabled = false;
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserIndexedPrimitives(
PrimitiveType.TriangleList, vertices, 0, pointCount * 2,
indices, 0, (pointCount - 1) * 2);
}
}
private static int BuildVertices(TrailDrawState state, float width,
Color color, int backdropLayer = -1)
{
int count = Math.Min(MaximumPoints, state.Points.Count);
for (int index = 0; index < count; index++)
{
Vector2 point = state.Points[index];
Vector2 previous = state.Points[Math.Max(0, index - 1)];
Vector2 next = state.Points[Math.Min(count - 1, index + 1)];
Vector2 tangent = (next - previous).SafeNormalize(Vector2.UnitX);
Vector2 normal = tangent.RotatedBy(MathHelper.PiOver2);
float progress = index / Math.Max(1f, count - 1f);
float taper = GetTrailTaper(state, progress);
float halfWidth = width * taper * 0.5f;
for (int side = 0; side < 2; side++)
{
Vector2 world = point + normal * (side == 0 ? -halfWidth : halfWidth);
Vector2 screen = world - Main.screenPosition;
Vector2 uv = backdropLayer >= 0
? DeathDomainBackdropTextureSystem.GetMatchingDomainUv(
backdropLayer, world, state.Owner,
GetBackgroundAnchor(state))
: Vector2.Zero;
vertices[index * 2 + side] = new VertexPositionColorTexture(
new Vector3(screen, 0f), color * GetEndNoiseOpacity(state,
progress, side == 0 ? -1f : 1f), uv);
}
}
return count;
}
private static short[] CreateIndices()
{
short[] result = new short[(MaximumPoints - 1) * 6];
int cursor = 0;
for (short index = 0; index < MaximumPoints - 1; index++)
{
short leftTop = (short)(index * 2);
short leftBottom = (short)(leftTop + 1);
short rightTop = (short)(leftTop + 2);
short rightBottom = (short)(leftTop + 3);
result[cursor++] = leftTop;
result[cursor++] = leftBottom;
result[cursor++] = rightTop;
result[cursor++] = rightTop;
result[cursor++] = leftBottom;
result[cursor++] = rightBottom;
}
return result;
}
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>
/// Clips the stationary Death Domain backdrop into an arbitrary blade-tip ribbon.
/// World-space texture coordinates keep the scenery fixed while the red rim is
/// drawn later by the owning projectile.
/// </summary>
[Autoload(Side = ModSide.Client)]
internal sealed class DeathDomainTrailVisualSystem : ModSystem
{
private const int MaximumPoints = 72;
private static readonly Dictionary<long, TrailDrawState> drawStates = [];
private static readonly VertexPositionColorTexture[] vertices =
new VertexPositionColorTexture[MaximumPoints * 2];
private static VertexPositionColorTexture[] mergedOutlineVertices =
new VertexPositionColorTexture[4096];
private static VertexPositionColorTexture[][] mergedBackdropVertices =
[
new VertexPositionColorTexture[4096],
new VertexPositionColorTexture[4096],
new VertexPositionColorTexture[4096]
];
private static readonly short[] indices = CreateIndices();
private static BasicEffect? effect;
private readonly record struct TrailDrawState(
int Owner,
IReadOnlyList<Vector2> Points,
float Width,
float Opacity,
bool MergeOverlappingRims,
float Fracture,
bool TaperEnds,
bool NoiseFadeEnds,
int Seed,
Vector4 Bounds,
ulong UpdateTick);
internal static void Record(int owner, int identity,
IReadOnlyList<Vector2> points, float width, float opacity,
bool mergeOverlappingRims = false, float fracture = 0f,
bool taperEnds = true, bool noiseFadeEnds = false)
{
if (Main.dedServ || points.Count < 2 || width <= 0.5f
|| opacity <= 0.001f)
{
return;
}
long key = ((long)owner << 32) | (uint)identity;
if (drawStates.TryGetValue(key, out TrailDrawState existing)
&& existing.MergeOverlappingRims == mergeOverlappingRims
&& existing.TaperEnds == taperEnds
&& existing.NoiseFadeEnds == noiseFadeEnds
&& ReferenceEquals(existing.Points, points))
{
// Persistent rifts submit the same immutable path every update. Only
// opacity and freshness change, so keep their cached world bounds.
drawStates[key] = existing with
{
Opacity = opacity,
Fracture = MathHelper.Clamp(fracture, 0f, 1f),
UpdateTick = Main.GameUpdateCount
};
return;
}
drawStates[key] = new TrailDrawState(owner, points, width, opacity,
mergeOverlappingRims, MathHelper.Clamp(fracture, 0f, 1f),
taperEnds, noiseFadeEnds,
unchecked(identity * 397 ^ owner * 7919),
CalculateTrailBounds(points, width * 1.72f),
Main.GameUpdateCount);
}
public override void PostUpdateEverything()
{
if (drawStates.Count == 0)
return;
List<long>? stale = null;
foreach ((long key, TrailDrawState state) in drawStates)
{
if (Main.GameUpdateCount <= state.UpdateTick + 1)
continue;
stale ??= [];
stale.Add(key);
}
if (stale is null)
return;
foreach (long key in stale)
drawStates.Remove(key);
}
public override void OnWorldUnload() => ClearVisualState();
public override void Unload()
{
ClearVisualState();
BasicEffect? oldEffect = effect;
effect = null;
if (oldEffect is not null && !Main.dedServ)
Main.QueueMainThreadAction(oldEffect.Dispose);
}
private static void ClearVisualState()
{
drawStates.Clear();
}
public override void PostDrawTiles()
{
if (Main.gameMenu || drawStates.Count == 0
|| Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
{
return;
}
effect ??= new BasicEffect(graphicsDevice)
{
TextureEnabled = true,
VertexColorEnabled = true,
LightingEnabled = false,
FogEnabled = false
};
effect.World = Main.GameViewMatrix.TransformationMatrix;
effect.View = Matrix.Identity;
effect.Projection = Matrix.CreateOrthographicOffCenter(
0f, Main.screenWidth, Main.screenHeight, 0f, 0f, 1f);
graphicsDevice.BlendState = BlendState.AlphaBlend;
graphicsDevice.DepthStencilState = DepthStencilState.None;
graphicsDevice.RasterizerState = RasterizerState.CullNone;
graphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
DrawMergedTrails(graphicsDevice);
foreach (TrailDrawState state in drawStates.Values)
{
if (Main.GameUpdateCount > state.UpdateTick + 1)
continue;
if (state.MergeOverlappingRims)
continue;
if (!IsOnScreen(state.Bounds))
continue;
int pointCount = Math.Min(MaximumPoints, state.Points.Count);
if (pointCount < 2)
continue;
DrawSolidRibbon(graphicsDevice, state, state.Width * 1.72f,
new Color(174, 0, 44, 0) * (state.Opacity * 0.24f));
DrawSolidRibbon(graphicsDevice, state, state.Width * 1.40f,
new Color(244, 12, 66, 235) * state.Opacity);
DrawSolidRibbon(graphicsDevice, state, state.Width * 1.17f,
new Color(255, 187, 178, 235)
* (state.Opacity * 0.88f));
effect.TextureEnabled = true;
for (int layer = 0; layer < 3; layer++)
{
BuildVertices(state, state.Width,
Color.White * (state.Opacity
* DeathDomainBackdropTextureSystem
.GetLayerOpacity(layer)), layer);
effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserIndexedPrimitives(
PrimitiveType.TriangleList, vertices, 0, pointCount * 2,
indices, 0, (pointCount - 1) * 2);
}
}
}
}
private static void DrawMergedTrails(GraphicsDevice graphicsDevice)
{
if (effect is null)
return;
// Render every rim first and the opaque, world-aligned domain ribbons
// afterwards. The common backdrop naturally covers rims inside an
// overlap, producing one continuous surface without any pairwise trail
// intersection tests. Work therefore stays linear as rifts accumulate.
DrawMergedOutlineBatch(graphicsDevice, 0.70f, 0.86f,
new Color(174, 0, 44, 0), 0.24f);
DrawMergedOutlineBatch(graphicsDevice, 0.585f, 0.70f,
new Color(244, 12, 66, 235), 1f);
DrawMergedOutlineBatch(graphicsDevice, 0.50f, 0.585f,
new Color(255, 187, 178, 235), 0.88f);
int backdropVertexCount = BuildMergedBackdropBatch();
if (backdropVertexCount < 3)
return;
effect.TextureEnabled = true;
for (int layer = 0; layer < 3; layer++)
{
effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
mergedBackdropVertices[layer], 0,
backdropVertexCount / 3);
}
}
}
private static void DrawMergedOutlineBatch(GraphicsDevice graphicsDevice,
float innerWidthRatio, float outerWidthRatio, Color baseColor,
float opacityMultiplier)
{
if (effect is null)
return;
int vertexIndex = 0;
foreach (TrailDrawState state in drawStates.Values)
{
if (!state.MergeOverlappingRims
|| Main.GameUpdateCount > state.UpdateTick + 1
|| !IsOnScreen(state.Bounds))
{
continue;
}
int pointCount = Math.Min(MaximumPoints, state.Points.Count);
float innerHalfWidth = state.Width * innerWidthRatio;
float outerHalfWidth = state.Width * outerWidthRatio;
Color color = baseColor * (state.Opacity * opacityMultiplier);
for (int segment = 0; segment < pointCount - 1; segment++)
{
if (!IsSegmentVisible(state, segment, pointCount))
continue;
Vector2 start = state.Points[segment];
Vector2 end = state.Points[segment + 1];
Vector2 startNormal = GetTrailNormal(state.Points, segment,
pointCount);
Vector2 endNormal = GetTrailNormal(state.Points, segment + 1,
pointCount);
float startTaper = GetTrailTaper(state, segment
/ Math.Max(1f, pointCount - 1f));
float endTaper = GetTrailTaper(state, (segment + 1f)
/ Math.Max(1f, pointCount - 1f));
float startProgress = segment
/ Math.Max(1f, pointCount - 1f);
float endProgress = (segment + 1f)
/ Math.Max(1f, pointCount - 1f);
for (int sideIndex = 0; sideIndex < 2; sideIndex++)
{
float side = sideIndex == 0 ? -1f : 1f;
Color startColor = color * GetEndNoiseOpacity(state,
startProgress, side);
Color endColor = color * GetEndNoiseOpacity(state,
endProgress, side);
Vector2 innerStart = start
+ startNormal * side * innerHalfWidth * startTaper;
Vector2 outerStart = start
+ startNormal * side * outerHalfWidth * startTaper;
Vector2 innerEnd = end
+ endNormal * side * innerHalfWidth * endTaper;
Vector2 outerEnd = end
+ endNormal * side * outerHalfWidth * endTaper;
EnsureBatchCapacity(ref mergedOutlineVertices,
vertexIndex + 6);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
outerStart, startColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
innerStart, startColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
outerEnd, endColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
outerEnd, endColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
innerStart, startColor, Vector2.Zero);
WriteBatchVertex(mergedOutlineVertices, ref vertexIndex,
innerEnd, endColor, Vector2.Zero);
}
}
}
if (vertexIndex < 3)
return;
effect.TextureEnabled = false;
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
mergedOutlineVertices, 0, vertexIndex / 3);
}
}
private static int BuildMergedBackdropBatch()
{
int vertexIndex = 0;
foreach (TrailDrawState state in drawStates.Values)
{
if (!state.MergeOverlappingRims
|| Main.GameUpdateCount > state.UpdateTick + 1
|| !IsOnScreen(state.Bounds))
{
continue;
}
int pointCount = Math.Min(MaximumPoints, state.Points.Count);
for (int segment = 0; segment < pointCount - 1; segment++)
{
if (!IsSegmentVisible(state, segment, pointCount))
continue;
Vector2 start = state.Points[segment];
Vector2 end = state.Points[segment + 1];
Vector2 startNormal = GetTrailNormal(state.Points, segment,
pointCount);
Vector2 endNormal = GetTrailNormal(state.Points, segment + 1,
pointCount);
float startHalfWidth = state.Width
* GetTrailTaper(state,
segment / Math.Max(1f, pointCount - 1f))
* 0.5f;
float endHalfWidth = state.Width
* GetTrailTaper(state, (segment + 1f)
/ Math.Max(1f, pointCount - 1f)) * 0.5f;
Vector2 leftStart = start - startNormal * startHalfWidth;
Vector2 rightStart = start + startNormal * startHalfWidth;
Vector2 leftEnd = end - endNormal * endHalfWidth;
Vector2 rightEnd = end + endNormal * endHalfWidth;
int bandCount = state.NoiseFadeEnds ? 6 : 1;
int segmentVertexCount = bandCount * 6;
for (int layer = 0; layer < 3; layer++)
EnsureBatchCapacity(ref mergedBackdropVertices[layer],
vertexIndex + segmentVertexCount);
Vector2 anchor = GetBackgroundAnchor(state);
float startProgress = segment
/ Math.Max(1f, pointCount - 1f);
float endProgress = (segment + 1f)
/ Math.Max(1f, pointCount - 1f);
for (int layer = 0; layer < 3; layer++)
{
int layerVertexIndex = vertexIndex;
Color baseColor = Color.White * (state.Opacity
* DeathDomainBackdropTextureSystem
.GetLayerOpacity(layer));
for (int band = 0; band < bandCount; band++)
{
float acrossStart = band / (float)bandCount;
float acrossEnd = (band + 1f) / bandCount;
float lateralStart = MathHelper.Lerp(-1f, 1f,
acrossStart);
float lateralEnd = MathHelper.Lerp(-1f, 1f,
acrossEnd);
Vector2 bandStartLeft = Vector2.Lerp(leftStart,
rightStart, acrossStart);
Vector2 bandStartRight = Vector2.Lerp(leftStart,
rightStart, acrossEnd);
Vector2 bandEndLeft = Vector2.Lerp(leftEnd, rightEnd,
acrossStart);
Vector2 bandEndRight = Vector2.Lerp(leftEnd, rightEnd,
acrossEnd);
Color startLeftColor = baseColor
* GetEndNoiseOpacity(state, startProgress,
lateralStart);
Color startRightColor = baseColor
* GetEndNoiseOpacity(state, startProgress,
lateralEnd);
Color endLeftColor = baseColor
* GetEndNoiseOpacity(state, endProgress,
lateralStart);
Color endRightColor = baseColor
* GetEndNoiseOpacity(state, endProgress,
lateralEnd);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandStartLeft, anchor,
startLeftColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandStartRight, anchor,
startRightColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandEndLeft, anchor,
endLeftColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandEndLeft, anchor,
endLeftColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandStartRight, anchor,
startRightColor);
WriteBackdropBatchVertex(layer, state.Owner,
ref layerVertexIndex, bandEndRight, anchor,
endRightColor);
}
}
vertexIndex += segmentVertexCount;
}
}
return vertexIndex;
}
private static void WriteBackdropBatchVertex(int layer, int owner,
ref int vertexIndex, Vector2 world, Vector2 anchor, Color color)
{
Vector2 uv = DeathDomainBackdropTextureSystem.GetMatchingDomainUv(
layer, world, owner, anchor);
WriteBatchVertex(mergedBackdropVertices[layer], ref vertexIndex, world,
color, uv);
}
private static void WriteBatchVertex(VertexPositionColorTexture[] target,
ref int vertexIndex, Vector2 world, Color color, Vector2 uv)
{
target[vertexIndex++] = new VertexPositionColorTexture(
new Vector3(world - Main.screenPosition, 0f), color, uv);
}
private static void EnsureBatchCapacity(
ref VertexPositionColorTexture[] target, int required)
{
if (target.Length >= required)
return;
int capacity = Math.Max(required, Math.Max(4096, target.Length * 2));
Array.Resize(ref target, capacity);
}
private static Vector2 GetTrailNormal(IReadOnlyList<Vector2> points,
int index, int pointCount)
{
Vector2 previous = points[Math.Max(0, index - 1)];
Vector2 next = points[Math.Min(pointCount - 1, index + 1)];
return (next - previous).SafeNormalize(Vector2.UnitX)
.RotatedBy(MathHelper.PiOver2);
}
private static float GetTrailTaper(TrailDrawState state, float progress)
=> state.TaperEnds
? (float)Math.Pow(Math.Max(0f,
Math.Sin(MathHelper.Clamp(progress, 0f, 1f)
* MathHelper.Pi)), 0.34f)
: 1f;
private static float GetEndNoiseOpacity(TrailDrawState state,
float progress, float lateral)
{
if (!state.NoiseFadeEnds)
return 1f;
progress = MathHelper.Clamp(progress, 0f, 1f);
const float fadeSpan = 0.28f;
float edgeDistance = Math.Min(progress, 1f - progress);
if (edgeDistance >= fadeSpan)
return 1f;
float fade = edgeDistance / fadeSpan;
int alongCell = (int)Math.Floor(progress * 53f);
int lateralCell = (int)Math.Floor((lateral + 1f) * 17f);
uint hash = unchecked((uint)(state.Seed * 747796405
+ alongCell * 2891336453 + lateralCell * 1181783497));
hash ^= hash >> 16;
hash *= 0x7FEB352Du;
hash ^= hash >> 15;
float noise = (hash & 0x00FFFFFFu) / 16777215f;
float dissolve = (fade - 0.06f) * 1.18f
+ (noise - 0.5f) * 0.4f;
return Smooth01(MathHelper.Clamp(dissolve, 0f, 1f));
}
private static bool IsSegmentVisible(TrailDrawState state, int segment,
int pointCount)
{
if (state.Fracture <= 0.001f)
return true;
uint hash = unchecked((uint)(state.Seed * 747796405
+ segment * 2891336453));
hash ^= hash >> 16;
hash *= 0x7FEB352Du;
hash ^= hash >> 15;
float random = (hash & 0x00FFFFFFu) / 16777215f;
float along = (segment + 0.5f) / Math.Max(1f, pointCount - 1f);
float ripple = 0.5f + 0.5f * (float)Math.Sin(
along * MathHelper.TwoPi * 5f + state.Seed * 0.017f);
float breakThreshold = 0.10f + random * 0.68f + ripple * 0.16f;
return Smooth01(state.Fracture) < breakThreshold;
}
private static float Smooth01(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
private static Vector4 CalculateTrailBounds(
IReadOnlyList<Vector2> points, float width)
{
Vector2 minimum = points[0];
Vector2 maximum = points[0];
int count = Math.Min(MaximumPoints, points.Count);
for (int index = 1; index < count; index++)
{
minimum = Vector2.Min(minimum, points[index]);
maximum = Vector2.Max(maximum, points[index]);
}
float padding = width * 0.5f + 4f;
return new Vector4(minimum.X - padding, minimum.Y - padding,
maximum.X + padding, maximum.Y + padding);
}
private static bool IsOnScreen(Vector4 bounds)
{
const float padding = 96f;
float left = Main.screenPosition.X - padding;
float top = Main.screenPosition.Y - padding;
float right = Main.screenPosition.X + Main.screenWidth + padding;
float bottom = Main.screenPosition.Y + Main.screenHeight + padding;
return bounds.Z >= left && bounds.W >= top
&& bounds.X <= right && bounds.Y <= bottom;
}
private static Vector2 GetBackgroundAnchor(TrailDrawState state)
=> new((state.Bounds.X + state.Bounds.Z) * 0.5f,
(state.Bounds.Y + state.Bounds.W) * 0.5f);
private static void DrawSolidRibbon(GraphicsDevice graphicsDevice,
TrailDrawState state, float width, Color color)
{
if (effect is null)
return;
int pointCount = BuildVertices(state, width, color);
effect.TextureEnabled = false;
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawUserIndexedPrimitives(
PrimitiveType.TriangleList, vertices, 0, pointCount * 2,
indices, 0, (pointCount - 1) * 2);
}
}
private static int BuildVertices(TrailDrawState state, float width,
Color color, int backdropLayer = -1)
{
int count = Math.Min(MaximumPoints, state.Points.Count);
for (int index = 0; index < count; index++)
{
Vector2 point = state.Points[index];
Vector2 previous = state.Points[Math.Max(0, index - 1)];
Vector2 next = state.Points[Math.Min(count - 1, index + 1)];
Vector2 tangent = (next - previous).SafeNormalize(Vector2.UnitX);
Vector2 normal = tangent.RotatedBy(MathHelper.PiOver2);
float progress = index / Math.Max(1f, count - 1f);
float taper = GetTrailTaper(state, progress);
float halfWidth = width * taper * 0.5f;
for (int side = 0; side < 2; side++)
{
Vector2 world = point + normal * (side == 0 ? -halfWidth : halfWidth);
Vector2 screen = world - Main.screenPosition;
Vector2 uv = backdropLayer >= 0
? DeathDomainBackdropTextureSystem.GetMatchingDomainUv(
backdropLayer, world, state.Owner,
GetBackgroundAnchor(state))
: Vector2.Zero;
vertices[index * 2 + side] = new VertexPositionColorTexture(
new Vector3(screen, 0f), color * GetEndNoiseOpacity(state,
progress, side == 0 ? -1f : 1f), uv);
}
}
return count;
}
private static short[] CreateIndices()
{
short[] result = new short[(MaximumPoints - 1) * 6];
int cursor = 0;
for (short index = 0; index < MaximumPoints - 1; index++)
{
short leftTop = (short)(index * 2);
short leftBottom = (short)(leftTop + 1);
short rightTop = (short)(leftTop + 2);
short rightBottom = (short)(leftTop + 3);
result[cursor++] = leftTop;
result[cursor++] = leftBottom;
result[cursor++] = rightTop;
result[cursor++] = rightTop;
result[cursor++] = leftBottom;
result[cursor++] = rightBottom;
}
return result;
}
}