/******************************************************************************
 * Copyright 1986-2002 by mental images GmbH & Co.KG, Fasanenstr. 81, D-10623
 * Berlin, Germany. All rights reserved.
 ******************************************************************************
 * Created:	16.1.96
 * Module:	contour
 * Purpose:	standard contour shaders: store, contrast, contour
 *
 * Exports:
 *	contour_store_function_version
 *	contour_store_function
 *	contour_store_function_simple_version
 *	contour_store_function_simple
 *
 *	contour_contrast_function_levels_version
 *	contour_contrast_function_levels
 *	contour_contrast_function_simple_version
 *	contour_contrast_function_simple
 *
 *	contour_shader_simple_version
 *	contour_shader_simple
 *	contour_shader_depthfade_version
 *	contour_shader_depthfade
 *	contour_shader_framefade_version
 *	contour_shader_framefade
 *	contour_shader_layerthinner_version
 *	contour_shader_layerthinner
 *	contour_shader_simple_curvature
 *	contour_shader_curvature
 *	contour_shader_factorcolor_version
 *	contour_shader_factorcolor
 *	contour_shader_widthfromcolor_version
 *	contour_shader_widthfromcolor
 *	contour_shader_widthfromlightdir_version
 *	contour_shader_widthfromlightdir
 *	contour_shader_widthfromlight_version
 *	contour_shader_widthfromlight
 *	contour_shader_combi_version
 *	contour_shader_combi
 *	contour_shader_maxcolor_version
 *	contour_shader_maxcolor
 *	contour_shader_silhouette_version
 *	contour_shader_silhouette
 *	contour_shader_randomwidth_version
 *	contour_shader_randomwidth
 *	contour_shader_randomtransparency_version
 *	contour_shader_randomtransparency
 *
 * History:
 *	14.9.01: reindented in manual style for publication
 *
 * Description:
 * The contour store function stores information (from the state and
 * material color) needed to compute the location, color, and width of
 * the contours.  The contour contrast function gets two such infos,
 * and decides whether there should be a contour there.  Each material
 * can have a contour shader, and this computes color and width of the
 * contour based on two infos (from neighboring points).
 *****************************************************************************/

#include <stdio.h>
#include <assert.h>
#include <math.h>
#include <shader.h>
#include <mi_contourshade.h>
#include <mi_version.h>

#define miEPS 0.0001

#ifdef DEBUG
#define miASSERT assert
#else
#define miASSERT(x)
#endif

#define mi_MAX(a,b)  ((a) > (b) ? (a) : (b))

char id[] = "==@@== compiled for ray " MI_VERSION_STRING " on " MI_DATE_STRING;

char *mi_contour_shader_version(void)
		{return("=@@= contour.so:\tversion 1.1" + 5);}


/*------------------------------------------------ contour store --------------
 *
 * Contour store functions.  They store the information needed by the
 * contour shaders.  This information can depend on the state and the
 * color computed by the material shader.
 */

int contour_store_function_version(void) {return(2);}

miBoolean contour_store_function(
	void		*info_void,
	int		*info_size,
	miState		*state,
	miColor		*color)
{
	miState		*s;

	/*
	 * The type of info is unknown to raylib, therefore a void * is passed
	 * and needs to be cast to miStdInfo * here.
	 */
	struct miStdInfo *info = (miStdInfo *) info_void;

	/*
	 * Ray intersection point, normal, material tag, label, triangle index.
	 */
	info->point       = state->point;
	info->normal      = state->normal;
	info->normal_geom = state->normal_geom;
	info->material    = state->material;
	info->label       = state->label;
	info->index       = state->pri_idx;

	/*
	 * Material color at the intersection.  This color is not just the
	 * color of the material (with texture) under a given illumination:
	 * if the material is transparent it also depends on deeper material
	 * colors.
	 */
	info->color = *color;

	/*
	 * Compute the refraction level.  Why is state->refraction_level not
	 * what we want?  Because some shaders dont increment it as they
	 * should.
	 */
	info->level = 0;
	for (s=state; s; s=s->parent, info->level++);
	miASSERT(info->level > 0);

	/*
	 * The size of miStdInfo. NOTE: this is obsolete and ignored in
	 * mental ray 3.0 but required for 2.1. In 3.0 the size is derived
	 * from the declaration, which must correctly declare the return type!
	 */
	*info_size = sizeof(miStdInfo);
	return(miTRUE);
}


