Skip to main content
GameDev.net gamedev.net
🔒 Locked

Procedural Galaxy

Started by Jim1664 Sep 8, 2011 at 2:48 PM 15 replies 5.4k views
Original Post
Jim1664
Jim1664
**Edit There is now a demo, see below for details [attachment=5417:star.zip]

The other day I decided to write a simple 3d based game, just something small and easy that I could and would actually finish. I decided to start by adding a pretty background, somehow this ended with me writing program to procedurally generate and render a galaxy of stars...

It is written in C++ using D3D10 pretty much all the work is done on the GPU, so really it's mostly in HLSL. It is split into two stages; generation and rendering, the generation stage is simply a geometry shader set to stream out a list of stars as point primitives, the rendering stages takes each point rendering as a quad sprite.
Currently I am focused on getting dynamic LoD working nicely; there are billions of stars in a galaxy, so obviously not all be drawn at the same time. To solve this; the galaxy is split into an 3d array of cubes, at each cube I take into account its distance from the camera and a density function to determine how many stars in this sector will be visible, the stars are generated in order of visibility, brightest first. The density function takes position of the cube as input and is of course what determines the shape the galaxy, changing this function would allow different shaped galaxies.
The drawing of the stars, as sprites is varied by distance so up close I can have clean points as stars and a cloudy galaxy when viewed from afar. Nothing pops in or out, every change is neatly faded, which is important and you can't really tell from screenshots.

The faint blue cloudiness in the background is simply a few octaves of Perlin noise, based on the two "Implementing Improved Perlin Noise" articles in Gpu Gems.

I think this could be useful Games of course; either to pregenerate a skybox/sphere, or rendered on the fly. The generation phase is most expensive but that only needs done when the camera moves a great distance (light years).

It is not finished, still a work in progress, a few things on the to-do list:
I intend when determining how many stars to draw, to try taking into account the density at the camera position, so less far away stars are drawn when actually in side a the galaxy.

When moving from a mid distance to a far distance the galaxy changes colour, probably as my render target becomes saturated, needs fixed.
Custom stars will be able to added at any position, rendered as normal but details specified instead generated.
Optimization of course, there's a lot to be done.

I thought I would post some screens and hopefully get some nice or useful feedback;

A far away shot;
http://i410.photobuc...amie1664/ss.jpg
A little closer;
http://i410.photobuc...mie1664/ss2.jpg
This is inside an arm looking at the core;
http://i410.photobuc...mie1664/ss3.jpg
This is inside the same arm again but looking away from the galaxy
http://i410.photobuc...mie1664/ss4.jpg
And finally this inside the core of the galaxy, the brighter strip at the bottom is the disc;
http://i410.photobuc...mie1664/ss5.jpg
SuperVGA
SuperVGA
Nice! Colourful! -Those stars make me think of maniac mansion and Hugo (Whodunnit etc.) I bet it looks awesome when animated.
-Are you planning on upscaling particles based on distance, or perhaps just in general?
Jim1664
Jim1664
Only one reply? Makes me sad, at least its a nice one. 8)

@SuperVGA
I intended for the individual stars to remain small sharp points at any range. Mostly I vary the luminosity of stars but I did use to vary the size of each star in the same way, slightly. I haven't spent much time on the look of individual stars yet, focused on how the system as a whole works. Obviously its on my to-do list.
Default_Ex
Default_Ex
Looks nice except the star colors are a bit too random looking. Check out the wiki article on Stellar Classification for some ideal color ranges and distributions of various star types.
Jim1664
Jim1664
@Default_Ex

I agree, I have not fully decided what I what the end image/style to be to be honest, guess it will depend on what this is a backdrop for.
But your definitely right the colors are off, easy to fix.

-Jim
AvengerDr
AvengerDr
Do you have any source code for this that you could share? I am interested in implementing a similar procedure so I'd appreciate any help.
If you want to improve it I think you should add some colorful nebula.. Some years ago a Gamedev user (Ysaneya) implemented a very impressive system that did just that. You should check his journal, since 2007 I think. It is REALLY good, here are some screenshots; and here on gamedev you can find the actual journal post where he explained how he did this.
Jim1664
Jim1664
@AvengerDr
I have heard of Infinity and the I-Novae engine but have not looked in on the project in awhile, thanks for the reminder.
Nebulae sounds like a good interesting idea but I know little about them so will have to some research first.
I will tidy up the shader for the stars and you can have a look, theres not much interesting C++ code but i will provide some details on the interface.
Jim1664
Jim1664

I have uploaded a simple demo, it does not do anything magical, but it demonstrates my star watsit better than screen shots, I haven't changed it much since the screenshots were taken, but the sizing of stars is now varied properly which makes a striking difference. Also distance is now a stronger factor to visibility of stars when the camera is in denser regions.

