/*
 * vp_shade.c
 *
 * Routines to implement the Phong shading equation using a lookup-table.
 *
 * Copyright (c) 1994 The Board of Trustees of The Leland Stanford
 * Junior University.  All rights reserved.
 *
 * Permission to use, copy, modify and distribute this software and its
 * documentation for any purpose is hereby granted without fee, provided
 * that the above copyright notice and this permission notice appear in
 * all copies of this software and that you do not sell the software.
 * Commercial licensing is available by contacting the author.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
 * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
 *
 * Author:
 *    Phil Lacroute
 *    Computer Systems Laboratory
 *    Electrical Engineering Dept.
 *    Stanford University
 */

/*
 * $Date: 2001/12/17 16:16:23 $
 * $Revision: 1.1 $
 */

#include "vp_global.h"

/*
 * Lookup Table Shading and Normal Vector Encoding
 *
 * The shader defined in this file implements the Phong shading equation
 * (I = Ia + Id*(N.L) + Is*(N.H)^n) using lookup tables.  To use this
 * shader you must include a "normal" field in each voxel.  This field
 * is computed by preprocessing the volume to estimate a surface normal
 * vector for each voxel (using the central-difference gradient operator)
 * and then encoding the normal in an index (13 bits in this program).
 * An index is stored in the normal field of each voxel.   At rendering
 * time the index is used to look up a color for the voxel in a table.
 *
 * There are many ways a normal vector can be encoded using the 13 bits of
 * the index.  A straight-forward method is to use 6 bits for the x component
 * of the vector, 6 bits for the y component, and 1 bit to indicate the sign
 * of the z component.  Assuming that the vector is normalized the z
 * component can be reconstructed from x and y.  Unfortunately this method
 * results in an uneven distribution of code points: the distance between
 * exactly-representable vectors is much smaller near N = +z or -z than
 * N = +x, -x, +y or -z (where x, y and z are the unit vectors).  This
 * can result in significant quantization error near the "equator", or more
 * table storage than necessary near the "poles".
 *
 * The normal vector encoding scheme used here is derived from a recursive
 * tesselation of a regular octahedron (an eight-sided solid with
 * equilateral triangular faces).  Consider subdividing each triangular
 * face into four smaller equilateral triangles, and then subdividing those
 * triangles recursively until a sufficiently large number of triangles
 * have been generated.  The representable normal vectors are the vectors
 * connecting the center of the solid to the vertices of the triangles.
 * The distribution of these vectors is not perfectly uniform (the density
 * is lower at the "mid-latitudes"), but the variation is relatively small.
 *
 * Each normal vector is now assigned a unique index as follows.  Let the
 * origin be at the center of the solid, and the x, y and z axes each
 * intersect a vertex of the original octahedron.  Use one bit of the index
 * to indicate the sign of the z component of the normal.  Now project the
 * subdivided triangle vertices onto the x-y plane.  This forms a square
 * grid of dots rotated 45 degrees relative to the x-y axes.  Starting at
 * (x=0, y=max), assign sequential integers to each normal vector projection,
 * proceeding left-to-right and top-to-bottom.  Finally, append the sign bit
 * for the z component to the least-significant end of the integer to get
 * the normal vector encoding.
 *
 * This scheme is useful because it is easy to compute the vector components
 * from an index and conversely to find the closest index for a given vector,
 * yet the distribution of representable vectors is pretty uniform.
 *
 * XXX better method is to rotate 45 degrees (M = [1 -1 ; 1 1]) and then
 * assign points in scanline order; no lookup tables needed; implement this!
 *
 * The layout of the shading lookup table is as follows:
 *    float shade_table[MAX_NORMAL+1][materials][color_channels]
 * where materials is the number of materials and color_channels is 1
 * for grayscale intensities or 3 for RGB intensities (stored in R,G,B
 * order).
 */

/* define normal index parameters; if you change these then you
   must change VP_NORM_MAX in volpack.h */
