Imitate a textured sphere in 2D

I'm writing planet generator, using pico8. I got surface generation done:

And the last thing to do is to map the texture onto a sphere. Pico8 has no triangle function. So my question is, how do I make a sphere in my 2d space, and how do I apply a texture on it?

Looking for an effect like this (with no lighting, of course!):

I have really bad knowledge of 3d space manipulations. The full list of available API functions can be found here.

Update 4:

Solved!

Try it live here.

• Great question! As a note: you may need to fix your planet generation a little bit, too. The land on the right joins up to the land on the left, but look at the bit at the bottom! If the entire bottom of the image is the south pole, your land at the moment is forming a triangle. Take a look at Antarctica on a rectangular map and you should see what I mean. – Aric Aug 15 '17 at 9:35
• With the bottom part is fine, that part will be just really tiny on the sphere. I resize texture near the poles. Added another image with generator output. – user64142 Aug 15 '17 at 9:43
• Ironically, the polar regions of the animation shown in your question are moving more quickly than the equatorial regions. This is backwards. Either the generated terrain is stretched out to be rectangular, in which case it moves at the same speed or its not, in which case the equator is longer and needs to move more quickly to keep up. – Draco18s Aug 15 '17 at 16:58
• You may find it interesting to see how this was done in Star Control II, by scrolling masks along a pre-shaded sphere sprite. :) – DMGregory Aug 15 '17 at 17:10
• Very nice work! It looks great! If you want to make the sparkling at the edge slightly less noticeable, you can reduce the threshold on magSq just a bit below 1 to crop out the outermost ring of pixels (possibly scaling up px & py first so you still fill your target square) – DMGregory Aug 16 '17 at 11:47

Here's a rundown of a few different ways we can sample a panning texture to make it look like a globe...

Since I'm not deeply familiar with Pico-8 syntax & conventions, I'll try to present these as pseudo-code that should be reasonably straightforward to translate / token-reduce as needed.

// iterate over each pixel in the discSize x discSize bounding square
for x = left...left + discSize
for y = top...top + discSize

// convert pixel position into a vector relative to the center,
// normalized into the range -1...1
px = (x - left) * 2/discSize - 1
py = (y - top) * 2/discSize - 1

// get the squared magnitude of this vector
magSq = px * px + py * py

// if we're outside the circle, draw black/background and skip ahead
if ( magSq > 1 )
plotPixel(x, y, BLANK)
continue

// TODO: warp our local offset vector px py to imitate 3D bulge

// convert our local offsets into lookup coordinates into our map texture
u = time * rotationSpeed + (px + 1) * (mapHeight/2)
v = (py + 1) * (mapHeight/2)
// wrap the horizontal coordinate around our map when it goes off the edge
u = u % (2 * mapHeight)

// look up the corresponding colour from the map texture & plot it
color = lookupFromMap(u, v)
plotPixel(x, y, color)


If you just use this as-is, you'll get the left version: it's cropped to a circle and wraps around, but it's very flat-looking.

By tweaking px and py where it says "TODO" we can get different bending effects.

Here's an equivalent implementation of Bálint's suggestion in this form:

widthAtHeight = sqrt(1 - py * py)
px = px / widthAtHeight


If we want an exactly correct sphere as viewed by an orthographic camera, we can use inverse trig functions to convert to latitude & longitude:

widthAtHeight = sqrt(1 - py * py)
px = asin(px / widthAtHeight) * 2/3.141592653589
py = asin(py) * 2/3.141592653589


(This assumes asin's return value is in radians. If using another unit, divide by whatever value corresponds to 90 degrees, instead of by pi/2. The idea is to map asin(-1)...asin(1) back into the range -1...1)

This gives the version second from the right, which you can see is a very good match for rendering a real 3D sphere (far right). The downside is that transcendentals like the arcsine function can be expensive. This may or may not be an issue in your case.

If you're looking for something to give a more bulgy look without the trig, you can use something like a lens distortion for a cheap hack:

scale = 0.35 * magSq + (1 - 0.35)
px = px * scale
py = py * scale


At the edges of the disc, scale is 1 so there's no difference versus the flat version. Closer to the center, this shrinks the length of our local offset vector (down to a controllable minimum set by the 0.35 linear interpolation constant), which has the effect of magnifying features close to the middle.

Of course these are just a few different ways you could get a bulged look. You could try all kinds of other formulae, or even go for a perspective camera look instead of the orthographic version I've shown here.

• Man, the last three look amazing. One question: how do you do create the real sphere? I thought about generating the 3d vector the pixel corresponds to then using the good ol' divide by z trick, but I thought it would use up a huge amount from the 8k token limitation – Bálint Aug 15 '17 at 17:40
• One thing more, ~1000 is used for generating the surface. Really looking forward to the explanation :D – user64142 Aug 15 '17 at 17:41
• These aren't rendered in Pico8 — I used a 3D engine so I could quickly iterate with shader code that's more familiar to me. So the last one is actually a polygonal sphere rendered with traditional texture mapping, as a check that the latitude/longitude math was working right (it looked funny to me so I wanted to be sure). @egordorichev what's the ~1000 mean? As I mentioned above I don't know a lot about Pico8 specifics unfortunately. ^_^; – DMGregory Aug 15 '17 at 17:42
• Hmmm... my best guess off the top of my head is the asin function is returning a value in degrees instead of radians, in which case you'd want to divide by 90 instead of pi/2. I'm assuming pixel lookups are done in pixel coordinates (eg 0...128), not normalized 0....1 coordinates? – DMGregory Aug 16 '17 at 11:07
• It sounds like your trig functions are using a custom angle system, where a full circle of rotation is 1.0, so dividing by 0.25 (ie. multiplying by 4) instead of by pi/2 should get the right result, rather than needing another magic multiplier number to compensate. – DMGregory Aug 16 '17 at 11:36

First of all, this is going to cut corners, as this API is very small (I have concerns about the maximum 8192 tokens rule, which technically makes this non-turing-complete). The algorithms you need to use are the pythagorean theorem (√(x² + y²)) and the width of a circle at a specific height (2√(r² + |y - cy|²) where cy is the y coordinate of the circle).

So, you need to loop through the square that contains the circle. If the radius of the sphere is r and the center of it is (cx; cy), then you need to loop through the coordinates between (cx - r; cy - r) and (cx + r; cy + r). Let the coordinates of the current pixel be (x; y).

If √((x - cx)² + (y - cy)²) is less then or equal to the radius of the sphere, then the pixel is inside the 2d projection of the sphere.

Next we need to map this coordinate to a coordinate on the texture. Here we use the circle width formula. Get the width of the circle at the current position, let's name it segmentWidth Because we usually see around half of a sphere, we can just say that the coordinates of the pixel on the texture is

x = round(((x - cx) / segmentWidth + 0.5) * textureWidth / 2)
y = round(((y - cy) / r / 2 + 0.5) * textureHeight)

• Comments are not for extended discussion; this conversation has been moved to chat. – Josh Aug 15 '17 at 15:15