Controls are simple WASD will rotate the camera about the origin, QE zooms, all you can do is move the camera.

Geometry shaders are in use so obviously a Dx10+ card is a requirement. If performance is an issue please let me know but I very much doubt any vaguely modern card will struggle. Well actually if you have any problems please let me know.

Any constructive or just congratulate input much appreciated.

@AvengerDr

Here you go this is the .fx file used to make the stars, two pass's "Generate" and "Render" their names somewhat explain their purpose.


/*
Copyright (c) 2011 Jim Crawford
Awesome Super Sauce License:
Permission is granted to anyone to use this file for any purpose, however this in exchange for your soul, by using this in any manner you are acknowledging that your soul is now the exclusive non-returnable property of the aforementioned party.
If you have no soul, a cookie will then instead be acceptable. It should be noted that a reasonably sized cookie is required and that double chocolate cookies or ones inhabited by any form nut are quite simply unsatisfactory as of course are over or under cooked cookies. Plain chocolate chip is ideal, the chocolate can be white or dark, oatmeal-raisin cookies are also an excellent choice. Other varieties may or may not be acceptable; if in doubt please do not hesitate to contact me with enquiries regarding the suitability of a particular flavour and style of cookie.

Also note I, Jim Crawford, accept no liability for anything, ever, no warranty of any form is given.
Obviously no harm is intended but if something freaky-deaky does happen to happen I do not really give a pooper doodle.
*/

#include "Noise.fxh" //i use a 'float Perin(float3 xyz)' function from here, its not particulary needed

Texture2D StarShape; //this defines the shape of the star, for me this is 128 square, black background with very dark circle(0x050505 ish), then a much smaller, ( maybe 16 radius) rough soft bright circle
Texture1D StarColTex; //this is texture is just a list of possible colors
Texture1D RandomTex; //randomly set rgba8_SNorm, (technically not randomly set, but procedurally, if you use a different see you will get a different galaxy)

SamplerState LinSamp {
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};

SamplerState PointSamp {
Filter = MIN_MAG_MIP_POINT;
AddressU = Wrap;
AddressV = Wrap;
};

struct Sector_Input { float3 Pos : Position1; };

struct Star_Input {
float3 Pos : Position1;
float Col : Color;
float Vis : Color1;
float Radius : Color2;
float2 Padding : Normal;
};

struct PS_Input {
float4 Pos : SV_Position;
float2 UV : TexCoord1;
float3 Col : Color1;
float Clip : Color2;
};

shared cbuffer CBPerCamera {
float4x4 ViewProjection; //required for rendering
float3 CamPos; //required for generation pass
};

Sector_Input VSGenerate( Sector_Input input ) { return input; }

float3 Rando3( float seed ) { return RandomTex.SampleLevel( PointSamp, seed,0 ).xyz; } //returns [-1 <-> 1] distribution is unexamined

float Density( float3 pos ) {
//the following lump determines density, ie how many stars are in the given sector, which in turn determines shape of galaxy

// [lengths]
float sectorWidth = 2;
float coreRadius = 5.5; coreRadius *= sectorWidth;
float discRadius = 32; discRadius *= sectorWidth;
// [unitless]
float coreSkew = 2.0; //coreRadius the height of the core section, the xz of core is the height*coreSkew
float roundEdge = 0.3; //see below
float armCount = 7; //how many arms the spiral has
float armCurve = 2.1; //how curved the arms are
float armMin = 0.1; //minimum modification by armMod, ie space between arms won't be completely depeleted

float iDis = 1 - length( float2(pos.x, pos.z) / discRadius ); //xz ratio of distance to edge of disc; 1.0 is dead centre, 0.0 is the very edge [unitless]
if( iDis <= 0 ) return 0; //early exit; sector lies outside the disc

float3 coreP = pos/float3( coreRadius*coreSkew, coreRadius, coreRadius*coreSkew );
float coreDensity = 1- sqrt( dot(coreP,coreP) ); //basic; closer to origin == to higher density, coreRadius and coreSkew determine the boundary

float yMod = coreP.y * (1.2-iDis )*4; //the shaping of the disc region by this section results in a bicycle wheel, (convex - no spokes)
float discDensity = (0.5 + iDis*0.5); //that is its a cylindar but tapered so the height is highest in the centre, and curves to zero at the edge
if( iDis > roundEdge ) { //the tapering is linear between the cente and roundEdge*disRadius after which it is eliptical
discDensity *= saturate( 0.1 + (1-abs(yMod))*0.9 );
} else {
discDensity *= 1- sqrt( (roundEdge-iDis)*(roundEdge-iDis) + yMod*yMod );
}

#define PI (3.14159265358979323846)
float ang = atan2(pos.x,pos.z); ang = ang/PI*0.5 +0.5; //this bit produces the arms, just modifies density based on x/z angle
ang *= armCount; ang += armCurve*iDis; ang -= floor(ang);
ang = abs(ang - 0.5)/0.5; ang *= ang;
ang = armMin + ang*(1-armMin);
discDensity *=ang;

float density = max( coreDensity, discDensity*1.2 );
return density;
}

