A while ago I wrote a little bump mapping demo that used ARB_vertex_program and ARB_fragment_program. . . I lost the source code, but still have the demo itself and its shader programs.
!!ARBvp1.0
PARAM mvp[4] = {state.matrix.mvp};
PARAM light1pos = state.light[1].position;
PARAM light2pos = state.light[2].position;
#PARAM light2pos = {-2.5, 2.5, -2.5, 1.0};
PARAM one = {1.0, 1.0, 1.0, 1.0};
PARAM lightradius = {0.125, 0.125, 0.125, 1.0};
ATTRIB normal = vertex.normal;
ATTRIB binormal = vertex.attrib[9];
ATTRIB tangent = vertex.attrib[10];
TEMP light1vector, light1ts;
TEMP light2vector, light2ts;
TEMP temp;
# Light 1
SUB light1vector, light1pos, vertex.position;
DP3 light1ts.x, light1vector, tangent;
DP3 light1ts.y, light1vector, binormal;
DP3 light1ts.z, light1vector, normal;
MOV light1ts.w, one;
DP3 temp, light1ts, light1ts;
RSQ temp, temp.x;
MUL light1ts, light1ts, temp;
MOV result.texcoord[1], light1ts;
MUL result.texcoord[2], light1vector, lightradius;
# Light 2
SUB light2vector, light2pos, vertex.position;
DP3 light2ts.x, light2vector, tangent;
DP3 light2ts.y, light2vector, binormal;
DP3 light2ts.z, light2vector, normal;
MOV light2ts.w, one;
DP3 temp, light2ts, light2ts;
RSQ temp, temp.x;
MUL light2ts, light2ts, temp;
MOV result.texcoord[3], light2ts;
MUL result.texcoord[4], light2vector, lightradius;
# Output everything else
MOV result.color, vertex.color;
DP4 result.position.x, mvp[0], vertex.position;
DP4 result.position.y, mvp[1], vertex.position;
DP4 result.position.z, mvp[2], vertex.position;
DP4 result.position.w, mvp[3], vertex.position;
MOV result.texcoord[0], vertex.texcoord[0];
END
!!ARBfp1.0
OUTPUT final = result.color;
TEMP base, normal, temp, bump, total;
TEMP light1, atten1;
TEMP light2, atten2;
PARAM light0colour = state.light[0].ambient;
#PARAM light1colour = state.light[1].diffuse;
PARAM light1colour = {0.0, 1.0, 0.0, 1.0};
#PARAM light2colour = state.light[2].diffuse;
PARAM light2colour = {1.0, 0.0, 0.0, 1.0};
TEX base, fragment.texcoord[0], texture[1], 2D;
TEX normal, fragment.texcoord[0], texture[0], 2D;
# remove scale and bias from the normal map
SUB normal, normal, 0.5;
MUL normal, normal, 2.0;
# normalize the normal map
DP3 temp, normal, normal;
RSQ temp, temp.r;
MUL normal, normal, temp;
normalize the light1 vector
DP3 temp, fragment.texcoord[1], fragment.texcoord[1];
RSQ temp, temp.x;
MUL light1, fragment.texcoord[1], temp;
N.L
DP3 bump, normal, light1;
calculate and add attenuation
DP3_SAT atten1, fragment.texcoord[2], fragment.texcoord[2];
SUB atten1, 1.0, atten1;
MUL bump, bump, atten1;
add colour
MUL total, bump, light1colour;
normalize the light2 vector
DP3 temp, fragment.texcoord[3], fragment.texcoord[3];
RSQ temp, temp.x;
MUL light2, fragment.texcoord[3], temp;
N.L
DP3 bump, normal, light2;
calculate and add attenuation
DP3_SAT atten2, fragment.texcoord[4], fragment.texcoord[4];
SUB atten2, 1.0, atten2;
MUL bump, bump, atten2;
add colour and add to the total
MAD_SAT total, bump, light2colour, total;
add ambient lighting
ADD_SAT total, total, light0colour;
MUL_SAT total, base, total;
MOV final, total;
#MOV final, bump;
END