Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 19 additions & 97 deletions OpenDreamClient/Rendering/ClientDreamParticlesSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ public sealed class ClientDreamParticlesSystem : SharedDreamParticlesSystem {
[Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly IDreamInterfaceManager _dreamInterfaceManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IRobustRandom _random = default!;

private RenderTargetPool _renderTargetPool = default!;
private readonly Random _random = new();

//used for icon GetTexture(), never needs anything but default settings
private readonly RendererMetaData _defaultRenderMetaData = new();
Expand Down Expand Up @@ -56,25 +56,31 @@ private ParticleSystemArgs GetParticleSystemArgs(DreamParticlesComponent compone
textureFunc = () => _random.Pick(icons).GetTexture(null!, null!, _defaultRenderMetaData, null, null);
}

var result = new ParticleSystemArgs(textureFunc, new Vector2i(component.Width, component.Height), (uint)component.Count, component.Spawning) {
Lifespan = GetGeneratorFloat(component.LifespanLow, component.LifespanHigh, component.LifespanDist),
Fadein = GetGeneratorFloat(component.FadeInLow, component.FadeInHigh, component.FadeInDist),
Fadeout = GetGeneratorFloat(component.FadeOutLow, component.FadeOutHigh, component.FadeOutDist),
var perTick = (1f / 10f); // "Tick" refers to a BYOND standard tick of 0.1s. --DM Reference
var result = new ParticleSystemArgs(textureFunc, new Vector2i(component.Width, component.Height), (uint)component.Count, component.Spawning / perTick) {
Lifespan = () => (component.Lifespan?.Generate(_random) ?? 1f) * perTick,
Fadein = () => (component.FadeIn?.Generate(_random) ?? 0f) * perTick,
Fadeout = () => (component.FadeOut?.Generate(_random) ?? 0f) * perTick,
Color = component.Gradient.Length > 0
? lifetime => {
var colorIndex = (int)(lifetime * component.Gradient.Length);
colorIndex = Math.Clamp(colorIndex, 0, component.Gradient.Length - 1);
return component.Gradient[colorIndex];
}
: _ => Color.White,
Acceleration = (_ , velocity) => GetGeneratorVector3(component.AccelerationLow, component.AccelerationHigh, component.AccelerationType, component.AccelerationDist)() + GetGeneratorVector3(component.DriftLow, component.DriftHigh, component.DriftType, component.DriftDist)() - velocity*GetGeneratorVector3(component.FrictionLow, component.FrictionHigh, component.FrictionType, component.FrictionDist)(),
SpawnPosition = GetGeneratorVector3(component.SpawnPositionLow, component.SpawnPositionHigh, component.SpawnPositionType, component.SpawnPositionDist),
SpawnVelocity = GetGeneratorVector3(component.SpawnVelocityLow, component.SpawnVelocityHigh, component.SpawnVelocityType, component.SpawnVelocityDist),
Transform = _ => {
var scale = GetGeneratorVector2(component.ScaleLow, component.ScaleHigh, component.ScaleType, component.ScaleDist)();
var rotation = GetGeneratorFloat(component.RotationLow, component.RotationHigh, component.RotationDist)();
var growth = GetGeneratorVector2(component.GrowthLow, component.GrowthHigh, component.GrowthType, component.GrowthDist)();
var spin = GetGeneratorFloat(component.SpinLow, component.SpinHigh, component.SpinDist)();
Acceleration = (_, velocity) => { // TODO: Acceleration needs to only update every tick
var drift = (component.Drift?.GenerateVector3(_random) ?? Vector3.Zero);
var friction = (component.Friction?.GenerateVector3(_random) ?? Vector3.Zero); // TODO: Only calculated once per particle

return drift - (velocity * friction);
},
SpawnPosition = () => component.SpawnPosition?.GenerateVector3(_random) ?? Vector3.Zero,
SpawnVelocity = () => component.SpawnVelocity?.GenerateVector3(_random) ?? Vector3.Zero,
Transform = _ => { // TODO: Needs to only be performed every tick
var scale = component.Scale.GenerateVector2(_random);
var rotation = component.Rotation?.Generate(_random) ?? 0f;
var growth = component.Growth?.GenerateVector2(_random) ?? Vector2.Zero;
var spin = component.Spin?.Generate(_random) ?? 0f;
return Matrix3x2.CreateScale(scale.X + growth.X, scale.Y + growth.Y) *
Matrix3x2.CreateRotation(rotation + spin);
},
Expand All @@ -83,88 +89,4 @@ private ParticleSystemArgs GetParticleSystemArgs(DreamParticlesComponent compone

return result;
}

private Func<float> GetGeneratorFloat(float low, float high, GeneratorDistribution distribution) {
switch (distribution) {
case GeneratorDistribution.Constant:
return () => high;
case GeneratorDistribution.Uniform:
return () => _random.NextFloat(low, high);
case GeneratorDistribution.Normal:
return () => (float)Math.Clamp(_random.NextGaussian((low + high) / 2, (high - low) / 6), low, high);
case GeneratorDistribution.Linear:
return () => MathF.Sqrt(_random.NextFloat(0, 1)) * (high - low) + low;
case GeneratorDistribution.Square:
return () => MathF.Cbrt(_random.NextFloat(0, 1)) * (high - low) + low;
default:
throw new NotImplementedException();
}
}

private Func<Vector2> GetGeneratorVector2(Vector2 low, Vector2 high, GeneratorOutputType type, GeneratorDistribution distribution){
switch (type) {
case GeneratorOutputType.Num:
return () => new Vector2(GetGeneratorFloat(low.X, high.X, distribution)(), GetGeneratorFloat(low.Y, high.Y, distribution)());
case GeneratorOutputType.Vector:
return () => Vector2.Lerp(low, high, GetGeneratorFloat(0, 1, distribution)());
case GeneratorOutputType.Box:
return () => new Vector2(GetGeneratorFloat(low.X, high.X, distribution)(), GetGeneratorFloat(low.Y, high.Y, distribution)());
case GeneratorOutputType.Circle:
var theta = _random.NextFloat(0, 360);
//polar -> cartesian, radius between low and high, angle uniform sample
return () => new Vector2(MathF.Cos(theta) * GetGeneratorFloat(low.X, high.X, distribution)(), MathF.Sin(theta) * GetGeneratorFloat(low.Y, high.Y, distribution)());
case GeneratorOutputType.Square:
return () => {
var x = GetGeneratorFloat(-high.X, high.X, distribution)();
var y = GetGeneratorFloat(-high.Y, high.Y, distribution)();
if (MathF.Abs(x) < low.X)
y = _random.NextByte() > 128
? GetGeneratorFloat(-high.Y, -low.Y, distribution)()
: GetGeneratorFloat(low.Y, high.Y, distribution)();
return new(x, y);
};
default:
throw new NotImplementedException($"Unimplemented generator output type {type}");
}
}

private Func<Vector3> GetGeneratorVector3(Vector3 low, Vector3 high, GeneratorOutputType type, GeneratorDistribution distribution){
switch (type) {
case GeneratorOutputType.Num:
return () => new Vector3(GetGeneratorFloat(low.X, high.X, distribution)(), GetGeneratorFloat(low.Y, high.Y, distribution)(), GetGeneratorFloat(low.Z, high.Z, distribution)());
case GeneratorOutputType.Vector:
return () => Vector3.Lerp(low, high, GetGeneratorFloat(0, 1, distribution)());
case GeneratorOutputType.Box:
return () => new Vector3(GetGeneratorFloat(low.X, high.X, distribution)(), GetGeneratorFloat(low.Y, high.Y, distribution)(), GetGeneratorFloat(low.Z, high.Z, distribution)());
case GeneratorOutputType.Sphere:
var theta = _random.NextFloat(0, 360);
var phi = _random.NextFloat(0, 180);
//3d polar -> cartesian, radius between low and high, angle uniform sample
return () => new Vector3(
MathF.Cos(theta) * MathF.Sin(phi) * GetGeneratorFloat(low.X, high.X, distribution)(),
MathF.Sin(theta) * MathF.Sin(phi) * GetGeneratorFloat(low.Y, high.Y, distribution)(),
MathF.Cos(phi) * GetGeneratorFloat(low.Z, high.Z, distribution)()
);
case GeneratorOutputType.Cube:
return () => {
var x = GetGeneratorFloat(-high.X, high.X, distribution)();
var y = GetGeneratorFloat(-high.Y, high.Y, distribution)();
var z = GetGeneratorFloat(-high.Z, high.Z, distribution)();
if (MathF.Abs(x) < low.X)
y = _random.NextByte() > 128
? GetGeneratorFloat(-high.Y, -low.Y, distribution)()
: GetGeneratorFloat(low.Y, high.Y, distribution)();
if (MathF.Abs(y) < low.Y)
z = _random.NextByte() > 128
? GetGeneratorFloat(-high.Z, -low.Z, distribution)()
: GetGeneratorFloat(low.Z, high.Z, distribution)();
return new(x, y, z);
};
case GeneratorOutputType.Circle:
case GeneratorOutputType.Square:
return () => new Vector3(GetGeneratorVector2(new(low.X, low.Y), new(high.X, high.Y), type, distribution)(), 0);
default:
throw new NotImplementedException($"Unimplemented generator output type {type}");
}
}
}
2 changes: 1 addition & 1 deletion OpenDreamClient/Rendering/Particles/ParticlesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public void FrameUpdate(FrameEventArgs args) {
_particles[i].Lifetime += args.DeltaSeconds;
_particles[i].Transform = _baseTransform * _transform(_particles[i].Lifetime);
_particles[i].Color = _color(_particles[i].Lifetime);
_particles[i].Velocity += _acceleration(_particles[i].Lifetime, _particles[i].Velocity) * args.DeltaSeconds;
_particles[i].Velocity += _acceleration(_particles[i].Lifetime, _particles[i].Velocity);
_particles[i].Position += _particles[i].Velocity*args.DeltaSeconds;
if(_particles[i].Fadein > _particles[i].Lifetime)
_particles[i].Color.A = Math.Clamp(_particles[i].Lifetime/_particles[i].Fadein, 0, 1);
Expand Down
4 changes: 2 additions & 2 deletions OpenDreamRuntime/Objects/DreamObjectTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ public DreamObject CreateObject(TreeEntry type) {
return new DreamObjectArea(type.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Atom))
return new DreamObjectAtom(type.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Generator))
throw new Exception("Cannot create objects of type /generator without the generator() proc");
if (type.ObjectDefinition.IsSubtypeOf(Particles))
return new DreamObjectParticles(type.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Client))
Expand All @@ -194,6 +192,8 @@ public DreamObject CreateObject(TreeEntry type) {
return new DreamObjectCallee(type.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Vector))
return new DreamObjectVector(type.ObjectDefinition);
if (type.ObjectDefinition.IsSubtypeOf(Generator))
return new DreamObjectGenerator(type.ObjectDefinition);

return new DreamObject(type.ObjectDefinition);
}
Expand Down
32 changes: 23 additions & 9 deletions OpenDreamRuntime/Objects/Types/DreamObjectAtom.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using OpenDreamRuntime.Procs;

namespace OpenDreamRuntime.Objects.Types;
namespace OpenDreamRuntime.Objects.Types;

[Virtual]
public class DreamObjectAtom : DreamObject {
Expand Down Expand Up @@ -149,12 +147,28 @@ protected override void SetVar(string varName, DreamValue value) {
case "filters": {
Filters.Cut();

if (value.TryGetValueAsDreamList(out var valueList)) { // filters = list("type"=...)
var filterObject = DreamObjectFilter.TryCreateFilter(ObjectTree, valueList);
if (filterObject == null) // list() with invalid "type" is ignored
break;

Filters.AddValue(new(filterObject));
// filters = list("type"=...) or list(filter(...), filter(...))
if (value.TryGetValueAsDreamList(out var valueList)) {
if (valueList.GetValue(new("type")) != DreamValue.Null) { // It's a single filter
var filterObject = DreamObjectFilter.TryCreateFilter(ObjectTree, valueList);
if (filterObject == null) // list() with invalid "type" is ignored
break;

Filters.AddValue(new(filterObject));
} else { // It's a list of filters
foreach (var filter in valueList.EnumerateValues()) {
if (!filter.TryGetValueAsDreamObject<DreamObjectFilter>(out var filterObject)) {
if (!filter.TryGetValueAsDreamList(out var filterValues))
continue;

filterObject = DreamObjectFilter.TryCreateFilter(ObjectTree, filterValues);
if (filterObject == null)
continue;
}

Filters.AddValue(new(filterObject));
}
}
} else if (!value.IsNull) {
Filters.AddValue(value);
}
Expand Down
85 changes: 79 additions & 6 deletions OpenDreamRuntime/Objects/Types/DreamObjectGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,84 @@
using OpenDreamShared.Rendering;
using OpenDreamRuntime.Procs;
using OpenDreamShared.Dream;

namespace OpenDreamRuntime.Objects.Types;

public sealed class DreamObjectGenerator(DreamObjectDefinition objectDefinition, DreamValue a, DreamValue b, GeneratorOutputType outputType, GeneratorDistribution distribution) : DreamObject(objectDefinition) {
public DreamValue A { get; private set; } = a;
public DreamValue B { get; private set; } = b;
public GeneratorOutputType OutputType { get; private set; } = outputType;
public GeneratorDistribution Distribution { get; private set; } = distribution;
public sealed class DreamObjectGenerator(DreamObjectDefinition objectDefinition) : DreamObject(objectDefinition) {
public IGenerator Generator { get; private set; } = default!;

public override void Initialize(DreamProcArguments args) {
var type = args.GetArgument(0);
if (!type.TryGetValueAsString(out var typeStr))
throw new Exception($"Invalid generator type {type}");

var a = args.GetArgument(1);
var b = args.GetArgument(2);
var distribution = DistributionNumberToEnum((int)args.GetArgument(3).UnsafeGetValueAsFloat());

switch (typeStr) {
case "num":
case "circle":
case "sphere": {
var low = a.UnsafeGetValueAsFloat();
var high = (b.Type == DreamValue.DreamValueType.Float) ? b.UnsafeGetValueAsFloat() : 1f;

Generator = typeStr switch {
"num" => new GeneratorNum(low, high, distribution),
"circle" => new GeneratorCircle(low, high, distribution),
"sphere" => new GeneratorSphere(low, high, distribution),
_ => throw new ArgumentOutOfRangeException()
};

break;
}
case "vector":
case "box": {
var low = DreamObjectVector.CreateFromValue(a, ObjectTree);
var high = DreamObjectVector.CreateFromValue(b, ObjectTree);

if (low.Is3D || high.Is3D)
Generator = typeStr == "vector"
? new GeneratorVector3(low.AsVector3, high.AsVector3, distribution)
: new GeneratorBox3(low.AsVector3, high.AsVector3, distribution);
else
Generator = typeStr == "vector"
? new GeneratorVector2(low.AsVector2, high.AsVector2, distribution)
: new GeneratorBox2(low.AsVector2, high.AsVector2, distribution);
break;
}
case "square":
case "cube": {
var low = DreamObjectVector.CreateFromValue(a, ObjectTree);
var high = DreamObjectVector.CreateFromValue(b, ObjectTree);

Generator = typeStr switch {
"square" => new GeneratorSquare(low.AsVector2, high.AsVector2, distribution),
"cube" => new GeneratorCube(low.AsVector3, high.AsVector3, distribution),
_ => throw new ArgumentOutOfRangeException()
};

break;
}
default:
throw new Exception($"Invalid generator type {type}");
}
}

public T RequireType<T>() where T : IGenerator {
if (Generator is not T casted)
throw new Exception($"Expected generator type {typeof(T)} but got {Generator.GetType()}");

return casted;
}

private GeneratorDistribution DistributionNumberToEnum(int number) {
return number switch {
0 => GeneratorDistribution.Uniform,
1 => GeneratorDistribution.Normal,
2 => GeneratorDistribution.Linear,
3 => GeneratorDistribution.Square,
_ => GeneratorDistribution.Uniform // Default to UNIFORM_RAND
};
}
}

26 changes: 21 additions & 5 deletions OpenDreamRuntime/Objects/Types/DreamObjectImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,28 @@ protected override void SetVar(string varName, DreamValue value) {

_filters.Cut();

// filters = list("type"=...) or list(filter(...), filter(...))
if (valueList != null) { // filters = list("type"=...)
var filterObject = DreamObjectFilter.TryCreateFilter(ObjectTree, valueList);
if (filterObject == null) // list() with invalid "type" is ignored
break;

_filters.AddValue(new(filterObject));
if (valueList.GetValue(new("type")) != DreamValue.Null) { // It's a single filter
var filterObject = DreamObjectFilter.TryCreateFilter(ObjectTree, valueList);
if (filterObject == null) // list() with invalid "type" is ignored
break;

_filters.AddValue(new(filterObject));
} else { // It's a list of filters
foreach (var filter in valueList.EnumerateValues()) {
if (!filter.TryGetValueAsDreamObject<DreamObjectFilter>(out var filterObject)) {
if (!filter.TryGetValueAsDreamList(out var filterValues))
continue;

filterObject = DreamObjectFilter.TryCreateFilter(ObjectTree, filterValues);
if (filterObject == null)
continue;
}

_filters.AddValue(new(filterObject));
}
}
} else if (!value.IsNull) {
_filters.AddValue(value);
}
Expand Down
Loading
Loading