#define NORM_13			/* use 13 bit normals */
#undef NORM_15			/* don't use 15 bit normals */

#ifdef NORM_13			/* parameters for 13 bit normals */
#define NORM_N		44	/*   N parameter for normal computation */
#define NORM_BITS	13	/*   number of bits to encode a normal:
				     (1+ceil(log2(2*(N*N+N+1)))) */
#define MAX_NORMAL	7923	/*   maximum normal index */
#endif

#ifdef NORM_15			/* parameters for 15 bit normals */
#define NORM_N		90
#define NORM_BITS	15
#define MAX_NORMAL	16131
#endif


/* static lookup tables (computed only once, used for all vpContexts) */
static int NormalTablesInitialized = 0; /* set to 1 after initialization */
static short *NormalPy;	/* NormalPy[py] = normal index for the normal
				   whose projection in the x-y plane is (0,py)
				   and whose z component is +
				   (py = -NORM_N to +NORM_N) */
static short NormalPyStorage[1+2*NORM_N];	/* storage for NormalPy */
static short *NormalXLimit;	/* max abs(x) allowed for a given y */
static short NormalXLimitStorage[1+2*NORM_N]; /* storage for NormalXLimit */
static void InitNormalTables ANSI_ARGS((void));

/*
 * InitNormalTables
 *
 * Initialize lookup tables for computing normal indices.
 */

static void
InitNormalTables()
{
    int y, xcount, codepoint;
    int sum;
    double value;

    /* initialize NormalPy */
    xcount = 1;
    codepoint = 2;
    NormalPy = &NormalPyStorage[NORM_N];
    NormalXLimit = &NormalXLimitStorage[NORM_N];
    for (y = -NORM_N; y <= NORM_N; y++) {
	NormalPy[y] = codepoint + (((xcount-1)/2) << 1);
	codepoint += (xcount << 1);
	NormalXLimit[y] = (xcount-1)/2;
	if (y < 0)
	    xcount += 2;
	else
	    xcount -= 2;
    }

    NormalTablesInitialized = 1;
}

/*
 * vpNormalIndex
 *
 * Return the best normal index for the given normal vector.
 */

int
vpNormalIndex(nx, ny, nz)
double nx, ny, nz;
{
    int n, x, y, xlimit;
    double denom, denominv;

    if (!NormalTablesInitialized)
	InitNormalTables();
    denom = (nx < 0) ? -nx : nx;
    denom += (ny < 0) ? -ny : ny;
    denom += (nz < 0) ? -nz : nz;
    denominv = (double)NORM_N / denom;
    x = (int)rint(nx * denominv);
    y = (int)rint(ny * denominv);

    /* clamp x */
    xlimit = NormalXLimit[y];
    if (x < 0) {
	if (-x > xlimit)
	    x = -xlimit;
    } else {
	if (x > xlimit)
	    x = xlimit;
    }

    n = NormalPy[y] + (x << 1);
    if (nz < 0)
	n |= 1;
    ASSERT(n >= 0 && n <= VP_NORM_MAX);
    return(n);
}

/*
 * vpNormal
 *
 * Compute normal vector components given a normal vector index.
 */

vpResult
vpNormal(n, nx, ny, nz)
int n;			/* normal index */
double *nx, *ny, *nz;	/* storage for result */
{
    int py, px, pz, pxlimit2;
    double pxd, pyd, pzd, plength;

    if (!NormalTablesInitialized)
	InitNormalTables();
    for (py = -NORM_N; py <= NORM_N; py++) {
	pxlimit2 = 2 * ((py<0) ? (NORM_N + py) : (NORM_N - py));
	if (NormalPy[py] - pxlimit2 <= n &&
	    NormalPy[py] + pxlimit2 + 1 >= n) {
	    break;
	}
    }
    if (py > NORM_N) {
	return(VPERROR_BAD_VALUE);
    } else {
	px = (n - NormalPy[py]) >> 1;
	pz = NORM_N;
	if (px < 0)
	    pz += px;
	else
	    pz -= px;
	if (py < 0)
	    pz += py;
	else
	    pz -= py;
	if (n & 1)
	    pz = -pz;
	pxd = (double)px;
	pyd = (double)py;
	pzd = (double)pz;
	plength = 1. / sqrt(pxd*pxd+pyd*pyd+pzd*pzd);
	*nx = pxd * plength;
	*ny = pyd * plength;
	*nz = pzd * plength;
    }
    return(VP_OK);
}

