PS2 Linux Programming

One Simple Directional Light & Ambient Light

Introduction

This tutorial demonstrates the application of lighting to a 3D model using a single white directional light. The surface of the model is considered to be an ideal diffuse reflector. Background to the lighting process is presented.

Directional Lights

Directional lights have only colour and direction, not position. They emit parallel light. This means that all light generated by directional lights travels through a scene in the same direction. A directional can be considered as a light source at near infinite distance, such as the sun. Directional lights are not affected by attenuation or range, so the direction and colour specified are the only factors considered when calculating vertex colours. Because of the small number of illumination factors, these are the least computationally intensive lights to use.

Ambient Light

The ambient light component is one that affects all surfaces equally, regardless of their position or orientation. There is really no such thing as an ambient light; it is just a “fudge factor” to approximate the complex reflections between objects in a virtual environment. Ambient light can also be considered as a background light, that affects everything equally.

Ambient light can be specified as a single constant value of diffuse white light, or as a triple of constants in the red, green, and blue spectra.

Consider a point on the surface of an object having colour (Ro, Go, Bo). If a single white light constant for ambient light is used, Aw, then the light emitted from the object due to ambient light is:

Ambient Illumination = (Ro*Aw, Go*Aw, Bo*Aw)

If the ambient light is specified as a triplet of constants, (Ra, Ga, Ba), then the light emitted from the object due to ambient light, (R, G, B) is:

Ideal Diffuse Reflection

An ideal diffuse surface is, at the microscopic level, a very rough surface. Chalk is a good approximation to an ideal diffuse surface. Because of the microscopic variations in the surface, an incoming ray of light is equally likely to be reflected in any direction over the a hemisphere as shown in the figures below.

Ideal diffuse reflectors reflect light according to Lambert's cosine law, (they are sometimes called Lambertian reflectors). Lambert's law states that the reflected energy from a small surface area in a particular direction is proportional to cosine of the angle between that direction and the surface normal. Lambert's law determines how much of the incoming light energy is reflected. Remember that the amount of energy that is reflected in any one direction is constant in this model. In other words the reflected intensity is independent of the viewing direction. The intensity does however depend on the orientation of the light source relative to the surface, and it is this property that is governed by Lambert's law.

The angle between the surface normal and the incoming light ray is called the angle of incidence and the reflected diffuse light intensity can be expressed in terms of this angle.

The Ilight term represents the intensity of the incoming light at a particular colour. The kd term represents the diffuse reflectivity of the surface which is a value between zero and one.  A value of zero means that all of the light is absorbed and converted to heat; a value of one is an ideal reflector.

When computing this equation we can make use  of vector maths to compute the cosine term without directly determining theta. If both the normal vector and the incoming light vector are normalised (unit length) then the diffuse shading component is given by:

where n is the surface normal and l is the normalised light direction vector. In this equation only angles from 0 to 90 degrees need to be considered. Greater angles are blocked by the surface, and the reflected energy is zero.

Program Overview

In order to compute the colour and illumination level of a vertex it is necessary to know the vertex normal. The information associated with a vertex is therefore modified to contain the vertex normal rather than the vertex colour. The definition of one of the faces of the cube is repeated below to illustrate this point:

// Front face

VIFStaticDMA.AddVector(Vector4(0, 0, 1, 0));              // TexCoord (STQ)

VIFStaticDMA.AddVector(Vector4( 0, 0, 1, 0));             // Normal

VIFStaticDMA.AddVector(Vector4(-1.0f, 1.0f, 1.0f, 1.0f)); // Vert (xyzw)

VIFStaticDMA.AddVector(Vector4(1, 0, 1, 0));              // TexCoord (STQ)

VIFStaticDMA.AddVector(Vector4( 0, 0, 1, 0));             // Normal

VIFStaticDMA.AddVector(Vector4(1.0f,  1.0f, 1.0f, 1.0f)); // Vert (xyzw)

VIFStaticDMA.AddVector(Vector4(0, 1, 1, 0));              // TexCoord (STQ)

VIFStaticDMA.AddVector(Vector4( 0, 0, 1, 0));             // Normal

VIFStaticDMA.AddVector(Vector4(-1.0f,-1.0f, 1.0f, 1.0f)); // Vert (xyzw)

