EmittersShapes/ConchplexCylinderEmitter.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Sandbox.VertexLayout;
namespace ConchplexEmitters;
/// <summary>
/// Emits particles within a cylinder / disk
/// </summary>
[Title( "Conchplex Cylinder Emitter" ), Category( "Particles" ), Icon( "donut_large" )]
public sealed class ConchplexCylinderEmitter : ConchplexEmitter
{
/// <summary>
/// 1 = spawns on edge, 0 = spawns in centre
/// </summary>
[Property, Group("Spawn Bias"), Order(0)]
[Range(0f, 1f), Step(0.01f)]
public float DistanceBias { get; set; } = 0.5f;
/// <summary>
/// Direction + intensity to bias particles to spawn towards
/// </summary>
[Property, Group("Spawn Bias"), Order(0)]
[Range(-1, 1)]
public Vector3 PositionBias { get; set; } = 0f;
/// <summary>
/// Particles only spawn on the axes marked
/// </summary>
[Property, Group("Spawn Bias"), Order(0)]
public AxisClampFlags AxisClamp { get; set; }
/// <summary>
/// Which axis to clamp to / in between
/// </summary>
public enum EmitFaces
{
Both,
Sides,
}
public enum ThicknessMode
{
Hollow,
hole
}
[Property]
public float OutterRadius { get; set; } = 25f;
[Property]
[Range(0, 1)]
public float Thickness { get; set; } = 0;
[Property]
public float height { get; set; } = 50f;
[Property]
public ThicknessMode ThickMode { get; set; }
[Property, Group( "Initial Velocity" )]
public float VelocityFromCylinder = 0f;
private float ThickRadius => MathX.Lerp(0, 1, Thickness);
private float MidRadius => Thickness > 0 ? (ThickRadius + 1) * 0.5f : 0f;
private float MidHeight => (1 + Thickness) * 0.5f;
protected override void DrawGizmos()
{
// TODO - THIS IS FUCKED
if (!Gizmo.IsSelected) return;
Gizmo.Draw.Color = Color.White.WithAlpha(0.5f);
Gizmo.GizmoDraw draw = Gizmo.Draw;
// draw outter ring
var top = Vector3.Zero.WithZ(height * 0.5f);
var bottom = top.WithZ(-top.z);
draw.LineCylinder(bottom, top, OutterRadius, OutterRadius, 12);
var sThick = ThickRadius * OutterRadius;
var sMid = MidRadius * OutterRadius;
var sThickHeight = MidHeight * height;
// draw thickness
if( Thickness != 0 )
{
var thickTop = ThickMode == ThicknessMode.Hollow ? top * Thickness : top;
draw.LineCylinder(thickTop.WithZ(-thickTop.z), thickTop, sThick, sThick, 12);
}
// draw distance bias
if( DistanceBias != 0 )
{
draw.Color = Color.Red.WithAlpha(0.5f);
var biasHeight = MathX.Lerp(sThickHeight * 0.5f, top.z, DistanceBias);
var thickBiasHeight = ThickMode == ThicknessMode.Hollow ? top.z * Thickness : top.z;
var fucksake = MathX.Lerp(sThickHeight * 0.5f, thickBiasHeight, DistanceBias);
var biasOutterRadius = MathX.Lerp(sMid, OutterRadius, DistanceBias);
var biasInnerRadius = MathX.Lerp(sMid, sThick, DistanceBias);
if(ThickMode == ThicknessMode.Hollow)
{
var biasHollowRadius = MathX.Lerp(sThick, OutterRadius, DistanceBias);
var biasHollowHeight = MathX.Lerp(top.z * Thickness, top.z, DistanceBias);
draw.LineCylinder(bottom.WithZ(-biasHollowHeight), top.WithZ(biasHollowHeight), biasHollowRadius, biasHollowRadius, 12);
}
else
{
draw.LineCylinder(bottom.WithZ(-biasHeight), top.WithZ(biasHeight), biasOutterRadius, biasOutterRadius, 12);
draw.LineCylinder(bottom.WithZ(-fucksake), top.WithZ(fucksake), biasInnerRadius, biasInnerRadius, 12);
}
}
// draw Position Bias
if (PositionBias.IsNearlyZero()) return;
Gizmo.Draw.Color = Color.Red.WithAlpha(0.8f);
Vector3 biasClamped = Vector3.Clamp(PositionBias, new Vector3(-1f), new Vector3(1f)) * (OutterRadius) * 1.1f;
draw.Arrow(Vector3.Zero, biasClamped, biasClamped.Length * 0.1f, biasClamped.Length * 0.1f);
}
public override Particle Emit(ParticleEffect target )
{
// Random point in a unity cylinder
Vector3 random = Random.Shared.VectorInCircle(1f);
random.z = Random.Shared.Float(-0.5f, 0.5f);
// POSITION BIAS
var positionBias = PositionBias.Clamp(-1, 1);
random = random + positionBias;
// Correct bounds
var randomXY = new Vector2(random.x, random.y);
Vector2 randomXYNormal = randomXY.Normal;
random.x = random.x.Clamp(0f, randomXYNormal.x);
random.y = random.y.Clamp(0f, randomXYNormal.y);
random.z = random.z.Clamp(0.5f,-0.5f);
randomXY = new Vector2(random.x, random.y);
// get probabilty if a particle should be shoved to sides or top
var outterTopBottom = 2 * (OutterRadius* OutterRadius);
var innerTopBottom = 2 * ((OutterRadius * Thickness) * (OutterRadius * Thickness));
outterTopBottom = positionBias.z != 0f ? outterTopBottom * 0.5f : outterTopBottom;
innerTopBottom = positionBias.z != 0f ? innerTopBottom * 0.5f : innerTopBottom;
var outterSides = (height * OutterRadius);
var innerSides = (height * (Thickness * OutterRadius));
var totalArea = outterTopBottom + outterSides + innerTopBottom + innerSides;
var totalTopBottomArea = outterTopBottom + innerTopBottom;
var sidesProbability = (outterSides + innerSides) / totalArea;
var innerChance = (innerTopBottom + innerSides) / totalArea;
// TODO
/*
- get all points pushed to cylinder outter edges
- get all points pushed to cylinder inner thickness edges
- get all points pushed into the "middle"
- new Random
- lerp between Outter edge / Inner edge, based on old random.length
- Max pos
- if pointsXY & or pointsZ are greater than the mid bounds
TRUE - use outter edge
FALSE - use innter edge
- Min pos
- use middle points
*/
// Prepare min/max bounds for hollow/hole modes
Vector3 randomHollowMin = random, randomHollowMax = random;
Vector2 minHollowRandomXY = randomXY, maxHollowRandomXY = randomXY;
Vector3 randomHoleMax = random;
Vector2 maxHoleRandomXY = randomXY;
float randVal = Random.Shared.Float();
// Only calculate min/max for the current mode
if (ThickMode == ThicknessMode.Hollow)
{
if (randVal > sidesProbability)
{
// Top/bottom face
bool isTop = random.z >= 0;
if (positionBias.z > 0f) isTop = true;
else if (positionBias.z < 0f) isTop = false;
float zPos = isTop ? 0.5f : -0.5f;
randomHollowMin *= Thickness;
randomHollowMin.z = zPos * Thickness;
randomHollowMax.z = zPos;
}
else
{
// Sides
minHollowRandomXY = minHollowRandomXY.Normal * Thickness;
randomHollowMin.x = minHollowRandomXY.x;
randomHollowMin.y = minHollowRandomXY.y;
randomHollowMin.z *= Thickness;
maxHollowRandomXY = maxHollowRandomXY.Normal;
randomHollowMax.x = maxHollowRandomXY.x;
randomHollowMax.y = maxHollowRandomXY.y;
}
}
else // Hole mode
{
if (randVal > sidesProbability)
{
bool isTop = random.z >= 0;
if (positionBias.z > 0f) isTop = true;
else if (positionBias.z < 0f) isTop = false;
float zPos = isTop ? 0.5f : -0.5f;
randomHoleMax.z = zPos;
// Remap for hole
randomHoleMax.x = MathX.Remap(randomHoleMax.x, 0f, maxHoleRandomXY.Normal.x, maxHoleRandomXY.Normal.x * Thickness, maxHoleRandomXY.Normal.x);
randomHoleMax.y = MathX.Remap(randomHoleMax.y, 0f, maxHoleRandomXY.Normal.y, maxHoleRandomXY.Normal.y * Thickness, maxHoleRandomXY.Normal.y);
}
else
{
float innerSideChance = innerSides / totalArea;
maxHoleRandomXY = Random.Shared.Float() < innerSideChance ? maxHoleRandomXY.Normal * Thickness : maxHoleRandomXY.Normal;
randomHoleMax.x = maxHoleRandomXY.x;
randomHoleMax.y = maxHoleRandomXY.y;
}
}
// Interpolate between min/max for hollow/hole
if (ThickMode == ThicknessMode.Hollow)
{
float bias = 1 - MathF.Pow(randVal, (Thickness + 1) * 10);
random = Vector3.Lerp(randomHollowMin, randomHollowMax, bias);
}
else
{
random.x = MathX.Remap(random.x, 0, randomXYNormal.x, randomXYNormal.x * Thickness, randomXYNormal.x);
random.y = MathX.Remap(random.y, 0, randomXYNormal.y, randomXYNormal.y * Thickness, randomXYNormal.y);
}
// Midpoint for distance bias
Vector3 randomMid = random;
randomMid.z *= MidHeight;
randomXY = randomXYNormal * MidRadius;
randomMid.x = randomXY.x;
randomMid.y = randomXY.y;
// Distance bias
if (DistanceBias <= 0.5f)
{
float scaledBias = DistanceBias / 0.5f;
var lowBias = ThickMode == ThicknessMode.Hollow ? randomHollowMin : randomMid;
random = Vector3.Lerp(lowBias, random, scaledBias);
}
else
{
var maxRandom = ThickMode == ThicknessMode.Hollow ? randomHollowMax : randomHoleMax;
float scaledBias = (DistanceBias - 0.5f) / 0.5f;
random = Vector3.Lerp(random, maxRandom, scaledBias);
}
// Scale to world
random.x *= OutterRadius;
random.y *= OutterRadius;
random.z *= height;
var cylinderVelocity = random.WithZ(0f).Normal;
var clampedParticlePoint = ProcessClampPosition(random, cylinderVelocity, AxisClamp);
Vector3 pos = (clampedParticlePoint.Position * LocalScale * LocalRotation) + WorldPosition;
Particle particle = target.Emit(pos, base.Delta);
particle.Velocity += (clampedParticlePoint.Velocity * LocalRotation) * VelocityFromCylinder;
return particle;
}
}