Advertisement

How were shadow textures implemented in this 3D game from the 90s?

Started by September 12, 2023 04:19 PM
4 comments, last by kromenak 1 year, 2 months ago

Hi everyone, I'm working on a project where I recreate an old game in a new engine, but still make use of the game's original art/resources ( https://github.com/kromenak/gengine​ ). One interesting thing about this process is that older 3D games often either used nonstandard techniques or techniques that are now considered outdated. This post/question is about one such example and determining what technique was used.

In this game, dynamic shadows (and sometimes decals) are implemented using “shadow textures," which are fully opaque RGB images where whiter parts of the image correspond to transparency. At runtime, fully white pixels are totally transparent while black pixels are totally opaque. Everything in between (including colors) are semi-transparent.

Here's an example of one of the shadow textures:

And here's how it's supposed to appear in-game (notice the semi-transparent darker texture beneath the character):

I've tried a few different methods to get the source asset to look this way in-game, but no luck yet.

The source asset does not have alpha - but at load time, I tried to calculate “how white” a pixel is and then set the alpha based on that.

// The more white, the more transparent this pixel should be.
float average = (r + g + b) / 3.0f;
float a = 1.0f - average;

From there, I can use alpha-blending (glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)). However, this approach has some problems: the color is off, and the outer pixels have a white halo present (makes sense because of the white pixels in the original image).

I also tried to do “pre-multiplied” alpha at load time. After calculating the alpha, I multiplied the alpha into the colors.

r *= a;
g *= a;
b *= a;

I can then use (glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)) instead. This helps to reduce the “white halo” problem, but the color is still off. The result looks better in the original game!

Part of me suspects that there is some simple known/documented technique for using such a source asset as a shadow in this way. Given that the original game achieved this back in 1997, I imagine the technique used isn't super complex or taxing, and probably just uses the fixed function pipeline to achieve this blending! I've tried using different combinations for glBlendFunc on the unaltered source texture, but so far haven't found the magic combination.

One note is that the game also uses this technique for a few color textures that act as decals (a pool of dark red blood on the ground is one example). So, beyond shadow techniques, there could also be some general decal technique being used?

So, I'm posting here to see whether anyone is aware of an algorithm or technique for implementing shadows using an opaque RGB image like the one above using older OpenGL/D3D techniques. Rather than fumbling around trying to find the answer in old articles and forum posts, I'm hoping someone here might be able to point me in the righty direction based on past experience and knowledge.

I think your shadow resolution is much too high, and I don't understand why it needs to be so irregular.
Aside from perspective distortion, your approach matches what you'd find in an early texture-mapped game with simple shadows.
Check that your texture goes from 0 to 1; you should not see any halos, and any banding goes with the approach/"late 90's look", I'd say.

Unless you're planning on doing some colourised stuff later on, you can really just stick with 1 channel. No need to get into averages or anything;

a = 1 - rTex;

rTex being the channel value from the texture. Your blendfunc seems fine, but I think a real multiplication (without involving an alpha channel) would look better and even simpler:

glBlendFunc(GL_DST_COLOR, GL_ZERO);

Check out this for reference:

Classic table of various blend modes (not sure where it originated, but I think I've seen it on NeHe and Flipcode a while ago…)

It's cool that you want to do stuff in an old-school way, but I still think a shader would provide some control, in case you need it…

Advertisement

If you take a screenshot of the same area with and without person, you should be able to at least see how the color changes by inspecting pixels. That may give clues how it was computed.

One other thing I was thinking, maybe the used HSV/GSL color space?

Looking at wikipedia: https://en.wikipedia.org/wiki/HSL_and_HSV​​ and the first sentence say “HSL (for hue, saturation, lightness) and HSV (for hue, saturation, value; also known as HSB, for hue, saturation, brightness) are alternative representations of the RGB color model, designed in the 1970s by computer graphics researchers. In these models, colors of each hue are arranged in a radial slice, around a central axis of neutral colors which ranges from black at the bottom to white at the top.”

Where emphasis is mine.

Probably uses multiplicative blending, i.e. output_pixel = ground_pixel * shadow_texture_pixel. In OpenGL, set the source factor to GL_DST_COLOR and the destination factor to GL_ZERO. Or equivalently, set the source factor to GL_ZERO and the destination factor to GL_SRC_COLOR.

Thanks for the ideas everyone!

@supervga and a-light-breeze, your note was actually RIGHT on the money - “glBlendFunc(GL_DST_COLOR, GL_ZERO)" gives the exact output I see in the original game, and I don't need to pre-process the texture to calculate alpha at all. That's amazing - I'll have to do some math and understand how that's working.

That chart you linked is also a nice reference - I'll save that.

I agree the shadow resolution might be a bit too high, but that's one of the interesting challenges here - rather than creating new assets, I try to make use of the existing assets from the original game as much as possible.

This topic is closed to new replies.

Advertisement