Procedural planets

Something I did today, might be useful to some. Interested in feedback or improvements.

It’s ugly, I know - once its more final, I’m gonna wrap it up in a class.

Goal is to procedurally generate, from a single seed value, a “nice looking” planet. If you don’t think this is nice - you shoulda seen it before! If you think you can do better - do! And share code. Please. Don’t make me beg. It’s not pretty.

Here’s a screenshot… https://twitter.com/#!/bortels/status/197206189606584320/photo/1

This takes about 4 seconds to run on my new ipad. If you’re impatient, or bored, reduce “radius” to 50 and it’ll run in a second or less. In the long run, I want to break out the loop so it can run a bit at a time, in the “background”, while Codea does other stuff. This seems like a prime place for a coroutine, if I understood how they work. :slight_smile:


-- Procedural planets
-- Tom Bortels - bortels@gmail.com
--
-- todo:
-- clouds
-- more color ranges (tint?)
-- gas giants (banding)
-- ice caps

function setup()
    radius = 150
    ambient = 0.05
    i = image(radius*2+5, radius*2+5)
    sphere(i, radius, 2, ambient)
end

function sphere(img, radius, k, ambient)
    
    --math.randomseed(2)
    
    local topolevel = {-11000,-5500,-3000,-2000,-750,-70,-20,0,
                         0,250,500,1200,1700,2800,4000,6000 }
    
    local topocolor = {
    {36,38,175}, {56,58,195}, {70,72,214}, {81,102,217},
    {100,129,223}, {131,161,230}, {164,192,240}, {170,200,255},
    {0,97,71}, {16,122,47}, {232,215,125}, {161,67,0},
    {130,30,30}, {110,110,110}, {255,255,255}, {255,255,255} }
    
    local col = {}
    
    for n=0,255 do
        nn = ((n/255)*10000) - 5000
        col[n] = {}
        for d=1,15 do
            low=topolevel[d]
            hi = topolevel[d+1]        
            if (low < nn) and (hi >= nn) then
                local r1,g1,b1 = unpack(topocolor[d])
                local r2,g2,b2 = unpack(topocolor[d+1])
                local range = hi - low
                local fract = (nn-low)/range
                col[n].r = ((r2-r1)*fract)+r1
                col[n].g = ((g2-g1)*fract)+g1
                col[n].b = ((b2-b1)*fract)+b1
            end        
        end
    end
    
    local function normalize (vec)
        len = math.sqrt(vec[1]^2 + vec[2]^2 + vec[3]^2)
        return {vec[1]/len, vec[2]/len, vec[3]/len}
    end
 
    local light = normalize{30, -30, -50}
 
    local function dot (vec1, vec2)
        d = vec1[1]*vec2[1] + vec1[2]*vec2[2] + vec1[3]*vec2[3]
        return d < 0 and -d or 0
    end
    
    local function m255(a, b)
        local r = a * b
        if (r>255) then r=255 end
        return r
    end
    
    ofs = radius + math.random(50)
    ofss = radius + math.random(50)
    nf = radius / math.random(1,3) -- continents
   -- nff = radius / 7.7 -- coastlines
    nff = radius / math.random(6,9)
    trange = math.random()/2+0.5
    toffset = (1-trange)*math.random()
    --print(trange .. " " .. toffset)
    local i,x,j,vec,b
    for i = math.floor(-radius),-math.floor(-radius) do
        x = i + 0.5
        for j = math.floor(-2*radius),-math.floor(-2*radius) do
            y = j / 2 + 0.5
            if x^2 + y^2 <= radius^2 then
                z = math.sqrt(radius^2 - x^2 - y^2)
                c = (noise(ofs+x/nf, ofs+y/nf, ofs+z/nf) + 1) * 120
                c = c + noise(ofss+x/nff, ofss+y/nff, ofss+z/nff) * 16
                c = c * trange + (toffset * 255)
                c = math.floor(c)
                local rr=col[c].r
                local gg=col[c].g
                local bb=col[c].b
                vec = normalize{x, y, z}
                b = dot(light,vec) ^ k + ambient
                local ba = b+ambient
                img:set(x+img.width/2, y+img.height/2,
                m255(rr, ba), m255(gg, ba),
                m255(bb, ba), 255)
            end
        end
    end
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(19, 19, 21, 255)

    -- This sets the line thickness
    strokeWidth(5)

    -- Do your drawing here
    sprite(i, WIDTH/2, HEIGHT/2)
    