#define MAXSTAR 32
[maxvertexcount(MAXSTAR)]
void GSGenerate( point Sector_Input input[1], inout PointStream<Star_Input> stream ) {

Star_Input output = (Star_Input)0;
output.Padding = 0;

float3 pos = input[0].Pos;

//baseSeed used for all random values, position based .. perlin seemd like a good idea, spread not fully examined, scaleing might need changed
float baseSeed = Perlin( pos*11.34); //Perlin() is unnesscary, baseSeed must just be a value which is based on Position making each sector different, hopefully; I use Perlin because... I can

float density = Density(pos);

if( density <= 0 ) return;

float dis = length( pos-CamPos);
float disMod = clamp( dis, 3, 50 )/50.0;
disMod *= 1 + saturate(Density(CamPos))*0.5;

density = lerp( density, density/disMod, 0.6 );

//stars are generated in order of visibility, most visible stars first ergo if this star is to dim/small to be visible then the next stars will also be so
float starBrightness = 1;
for( uint i = 0; i != MAXSTAR; i++ ) { //MAXSTAR is maxium possible stars per sector

float3 rnd = Rando3( (baseSeed + 78.4)*(i+96) ).xyz;

float starCountMod = 10; //this number affects how many stars per sector, however it does not mean there will be 'starCountMod' amount of stars at any particular density but bigger number == more stars
float b = (rnd.y*0.5 + 1)*(1/starCountMod);
starBrightness -= b;
if( starBrightness < (1-density) ) break;

output.Pos = Rando3( (baseSeed + 10.247)*(i+123) )+pos;

output.Col = saturate(rnd.x);
// 0.-0.25 white | 0.25 - 0.5 blue | 0.5 - 0.75 yellow| 0.75 - 1 red //note intentionally biased towards white as Rand = neg 1 to pos 1

output.Radius = pow(rnd.z*0.5+0.5,3)*0.5*starBrightness +0.5; //0.5 -> 1.0 //curved to favor smaller stars

float pp = saturate((starBrightness-(1-density)-0.01)*0.4);
pp *= 1- saturate((dis-100)/400)*0.8;
output.Vis = pp;

stream.Append( output );
}
}


Star_Input VS( Star_Input input ) { return input; }

float Pow2( float a ) { return a*a; }

[maxvertexcount(4)]
void GSPointToSprite( point Star_Input input[1], inout TriangleStream<PS_Input> stream) {

float radiusMax = 0.06;

float4 sPos = mul( float4(input[0].Pos,1), ViewProjection );
if(sPos.z/sPos.w > 1 | sPos.z/sPos.w < 0 ) return;

sPos.xy /= sPos.w;
float2 t = abs(sPos.xy)-radiusMax;
if( (t.x > 1) | (t.y > 1) ) return;

float popup = input[0].Vis;

float radius = radiusMax*input[0].Radius; //*saturate(popup*2);
float3 col = StarColTex.SampleLevel( PointSamp, input[0].Col,0 ).rgb *popup;

float aspectCorrection = 768.f/1024.f; //this should probably not be hardcoded <img src='http://public.gamedev.net/public/style_emoticons/<#EMO_DIR#>/tongue.gif' class='bbc_emoticon' alt=':P' />

float dis = sPos.z;

float clip = saturate((15-dis)/15)*0.05;

float4 corners = float4(-1,1,float2(1,-1)*aspectCorrection )*radius;

PS_Input output[4];
output[0].Pos = float4( sPos.xy + float2(corners.x, corners.y), 1, 1 );
output[1].Pos = float4( sPos.xy + float2(corners.z, corners.y), 1, 1 );
output[2].Pos = float4( sPos.xy + float2(corners.z, corners.w), 1, 1 );
output[3].Pos = float4( sPos.xy + float2(corners.x, corners.w), 1, 1 );

output[0].UV = float2( 0, 0 ); output[1].UV = float2( 1, 0 );
output[2].UV = float2( 1, 1 ); output[3].UV = float2( 0, 1 );

output[0].Col = output[1].Col = output[2].Col = output[3].Col = col;
output[0].Clip = output[1].Clip = output[2].Clip = output[3].Clip = clip;

stream.Append(output[3]); stream.Append(output[0]);
stream.Append(output[2]); stream.Append(output[1]);
stream.RestartStrip();
}

