A particle emitter component that spawns particles inside a cylinder or disk shape. It computes spawn positions with configurable outer radius, thickness modes (Hollow or hole), height, distance and position bias, applies axis clamping and optionally gives initial velocity from the cylinder surface, and draws editor gizmos.
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 * WorldRotation) + WorldPosition;
Particle particle = target.Emit(pos, base.Delta);
particle.Velocity += (clampedParticlePoint.Velocity * WorldRotation) * VelocityFromCylinder;
return particle;
}
}