/*
 * vpScanlineNormals
 *
 * Compute normals and/or gradients for a scanline of voxels.
 */

vpResult
vpScanlineNormals(vpc, length, scalar_data, scalar_minus_y,
		  scalar_plus_y, scalar_minus_z, scalar_plus_z,
		  voxel_data, scalar_field, grad_field, norm_field)
vpContext *vpc;		/* context */
int length;		/* number of scalars in scanline */
unsigned char *scalar_data;	/* scanline of scalar data */
unsigned char *scalar_minus_y;	/* adjacent scanline of scalar data (-y) */
unsigned char *scalar_plus_y;	/* adjacent scanline of scalar data (+y) */
unsigned char *scalar_minus_z;	/* adjacent scanline of scalar data (-z) */
unsigned char *scalar_plus_z;	/* adjacent scanline of scalar data (+z) */
void *voxel_data;	/* location to store first voxel */
int scalar_field;	/* voxel field for scalar, or VP_SKIP_FIELD */
int grad_field;		/* voxel field for gradient, or VP_SKIP_FIELD */
int norm_field;		/* voxel field for normal, or VP_SKIP_FIELD */
{
    int x;			/* voxel index */
    double grad_x;		/* components of the gradient vector */
    double grad_y;
    double grad_z;
    double twice_grad_mag;	/* twice the magnitude of the gradient */
    int grad;			/* gradient magnitude */
    int norm;			/* normal index */
    int edge;			/* true if this scanline is on the edge
				   of the volume */
    int voxel_size;		/* size of a voxel in bytes */
    int scalar_offset;		/* byte offset for scalar in voxel */
    int grad_offset;		/* byte offset for gradient in voxel */
    int norm_offset;		/* byte offset for normal in voxel */
    char *voxel;		/* pointer to current voxel */
    int retcode;

    /* check for errors */
    if ((retcode = VPCheckVoxelFields(vpc)) != VP_OK)
	return(retcode);
    if (scalar_field != VP_SKIP_FIELD) {
	if (scalar_field < 0 || scalar_field >= vpc->num_voxel_fields)
	    return(VPSetError(vpc, VPERROR_BAD_VALUE));
	if (vpc->field_size[scalar_field] != VP_SCALAR_SIZE)
	    return(VPSetError(vpc, VPERROR_BAD_VALUE));
	scalar_offset = vpc->field_offset[scalar_field];
    }
    if (grad_field != VP_SKIP_FIELD) {
	if (grad_field < 0 || grad_field >= vpc->num_voxel_fields)
	    return(VPSetError(vpc, VPERROR_BAD_VALUE));
	if (vpc->field_size[grad_field] != VP_GRAD_SIZE)
	    return(VPSetError(vpc, VPERROR_BAD_VALUE));
	grad_offset = vpc->field_offset[grad_field];
    }
    if (norm_field != VP_SKIP_FIELD) {
	if (norm_field < 0 || norm_field >= vpc->num_voxel_fields)
	    return(VPSetError(vpc, VPERROR_BAD_VALUE));
	if (vpc->field_size[norm_field] != VP_NORM_SIZE)
	    return(VPSetError(vpc, VPERROR_BAD_VALUE));
	norm_offset = vpc->field_offset[norm_field];
    }
    voxel_size = vpc->raw_bytes_per_voxel;

    /* compute the scanline */
    voxel = voxel_data;
    if (scalar_minus_y == NULL || scalar_plus_y == NULL ||
	scalar_minus_z == NULL || scalar_plus_z == NULL) {
	edge = 1;
    } else {
	edge = 0;
    }
    for (x = 0; x < length; x++) {
	/* compute gradient and normal for voxel x and store */
	if (edge || x == 0 || x == length-1) {
	    if (scalar_field != VP_SKIP_FIELD)
		ByteField(voxel, scalar_offset) = scalar_data[x];
	    if (grad_field != VP_SKIP_FIELD)
		ByteField(voxel, grad_offset) = 0;
	    if (norm_field != VP_SKIP_FIELD)
		ShortField(voxel, norm_offset) = 0;
	} else {
	    grad_x = (int)scalar_data[x+1] - (int)scalar_data[x-1];
	    grad_y = (int)scalar_plus_y[x] - (int)scalar_minus_y[x];
	    grad_z = (int)scalar_plus_z[x] - (int)scalar_minus_z[x];
	    twice_grad_mag = sqrt(grad_x*grad_x+grad_y*grad_y+grad_z*grad_z);
	    if (scalar_field != VP_SKIP_FIELD)
		ByteField(voxel, scalar_offset) = scalar_data[x];
	    if (grad_field != VP_SKIP_FIELD) {
		grad = (int)rint(0.5 * twice_grad_mag);
		ASSERT(grad >= 0 && grad <= VP_GRAD_MAX);
		ByteField(voxel, grad_offset) = grad;
	    }
	    if (norm_field != VP_SKIP_FIELD) {
		if (twice_grad_mag < VP_EPS)
		    norm = 0;
		else
		    norm = vpNormalIndex(grad_x / twice_grad_mag,
					 grad_y / twice_grad_mag,
					 grad_z / twice_grad_mag);
		ShortField(voxel, norm_offset) = norm;
	    }
	}

	/* go on to next voxel */
	voxel += voxel_size;
    }
    return(VP_OK);
}

