[All code in this post that was created by me is released under the CC0 license]
I wanted to create a realistic galaxy for my game, but trying to render all of the stars as static meshes soon proved to be too slow. So, I thought I could create the desired effect using particles, but unfortunately I could not get the particles to spawn at the right locations to create a good looking galaxy.
So I wrote my own location module for the particle editor, so instead of using a sphere or cube-like distribution, the particles are now placed to form a galaxy:
As you can see, it uses a realistic density distribution of a spiral galaxy to place the particles. The size, angle, etc. is fully customizable with parameters.
If you want to use this, you have to edit the engine source code (as far as I know). So you can not use this with a blueprint-only project.
Here is a zip-file that you can extract into your engine folder: GalaxyParticleModule.zip (16.9 KB) (maybe you have to add the ParticleModuleLocationGalaxy.h to the engine-solution in Viusual Studio).
Here is the source code for the new ParticleModuleLocationGalaxy.h:
#pragma once
#include "Particles/Location/ParticleModuleLocation.h"
#include "ParticleModuleLocationGalaxy.generated.h"
UCLASS(editinlinenew, hidecategories = Object, meta = (DisplayName = "Galaxy Location"))
class UParticleModuleLocationGalaxy : public UParticleModuleLocation
{
GENERATED_UCLASS_BODY()
/** The random seed(s) to use for looking up values in StartLocation */
UPROPERTY(EditAnywhere, Category = RandomSeed)
struct FParticleRandomSeedInfo RandomSeedInfo;
/** The radius of the galaxy. Retrieved using EmitterTime. */
UPROPERTY(EditAnywhere, Category = Galaxy)
struct FRawDistributionFloat Radius;
/** The angle used to determine the twist of the spiral arms. */
UPROPERTY(EditAnywhere, Category = Galaxy)
float DeltaAngle = 4;
UPROPERTY(EditAnywhere, Category = Galaxy)
float EllipseA = 25;
UPROPERTY(EditAnywhere, Category = Galaxy)
float EllipseB = 15;
/** The height of the galaxy disc. */
UPROPERTY(EditAnywhere, Category = Galaxy)
FRawDistributionFloat DiscHeight;
UPROPERTY(EditAnywhere, Category = Galaxy)
float FalloffFactor = 1;
/** Initializes the default values for this property */
void InitializeDefaults();
// Begin UObject Interface
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif // WITH_EDITOR
virtual void PostInitProperties() override;
// End UObject Interface
//Begin UParticleModule Interface
virtual void Spawn(FParticleEmitterInstance* Owner, int32 Offset, float SpawnTime, FBaseParticle* ParticleBase) override;
virtual uint32 RequiredBytesPerInstance(FParticleEmitterInstance* Owner = NULL) override;
virtual uint32 PrepPerInstanceBlock(FParticleEmitterInstance* Owner, void* InstData) override;
virtual FParticleRandomSeedInfo* GetRandomSeedInfo() override
{
return &RandomSeedInfo;
}
virtual void EmitterLoopingNotify(FParticleEmitterInstance* Owner) override;
//End UParticleModule Interface
void SpawnEx(FParticleEmitterInstance* Owner, int32 Offset, float SpawnTime, struct FRandomStream* InRandomStream, FBaseParticle* ParticleBase) override;
};
And here is the code that has to be added to the ParticleModules_Location.cpp (original file copyright of EPIC):
#include "Particles/Location/ParticleModuleLocationGalaxy.h"
/*-----------------------------------------------------------------------------
UParticleModuleLocationGalaxy implementation.
-----------------------------------------------------------------------------*/
UParticleModuleLocationGalaxy::UParticleModuleLocationGalaxy(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bSpawnModule = true;
bSupportsRandomSeed = true;
bRequiresLoopingNotification = true;
bUpdateModule = false;
}
void UParticleModuleLocationGalaxy::InitializeDefaults()
{
if (!Radius.Distribution)
{
UDistributionFloatUniform* DistributionRadius = NewNamedObject<UDistributionFloatUniform>(this, TEXT("DistributionRadius"));
DistributionRadius->Min = 0;
DistributionRadius->Max = 150.0f;
Radius.Distribution = DistributionRadius;
}
if (!DiscHeight.Distribution)
{
UDistributionFloatUniform* DistributionHeight = NewNamedObject<UDistributionFloatUniform>(this, TEXT("DistributionHeight"));
DistributionHeight->Min = -70;
DistributionHeight->Max = 70;
DiscHeight.Distribution = DistributionHeight;
}
}
void UParticleModuleLocationGalaxy::PostInitProperties()
{
Super::PostInitProperties();
if (!HasAnyFlags(RF_ClassDefaultObject | RF_NeedLoad))
{
InitializeDefaults();
}
}
#if WITH_EDITOR
void UParticleModuleLocationGalaxy::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
InitializeDefaults();
Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif // WITH_EDITOR
void UParticleModuleLocationGalaxy::Spawn(FParticleEmitterInstance* Owner, int32 Offset, float SpawnTime, FBaseParticle* ParticleBase)
{
FParticleRandomSeedInstancePayload* Payload = (FParticleRandomSeedInstancePayload*)(Owner->GetModuleInstanceData(this));
SpawnEx(Owner, Offset, SpawnTime, (Payload != NULL) ? &(Payload->RandomStream) : NULL, ParticleBase);
}
void UParticleModuleLocationGalaxy::SpawnEx(FParticleEmitterInstance* Owner, int32 Offset, float SpawnTime, struct FRandomStream* InRandomStream, FBaseParticle* ParticleBase)
{
SPAWN_INIT;
UParticleLODLevel* LODLevel = Owner->SpriteTemplate->GetCurrentLODLevel(Owner);
check(LODLevel);
bool hasStream = InRandomStream != NULL;
// Determine the radius, disc height and start center
float radius = Radius.GetValue(Owner->EmitterTime, Owner->Component, InRandomStream);
float discHeight = DiscHeight.GetValue(Owner->EmitterTime, Owner->Component, InRandomStream);
FVector center = StartLocation.GetValue(Owner->EmitterTime, Owner->Component, 0, InRandomStream);
// Scale the radius to reduce the density of stars near the edge of the galaxy
float factor = hasStream ? InRandomStream->GetFraction() : FMath::SRand();
radius *= (1 - (1 - factor) * FalloffFactor);
// Calculate position on an elliptical star path -> creates the spiral arms
FVector offset(0, 0, discHeight);
float discEllipseA = EllipseA * radius;
float discEllipseB = EllipseB * radius;
float angle = DeltaAngle * radius;
bool useXasRandom = hasStream ? InRandomStream->GetFraction() > 0.5 : FMath::SRand() > 0.5;
if (useXasRandom) {
offset.X = hasStream ? InRandomStream->FRandRange(-discEllipseA, discEllipseA) : FMath::FRandRange(-discEllipseA, discEllipseA);
offset.Y = FMath::Sqrt((1 - FMath::Square(offset.X) / FMath::Square(discEllipseA)) * FMath::Square(discEllipseB));
bool switched = hasStream ? InRandomStream->GetFraction() > 0.5 : FMath::SRand() > 0.5;
if (switched) {
offset.Y *= -1;
}
}
else {
offset.Y = hasStream ? InRandomStream->FRandRange(-discEllipseB, discEllipseB) : FMath::FRandRange(-discEllipseB, discEllipseB);
offset.X = FMath::Sqrt((1 - FMath::Square(offset.Y) / FMath::Square(discEllipseB)) * FMath::Square(discEllipseA));
bool switched = hasStream ? InRandomStream->GetFraction() > 0.5 : FMath::SRand() > 0.5;
if (switched) {
offset.X *= -1;
}
}
offset = offset.RotateAngleAxis(angle, FVector::UpVector);
// combine the vectors and transform the result
offset += center;
Particle.Location += Owner->EmitterToSimulation.TransformVector(offset);
}
uint32 UParticleModuleLocationGalaxy::RequiredBytesPerInstance(FParticleEmitterInstance* Owner)
{
return RandomSeedInfo.GetInstancePayloadSize();
}
uint32 UParticleModuleLocationGalaxy::PrepPerInstanceBlock(FParticleEmitterInstance* Owner, void* InstData)
{
return PrepRandomSeedInstancePayload(Owner, (FParticleRandomSeedInstancePayload*)InstData, RandomSeedInfo);
}
void UParticleModuleLocationGalaxy::EmitterLoopingNotify(FParticleEmitterInstance* Owner)
{
if (RandomSeedInfo.bResetSeedOnEmitterLooping == true)
{
FParticleRandomSeedInstancePayload* Payload = (FParticleRandomSeedInstancePayload*)(Owner->GetModuleInstanceData(this));
PrepRandomSeedInstancePayload(Owner, Payload, RandomSeedInfo);
}
}
Here is a nice looking result using only particles (rendering with a steady 120fps):
Have fun!