/*
 * Fast, simple contour store function.  It only stores the material tag ---
 * used with fast, simple contour contrast functions that only needs the
 * material tag.
 */

int contour_store_function_simple_version(void) {return(1);}

miBoolean contour_store_function_simple(
	miTag		*info,
	int		*info_size,
	miState		*state,
	miColor		*color)
{
	*info = state->material;
	*info_size = sizeof(miTag);	/* for 2.1 only, ignored in 3.0 */
	return(miTRUE);
}


/*------------------------------------------------ contour contrast -----------
 *
 * Contour contrast functions.  Based on the information
 * stored by the contour store function for two adjacent samples, they
 * determine whether there should be a contour between them or not.
 */

int contour_contrast_function_levels_version(void) {return(2);}

miBoolean contour_contrast_function_levels(
	miStdInfo	*info1,
	miStdInfo	*info2,
	int		level,
	miState		*state,
	Contour_Contrast_Parameters_Levels *paras)
{
	miASSERT(info1 || info2);

	/*
	 * No contour if level too near or too deep
	 */
	if (level < paras->min_level || paras->max_level < level)
		return(miFALSE);

	/*
	 * Contour if one ray intersected an object and one ray hit background
	 */
	if ((info1 == NULL) != (info2 == NULL))
		return(miTRUE);

	miASSERT(info1 && info2);

	/*
	 * Contour if sufficiently large difference in depth
	 */
	if (fabs(info1->point.z - info2->point.z) > paras->zdelta)
		return(miTRUE);

	/*
	 * Contour if sufficiently large change in normal
	 */
	if (mi_vector_dot(&info1->normal, &info2->normal) <
					cos(paras->ndelta * M_PI / 180.0))
		return(miTRUE);

	/*
	 * Contour if different materials (if specified)
	 */
	if (paras->diff_mat && info1->material != info2->material)
		return(miTRUE);

	/*
	 * Contour if different object labels (if specified)
	 */
	if (paras->diff_label && info1->label != info2->label)
		return(miTRUE);

	/*
	 * Contour if different triangle indices (if specified)
	 */
	if (paras->diff_index && (info1->index != info2->index ||
				  mi_vector_dot(&info1->normal_geom,
						&info2->normal_geom) < 0.9999))
		return(miTRUE);

	/*
	 * Contour if color contrast (if specified) --- doesn't work properly
	 * on objects behind semitransparent objects since there can be a
	 * contrast on the semistransparent object caused by the color
	 * difference behind it (as a result, it looks like the contrast
	 * shader of the semitransparent object is used on the object behind
	 * even though it isn't).
	 */
	if (paras->contrast &&
	   (fabs(info1->color.r-info2->color.r) > state->options->contrast.r ||
	    fabs(info1->color.g-info2->color.g) > state->options->contrast.g ||
	    fabs(info1->color.b-info2->color.b) > state->options->contrast.b))
		return(miTRUE);

	/*
	 * No contour otherwise
	 */
	return(miFALSE);
}


/*
 * Used with contour_store_function_simple() for fast, simple contours
 */

int contour_contrast_function_simple_version(void) {return(1);}

miBoolean contour_contrast_function_simple(
	miTag		*info1,
	miTag		*info2,
	int		level,
	miState		*state,
	void		*paras)   /* no parameters */
{
	/*
	 * Contour if one ray intersected an object and one ray hit background
	 */
	if ((info1 == NULL) != (info2 == NULL))
		return(miTRUE);

	/*
	 * Contour if different materials
	 */
	return(*info1 != *info2);
}


/*------------------------------------------------ contour --------------------
 *
 * Contour shaders.  Based on the information stored by
 * the contour store function for two adjacent samples, the contour
 * shader determines what the contour color and width should be.
 * Use fixed color and width of the contour, corresponding to ray 1.9.
 */

int contour_shader_simple_version(void) {return(2);}