/*
 * vpVolumeNormals
 *
 * Compute normals and/or gradients for a volume.  Result is stored
 * in raw_voxels in the current context.
 */

vpResult
vpVolumeNormals(vpc, scalar_data, length, scalar_field, grad_field, norm_field)
vpContext *vpc;		/* context */
unsigned char *scalar_data;	/* 3D array of scalar data */
int length;		/* number of scalars in scalar_data */
int scalar_field;	/* voxel field for scalar, or VP_SKIP_FIELD */
int grad_field;		/* voxel field for gradient, or VP_SKIP_FIELD */
int norm_field;		/* voxel field for normal, or VP_SKIP_FIELD */
{
    int xlen, ylen, zlen;	/* volume dimensions */
    int y, z;			/* loop indices */
    unsigned char *scalar;	/* pointer to current scalar */
    int scalar_ystride;		/* stride to next scalar scanline */
    int scalar_zstride;		/* stride to next scalar slice */
    char *voxel;		/* pointer to current voxel */
    int voxel_ystride;		/* stride to next voxel scanline */
    int voxel_zstride;		/* stride to next voxel slice */
    unsigned char *s_py, *s_my, *s_pz, *s_mz; /* ptrs to adjacent scans */
    int retcode;

    /* check for errors */
    if ((retcode = VPCheckRawVolume(vpc)) != VP_OK)
	return(retcode);
    xlen = vpc->xlen;
    ylen = vpc->ylen;
    zlen = vpc->zlen;
    if (xlen * ylen * zlen != length)
	return(VPSetError(vpc, VPERROR_BAD_SIZE));

    /* initialize */
    scalar = scalar_data;
    scalar_ystride = xlen;
    scalar_zstride = xlen*ylen;
    voxel = vpc->raw_voxels;
    voxel_ystride = vpc->ystride;
    voxel_zstride = vpc->zstride;

    /* compute volume data */
    for (z = 0; z < zlen; z++) {
	ReportStatus(vpc, (double)z / (double)zlen);
	for (y = 0; y < ylen; y++) {
	    s_my = (y == 0)      ? NULL : scalar - scalar_ystride;
	    s_py = (y == ylen-1) ? NULL : scalar + scalar_ystride;
	    s_mz = (z == 0)      ? NULL : scalar - scalar_zstride;
	    s_pz = (z == zlen-1) ? NULL : scalar + scalar_zstride;
	    retcode = vpScanlineNormals(vpc, xlen, scalar, s_my, s_py,
					s_mz, s_pz, voxel, scalar_field,
					grad_field, norm_field);
	    if (retcode != VP_OK)
		return(retcode);
	    scalar += scalar_ystride;
	    voxel += voxel_ystride;
	}
	scalar += scalar_zstride - ylen*scalar_ystride;
	voxel += voxel_zstride - ylen*voxel_ystride;
    }
    ReportStatus(vpc, 1.0);
    return(VP_OK);
}

