Log In  


Cart #normaldemo1_1a-0 | 2025-02-09 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
3

I've been trying out Pico-8 for a couple weeks now, mostly to prototype a couple game ideas, but got the idea in my head to try and emulate 2d sprite normal mapping for dynamic lighting. 24 hours later, this is the initial result.

About Normal Maps

Normal maps are traditionally used in 3D games to give texture to otherwise flat polygon faces, but more modern 2D and 2.5D games also make use of normal maps, usually for lighting effects. A normal map itself is just a image, usually laid out to match the base color (sometimes called albedo) map of the model/sprite, but for the normal map, the RGB values of each pixel correspond to the XYZ facing of that pixel, relative to the face of the polygon/sprite.

Assuming each value ranges from 0 to 1, a red value of 0 would be facing all the way to the left, 1.0 would be right, and 0.5 would be neutral. The same goes for green with up and down. Blue is the Z value, coming out perpendicular to the surface, so that is usually around 1, and never below 0.5, which is why normal maps are usually a blueish color overall. When lighting that pixel, you calculate the angle of the light against the "angle" of the pixel as defined by the RGB/XYZ values, and you lighten/darken it depending on how directly that pixel is facing the light.

Source

Translating to Pico-8

For PICO-8, obviously we don't have RGB values; we have 16 base colors and no meaningful access to their RGB values. But this is also PICO-8 so we're embracing limitations. Similar to the right two figures in the image above, if you're dealing with a few faces of an object, you really only need about 8 colors to correspond to combinations of up and down. I opted to try and match the traditional normal map color scheme, but you could just as easily have gone with colors 1-8 (note, I am using color 0 to represent no normal influence on a sprite).

Now, much like a normal map, I can create a secondary, normalmap sprite where color set for each pixel will tell us which way that pixel is "facing" when a light source is present.

Drawing with Normals

To draw a sprite with normals, I have a function called ndraw() that just takes the standard color/albedo sprite's ID in the sprite sheet, and the x and y coordinates to draw at onscreen (I do offset this by -4 on X and Y so the sprite is centered on the coordinate). First thing that happens is we draw the sprite as normal. Next, we find the angle of the light source relative to the sprite using atan2(), and then get the normal color values that are highlighted or that are in shadow based on that angle. All of these values are kept in lookup tables; each angle has 3 highlighted faces and 3 shadow faces.

Then, we iterate through the pixels of the normalmap sprite (assumed to be the sprite directly below the base color sprite. If the pixel isn't set to 0, which is either transparent or not affected by normals, then we check if the face of a given pixel is in the highlighted set or in the shadowed set. If so, we get the highlighted or shadowed color relative to the base color, again stored in lookup tables (e.g., if a pixel with color 3 (green) is highlighted, we look up lighter[3] and get back 11 (light green)). Repeat for all pixels in the sprite.

Limitations and Improvements

This represents a few hours work, and I already am already aware of certain limitations and opportunities to improve on this technique:

  • Only works with one light source
  • Doesn't work with the map
  • Might be better to skip drawing the initial sprite and just draw each pixel to avoid double-draws
  • Could increase the normal maps to 12 colors (more if using the second palette?)
  • Faces don't take other objects into account that should obscure light hitting them
  • The functions that take the atan value to search the lookup table could probably be trimmed down by normalizing the values instead of using if-else statements to check ranges

If nothing else, this was fun to experiment with in PICO-8. I don't have any plans to use this in a particular game at this time, but if you decide to use it, let me know; I'd love to check it out! Or feel free to let me know this is a silly use of 374 tokens

3


This is an interesting!

If the target image is circular, you can achieve the same lighting by rotating the image (using tline(), for example).
If the image is not circular, this palette switching method may be an advantage.

It may also be useful for the bitplane method.


1

> It may also be useful for the bitplane method.

I hadn’t heard of this! Would be interesting to see if they work together. In theory you could still use all colors for the normal maps in this case, since they’re informational.



[Please log in to post a comment]