miBoolean contour_shader_simple(
	miContour_endpoint *result,
	miStdInfo	   *info_near,
	miStdInfo	   *info_far,
	miState		   *state,
	Simple_Parameters  *paras)
{
	miASSERT(paras->color.r >= 0.0 && paras->color.g >= 0.0);
	miASSERT(paras->color.b >= 0.0 && paras->width >= 0.0);

	/*
	 * Contour color given by a parameter
	 */
	result->color = paras->color;

	/*
	 * Contour width given by a parameter
	 */
	result->width = paras->width;
	return(miTRUE);
}


/*
 * The width of the contour fades into the background (from near_width
 * to far_width), and the color fades from near_color to far_color
 * The contour width and color changes with a ramp function between
 * distances near_z and far_z
 */

int contour_shader_depthfade_version(void) {return(1);}

miBoolean contour_shader_depthfade(
	miContour_endpoint   *result,
	miStdInfo	     *info_near,
	miStdInfo	     *info_far,
	miState		     *state,
	Depthfade_Parameters *paras)
{
	miScalar	     depth = info_near->point.z;
	double		     near_z, far_z, w_near, w_far;

	/* Ensure that near_z and far_z are negative as they should be */
	near_z = -fabs(paras->near_z);
	far_z  = -fabs(paras->far_z);

	if (depth > near_z) {		/* contour is closer than near_z */
		result->color = paras->near_color;
		result->width = paras->near_width;
	} else if (depth < far_z) {	/* contour is more distant than far_z*/
		result->color = paras->far_color;
		result->width = paras->far_width;
	} else {			/* contour is betwn near_z and far_z */
		/* Weights w_near and w_far depend on depth */
		w_near = (depth - far_z) / (paras->near_z - far_z);
		miASSERT(0.0 <= w_near && w_near <= 1.0);
		w_far = 1.0 - w_near;

		/* Mix of near_color and far_color according to weights */
		result->color.r = w_near * paras->near_color.r +
						w_far * paras->far_color.r;
		result->color.g = w_near * paras->near_color.g +
						w_far * paras->far_color.g;
		result->color.b = w_near * paras->near_color.b +
						w_far * paras->far_color.b;
		result->color.a = w_near * paras->near_color.a +
						w_far * paras->far_color.a;
		/* Width depending on weights */
		result->width = w_near * paras->near_width +
						w_far * paras->far_width;
	}
	return(miTRUE);
}


/*
 * The color and width of the contour depends linearly on the frame
 * number (ramp function).
 */

int contour_shader_framefade_version(void) {return(1);}

miBoolean contour_shader_framefade(
	miContour_endpoint   *result,
	miStdInfo	     *info_near,
	miStdInfo	     *info_far,
	miState		     *state,
	Framefade_Parameters *paras)
{
	int		     frame = state->camera->frame;
	double		     w1, w2;

	miASSERT(frame >= 0);
	miASSERT(paras->frame1 >= 0);
	miASSERT(paras->frame2 >= 0);

	if (frame <= paras->frame1) {		/* before frame1 */
		result->color = paras->color1;
		result->width = paras->width1;
	} else if (paras->frame2 <= frame) {	/* after frame2 */
		result->color = paras->color2;
		result->width = paras->width2;
	} else {				/* between frame1 and frame2 */
		/* Weights w1 and w2 depend on frame number */
		miASSERT(paras->frame1 < paras->frame2);
		w1 = ((double)paras->frame2 - frame) /
			     (paras->frame2 - paras->frame1);
		miASSERT(0.0 <= w1 && w1 <= 1.0);
		w2 = 1.0 - w1;

		/* Mix of color1 and color2 according to weights */
		result->color.r = w1 * paras->color1.r + w2 * paras->color2.r;
		result->color.g = w1 * paras->color1.g + w2 * paras->color2.g;
		result->color.b = w1 * paras->color1.b + w2 * paras->color2.b;
		result->color.a = w1 * paras->color1.a + w2 * paras->color2.a;

		/* Width depending on weights */
		result->width   = w1 * paras->width1   + w2 * paras->width2;
	}
	return(miTRUE);
}


/*
 * The width of the contour changes by a factor each time the ray gets deeper
 */

int contour_shader_layerthinner_version(void) {return(1);}