end

Nifty. Can’t wait to storm and occupy… I mean explore, these worlds.

looks really nice! thanks for sharing

@Bortels really like the way this turned out. I like the first tweet you posted (sans-height map). It had a really soft look, and the colours were very nice. Great atmosphere.

I think in the end, what I’ll have is a selector at the beginning that figures out what type of planet you’re making - terrestrial, gas giant, airless, etc - then chooses parameters according to that type.

Looking at atmosphere now - reference image: http://eoimages.gsfc.nasa.gov/images/imagerecords/57000/57723/globe_east_540.jpg

Nice! Here’s an idea for the game:
I started an app a while ago in BASIC that was an adventure through space in a ship. It was pretty sweet. I also started a function where you could choose your own ship Inge and place the weapons in the correct places. I discontinued it because I started it when I was 9, and I was pretty clueless about good programming techniques. Anyways, that wouldn’t be a bad idea when using http.get to get a ship image. Just an idea.

Can’t wait till I get home so I can try this out.
On a side note - have you tried making local copies of the various math functions - accessing locals rather than globals is supposed to be a LOT faster.

just add lines like

local mSqrt = math.sqrt
local mFloor = math.floor

```


at the top of your code - just a thought...

EDIT : Also I noticed that there is a line (not just the only one)

local r1,g1,b1 = unpack(topocolor[d])
```


Inside a nested loop - creating locals here is very wasteful of memory and add's unnecessary overhead in terms of time taken to allocate some locals and extra GC processing later on.

You could just move the local to the top of the function and then just reference r1,g1, and b1 in the same way in the inner loop, again this might make a difference in time to generate and the size of the app footprint.

Another $0.02 :)

Thanks for the tips - I tried them, and it seems faster, maybe, a bit? The real issue here is that the time is dominated by the fact that I have it iterate thru each pixel. The right way to do this is GLSL, but… well, maybe someday. :slight_smile:

Just got round to having a quick play with this (does look nice BTW) and I had a thought.
Rather than trying to map (wrap) the noise function around a sphere have you considered creating a rectangular texture map and then wrapping that around a sphere instead? That way you’ll be able to spin the planet in 3d and I suspect it’ll also speed up the generation process quite quickly

Must my $0.02 :slight_smile:

I :X It

The problem with wrapping the sphere with a rectangular map (and yes, I’d thought of it - that’s what I was trying to do originally) is that there is no good way to map a flat surface to a sphere - you get seams/artifacts, usually at the poles. You can distort the image before you map it to minimize the artifacts, but they’re still pretty visible (at least to me).

Instead of mapping a 2d image to a 3d-sphere, I cut out the middle-man and simply use 3-d noise - the surface of the sphere is simply the 3-d noise value where the sphere intersects the noise; simple, and no artifacting (and likely faster than creating 2d noise then trying to wrap it).

The noise isn’t what makes this slow - it’s the complete lack of taking advantage of any high level operations or the GPU. This image is calculated and drawn pixel-by-pixel - very, very old school. I could, in theory, make a spherical mesh, and set the color values to the 3d noise, and have something I could rotate - I might even do that, it sounds like fun. But it wouldn’t be any faster to generate, and my goal wasn’t to rotate things (mmm, although I can see uses for that) - it’s been to make pretty, but static, planet images for a game I’m making… (We’d really need lighting in the GPU to do it right, or GLSL - right now, to “light” a mesh, you have to manually adjust the color values)

I see your point, I’ve seen a few examples using cubemaps that limit the distortion but the wrapping code is still a little “hairy” :slight_smile:

Hmm - I wonder how fast it would be to render actual raytraced images (must dig out my collection of old graphics text books) :slight_smile:

@bortels mack a game such as wings O:) for iPod or iPhone

But it is a flying sandbox but i :X It

Well, boo - when I terrain-map with enough triangles that the terrain looks ok, iterating through my triangles to light them per-frame is too time-consuming.