Free Particle Editor Module for Spiral Galaxies

[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! :slight_smile:

This looks great! can’t wait to give it a try. :smiley: Thanks!!

I will be happy to see your results :slight_smile:

For anyone willing to use this: you will most certainly want to use it with GPU particles, as you want to simulate as many stars as possible. However, a GPU emitter can NOT create particles with different starting colors, so once you found a nice pattern, you have to duplicate the emitter to create a new one with different particle colors.

If you wonder how I created the nebula: I took the volumetric clouds from the content examples folder and made them blue.

Also, I was really surprised how easy it was to add a new particle editor module, great work there EPIC! :slight_smile:

I am new to UE and I´ve downloaded the 4.9 branch added the galaxy solution but, as I am still learning the particle system, could you make a tutorial with the full creation of the galaxy?

Create your own galaxy tutorial

OK, here is a quick tutorial for everyone who wants to create a kickass galaxy for their game :wink:

  1. Import the code posted above into your engine source folder
    Unzip the file into the UE4 github project folder or place the code there manually.

  2. Compile your UE4 project
    If you encounter any compile-time problems, make sure you added the ParticleModuleLocationGalaxy.h file to your Visual Studio solution:

  1. Create a new particle system

  1. Right-click on the emitter and set TypeData to “GPU Sprites”
    We want to have a lot of stars in our galaxy and this is only possible with GPU sprites.

  1. Create a particle material
    I just used the “radial gradient” material from the starter content. It is a really simple material but gets the job done. Make sure “Responsive AA” is enabled, otherise the stars smear when the galaxy is moving on the screen.

GPU sprites.png

  1. Set up the basic particle modules
    Here are the settings for the basic emitter modules:

  1. Add the galaxy location module
    Right now the particle editor shows just a blob of particles, which does not look very interesting. Now we add the galaxy location module and set it up. Play around with the values (especially the delta angle and the ellipse values) until you are satisfied with the shape of the galaxy. You do NOT want to change these values later, because we duplicate emitters and then you have to change these values on each emitter separately.

  1. Copy the emitter to add stars with different color/size/location
    To add some variation to your galaxy you have to duplicate the emitter.

For example, let’s add some big blue stars, but only to the fringes of our galaxy, not to the core.
This time, we spawn only 500 particles with a size of 50 (twice as big as the other stars) and a shiny blue color. The trick to get them to only spawn at a certain distance is to modify the “start radius” min value. You also have to set the falloff-factor to 0, otherwise they are partially pulled back into the core of the galaxy. Just play around with the value to see what looks good.

  1. Add nebulas and other stuff to the galaxy
    I used the cloud-particles from the content examples folder to create a nebula, the process to add them is them same as above. You can also add other stuff now, e.g. lights or some blinking stars, stars with cross-shapes, etc.

  2. PROFIT!

As an additional tip, the galaxy looks best in-game when slightly rotating, because then the player can really see the spatial positions of the individual stars as they move relative to each other.

Thank you very much. Very appreciated.

Here is a quick video update from the final result:

I think it really shows how important a little bit of movement is for the human perception to see spatial details.

Just found this thread, and I got to say… that is an amazing addition :slight_smile:
Wouldn’t it be great if this was added to the main source code!

Def. Bookmarking this if I ever need a solar system effect :slight_smile:

This is great, awesome work!

Thanks, glad you like it :slight_smile:

I mean I could try to open a pull request to EPIC, but on the other hand the use case is pretty specific, so I don’t know if they would want this as a general addition into their particle editor modules.

I also doubt that with Niagara particle editor on the very long horizon… they’d add new content to cascade.
But you could always try right? :slight_smile:

Maybe you could try looking into making it a plugin. Or is it not possible to add new particle modules from plugins?

plugins do just that, plug in something.
so it should be possible :slight_smile:

This is pretty **** brilliant!

Im curious, was this all C++? Or were there blueprints involved?

And if it wasnt in BP, do you have any advice or suggestions for someone who is endeavoring to go that route in producing such results?

How expensive is this on performance? Pretty curious about that. Thanks!

Overdraw with transculent materials are really expensive (redrawing the actual star and cloud texture information on screen).

The method that **@ ** have presented runs only once (calculates the particle positions when the particle system gets created) therefore it have no expense later. Shader Complexity of the projected content can always be inspected to see what are the real implications of alot of overdrawn pixels.

This c++ implementation is still a little less expensive than creating similar results purely with materials and not using this engine modification at all. The key difference here is that the galaxymap informations can be recalculated always at runtime, or you can simply generate them only once at the begining of the particle system’s life. But it have very little expense actually to recalculate them live. GPU’s are rather good at calculating stuff. :slight_smile:

A particle material is capable to modify the world position offset of each individual particle, which can be calculated at runtime:

Implemented the same calculus from the c++ code purely into materials using built in nodes and it works just fine

After a little tweaking and adding a some effects to the material the result looks even better

The real expenses here the transculent material, and more importantly the blended-textured areas, they generate the real expenses (eg the smoke)

Even, if your material looks this large, the calculus can have very little footprint on the actual peformance

so I’ve been trying to get this to work for a while now and i just cant seem to get the galaxy location module to show up under the location dropdown. Visual studio is complaining about not being able to open the “ParticleModuleLocationGalaxy.generated.h” file. Probably because it doesn’t exist and i don’t know how to make it, I’ve tried re-generating the project files and everything i could think of. “ParticleModuleLocationGalaxy.h” is in the correct folder, I’ve tried both modifying the existing “ParticleModules_Location.cpp” and replacing it. But nothing works. Would appreciate it if someone could tell me how to fix it and actually make it show up under the location dropdown.

Could you explain a bit how your shader works, especially what the function your are using in your screenshot does ?
Thank you in advance.

В новой версии движка 4.18.3 не работает. Не видно в меню выбора. Перепробовал куча вариантов, про бывал с готовыми проектами C++ и Blueprint. Может кто сказать в чём подвох. Выше указанные инструкция, не пашет.

Thanks for the great example. It looks like the relevant classes, such as UParticleModuleLocation, have now been exposed to the ENGINE_API, so modules like this can now be implemented as C++ plugins for a project without having to modify and re-compile the engine source code.