miBoolean contour_shader_layerthinner(
	miContour_endpoint	*result,
	miStdInfo		*info_near,
	miStdInfo		*info_far,
	miState			*state,
	Layerthinner_Parameters *paras)
{
	/* Constant contour color */
	result->color = paras->color;

	/* Width decreases by factor for each refraction_level */
	result->width = paras->width *
			pow(paras->factor, (double) info_near->level - 1.0);
	return(miTRUE);
}


/*
 * The width of the contour depends on the curvature, i.e. the
 * difference between the two normals.
 */

int contour_shader_simple_curvature(void) {return(1);}

miBoolean contour_shader_curvature(
	miContour_endpoint	  *result,
	miStdInfo		  *info_near,
	miStdInfo		  *info_far,
	miState			  *state,
	Widthfromcolor_Parameters *paras)
{
	double			  d;

	miASSERT(info_near || info_far);

	/* Constant contour color */
	result->color = paras->color;

	if ((info_near == NULL) != (info_far == NULL)) {
		/* Max contour width if one point hit background */
		result->width = paras->max_width;

	} else if (fabs(info_near->point.z - info_far->point.z) > 1.0) {
		/* Max contour width if large difference in depth */
		result->width = paras->max_width;
	} else {
		/* Otherwise, the contour width depends on the curvature */
		result->width = paras->min_width;
		d = mi_vector_dot(&info_near->normal, &info_far->normal);
		miASSERT(-1.0 <= d && d <= 1.0);
		result->width += 0.5 * (1.0 - d) *
					(paras->max_width-paras->min_width);
	}
	miASSERT(paras->min_width <= result->width &&
		 result->width <= paras->max_width);
	return(miTRUE);
}


/*
 * The color of the contour is a factor times the material color.
 * factor = 0 gives black contours,
 * 0 < factor < 1 gives dark contours,
 * factor = 1 gives contours the same color as the object material,
 * factor > 1 gives bright contours.
 * The width is constant.
 */

int contour_shader_factorcolor_version(void) {return(1);}

miBoolean contour_shader_factorcolor(
	miContour_endpoint	*result,
	miStdInfo		*info_near,
	miStdInfo		*info_far,
	miState			*state,
	Factorcolor_Parameters  *paras)
{
	double			f = paras->factor;

	miASSERT(info_near->color.r >= 0.0 && info_near->color.g >= 0.0);
	miASSERT(info_near->color.b >= 0.0);
	miASSERT(paras->factor >= 0.0 && paras->width >= 0.0);

	/*
	 * Set contour color to a factor times material color.  The opacity
	 * color->a is set to 1.0, otherwise the material will shine through
	 * the contour.
	 */
	result->color.r = f * info_near->color.r;
	result->color.g = f * info_near->color.g;
	result->color.b = f * info_near->color.b;
	result->color.a = 1.0;

	/* The contour width is a parameter */
	result->width = paras->width;
	return(miTRUE);
}


/*
 * The color of the contour is constant.  The width depends on the
 * material color.
 */

int contour_shader_widthfromcolor_version(void) {return(1);}

miBoolean contour_shader_widthfromcolor(
	miContour_endpoint	  *result,
	miStdInfo		  *info_near,
	miStdInfo		  *info_far,
	miState			  *state,
	Widthfromcolor_Parameters *paras)
{
	double			  maxcolor;

	/* Contour color given by a parameter */
	result->color = paras->color;

	/* The contour gets wider when the material color is dark */
	maxcolor = mi_MAX(info_near->color.r, info_near->color.g);
	maxcolor = mi_MAX(info_near->color.b, maxcolor);

	/* Softimage likes to have rgb colors larger than 1.0 */
	if (maxcolor > 1.0)
		maxcolor = 1.0;
	miASSERT(0.0 <= maxcolor && maxcolor <= 1.0);

	result->width = (paras->max_width - paras->min_width) *
					(1.0 - maxcolor) + paras->min_width;
	return(miTRUE);
}


/*
 * The color of the contour is a parameter.  The width depends on the
 * surface normal relative to a light source direction.
 * light dir = normal:   width = max_width
 * light dir = -normal:  width = min_width
 */

