Light sources: how they work and what you can do with them

Part 3 of the articles about PlayStation development. Get ready for the next battle after the jump!

Typically this is how a matrix is stored on PlayStation:

typedef struct tagMatrix
{
	s16 m[3][3];	// rotation
	s32 t[3];	    // translation
} MATRIX;

In short, these are 3×4 matrices, where m represents rotation and t is a translation vector. They apply mostly the same for all transformations, but lights are a separate case where only m is really used to represent operating data. This means we use matrices for light sources as well, two to be exact, one for light and another for color, which can hold a maximum of 3 sources.

This is how a local light matrix is represented:

Local light matrixX/Y/Z 0/1/2 store normalized coordinates of each light source. That means that if you have light sources placed anywhere on a field, they will have to be processed in order to apply correctly to a mesh. We will see later how this works.

Now, this is a local color matrix:

Local color matrixIf you look carefully, a local light matrix stores data in rows, while here it’s columns. These RGB values work as 12 bit values, which means ONE (=4096) makes a light fully lit for an RGB channel and you can go beyond to make the effect more prominent. In most cases you can simply take 0-255 RGB values from a structure of your choice and multiply them by 16 to obtain a correct scale to 0-ONE values.

Now, let’s see how light/color matrices work on LibGS with some custom code that clones its internals:

void Set_flat_light(const int index, VECTOR *lpos, const CVECTOR *col, const int Mag)
{
	int len;

	len=SquareRoot0(lpos->vx*lpos->vx + lpos->vy*lpos->vy + lpos->vz*lpos->vz);
	if(len==0) return;

	// set position row
	M_ll.m[index][0]=((-lpos->vx)<<12)/len*Mag;
	M_ll.m[index][1]=((-lpos->vy)<<12)/len*Mag;
	M_ll.m[index][2]=((-lpos->vz)<<12)/len*Mag;

	// set color column
	M_lc.m[0][index]=(col->r*ONE)/0xFF;
	M_lc.m[1][index]=(col->g*ONE)/0xFF;
	M_lc.m[2][index]=(col->b*ONE)/0xFF;
}

In this function M_ll is the local light matrix, while M_lc is the local color matrix. You can see a Mag variable there also being taken into calculation, which simply represents the intensity of a light.
So how do we you apply those values to a mesh? There are a number of strategies you can apply, depending on what your game is going to support. Typically you need a helper function that calculates the light position for each mesh, so you can’t simply apply a global set of matrices and get away with that; you effectively need to recalculate the local light matrix for every new mesh that passes through a rendering procedure, while local color can stay the same across multiple meshes. Let’s see that in detail with some sample code from my engine:

// set local color matrix for an entity
Set_light((VECTOR*)&em->Pos_x,&G.pRoom->pLight[G.Cut_no]);
gte_SetColorMatrix(&M_lc);

// --------------------

// set local light matrix for a mesh
gte_MulMatrix0(&M_ll,&p->Workm,&m_light);
gte_SetLightMatrix(&m_light);

The first slice of code calls Set_light, which is a handler that performs calculations to generate full light and color matrices, while the second one multiplies the results of the helper to generate a new proper light matrix which is calculate by multiplying a work matrix by the current local light. Let’s give a look at how light vectors are calculated inside Set_light():

		// RE1 point light
		case LM_FALL2:
			{
				int x, y, z;
				// calculate local range
				x=(pos->vx - l->Pos[i][0]);
				y=(pos->vy - l->Pos[i][1]);
				z=(pos->vz - l->Pos[i][2]);
				lpos.vx=x, lpos.vy=y, lpos.vz=z;
				// get magnitude for intensity check
				mag=SquareRoot0(x*x + /*y*y +*/ z*z);
				power=l->L[i]>>1;
				// attenuation
				if(power!=0)
				{
					intensity=power-mag;
					if(intensity<0) intensity=0;
					cvec.r=intensity*l->Col[i][0]/power;
					cvec.g=intensity*l->Col[i][1]/power;
					cvec.b=intensity*l->Col[i][2]/power;
				}
				else *(u32*)&cvec.r=0;
				// send data
				Set_flat_light(i,&lpos,&cvec,l->Mag);
			}
			break;
		// RE1 omni light
		case LM_OMNI2:
			lpos.vx=l->Pos[i][0];
			lpos.vy=l->Pos[i][1];
			lpos.vz=l->Pos[i][2];
			Set_flat_light(i,&lpos,(CVECTOR*)&l->Col[i],l->Mag);
			break;

LM_FALL2 and LM_OMNI2 generate different effects, depending on the complexity you need. LM_FALL2 simulates a local light with fall off, while LM_OMNI2 is an infinite light, providing the same amount of light everywhere.
As for the actual rendering, you need to take into account two more aspects of the GTE: ambient light and color diffuse. Ambient light is the neutral color of a mesh when there’s no light applied to it, while diffuse is the color of the mesh itself. For example, you could have a very dark room with almost no light, which requires a low ambient RGB (say 0x303030). As for color diffuse, it applies the same rules as RGB values on primitives, so it influences hue in the same exact way (0x808080 = neutral color, 0x000000 = totally black, 0xF0F0F0 = overly bright). Ambient can be assigned via GTE instructions gte_SetBackColor as 0-255 ranges, while color diffuse needs a call to gte_ldrgb and a CVECTOR as its input (again 0-255 color values).
Finally, let’s talk about the actual rendering code. Now that you have your light/color matrices, ambient, and diffuse set up what you need is the correct GTE commands to apply the effect on primitives. Typically you want to apply lights on entities (say animated player and enemies) with a smooth effect, while the environment can get flat lights which are not as intensive to calculate. In either case, you need to load into the GTE one or a set of normal vectors, call gte_ncXY() commands, then retrieve the results and apply them to rgb structures of a primitive of your choice. As for gte_ncXY, X can be nothing, ‘d’ (fog) or ‘c’ (no fog), while Y is ‘s’ (single) or ‘t’ (triplet). ‘t’ and ‘s’ versions can be used together or separately, depending on your primitive and lighting effect. A sample of quad rendering with smooth lighting applied:

		gte_ldv3(&vn[q->n0], &vn[q->n1], &vn[q->n2]);
		gte_ncct();
		gte_strgb3_gt4(si);

		gte_ldv0(&vn[q->n3]);
		gte_nccs();
		gte_strgb(&si->r3);

As you can see, I’m using both ncct and nccs together to fill all four rgb channels. Triangles would be similar, but you only need to call ncct. You working with POLY_FT3/4 rendering? Then you only need nccs to apply one flat light to all three/four points of the primitive.

(Visited 38 times, 1 visits today)

Leave a Reply

Your email address will not be published. Required fields are marked *