VIFStaticDMA.AddVector(Vector4(0, 1, 1, 0));              // TexCoord (STQ)

VIFStaticDMA.AddVector(Vector4( 0, 0, 1, 0));             // Normal

VIFStaticDMA.AddVector(Vector4(-1.0f,-1.0f, 1.0f, 1.0f)); // Vert (xyzw)

VIFStaticDMA.AddVector(Vector4(1, 0, 1, 0));              // TexCoord (STQ)

VIFStaticDMA.AddVector(Vector4( 0, 0, 1, 0));             // Normal

VIFStaticDMA.AddVector(Vector4(1.0f,  1.0f, 1.0f, 1.0f)); // Vert (xyzw)

VIFStaticDMA.AddVector(Vector4(1 , 1, 1, 0));             // TexCoord (STQ)

VIFStaticDMA.AddVector(Vector4( 0, 0, 1, 0));             // Normal

VIFStaticDMA.AddVector(Vector4(1.0f, -1.0f, 1.0f, 1.0f)); // Vert (xyzw)

It is also necessary to specify a light direction vector for the directional light:

Vector4 vLight(1.0f, 0.0f, -0.5f, 1.0f);

vLight.NormaliseSelf();

In this case the light vector is coming from the left and slightly down the negative z axis. The light vector is also normalised. It is important to point out that this light vector is defined in the World Coordinate System. The light vector does not change in this application so it is packed into the static memory area that is sent to the VU every frame.

In the previous tutorials, only the combined vertex transformation matrix was sent to the VU for processing the vertex data. However, when computing directional lighting it is also necessary to send the world transformation matrix for the object being rendered. This is in order to transform the vertex normals from local space into world space before the lighting calculation can be done. Remember that the vertex normals are defined in local model space and the light vector is defined in world space. It is therefore necessary to transform the vertex normals from local to world space, and this requires the use of the world transformation matrix.

The layout of data in VU data memory is therefore as illustrated below:

 Address Data 0 Transformation Matrix Row #0 1 Transformation Matrix Row #1 2 Transformation Matrix Row #2 3 Transformation Matrix Row #3 4 World Matrix Row #0 5 World Matrix Row #1 6 World Matrix Row #2 7 World Matrix Row #3 8 Scaling Vector (number of vertices in w) 9 Directional Light Vector (normalised) 10 GIFTag 11 Texture Coordinate (STQ) for Vertex #1 12 Normal for Vertex #1 13 Position for Vertex #1 14 Texture Coordinate (STQ) for Vertex #2 15 Normal for Vertex #2 16 Position for Vertex #2 17 Texture Coordinate (STQ) for Vertex #3 18 Normal for Vertex #3 19 Position for Vertex #3 etc etc

VU1 Data Memory Layout

VU1 Micro Program.

The complete VU1 micro program is repeated below for clarity. Only the new sections associated with the lighting will be described here.

ProjMat        .equ 0

WorldMat      .equ 4

Scale         .equ 8

NumVertsMem   .equ 8

LightMem      .equ 9

StartSTQ      .equ 11

StartNorm     .equ 12

StartVert     .equ 13

; This is a file full of macros that comes with VCL

.include "vcl_sml.i"

; Tell VCL that we want to be able to use all vf, and vi registers

.init_vf_all

.init_vi_all

.syntax new

.vu

; This is the entry point, and is where execution will start on VU1

--enter

--endenter

fcset         0x000000            ; VCL won't let us use CLIP without first zeroing

; the clip flags

iaddiu        iVert, vi00, 0      ; Start vertex counter

iaddiu        iVertPtr, vi00, 0   ; Point to the first vert

ilw.w         iNumVerts, NumVertsMem(vi00) ; Load the number of verts from memory

lq            fScales, Scale(vi00)         ; A scale vector that we will add

; to and scale the vertices by after

; projecting them

lq            fLight, LightMem(vi00)       ; Load the light direction

sub.xyz       fLight, vf00, fLight         ; Reverse the light vector so that we can

; dot product it with the normals to give

; the light's intensity

loop:                                      ; For each vertex

lq        Vert, StartVert(iVertPtr)   ; Load the vector (currently in object space

; and floating point format)

; Transform the vertex by the projection matrix

MatrixMultiplyVertex Vert, fTransform, Vert