int contour_shader_widthfromlightdir_version(void) {return(1);}

miBoolean contour_shader_widthfromlightdir(
	miContour_endpoint  *result,
	miStdInfo	    *info_near,
	miStdInfo	    *info_far,
	miState		    *state,
	Lightdir_Parameters *paras)
{
	double		    d;

	/* Contour color given by a parameter */
	result->color = paras->color;

	/* Normalize light direction (just in case user didn't) */
	mi_vector_normalize(&paras->light_dir);

	/* The contour gets wider the more the normal differs from the light
	   source direction */
	d = mi_vector_dot(&paras->light_dir, &info_near->normal);
	result->width = paras->min_width +
		0.5 * (paras->max_width - paras->min_width) * (1.0 - d);

	miASSERT(paras->min_width <= result->width &&
			 result->width <= paras->max_width);
	return(miTRUE);
}


/*
 * The color of the contour is a parameter.  The width depends on the
 * surface normal relative to a light source direction.
 * light dir = normal:   width = max_width
 * light dir = -normal:  width = min_width
 */

int contour_shader_widthfromlight_version(void) {return(1);}

miBoolean contour_shader_widthfromlight(
	miContour_endpoint *result,
	miStdInfo	   *info_near,
	miStdInfo	   *info_far,
	miState		   *state,
	Light_Parameters   *paras)
{
	miVector	   orgp;	/* light source origin */
	miVector	   dirp;	/* light source direction */
	miVector	   dir;
	double		   d;

	/* Contour color given by a parameter */
	result->color = paras->color;

	/* Get light origin or direction */
	mi_light_info(paras->light, &orgp, &dirp, 0);
	/* Now orgp or dirp is different from the null vector */

	/* Compute direction from light to point */
	if (mi_vector_dot(&orgp, &orgp) > miEPS) {   /* point light source */
		mi_vector_sub(&dir, &info_near->point, &orgp);

	} else {				/* directional light source */
		miASSERT(mi_vector_dot(&dirp, &dirp) > miEPS);
		dir = dirp;
	}
	mi_vector_normalize(&dir);

	/* The contour gets wider the more the normal is pointing in the same
	   direction as the light source */
	d = mi_vector_dot(&dir, &info_near->normal);
	result->width = paras->min_width +
			0.5 * (paras->max_width - paras->min_width) * (d+1.0);

	miASSERT(paras->min_width <= result->width &&
			 result->width <= paras->max_width);
	return(miTRUE);
}


/*
 * This is a combination of the depthfade, layerthinner, and widthfromlight
 * contour shaders.
 * The width of the contour fades into the background (from near_width
 * to far_width), and the color fades from near_color to far_color.
 * The contour width and color changes with a ramp function between
 * distances near_z and far_z.
 * For each layer the ray has passed through, a factor is multiplied on
 * to the width.
 * If the light source is different from the NULLtag, the width also
 * depends on the surface normal relative to a light source direction.
 */

int contour_shader_combi_version(void) {return(2);}

