QBXL Technical Articles

Raycasting Light Mapping system using Blender-maps in 256 colours

By SJ Zero

Raycasting is a technique with a huge stigma on it because it's a major factor in realtime 3d rendering. You're in luck today, because I'm not covering it in that context. 

For those out of the loop, Raycasting is the technique of sending out "feelers" to search for objects in a circle. It's often used for fast 3d because it takes care of the hard parts. Lightmapping is the technique of using a bitmap to bind

Today we will be implementing a simple raycaster, along with some other tricks. I'll explain along the way how to implement a simple lightmap system with real-time shadows, which will be created by “shooting rays” in all directions from a point, just like actual light. For simplicity, we'll only be dealing with a square blocking object and we'll ignore the fancier aspects of light behaviour like photons bouncing off of surfaces. We'll even simplify light interactions because of the way we'll be doing the lightmapping.

 The first thing is to set up a renderer in screen mode 13h. VGA Mode 13h is 320x240, with a 255 colour palette, which is stored as RGB values on the video card. Since this is 13h, we can write a fairly fast renderer particularly easily. We'll tell it to write random garbage to the screen since all we want to do is see the general effects of our lightmapper.

 DEFINT A-Z

DEF SEG = &HA000

SCREEN 13

 WHILE INKEY$ <> " "

    PaintScreen

WEND

 

 

SUB PaintScreen ()

    DIM pointer AS LONG

        'memory location = x * 320 + y

    FOR a = 1 TO 320

     FOR b = 1 TO 200

      pointer = pointer + 1

      POKE pointer, INT(RND(1) * 255)

     NEXT b

    NEXT a

END SUB

 

Now that we have a working renderer, we can set up the light mapping portion of our porgram. To do this, we'll set up a blender map for different light levels, a bitmap to store light levels on the screen, and finally the raycaster itself.

 

Blender maps are basically arrays which hold “alternate” palette configurations which point to colours in the current palette. In our case, we'll hold 3 maps; one for each light level, from darkest(0) to lightest(2). To get the blender maps made, we'll have to read from the palette so we can get the Red, Green, and Blue(RGB) values of each colour. To retrieve the values, we can use the following commands:

 

OUT &H3C7, 0

FOR a = 0 TO 255

    palet(a, 0) = INP(&H3C9)    ' red value

    palet(a, 1) = INP(&H3C9)    ' green value

    palet(a, 2) = INP(&H3C9)    ' blue value

NEXT a

Since there are three maps, we'll take the RGB values at 3/3, 2/3, and 1/3 intensity for our light levels, and search for the closest colour in our current palette to the RGB values.

To do that, we take the RGB value for a colour, alter them to be at 3/3, 2/3, or 1/3 intensity, and compare the RGB values to those of every colour in the current palette. We take the square of the difference and add them together. The colour with the lowest number has the closest colour. We square them because we prefer colours which are slightly off on all 3 values to colours which are far off on one, but fine on the others.

SUB makeBlenderMaps ()

    DIM pal(0 TO 255, 1 TO 3)

    DIM high AS LONG

    OUT &H3C7, 0

    FOR a = 0 TO 255

        pal(a, 1) = INP(&H3C9)   ' red value

        pal(a, 2) = INP(&H3C9)   ' green value

        pal(a, 3) = INP(&H3C9)   ' blue value

       ' BlendMap(a, 2) = a

    NEXT a

 

    FOR map = 0 TO 2

        FOR a = 0 TO 255

           r = pal(a, 1) * ((map + 1) / 3!) + 0!

           g = pal(a, 2) * ((map + 1) / 3!) + 0!

           b = pal(a, 3) * ((map + 1) / 3!) + 0!

           max = 30000

         

           FOR c = 0 TO 255

               rd = (pal(c, 1) - r) ^ 2

               gd = (pal(c, 2) - g) ^ 2

               bd = (pal(c, 3) - b) ^ 2

               fd = rd + gd + bd

               IF fd < max THEN max = fd: highest = c

           NEXT c

 

           BlendMap(a, map) = highest

        NEXT a

    NEXT map

 

END SUB

 