float4 PSSprite( PS_Input input ) : SV_Target {
float3 tex = StarShape.Sample( LinSamp, input.UV ).rgb;
tex = (tex-input.Clip)/(1-input.Clip);
return float4(tex*input.Col,1.0);
}

RasterizerState RS { FillMode = Solid; CullMode = None; };

/* A note about depth
This was designed as a pretty background, I render my background(some cloudiness then stars) after the main scene with a depth test of Less_Equal
and all background stuff allways writes depth value of 1, ergo background is rendered only where nothing else is
*/

DepthStencilState DepthTestOnly {
DepthEnable = true;
DepthWriteMask = zero;
DepthFunc = less_equal;
};

BlendState BlendAdditive { //currently blending is simply additive
AlphaToCoverageEnable = FALSE;
BlendEnable[0] = TRUE;
SrcBlend = INV_DEST_COLOR;
DestBlend = ONE;
BlendOp = ADD;
SrcBlendAlpha = ZERO;
DestBlendAlpha = ZERO;
BlendOpAlpha = ADD;
RenderTargetWriteMask[0] = 0x0F;
};

technique10 Generate {
pass P0 {
SetVertexShader( CompileShader( vs_4_0, VSGenerate() ) );
SetGeometryShader( ConstructGSWithSO( CompileShader( gs_4_0, GSGenerate() ), "POSITION1.xyz; COLOR.x; COLOR1.x; COLOR2.x; NORMAL.xy" ) );
SetPixelShader( NULL );
}
}
technique10 Render {
pass P0 {
SetVertexShader( CompileShader( vs_4_0, VS() ) );
SetGeometryShader( CompileShader( gs_4_0, GSPointToSprite() ) );
SetPixelShader( CompileShader( ps_4_0, PSSprite() ) );
SetDepthStencilState( DepthTestOnly, 0 );
SetBlendState( BlendAdditive, float4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xFFFFFFFF );
SetRasterizerState( RS );
}
}



As I mentioned earlier the generation pass is fed with a list of sectors, which currently don't need any data other than positioning; as an example this the code I use to setup the vertex buffer.



const u32 dimx = 64, dimy = 12, dimz = 64;
const vec3f add = -(vec3f( float( dimx/2), float( dimy/2 ),float( dimz/2 ))-0.5f );
const u32 vCount = dimx*dimy*dimz;

//vec3f is just a simple float[3] wrapping class
vec3f* verts = new vec3f[vCount];



for( u32 z = 0; z != dimz; z++ )
for( u32 y = 0; y != dimy; y++ )
for( u32 x = 0; x != dimx; x++ ) {
verts[x+y*dimx+z*dimx*dimy] = (vec3f((f32)x,(f32)y,(f32)z)+add)*2.f;
}


All the both code is rather unoptimized, it does not need to be at this point.



Enalis
Enalis
I believe you forgot to upload the required dlls and such.
Douglas Eugene Reisinger II
Projects/Profile Site
Jim1664
Jim1664
I have uploaded new version, only change is no memory leak detection hence one less distributable to download. See top for file..
Jim1664
Jim1664
[color="#606060"]"Number of downloads: 4"
A few must have gotten the old version to work,
And yet no comments..

Also; bump.
AvengerDr
AvengerDr

[color="#606060"]"Number of downloads: 4"
A few must have gotten the old version to work,
And yet no comments..

Also; bump.



Hi Jim, thanks for the code. I'll have a look into it soon enough!
Right now I'm struggling with some code used to implement WPF-esque UI markup functionalitiy in DX11

IainC
IainC
Just for ref, it won't run for me -- missing d3dx10_43.dll.

Also, a 7z inside a zip forwhy?
[size="2"]www.coldcity.com code, art, life
HerrPuddinsky
HerrPuddinsky
Good day. I honestly can say that I am happy to find this post. I have been working on a solar system generator. Rather realistic. I am stuck on implementing graphics and a galaxy map though. I am not a programer in the strict sense, but I am learning fast. Please see www.herrpudding.tumblr.com. I am posting Vanguard edge alpha 0.8.0 tonight. But there is a working download already in place. Otherwise you can tweet me @herrpuddinsky. I think we can work well together if you are interested.
Jim1664
Jim1664
I do not have twitter I'm afraid, nor any desire to get it. You can contact me through GameDev or just email.

I will look at your site properly when I get home, I am at college just now with rather restricted internet.

You should check out Infinity if you have not, http://www.fl-tw.com
Which was mentioned by someone else above I think.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.