miBoolean contour_shader_combi(
	miContour_endpoint *result,
	miStdInfo	   *info_near,
	miStdInfo	   *info_far,
	miState		   *state,
	Combi_Parameters   *paras)
{
	miScalar	   depth = info_near->point.z;
	double		   near_z, far_z, w_near, w_far;
	miVector	   orgp;		/* light source origin */
	miVector	   dirp;		/* light source direction */
	miVector	   dir;
	double		   d;

	/* Ensure that near_z and far_z are negative as they should be */
	near_z = -fabs(paras->near_z);
	far_z  = -fabs(paras->far_z);

	if (depth > near_z) {		/* contour is closer than near_z */
		result->color = paras->near_color;
		result->width = paras->near_width;

	} else if (depth < far_z) {	/* contour is more distant than far_z*/
		result->color = paras->far_color;
		result->width = paras->far_width;
	} else {			/* contour is betwn near_z and far_z */
		/* Weights w_near and w_far depend on depth */
		w_near = (depth - far_z) / (paras->near_z - far_z);
		miASSERT(0.0 <= w_near && w_near <= 1.0);
		w_far = 1.0 - w_near;

		/* Mix of near_color and far_color according to weights */
		result->color.r = w_near * paras->near_color.r +
						w_far * paras->far_color.r;
		result->color.g = w_near * paras->near_color.g +
						w_far * paras->far_color.g;
		result->color.b = w_near * paras->near_color.b +
						w_far * paras->far_color.b;
		result->color.a = w_near * paras->near_color.a +
						w_far * paras->far_color.a;
		/* Width depending on weights */
		result->width   = w_near * paras->near_width +
						w_far * paras->far_width;
	}

	/* Width decreases by factor for each refraction_level */
	if (paras->factor > miEPS)
		result->width *= pow(paras->factor,
					(double)info_near->level - 1.0);

	if (paras->light) {
		/* Get light origin or direction */
		mi_light_info(paras->light, &orgp, &dirp, 0);
		/* Now orgp or dirp is different from the null vector */

		/* Compute direction from light to point */
		if (mi_vector_dot(&orgp, &orgp) > miEPS) /* point light */
			mi_vector_sub(&dir, &info_near->point, &orgp);

		else {				/* directional light source */
			miASSERT(mi_vector_dot(&dirp, &dirp) > miEPS);
			dir = dirp;
		}
		mi_vector_normalize(&dir);

		/* The contour gets wider the more the normal is pointing in
		   the same direction as the light source */
		d = mi_vector_dot(&dir, &info_near->normal);
		result->width *= 0.5 * (d + 1.0) *
					(1.0 - paras->light_min_factor) +
						paras->light_min_factor;
	}
	miASSERT(result->width <= paras->near_width);
	return(miTRUE);
}


/*
 * The color of the contour is the max of the two point colors.
 * The width is a parameter.
 */

int contour_shader_maxcolor_version(void) {return(1);}

miBoolean contour_shader_maxcolor(
	miContour_endpoint *result,
	miStdInfo	   *info_near,
	miStdInfo	   *info_far,
	miState		   *state,
	float		   *width)		/* only one parameter */
{
	/* Contour color is max of info_near->color and info_far->color */
	if (info_far) {
		result->color.r = mi_MAX(info_near->color.r,info_far->color.r);
		result->color.g = mi_MAX(info_near->color.g,info_far->color.g);
		result->color.b = mi_MAX(info_near->color.b,info_far->color.b);
		result->color.a = 1.0;
	} else
		result->color = info_near->color;

	/* Contour width given by a parameter */
	result->width = *width;
	return(miTRUE);
}


/*
 * Fixed color and width of the contour, only at silhouette lines
 */

int contour_shader_silhouette_version(void) {return(1);}

miBoolean contour_shader_silhouette(
	miContour_endpoint *result,
	miStdInfo	   *info_near,
	miStdInfo	   *info_far,
	miState		   *state,
	Simple_Parameters  *paras)
{
	miASSERT(paras->color.r >= 0.0 && paras->color.g >= 0.0);
	miASSERT(paras->color.b >= 0.0 && paras->width >= 0.0);

	if (info_far) {
		result->color.a = 0.0;
		result->width   = 0.0;
	} else {
		result->color   = paras->color;
		result->width   = paras->width;
	}
	return(miTRUE);
}


/*
 * The color of the contour is constant.  The width is random.
 */

int contour_shader_randomwidth_version(void) {return(1);}

miBoolean contour_shader_randomwidth(
	miContour_endpoint *result,
	miStdInfo	   *info_near,
	miStdInfo	   *info_far,
	miState		   *state,
	Simple_Parameters  *paras)
{
	/* Contour color given by a parameter */
	result->color = paras->color;

	/* Random width */
	result->width = paras->width * mi_random();
	return(miTRUE);
}


/*
 * The transparency of the contour is random.  The width is constant.
 */

int contour_shader_randomtransparency_version(void) {return(1);}

miBoolean contour_shader_randomtransparency(
	miContour_endpoint *result,
	miStdInfo	   *info_near,
	miStdInfo	   *info_far,
	miState		   *state,
	Simple_Parameters  *paras)
{
	/* Contour color given by a parameter and random transparency */
	result->color   = paras->color;
	result->color.a = mi_random();

	/* Width given by a parameter */
	result->width   = paras->width;
	return(miTRUE);
}