/*
 * vpShadeTable
 *
 * Compute a shading lookup table for the current lighting and
 * model matrix.
 */

vpResult
vpShadeTable(vpc)
vpContext *vpc;
{
    int num_lights;		/* number of enabled lights */
    vpVector3 light_color[VP_MAX_LIGHTS]; /* light colors */
    vpVector4 obj_light[VP_MAX_LIGHTS]; /* light_vector in object space */
    vpVector4 obj_highlight[VP_MAX_LIGHTS]; /* halfway-vector */
    vpVector4 obj_viewpoint;	/* viewpoint in object coordinates */
    vpMatrix4 a;		/* linear system matrix */
    double *rhs[VP_MAX_LIGHTS+1];/* right-hand-side/solution vectors */
    int px, py, pz;		/* code point coordinates (integers) */
    double pxd, pyd, pzd;	/* code point coordinates (doubles) */
    double plength;
    int pxlimit;		/* maximum absolute value of px for this py */
    double nx, ny, nz;		/* normal vector components */
    double n_dot_v_xy;		/* normal_vector dot obj_viewpoint (x&y) */
    double n_dot_v_z;		/* normal_vector dot obj_viewpoint (z) */
    double n_dot_v;		/* normal_vector dot obj_viewpoint */
    double n_dot_l_xy;		/* normal_vector dot light_vector (x&y) */
    double n_dot_l_z;		/* normal_vector dot light_vector (z) */
    double n_dot_l;		/* normal_vector dot light_vector */
    double n_dot_h_xy;		/* normal_vector dot halfway_vector (x&y) */
    double n_dot_h_z;		/* normal_vector dot halfway_vector (z) */
    double n_dot_h;		/* normal_vector dot halfway_vector */
    float r, g, b;		/* intensities to store in shade table */
    float *table_row;		/* pointer to table row for current normal */
    float *table;		/* pointer to table entry */
    float *shadow_table_row;	/* pointer to table row for current normal */
    float *shadow_table;	/* pointer to shadow table entry */
    int surface_side;		/* EXT_SURFACE or INT_SURFACE */
    int znegative;		/* true iff nz is negative */
    int light_both_sides;	/* use two-sided lighting */
    int reverse_surface_sides;	/* reverse interior and exterior */
    int color_channels;		/* number of color channels */
    int num_materials;		/* number of materials */
    int retcode;
    double *matl_props;		/* material properties */
    int enable_shadows;		/* true if shadows are enabled */
    int shadow_light;		/* light index for light casting shadows */
    int clamp;			/* true if table entries should be clamped */
    int c, l, m;
#ifdef DEBUG
    vpVector4 tmpv;
#endif
    DECLARE_TIME(t0);
    DECLARE_TIME(t1);

    /* error checking */
    if (vpc->shading_mode != LOOKUP_SHADER)
	return(VP_OK);
    if ((retcode = VPCheckShader(vpc)) != VP_OK)
	return(retcode);
    if ((retcode = VPCheckShadows(vpc)) != VP_OK)
	return(retcode);
    ASSERT(vpc->color_channels == 1 || vpc->color_channels == 3);

    /* start timer */
    GET_TIME(vpc, t0);

    /* transform viewpoint vector and light vectors to object space */
    vpSetVector4(obj_viewpoint, 0., 0., 1., 1.);
    rhs[0] = obj_viewpoint;
    num_lights = 0;
    for (c = 0; c < VP_MAX_LIGHTS; c++) {
	if (vpc->light_enable[c]) {
	    bcopy(vpc->light_color[c], light_color[num_lights],
		  sizeof(vpVector3));
	    bcopy(vpc->light_vector[c], obj_light[num_lights],
		  sizeof(vpVector4));
	    rhs[num_lights+1] = obj_light[num_lights];
	    num_lights++;
	}
    }
    bcopy(vpc->transforms[VP_MODEL], a, sizeof(vpMatrix4));
    retcode = vpSolveSystem4(a, rhs, num_lights+1);
    if (retcode != VP_OK)
	return(retcode);

#ifdef DEBUG
    /* make sure the solver gave the right answer */
    vpMatrixVectorMult4(tmpv, vpc->transforms[VP_MODEL], obj_viewpoint);
    if (fabs(tmpv[0]) > VP_EPS || fabs(tmpv[1]) > VP_EPS ||
	fabs(tmpv[2] - 1.) > VP_EPS || fabs(tmpv[3] - 1.) > VP_EPS) {
	printf("\n");
	printf("Modelview:\n");
	printf("    %12.8f %12.8f %12.8f %12.8f\n",
	   vpc->transforms[VP_MODEL][0][0], vpc->transforms[VP_MODEL][0][1],
	   vpc->transforms[VP_MODEL][0][2], vpc->transforms[VP_MODEL][0][3]);
	printf("    %12.8f %12.8f %12.8f %12.8f\n",
	   vpc->transforms[VP_MODEL][1][0], vpc->transforms[VP_MODEL][1][1],
	   vpc->transforms[VP_MODEL][1][2], vpc->transforms[VP_MODEL][1][3]);
	printf("    %12.8f %12.8f %12.8f %12.8f\n",
	   vpc->transforms[VP_MODEL][2][0], vpc->transforms[VP_MODEL][2][1],
	   vpc->transforms[VP_MODEL][2][2], vpc->transforms[VP_MODEL][2][3]);
	printf("    %12.8f %12.8f %12.8f %12.8f\n",
	   vpc->transforms[VP_MODEL][3][0], vpc->transforms[VP_MODEL][3][1],
	   vpc->transforms[VP_MODEL][3][2], vpc->transforms[VP_MODEL][3][3]);
	VPBug("SolveSystem failed on viewpoint");
    }
    l = 0;
    for (c = 0; c < VP_MAX_LIGHTS; c++) {
	if (vpc->light_enable[c]) {
	    vpMatrixVectorMult4(tmpv, vpc->transforms[VP_MODEL], obj_light[l]);
	    if (fabs(tmpv[0] - vpc->light_vector[c][0]) > VP_EPS ||
		fabs(tmpv[1] - vpc->light_vector[c][1]) > VP_EPS ||
		fabs(tmpv[2] - vpc->light_vector[c][2]) > VP_EPS ||
		fabs(tmpv[3] - vpc->light_vector[c][3]) > VP_EPS)
		VPBug("SolveSystem failed on light %d\n", c);
	    l++;
	}
    }
#endif

    /* compute highlight vectors */
    for (l = 0; l < num_lights; l++) {
	obj_highlight[l][0] = obj_light[l][0] + obj_viewpoint[0];
	obj_highlight[l][1] = obj_light[l][1] + obj_viewpoint[1];
	obj_highlight[l][2] = obj_light[l][2] + obj_viewpoint[2];
	vpNormalize3(obj_highlight[l]);
    }

    /* initialize options */
    light_both_sides = vpc->light_both_sides;
    reverse_surface_sides = vpc->reverse_surface_sides;
    color_channels = vpc->color_channels;
    num_materials = vpc->num_materials;
    table = vpc->shade_color_table;
    enable_shadows = vpc->enable_shadows;
    if (enable_shadows) {
	shadow_table = vpc->shadow_color_table;
	shadow_light = vpc->shadow_light_num - VP_LIGHT0;
	bzero(shadow_table, vpc->shadow_color_table_size);
    } else {
	shadow_table = NULL;
	shadow_light = -1;
    }
    clamp = vpc->clamp_shade_table;

    /* store shade table entries for the zero-vector */
    for (znegative = 0; znegative <= 1; znegative++) {
	if (znegative) {
	    if (reverse_surface_sides)
		surface_side = EXT_SURFACE;
	    else
		surface_side = INT_SURFACE;
	} else {
	    if (reverse_surface_sides)
		surface_side = INT_SURFACE;
	    else
		surface_side = EXT_SURFACE;
	}
	for (m = 0; m < num_materials; m++) {
	    matl_props = vpc->matl_props[m][surface_side];
	    *table++ = matl_props[MATL_AMB_R];
	    if (color_channels == 3) {
		*table++ = matl_props[MATL_AMB_G];
		*table++ = matl_props[MATL_AMB_B];
	    }
	}
    }
    table_row = table;
    if (enable_shadows) {
	for (znegative = 0; znegative <= 1; znegative++) {
	    for (m = 0; m < num_materials; m++) {
		*shadow_table++ = 0;
		if (color_channels == 3) {
		    *shadow_table++ = 0;
		    *shadow_table++ = 0;
		}
	    }
	}
    }
    shadow_table_row = shadow_table;

    /* compute the shade table entries for nonzero normals */
    for (py = -NORM_N; py <= NORM_N; py++) {
	pxlimit = (py < 0) ? (NORM_N + py) : (NORM_N - py);
	pz = -1;
	pxd = (double)(-pxlimit-1);
	pyd = (double)py;
	pzd = (double)(-1);
	for (px = -pxlimit; px <= pxlimit; px++) {
	    /* compute normal vector components for this code point */
	    pxd += 1.0;
	    if (px <= 0) {
		pz++;
		pzd += 1.0;
	    } else {
		pz--;
		pzd -= 1.0;
	    }
	    plength = 1. / sqrt(pxd*pxd + pyd*pyd + pzd*pzd);
	    nx = pxd * plength;
	    ny = pyd * plength;
	    nz = pzd * plength;

	    /* compute n dot v (for determining surface side) */
	    n_dot_v_xy = nx*obj_viewpoint[0] + ny*obj_viewpoint[1];
	    n_dot_v_z = nz*obj_viewpoint[2];

	    /* store ambient light terms */
	    table = table_row;
	    for (znegative = 0; znegative <= 1; znegative++) {
		if (znegative)
		    n_dot_v = n_dot_v_xy - n_dot_v_z;
		else
		    n_dot_v = n_dot_v_xy + n_dot_v_z;
		if (reverse_surface_sides)
		    n_dot_v = -n_dot_v;
		if (n_dot_v >= 0)
		    surface_side = EXT_SURFACE;
		else
		    surface_side = INT_SURFACE;
		for (m = 0; m < num_materials; m++) {
		    matl_props = vpc->matl_props[m][surface_side];
		    *table++ = matl_props[MATL_AMB_R];
		    if (color_channels == 3) {
			*table++ = matl_props[MATL_AMB_G];
			*table++ = matl_props[MATL_AMB_B];
		    }
		}
	    }

	    /* loop over lights */
	    for (l = 0; l < num_lights; l++) {
		if (l == shadow_light)
		    table = shadow_table_row;
		else
		    table = table_row;

		/* compute n dot l and n dot h */
		n_dot_l_xy = nx*obj_light[l][0] + ny*obj_light[l][1];
		n_dot_l_z = nz*obj_light[l][2];
		n_dot_h_xy = nx*obj_highlight[l][0] + ny*obj_highlight[l][1];
		n_dot_h_z = nz*obj_highlight[l][2];

		/* loop over the two signs for z */
		for (znegative = 0; znegative <= 1; znegative++) {
		    if (znegative) {
			n_dot_v = n_dot_v_xy - n_dot_v_z;
			n_dot_l = n_dot_l_xy - n_dot_l_z;
			n_dot_h = n_dot_h_xy - n_dot_h_z;
		    } else {
			n_dot_v = n_dot_v_xy + n_dot_v_z;
			n_dot_l = n_dot_l_xy + n_dot_l_z;
			n_dot_h = n_dot_h_xy + n_dot_h_z;
		    }
		    if (reverse_surface_sides) {
			n_dot_v = -n_dot_v;
			n_dot_l = -n_dot_l;
			n_dot_h = -n_dot_h;
		    }
		    if (n_dot_v >= 0)
			surface_side = EXT_SURFACE;
		    else
			surface_side = INT_SURFACE;
		    if (light_both_sides) {
			n_dot_l = fabs(n_dot_l);
			n_dot_h = fabs(n_dot_h);
		    } else if (surface_side == EXT_SURFACE) {
			n_dot_l = MAX(n_dot_l, 0.0);
			n_dot_h = MAX(n_dot_h, 0.0);
		    } else {
			n_dot_l = MAX(-n_dot_l, 0.0);
			n_dot_h = MAX(-n_dot_h, 0.0);
		    }

		    /* loop over material types */
		    for (m = 0; m < num_materials; m++) {
			matl_props = vpc->matl_props[m][surface_side];
			*table++ += light_color[l][0]*(matl_props[MATL_DIFF_R]*
				    n_dot_l + matl_props[MATL_SPEC_R]*
				    pow(n_dot_h, matl_props[MATL_SHINY]));
			if (color_channels == 3) {
			    *table++ += light_color[l][1]*
				    (matl_props[MATL_DIFF_G]*
				    n_dot_l + matl_props[MATL_SPEC_G]*
				    pow(n_dot_h, matl_props[MATL_SHINY]));
			    *table++ += light_color[l][2]*
				    (matl_props[MATL_DIFF_B]*
				    n_dot_l + matl_props[MATL_SPEC_B]*
				    pow(n_dot_h, matl_props[MATL_SHINY]));
			}
		    } /* for m */
		} /* for znegative */
	    } /* for l */

	    /* clamp */
	    if (clamp) {
		if (enable_shadows) {
		    table = table_row;
		    shadow_table = shadow_table_row;
		    for (znegative = 0; znegative <= 1; znegative++) {
			for (m = 0; m < num_materials; m++) {
			    for (c = 0; c < color_channels; c++) {
				if (*table > 255.)
				    *table = 255.;
				if (*table + *shadow_table > 255.)
				    *shadow_table = 255. - *table;
				shadow_table++;
				table++;
			    }
			}
		    }
		} else {
		    table = table_row;
		    for (znegative = 0; znegative <= 1; znegative++) {
			for (m = 0; m < num_materials; m++) {
			    for (c = 0; c < color_channels; c++) {
				if (*table > 255.)
				    *table = 255.;
				table++;
			    }
			}
		    }
		}
	    }

	    if (num_materials == 1) {
		table_row += 2*color_channels;
	    } else {
		if (color_channels == 1)
		    table_row += 2*num_materials;
		else
		    table_row += 6*num_materials;
	    }

	    if (enable_shadows) {
		if (num_materials == 1) {
		    shadow_table_row += 2*color_channels;
		} else {
		    if (color_channels == 1)
			shadow_table_row += 2*num_materials;
		    else
			shadow_table_row += 6*num_materials;
		}
	    }
	} /* for px */
    } /* for py */

    /* stop timer */
    GET_TIME(vpc, t1);
    STORE_TIME(vpc, VPTIMER_SHADE, t0, t1);

    return(VP_OK);
}