clipw.xyz      Vert, Vert              ; This instruction checks if the vertex is outside

; the viewing frustum.

fcand         vi01, 0x3FFFF            ; Bitwise AND the clipping flags with 0x3FFFF

div       q, vf00[w], Vert[w]           ; Work out 1.0f / w

mul.xyz   Vert, Vert, q                 ; Project the vertex by dividing by W

lq        STQ,  StartSTQ(iVertPtr)      ; Load the texture coordinates

mul       STQ, STQ, q                   ; Divide by W

sq        STQ,  StartSTQ(iVertPtr)      ; Store the texture coordinates back

lq        Normal, StartNorm(iVertPtr)   ; Load the vertex normal

; Transform the normal into world space

MatrixMultiplyVertex Normal, fWorldMat, Normal

VectorDotProduct Normal, fLight, Normal ; Puts the light intensity into Normal.x

loi       0.1                     ; Ambient

maxi.x    Normal, Normal, i       ; if( Normal.x < Ambient ) Normal.x = Ambient;

loi       128

addi.xyz  FinalCol, vf00, i                 ; Light colour (0x80, 0x80, 0x80)

mul.xyz   FinalCol, FinalCol, Normal[x]     ; Scale by the intensity

ftoi0     FinalCol, FinalCol                ; Convert to integer

sq        FinalCol, StartNorm(iVertPtr)          ; Then write back to memory

mula.xyz  acc, fScales, vf00[w]        ; Move fScales into the accumulator

madd.xyz  Vert, Vert, fScales          ; (Vert = Vert * fScales + fScales)

ftoi4.xyz Vert, Vert                   ; Convert to 12:4 fixed point for the GS

sq.xyz    Vert, StartVert(iVertPtr)    ; Write the vertex back to VU memory

iaddiu        iVert, iVert, 1          ; Increment the vertex counter

iaddiu        iVertPtr, iVertPtr, 3    ; Increment the vertex pointer

ibne      iVert, iNumVerts, loop       ; Branch

xgkick        iGIFTag                  ; and tell the PS2 to send that to the GS

--exit

--endexit

The light direction vector is loaded and reversed in direction with the following two lines of code:

lq            fLight, LightMem(vi00)  ; Load the light direction

sub.xyz       fLight, vf00, fLight    ; Reverse the light vector so that we can

It is necessary to reverse the light direction so that the dot product with the surface normal will give the correct sign. As well as loading the vertex transformation matrix, the world transformation matrix is also loaded:

The following section of code performs the lighting calculation:

lq        Normal, StartNorm(iVertPtr)  ; Load the vertex normal

; Transform the normal into world space

MatrixMultiplyVertex Normal, fWorldMat, Normal

VectorDotProduct Normal, fLight, Normal ; Puts the light intensity into Normal.x

loi       0.1                           ; Ambient

maxi.x    Normal, Normal, i             ; if( Normal.x < Ambient ) Normal.x = Ambient;

loi       128

addi.xyz  FinalCol, vf00, i                 ; Light colour (0x80, 0x80, 0x80)

mul.xyz   FinalCol, FinalCol, Normal[x]          ; Scale by the intensity

ftoi0     FinalCol, FinalCol                ; Convert to integer

sq        FinalCol, StartNorm(iVertPtr)          ; Then write back to memory

Firstly the vertex normal is loaded and transformed into world space using the world transformation matrix. The dot product between the vertex normal and the light direction vector is then taken and stored in the x component of the normal vector register (see the macro file). An ambient light intensity of 0.1 is used, and the light intensity is clamped so that it cannot fall below 0.1. Next a default colour of white (R, G, B) = (0x80, 0x80, 0x80) is loaded into the FinalCol floating point register and this is scaled by the light intensity result. An alpha value of 0x80 is also loaded into the FinalCol register. Finally, the vertex colour data is converted from floating point into integer and stored back into VU memory.

Example Program

The example program displays a spinning cube which can be rotated round the viewer with the left and right DPads. The directional light comes form the left of the scene and it is seen to illuminated the left had side of the cube as it rotates.

Conclusions

This tutorial has provided background information on the use of directional light on a diffuse surface. A VU program has been presented which performs the required lighting calculations.

Dr Henry S Fortuna

University of Abertay Dundee