Next we set up the bitmap we'll be using to store the lightmap. This part is suprisingly easy, with one exception: Since this is done in real-mode DOS, memory is at a premium. Instead of a 320x200 bitmap, we're going to chop it in half widthwise, enduing up with a 160x200 map. The result is that we have to stretch it lengthwise when we use it. Since this is a lightmap used for an effect, not an actual lightmap, this won't be a problem aesthetically. In order to integrate this into our renderer, it's as easy as chaning our POKE pointer, INT(RND(1) * 255) command from earlier to a POKE pointer, BlendMap(INT(RND(1) * 255), LightMap(x/2,y)). 

Finally, we get to the raycaster. Its basic operation relies on two simple, nearly universal equations to start with:

X= X0 + Rcos(Theta)

Y = Y0 + Rsin(Theta)

All we do is set an X0 and Y0 to the point we want our light source, then we make a FOR statement to move in a straight line at angle Theta within the radius of the circle we're drawing, R. We stop when we hit either a wall or when we reach the final radius of the light source, however we decide that. Except for the fact that X and Y are calculated in a funny way, this is no different from scanning an array and stopping when a particular value is found. After we've checked one line, we cast lines in every other direction, usually casting one ray for every possible end pixel. This means that we're going to cast 2 * Pi * r rays, or the circumfrence of the circle.This works out well since we can use a STEP statement in our FOR which will check every point, in radians, in the 360 degree arc around the light source.

SUB putlight (xx%, yy%, r)

 

 topIntensity = (r / ((60 + r) / 60) ^ 2)

 

FOR a! = 0 TO 2 * 3.141579 STEP 1 / (r)

                yyy% = yy% \ 3

 FOR b% = 2 TO r

 x% = 0! + b * COS(a!) + xx%

 y% = (0! + b * SIN(a!) \ 3) + yyy

 

 NEXT b

 NEXT a!

 END SUB.

 Ok, so now we have code to send a ray out in all directions. To make our shadows work, we want to check if we've hit an object. First we'll define an object we'll use to block. To define this, we'll use two sets of co-ordinates to block off an area, which we'll call x1, x2, y1, and y2. Remember that this is just something we've arbitrarily chosen to be the thing that stops the ray. They can be blocked on any condition you choose, and in this case we're choosing a reigon we don't want rays to pass through. All we do is test to see if our ray is between (x1,y1) and (x2, y2). If it is, we stop the ray an dmove on to the next ray. If not, we'll set the lightmap level.

  

Recall that we created three blender maps. Because there are only three light levels, we can't realistically have light fade at intensity/distance^2 without fairly unrealistic looking lighting. Instead we'll just have full intensity light at ˝ of the radius of the circle, and 2/3rds integrity for the other half. Unlit areas will be at 1/3 intensity.

 

SUB putlight (xx%, yy%, r)

 

topintensity = 2

 

FOR a! = 0 TO 2 * 3.141579 STEP 1 / (r)

                yyy% = yy% \ 3

 FOR b% = 2 TO r

 

 x% = 0! + b * COS(a!) + xx%

 y% = (0! + b * SIN(a!) \ 3) + yyy

 

 'IF ((y% > blockerx) AND (y% < (blockerx + tilesize))) AND ((x% > blockery) AND (x% < (blockery + tilesize))) THEN

' IF x% = blockerx AND (y% * 3) = blockery THEN b = r: BEEP: EXIT FOR

 

  IF (y% >= (blockery \ 3)) AND (y% <= ((blockery + tilesize) \ 3)) AND (x% >= blockerx) AND (x% <= (blockerx + tilesize)) THEN b = r: EXIT FOR

 

 intensity = 2 * (r / b)

 IF intensity = 0 THEN b = r: EXIT FOR

 IF intensity > 3 THEN intensity = 3

 IF intensity > lightmap(x%, y%) THEN lightmap(x%, y%) = intensity

 NEXT b

 

NEXT a!

 

END SUB

The lightmapper is now complete! While simple(and you have to set up controls for the blocker), it's a more accurate lighting system than the vast majority of systems in 2d, and the concepts in use can be applied to many other applications, including 3d.

 

Congratulations, you have yourself a lightmapper.

 

SJ Zero

 

--SJ Zero once killed a man using a technique very similar to this. And a gun.