/*
 *			GPAC - MPEG-4 Systems C Development Kit
 *
 *			Copyright (c) Jean Le Feuvre 2000-2005
 *					All rights reserved
 *
 *  This file is part of GPAC / 3D rendering plugin
 *
 *  GPAC is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2, or (at your option)
 *  any later version.
 *   
 *  GPAC is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *   
 *  You should have received a copy of the GNU General Public License
 *  along with GNU Make; see the file COPYING.  If not, write to
 *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. 
 *
 */

#include "visual_surface.h"
#include "render3d_nodes.h"

VisualSurface *VS_New()
{
	VisualSurface *tmp = malloc(sizeof(VisualSurface));
	memset(tmp, 0, sizeof(VisualSurface));
	tmp->back_stack = NewChain();
	tmp->view_stack = NewChain();
	tmp->alpha_nodes_to_draw = NewChain();
	tmp->fog_stack = NewChain();
	tmp->navigation_stack = NewChain();
	return tmp;
}

void VS_Delete(VisualSurface *surf)
{
	BindableStackDelete(surf->back_stack);
	BindableStackDelete(surf->view_stack);
	BindableStackDelete(surf->fog_stack);
	BindableStackDelete(surf->navigation_stack);
	DeleteChain(surf->alpha_nodes_to_draw);
	free(surf);
}

DrawableStack *new_drawable(SFNode *owner, SceneRenderer *compositor)
{
	DrawableStack *tmp = malloc(sizeof(DrawableStack));
	if (tmp) stack_setup(tmp, owner, compositor);
	return tmp;
}
void delete_drawable(DrawableStack*d)
{
	drawablestack_predestroy(d);
	free(d);
}

void drawable_Node_PreDestroy(SFNode *n)
{
	DrawableStack *d = Node_GetPrivate(n);
	if (d) delete_drawable(d);
}

DrawableStack *BaseDrawableStack(SceneRenderer *sr, SFNode *node)
{
	DrawableStack *st = new_drawable(node, sr);
	Node_SetPrivate(node, st);
	Node_SetPreDestroyFunction(node, drawable_Node_PreDestroy);
	return st;
}


void delete_strikeinfo(StrikeInfo *info)
{
	if (info->outline) mesh_free(info->outline);
	free(info);
}

stack2D *new_stack2D(SFNode *owner, SceneRenderer *compositor)
{
	stack2D *tmp = malloc(sizeof(stack2D));
	if (tmp) {
		stack_setup(tmp, owner, compositor);
		tmp->path = m4_path_new();
		tmp->strike_list = NewChain();
	}
	return tmp;
}

void stack2D_predestroy(stack2D *d)
{
	Render3D *sr = (Render3D *)d->compositor->visual_renderer->user_priv;
	if (d->mesh) mesh_free(d->mesh);
	assert(d->path);
	m4_path_delete(d->path);

	while (ChainGetCount(d->strike_list)) {
		StrikeInfo *si = ChainGetEntry(d->strike_list, 0);
		ChainDeleteEntry(d->strike_list, 0);
		/*remove from main strike list*/
		ChainDeleteItem(sr->strike_bank, si);
		delete_strikeinfo(si);
	}
	DeleteChain(d->strike_list);
}

void delete_stack2D(stack2D *d)
{
	stack2D_predestroy(d);
	free(d);
}
void stack2D_node_predestroy(SFNode *n)
{
	stack2D *d = Node_GetPrivate(n);
	if (d) delete_stack2D(d);
}

stack2D *BaseStack2D(SceneRenderer *sr, SFNode *node)
{
	stack2D *st = new_stack2D(node, sr);
	Node_SetPrivate(node, st);
	Node_SetPreDestroyFunction(node, stack2D_node_predestroy);
	return st;
}

void stack2D_setup(stack2D *st, SceneRenderer *sr, SFNode *node)
{
	stack_setup(st, node, sr);
	st->path = m4_path_new();
	st->strike_list = NewChain();
}

void stack2D_reset(stack2D *d)
{
	Render3D *sr = (Render3D *)d->compositor->visual_renderer->user_priv;
	if (d->path) m4_path_reset(d->path);
	while (ChainGetCount(d->strike_list)) {
		StrikeInfo *si = ChainGetEntry(d->strike_list, 0);
		ChainDeleteEntry(d->strike_list, 0);
		ChainDeleteItem(sr->strike_bank, si);
		delete_strikeinfo(si);
	}
}

Bool VS_GetAspect2D(RenderEffect3D *eff, Aspect2D *asp)
{
	M_Material2D *mat;
	Bool has_mat;
	/*default values*/
	memset(asp, 0, sizeof(Aspect2D));
	asp->fill_color.red = asp->fill_color.green = asp->fill_color.blue = FLT2FIX(0.8f);
	/*this is a bug in the spec: by default line width is 1.0, but in meterMetrics this means half of the screen :)*/
	if (eff->is_pixel_metrics) 
		asp->pen_props.width = FIX_ONE;
	else
		asp->pen_props.width = m4_divfix(FIX_ONE, 2*eff->min_hsize);

	asp->line_color = asp->fill_color;
	asp->pen_props.cap = M4_LINE_CAP_FLAT;
	asp->pen_props.join = M4_LINE_JOIN_MITER;
	asp->pen_props.miterLimit = INT2FIX(4);
	asp->line_alpha = asp->alpha = FIX_ONE;
	has_mat = 1;


	if (!eff->appear) goto exit;
	mat = (M_Material2D *) ((M_Appearance *)eff->appear)->material;
	/*according to the spec only Material2D shall be used with 2D primitives*/
	if (!mat) goto exit;
	switch (Node_GetTag((SFNode *)mat)) {
	/*we need to indicate to text if it shall use 2D or 3D material...*/
	case TAG_MPEG4_Material:
	case TAG_X3D_Material:
		has_mat = 0;
		asp->filled = 1;
		asp->pen_props.width = 0;
		asp->fill_color = ((M_Material*)mat)->diffuseColor;
		goto exit;
	case TAG_MPEG4_MaterialKey:
		goto exit;
	}

	asp->line_color = asp->fill_color = mat->emissiveColor;
	asp->line_alpha = asp->alpha = FIX_ONE - mat->transparency;
	asp->filled = mat->filled;

	if (mat->lineProps == NULL) {
		if (asp->filled || (asp->alpha==0)) asp->pen_props.width =0;
	} else {
		asp->lp = mat->lineProps;
		switch (Node_GetTag(mat->lineProps)) {
		case TAG_MPEG4_LineProperties:
		{
			M_LineProperties *lp = (M_LineProperties *)mat->lineProps;
			asp->pen_props.width = lp->width;
			asp->pen_props.dash = lp->lineStyle;
			asp->line_color = lp->lineColor;
		}
			break;
		case TAG_MPEG4_XLineProperties:
		{
			M_XLineProperties *xlp = (M_XLineProperties *)mat->lineProps;
			asp->pen_props.dash = xlp->lineStyle;
			asp->line_color = xlp->lineColor;
			asp->line_alpha = FIX_ONE - xlp->transparency;
			asp->pen_props.width = xlp->width;
			asp->is_scalable = xlp->isScalable;
			asp->pen_props.align = xlp->isCenterAligned ? M4_PATH_LINE_CENTER : M4_PATH_LINE_INSIDE;
			asp->pen_props.cap = xlp->lineCap;
			asp->pen_props.join = xlp->lineJoin;
			asp->pen_props.miterLimit = xlp->miterLimit;
			asp->pen_props.dash_offset = xlp->dashOffset;
			/*dash settings strutc is the same as MFFloat from XLP, typecast without storing*/
			if (xlp->dashes.count) {
				asp->pen_props.dash_set = (M4DashSettings *) &xlp->dashes;
			} else {
				asp->pen_props.dash_set = NULL;
			}
			asp->txh = R3D_GetTextureHandler(xlp->texture);
			asp->tx_trans = xlp->textureTransform;
		}
			break;
		}
	}

exit:
	if (eff->has_cmat) 
		m4_cmx_apply_fixed(&eff->color_mat, &asp->alpha, &asp->fill_color.red, &asp->fill_color.green, &asp->fill_color.blue);
	
	/*update line width*/
	if (asp->pen_props.width) {
		M4Matrix mx;
		M4Rect rc;
		VS3D_GetMatrix(eff->surface, MAT_MODELVIEW, mx.m);
		rc.x = rc.y = 0;
		rc.width = rc.height = FIX_ONE;
		m4_mx_apply_rect(&mx, &rc);
		asp->line_scale = MAX(rc.width, rc.height);
		if (eff->has_cmat) 
			m4_cmx_apply_fixed(&eff->color_mat, &asp->line_alpha, &asp->line_color.red, &asp->line_color.green, &asp->line_color.blue);
	}
	return has_mat;
}

StrikeInfo *VS_GetStrikeInfo(stack2D *st, Aspect2D *asp, RenderEffect3D *eff)
{
	StrikeInfo *si;
	u32 now, i;
	Render3D *sr = (Render3D *)st->compositor->visual_renderer->user_priv;
	Bool vect_outline = !sr->raster_outlines;
	if (!asp->pen_props.width || !st->path) return NULL;

	si = NULL;
	for (i=0; i<ChainGetCount(st->strike_list); i++) {
		si = ChainGetEntry(st->strike_list, i);
		/*note this includes default LP (NULL)*/
		if (si->lineProps == asp->lp) break;
		si = NULL;
	}
	/*not found, add*/
	if (!si) {
		si = malloc(sizeof(StrikeInfo));
		memset(si, 0, sizeof(StrikeInfo));
		si->lineProps = asp->lp;
		si->node2D = st->owner;
		ChainAddEntry(st->strike_list, si);
		ChainAddEntry(sr->strike_bank, si);
	}

	if (vect_outline != si->is_vectorial) {
		if (si->outline) mesh_free(si->outline);
		si->outline = NULL;
	}

	/*node changed or outline not build*/
	now = asp->lp ? (1 + R3D_LP_GetLastUpdateTime(asp->lp)) : si->last_update_time;
	if (!si->outline 
		|| (si->is_vectorial && ((now!=si->last_update_time) || (si->line_scale != asp->line_scale) )) ) {
		si->last_update_time = now;
		si->line_scale = asp->line_scale;
		if (si->outline) mesh_free(si->outline);
		si->outline = new_mesh();
		si->is_vectorial = vect_outline;
		if (vect_outline) {
			u32 i;
			M4Path *outline_path;
			Fixed dash_o = asp->pen_props.dash_offset;
			Fixed w = asp->pen_props.width;
			asp->pen_props.width = m4_divfix(asp->pen_props.width, asp->line_scale);
			asp->pen_props.dash_offset = m4_mulfix(asp->pen_props.dash_offset, asp->pen_props.width);
			if (asp->pen_props.dash_set) {
				for(i=0; i<asp->pen_props.dash_set->num_dash; i++) {
					asp->pen_props.dash_set->dashes[i] = m4_mulfix(asp->pen_props.dash_set->dashes[i], asp->line_scale);
				}
			}
			
			outline_path = m4_path_get_outline(st->path, asp->pen_props);
			/*restore*/
			asp->pen_props.width = w;
			asp->pen_props.dash_offset = dash_o;
			if (asp->pen_props.dash_set) {
				for(i=0; i<asp->pen_props.dash_set->num_dash; i++) {
					asp->pen_props.dash_set->dashes[i] = m4_divfix(asp->pen_props.dash_set->dashes[i], asp->line_scale);
				}
			}

			TesselatePath(si->outline, outline_path, asp->txh ? 2 : 1);
			m4_path_delete(outline_path);
		} else {
			mesh_get_outline(si->outline, st->path);
		}
	}
	return si;
}


StrikeInfo *VS_GetStrikeInfoIFS(stack2D *st, Aspect2D *asp, RenderEffect3D *eff)
{
	StrikeInfo *si;
	u32 now, i;
	Render3D *sr = (Render3D *)st->compositor->visual_renderer->user_priv;
	if (!asp->pen_props.width || !st->path) return NULL;

	si = NULL;
	for (i=0; i<ChainGetCount(st->strike_list); i++) {
		si = ChainGetEntry(st->strike_list, i);
		/*note this includes default LP (NULL)*/
		if (si->lineProps == asp->lp) break;
		si = NULL;
	}
	/*not found, add*/
	if (!si) {
		si = malloc(sizeof(StrikeInfo));
		memset(si, 0, sizeof(StrikeInfo));
		si->lineProps = asp->lp;
		si->node2D = st->owner;
		ChainAddEntry(st->strike_list, si);
		ChainAddEntry(sr->strike_bank, si);
	}

	/*for this func the strike is always raster*/
	if (si->is_vectorial) {
		if (si->outline) mesh_free(si->outline);
		si->outline = NULL;
	}

	/*node changed or outline not build*/
	now = asp->lp ? R3D_LP_GetLastUpdateTime(asp->lp) : si->last_update_time;
	if ((now!=si->last_update_time) || (si->line_scale != asp->line_scale) ) {
		si->last_update_time = now;
		si->line_scale = asp->line_scale;
		if (si->outline) mesh_free(si->outline);
		si->outline = NULL;
		si->is_vectorial = 0;
	}
	return si;
}


Fixed Aspect_GetLineWidth(Aspect2D *asp)
{
	/*for raster outlines are already set to the proper width - note this may not work depending on GL...*/
	Fixed width = asp->pen_props.width;
	if (asp->is_scalable) width = m4_mulfix(width, asp->line_scale);
	return width;
}

void VS_Set2DStrikeAspect(VisualSurface *surf, Aspect2D *asp)
{
	if (asp->txh && tx_enable(asp->txh, asp->tx_trans)) return;
	/*no texture or not ready, use color*/
	VS3D_SetMaterial2D(surf, asp->line_color, asp->line_alpha);
}

TextureHandler *VS_setup_texture_2d(RenderEffect3D *eff, Aspect2D *asp)
{
	Bool ret;
	TextureHandler *txh;
	if (!eff->appear) return NULL;
	txh = R3D_GetTextureHandler(((M_Appearance *)eff->appear)->texture);
	if (!txh) return NULL;
	if (!asp->filled) {
		if (asp->alpha!=FIX_ONE) {
			VS3D_SetMaterial2D(eff->surface, asp->fill_color, asp->alpha);
			tx_set_blend_mode(txh, TX_MODULATE);
		} else {
			VS3D_SetState(eff->surface, F3D_BLEND, 0);
			tx_set_blend_mode(txh, TX_REPLACE);
		}
	}
	ret = tx_enable(txh, ((M_Appearance *)eff->appear)->textureTransform);
	if (ret) return txh;
	return NULL;
}

void stack2D_draw(stack2D *st, RenderEffect3D *eff)
{
	Aspect2D asp;
	StrikeInfo *si;
	TextureHandler *fill_txh;

	VS_GetAspect2D(eff, &asp);

	if (!asp.alpha) {
		fill_txh = NULL;
	} else {
		fill_txh = VS_setup_texture_2d(eff, &asp);
	}
	/*fill path*/
	if (fill_txh || (asp.alpha && asp.filled) ) {
		if (asp.filled) VS3D_SetMaterial2D(eff->surface, asp.fill_color, asp.alpha);
		VS3D_DrawMesh(eff, st->mesh, 0);
		/*reset texturing in case of line texture*/
		if (fill_txh) tx_disable(fill_txh);
	}

	/*strike path*/
	if (!asp.line_alpha) return;
	si = VS_GetStrikeInfo(st, &asp, eff);
	if (si) {
		VS_Set2DStrikeAspect(eff->surface, &asp);
		if (!si->is_vectorial) {
			VS3D_StrikeMesh(eff, si->outline, Aspect_GetLineWidth(&asp), asp.pen_props.dash);
		} else {
			VS3D_DrawMesh(eff, si->outline, 0);
		}
		if (asp.txh) tx_disable(asp.txh);
	}
}

static M4INLINE Bool VS_setup_material(RenderEffect3D *eff)
{
	SFColor def;
	SFNode *__mat;
	def.red = def.green = def.blue = FIX_ONE;

	if (!eff->appear) {
		/*use material2D to disable lighting*/
		VS3D_SetMaterial2D(eff->surface, def, FIX_ONE);
		return 1;
	}

	if (Node_GetTag(eff->appear)==TAG_X3D_Appearance) {
		X_FillProperties *fp = (X_FillProperties *) ((X_Appearance*)eff->appear)->fillProperties;
		if (fp && !fp->filled) return 0;
	}

	__mat = ((M_Appearance *)eff->appear)->material;
	if (!__mat) {
		/*use material2D to disable lighting*/
		VS3D_SetMaterial2D(eff->surface, def, FIX_ONE);
		return 1;
	} 
	
	switch (Node_GetTag((SFNode *)__mat)) {
	case TAG_MPEG4_Material:
	case TAG_X3D_Material:
	{
		SFColor diff, spec, emi;
		Fixed diff_a, spec_a, emi_a;
		Fixed vec[4];
		Bool has_alpha;
		u32 flag = F3D_LIGHT | F3D_COLOR;
		M_Material *mat = (M_Material *)__mat;

		diff = mat->diffuseColor;
		diff_a = FIX_ONE - mat->transparency;

		/*if drawing in 2D context disable lighting*/
		if (!eff->camera->is_3D) {
			if (eff->has_cmat) m4_cmx_apply_fixed(&eff->color_mat, &diff_a, &diff.red, &diff.green, &diff.blue);
			VS3D_SetMaterial2D(eff->surface, diff, diff_a);
			return 1;
		}

		spec = mat->specularColor;
		emi = mat->emissiveColor;
		spec_a = emi_a = FIX_ONE - mat->transparency;
		if (eff->has_cmat) {
			m4_cmx_apply_fixed(&eff->color_mat, &diff_a, &diff.red, &diff.green, &diff.blue);
			m4_cmx_apply_fixed(&eff->color_mat, &spec_a, &spec.red, &spec.green, &spec.blue);
			m4_cmx_apply_fixed(&eff->color_mat, &emi_a, &emi.red, &emi.green, &emi.blue);

			if ((diff_a+FIX_EPSILON<FIX_ONE) || (spec_a+FIX_EPSILON<FIX_ONE) || (emi_a+FIX_EPSILON<FIX_ONE )) {
				has_alpha = 1;
			} else {
				has_alpha = 0;
			}
		} else {
			has_alpha = (mat->transparency>FIX_EPSILON) ? 1 : 0;
			/*100% transparent DON'T DRAW*/
			if (mat->transparency+FIX_EPSILON>=FIX_ONE) return 0;
		}

		/*using antialiasing with alpha usually gives bad results (non-edge face segments are visible)*/
		VS3D_SetAntiAlias(eff->surface, !has_alpha);
		if (has_alpha) {
			flag |= F3D_BLEND;
			eff->mesh_is_transparent = 1;
		}
		VS3D_SetState(eff->surface, flag, 1);

		vec[0] = m4_mulfix(diff.red, mat->ambientIntensity);
		vec[1] = m4_mulfix(diff.green, mat->ambientIntensity);
		vec[2] = m4_mulfix(diff.blue, mat->ambientIntensity);
		vec[3] = diff_a;
		VS3D_SetMaterial(eff->surface, MATERIAL_AMBIENT, vec);

		VS3D_SetShininess(eff->surface, mat->shininess);

		vec[0] = spec.red;
		vec[1] = spec.green;
		vec[2] = spec.blue;
		vec[3] = spec_a;
		VS3D_SetMaterial(eff->surface, MATERIAL_SPECULAR, vec);

		vec[0] = diff.red;
		vec[1] = diff.green;
		vec[2] = diff.blue;
		vec[3] = diff_a;
		VS3D_SetMaterial(eff->surface, MATERIAL_DIFFUSE, vec);
		
		if (diff.red || diff.green || diff.blue || spec.red || spec.green || spec.blue) {
			VS3D_SetMaterial(eff->surface, MATERIAL_NONE, vec);
			vec[0] = emi.red;
			vec[1] = emi.green;
			vec[2] = emi.blue;
			vec[3] = emi_a;
			VS3D_SetMaterial(eff->surface, MATERIAL_EMISSIVE, vec);
		} else {
			VS3D_SetState(eff->surface, F3D_LIGHT, 0);
			VS3D_SetMaterial(eff->surface, MATERIAL_EMISSIVE, vec);
			vec[0] = emi.red;
			vec[1] = emi.green;
			vec[2] = emi.blue;
			vec[3] = emi_a;
			VS3D_SetMaterial(eff->surface, MATERIAL_NONE, vec);
		}
	}
		break;
	case TAG_MPEG4_Material2D:
	{
		SFColor emi;
		Fixed emi_a;
		M_Material2D *mat = (M_Material2D *)__mat;

		emi = mat->emissiveColor;
		emi_a = FIX_ONE - mat->transparency;
		if (eff->has_cmat) m4_cmx_apply_fixed(&eff->color_mat, &emi_a, &emi.red, &emi.green, &emi.blue);
		/*100% transparent DON'T DRAW*/
		if (emi_a<FIX_EPSILON) return 0;
		else if (emi_a+FIX_EPSILON<FIX_ONE) VS3D_SetState(eff->surface, F3D_BLEND, 1);


		/*this is an extra feature: if material2D.filled is FALSE on 3D objects, switch to TX_REPLACE mode
		and enable lighting*/
		if (!mat->filled) {
			TextureHandler *txh = R3D_GetTextureHandler(((M_Appearance *)eff->appear)->texture);
			if (txh) {
				tx_set_blend_mode(txh, TX_REPLACE);
				VS3D_SetState(eff->surface, F3D_COLOR, 0);
				VS3D_SetState(eff->surface, F3D_LIGHT, 1);
				return 1;
			}
		}
		/*regular mat 2D*/
		VS3D_SetState(eff->surface, F3D_LIGHT | F3D_COLOR, 0);
		VS3D_SetMaterial2D(eff->surface, emi, emi_a);
	}	
		break;
	default:
		break;
	}
	return 1;
}

Bool VS_setup_texture(RenderEffect3D *eff)
{
	TextureHandler *txh;
	if (!eff->appear) return 0;
	txh = R3D_GetTextureHandler(((M_Appearance *)eff->appear)->texture);
	if (txh) {
		/*according to VRML spec, texture alpha overrides material alpha*/
		tx_set_blend_mode(txh, TX_REPLACE);
		eff->mesh_is_transparent = txh->transparent;
		return tx_enable(txh, ((M_Appearance *)eff->appear)->textureTransform);
	}
	return 0;
}

void VS_disable_texture(RenderEffect3D *eff)
{
	if (eff->appear) tx_disable(R3D_GetTextureHandler(((M_Appearance *)eff->appear)->texture) );
}

Bool VS_SetupAppearance(RenderEffect3D *eff)
{
	/*setup material and check if 100% transparent - in which case don't draw*/
	if (!VS_setup_material(eff)) return 0;
	/*setup texture*/
	VS_setup_texture(eff);
	return 1;
}

void VS_DrawMesh(RenderEffect3D *eff, M4Mesh *mesh)
{
	if (VS_SetupAppearance(eff)) {
		VS3D_DrawMesh(eff, mesh, eff->has_scale);
		VS_disable_texture(eff);
	}

	if (eff->appear && Node_GetTag(eff->appear)==TAG_X3D_Appearance) {
		X_Appearance *ap = (X_Appearance *)eff->appear;
		X_FillProperties *fp = ap->fillProperties ? (X_FillProperties *) ap->fillProperties : NULL;
		if (fp && fp->hatched) VS3D_HatchMesh(eff, mesh, eff->has_scale, fp->hatchStyle, fp->hatchColor);
	}
}



/*uncomment to disable frustum cull*/
//#define DISABLE_VIEW_CULL

#define DEBUG_VIEW_CULL		0

#if DEBUG_VIEW_CULL
static const char *szPlaneNames [] = 
{
	"Near", "Far", "Left", "Right", "Bottom", "Top"
};
#endif

Bool node_cull(RenderEffect3D *eff, M4BBox *bbox, Bool skip_near) 
{
	M4BBox b;
	Fixed irad, rad;
	M4Camera *cam;
	Bool do_sphere;
	u32 i, p_idx;
	SFVec3f cdiff, vertices[8];

	if (eff->cull_flag == CULL_INSIDE) return 1;
	assert(eff->cull_flag != CULL_OUTSIDE);

#ifdef DISABLE_VIEW_CULL
	eff->cull_flag = CULL_INSIDE;
	return 1;
#endif

	/*empty bounds*/
	if (!bbox->is_set) {
		eff->cull_flag = CULL_OUTSIDE;
#if DEBUG_VIEW_CULL
		fprintf(stdout, "CULL OUT (bbox not set)\n");
#endif
		return 0;
	}

	/*get bbox in world space*/
	b = *bbox;
	m4_mx_apply_bbox(&eff->model_matrix, &b);
	cam = eff->camera;

	/*if camera is inside bbox consider we intersect*/
	if (m4_bbox_point_inside(&b, &cam->position)) {
		eff->cull_flag = CULL_INTERSECTS;
#if DEBUG_VIEW_CULL
		fprintf(stdout, "CULL INTER (camera in box test)\n");
#endif
		return 1;
	}
	/*first check: sphere vs frustum sphere intersection, this will discard far objects quite fast*/
	m4_vec_diff(cdiff, cam->center, b.center);
	rad = b.radius + cam->radius;
	if (m4_vec_len(cdiff) > rad) {
		eff->cull_flag = CULL_OUTSIDE;
#if DEBUG_VIEW_CULL
		fprintf(stdout, "CULL OUT (sphere-sphere test)\n");
#endif
		return 0;
	}

	/*second check: sphere vs frustum planes intersection, if any intersection is detected switch 
	to n/p vertex check.*/
	rad = b.radius;
	irad = -b.radius;
	do_sphere = 1;

	/*skip near/far tests in ortho mode, and near in 3D*/
	i = (eff->camera->is_3D) ? (skip_near ? 1 : 0) : 2;
	for (; i<6; i++) {
		if (do_sphere) {
			Fixed d = m4_plane_get_distance(&cam->planes[i], &b.center);
			if (d<irad) {
				eff->cull_flag = CULL_OUTSIDE;
#if DEBUG_VIEW_CULL
				fprintf(stdout, "CULL OUT (sphere-planes test) plane %s\n", szPlaneNames[i]);
#endif
				return 0;
			}
			/*intersect, move to n-p vertex test*/
			if (d<rad) {
				/*get box vertices*/
				m4_bbox_get_vertices(b.min_edge, b.max_edge, vertices);
				do_sphere = 0;
			} else {
				continue;
			}
		}
		p_idx = cam->p_idx[i];
		/*check p-vertex: if not in plane, we're out (since p-vertex is the closest point to the plane)*/
		if (m4_plane_get_distance(&cam->planes[i], &vertices[p_idx])<0) {
			eff->cull_flag = CULL_OUTSIDE;
#if DEBUG_VIEW_CULL
			fprintf(stdout, "CULL OUT (p-vertex test) plane %s\n", szPlaneNames[i]);
#endif
			return 0;
		}
		/*check n-vertex: if not in plane, we're intersecting*/
		if (m4_plane_get_distance(&cam->planes[i], &vertices[7-p_idx])<0) {
			eff->cull_flag = CULL_INTERSECTS;
#if DEBUG_VIEW_CULL
			fprintf(stdout, "CULL INTER (n-vertex test) plane %s\n", szPlaneNames[i]);
#endif
			return 1;
		}
	}

	eff->cull_flag = CULL_INSIDE;
#if DEBUG_VIEW_CULL
	fprintf(stdout, "CULL IN (%s test)\n", do_sphere ? "sphere-planes" : "n-p vertex");
#endif
	return 1;
}

void VS_SetupProjection(RenderEffect3D *eff)
{
	SFNode *bindable;
	u32 mode = eff->traversing_mode;
	eff->traversing_mode = TRAVERSE_RENDER_BINDABLE;

	/*setup viewpoint (this directly modifies the frustum)*/
	bindable = ChainGetEntry(eff->viewpoints, 0);
	if (Bindable_GetIsBound(bindable)) {
		Node_Render(bindable, eff);
		eff->camera->had_viewpoint = 1;
	} else if (eff->camera->had_viewpoint) {
		if (eff->camera->is_3D) {
			SFVec3f pos, center;
			SFRotation r;
			Fixed fov = M4_PI/4;
			/*default viewpoint*/
			pos.x = pos.y = 0; pos.z = 10 * FIX_ONE;
			center.x = center.y = center.z = 0;
			r.q = r.x = r.z = 0; r.y = FIX_ONE;
			/*this takes care of pixelMetrics*/
			VS_ViewpointChange(eff, NULL, 0, fov, pos, r, center);
			/*initial vp compute, don't animate*/
			if (eff->camera->had_viewpoint == 2) {
				camera_stop_anim(eff->camera);
				camera_reset_viewpoint(eff->camera, 0);
			}
		} else {
			eff->camera->zoom = FIX_ONE;
			eff->camera->trans.x = eff->camera->trans.y = eff->camera->rot.x = eff->camera->rot.y = 0;
			eff->camera->flags &= ~CAM_HAS_VIEWPORT;
			eff->camera->flags |= CAM_IS_DIRTY;
		}
		eff->camera->had_viewpoint = 0;
	}

	camera_update(eff->camera);

	/*setup projection/modelview*/
	VS3D_SetMatrixMode(eff->surface, MAT_PROJECTION);
	VS3D_LoadMatrix(eff->surface, eff->camera->projection.m);
	VS3D_SetMatrixMode(eff->surface, MAT_MODELVIEW);
	VS3D_LoadMatrix(eff->surface, eff->camera->modelview.m);
	m4_mx_init(eff->model_matrix);

	eff->traversing_mode = mode;
}

void VS_InitRender(RenderEffect3D *eff)
{
	Bool in_layer;
	u32 mode;
	SFNode *bindable;

	in_layer = (eff->backgrounds != eff->surface->back_stack) ? 1 : 0;

	/*if not in layer, render navigation
	FIXME: we should update the nav info according to the world transform at the current viewpoint (vrml)*/
	eff->traversing_mode = TRAVERSE_RENDER_BINDABLE;
	bindable = eff->navigations ? ChainGetEntry(eff->navigations, 0) : NULL;
	if (Bindable_GetIsBound(bindable)) {
		Node_Render(bindable, eff);
		eff->camera->had_nav_info = 1;
	} else if (eff->camera->had_nav_info) {
		/*if no navigation specified, use default VRML one*/
		eff->camera->avatar_size.x = FLT2FIX(0.25f); eff->camera->avatar_size.y = FLT2FIX(1.6f); eff->camera->avatar_size.z = FLT2FIX(0.75f);
		eff->camera->visibility = 0;
		eff->camera->speed = FIX_ONE;
		/*not specified in the spec, but by default we forbid navigation in layer*/
		if (in_layer) {
			eff->camera->navigation_flags = NAV_HEADLIGHT;
			eff->camera->navigate_mode = M4_NavNone;
		} else {
			eff->camera->navigation_flags = NAV_ANY | NAV_HEADLIGHT;
			if (eff->camera->is_3D) {
				/*X3D is by default examine, VRML/MPEG4 is WALK*/
				eff->camera->navigate_mode = (eff->surface->render->root_is_3D==2) ? M4_NavExamine : M4_NavWalk;
			} else {
				eff->camera->navigate_mode = M4_NavNone;
			}
		}
		eff->camera->had_nav_info = 0;

		if (eff->is_pixel_metrics) {
			eff->camera->visibility = m4_mulfix(eff->camera->visibility, eff->min_hsize);
			eff->camera->avatar_size.x = m4_mulfix(eff->camera->avatar_size.x, eff->min_hsize);
			eff->camera->avatar_size.y = m4_mulfix(eff->camera->avatar_size.y, eff->min_hsize);
			eff->camera->avatar_size.z = m4_mulfix(eff->camera->avatar_size.z, eff->min_hsize);
		}
	}

	/*animate current camera - if returns TRUE draw next frame*/
	if (camera_animate(eff->camera)) 
		SR_Invalidate(eff->surface->render->compositor, NULL);

	VS3D_SetViewport(eff->surface, eff->camera->vp);

	/*setup projection*/
	VS_SetupProjection(eff);

	/*turn off depth buffer in 2D*/
	VS3D_SetDepthBuffer(eff->surface, eff->camera->is_3D);

	/*set headlight if any*/
	VS3D_SetHeadlight(eff->surface, (eff->camera->navigation_flags & NAV_HEADLIGHT) ? 1 : 0, eff->camera);

	/*setup background*/
	mode = eff->traversing_mode;
	eff->traversing_mode = TRAVERSE_RENDER_BINDABLE;
	bindable = ChainGetEntry(eff->backgrounds, 0);

	/*if in layer clear z buffer (even if background)*/
	if (in_layer) VS3D_ClearDepth(eff->surface);

	if (Bindable_GetIsBound(bindable)) {
		Node_Render(bindable, eff);
	}
	/*clear if not in layer*/
	else if (!in_layer) {
		SFColor col;
		col.red = col.green = col.blue = 0;
		/*if composite surface, clear with alpha = 0*/
		VS3D_ClearSurface(eff->surface, col, (eff->surface==eff->surface->render->surface) ? FIX_ONE : 0);
	}
	eff->traversing_mode = mode;
}

void VS_SetupEffects(VisualSurface *surface, RenderEffect3D *eff)
{
	eff->surface = surface;
	eff->camera = &surface->camera;
	eff->backgrounds = surface->back_stack;
	eff->viewpoints = surface->view_stack;
	eff->fogs = surface->fog_stack;
	eff->navigations = surface->navigation_stack;
	eff->camera->vp.x = eff->camera->vp.y = 0;
	eff->min_hsize = INT2FIX(MIN(surface->width, surface->height) / 2);
	assert(eff->min_hsize);


	/*main surface, set AR*/
	if (surface->render->surface==surface) {
		if (eff->surface->render->compositor->has_size_info) {
			eff->camera->vp.x = INT2FIX(eff->surface->render->out_x); 
			eff->camera->vp.y = INT2FIX(eff->surface->render->out_y);
			eff->camera->vp.width = INT2FIX(eff->surface->render->out_width);
			eff->camera->vp.height = INT2FIX(eff->surface->render->out_height);
			eff->camera->width = INT2FIX(eff->surface->width);
			eff->camera->height = INT2FIX(eff->surface->height);
		} else {
			/*WATCHOUT: this will give weird results if pixelMetrics is ON and scene.sizeInfo is not set*/
			eff->camera->width = eff->camera->vp.width = INT2FIX(eff->surface->render->out_width);
			eff->camera->height = eff->camera->vp.height = INT2FIX(eff->surface->render->out_height);
		}
	}
	/*composite surface, no AR*/
	else {
		eff->camera->vp.width = eff->camera->width = INT2FIX(surface->width);
		eff->camera->vp.height = eff->camera->height = INT2FIX(surface->height);
	}

	if (!eff->is_pixel_metrics) {
		if (eff->camera->height > eff->camera->width) {
			eff->camera->height = 2*m4_divfix(eff->camera->height , eff->camera->width);
			eff->camera->width = 2*FIX_ONE;
		} else {
			eff->camera->width = 2 * m4_divfix(eff->camera->width, eff->camera->height);
			eff->camera->height = 2 * FIX_ONE;
		}
	}
	/*setup surface bounds*/
	eff->bbox.max_edge.x = eff->camera->width / 2;
	eff->bbox.min_edge.x = -eff->bbox.max_edge.x;
	eff->bbox.max_edge.y = eff->camera->height / 2;
	eff->bbox.min_edge.y = -eff->bbox.max_edge.y;
	eff->bbox.max_edge.z = eff->bbox.min_edge.z = 0;
	eff->bbox.is_set = 1;
}


void VS_NodeRender(RenderEffect3D *eff, SFNode *root_node)
{
	SFNode *fog;
	if (!eff->camera || !eff->surface) return;

	VS_InitRender(eff);

	/*main surface, handle collisions*/
	if ((eff->surface==eff->surface->render->surface) && eff->camera->is_3D) 
		VS_DoCollisions(eff, NULL);
	

	/*setup fog*/
	fog = ChainGetEntry(eff->surface->fog_stack, 0);
	eff->traversing_mode = TRAVERSE_RENDER_BINDABLE;
	if (Bindable_GetIsBound(fog)) Node_Render(fog, eff);

	/*turn global lights on*/
	eff->traversing_mode = TRAVERSE_LIGHTING;
	Node_Render(root_node, eff);
	
	/*sort graph*/
	eff->traversing_mode = TRAVERSE_SORT;
	Node_Render(root_node, eff);

	/*and draw*/
	VS_FlushContexts(eff->surface, eff);

	/*and turn off lights*/
	VS3D_ClearAllLights(eff->surface);
}


void VS_RootRenderChildren(RenderEffect3D *eff, Chain *children)
{
	u32 i, count;
	M4Matrix mx;
	SFNode *child;

	if (!eff->camera || !eff->surface) return;

	/*don't reset given matrix (used by compositeTexture2D when rescaling to POW2 sizes)*/
	m4_mx_copy(mx, eff->model_matrix);

	VS_InitRender(eff);
	
	/**/
	m4_mx_copy(eff->model_matrix, mx);
	VS3D_MultMatrix(eff->surface, mx.m);

	count = ChainGetCount(children);

	eff->traversing_mode = TRAVERSE_LIGHTING;
	for (i=0; i<count; i++) {
		child = ChainGetEntry(children, i);
		Node_Render(child, eff);
	}

	eff->traversing_mode = TRAVERSE_SORT;
	for (i=0; i<count; i++) {
		child = ChainGetEntry(children, i);
		Node_Render(child, eff);
	}
	/*setup fog*/
	child = ChainGetEntry(eff->fogs, 0);
	eff->traversing_mode = TRAVERSE_RENDER_BINDABLE;
	if (Bindable_GetIsBound(child)) Node_Render(child, eff);

	/*and draw*/
	VS_FlushContexts(eff->surface, eff);

	/*and turn off lights*/
	VS3D_ClearAllLights(eff->surface);
}

static M4INLINE Bool appear_has_alpha(RenderEffect3D *eff, SFNode *node_to_draw)
{
	u32 tag;
	Bool is_mat3D;
	DrawableStack *stack;
	SFNode *mat = eff->appear ? ((M_Appearance *)eff->appear)->material : NULL;

	is_mat3D = 0;
	if (mat) {
		tag = Node_GetTag(mat);
		switch (tag) {
		/*for M2D: if filled & transparent we're transparent - otherwise we must check texture*/
		case TAG_MPEG4_Material2D:
			if (((M_Material2D *)mat)->filled && ((M_Material2D *)mat)->transparency) return 1;
			break;
		case TAG_MPEG4_Material:
		case TAG_X3D_Material:
			is_mat3D = 1;
			if ( ((M_Material *)mat)->transparency) return 1;
			break;
		case TAG_MPEG4_MaterialKey:
			return 1;
			break;
		}
	}

	/*check alpha texture in3D or with bitmap*/
	if (is_mat3D || (Node_GetTag(((M_Shape *)node_to_draw)->geometry)==TAG_MPEG4_Bitmap)) {
		TextureHandler *txh = R3D_GetTextureHandler(((M_Appearance *)eff->appear)->texture);
		if (txh && txh->transparent) return 1;
	}
	/*if we have color matrix, consider there's alpha*/
	if (eff->has_cmat) return 1;

	stack = Node_GetPrivate(((M_Shape *)node_to_draw)->geometry);
	if (stack && (stack->mesh->flags & MESH_HAS_ALPHA)) return 1;
	return 0;
}


void VS_RegisterContext(RenderEffect3D *eff, SFNode *node_to_draw, M4BBox *bounds, Bool is_shape)
{
	u32 i, count;
	M4BBox b;
	DLightContext *nl, *ol;
	Draw3DContext *ctx;

	assert(eff->traversing_mode == TRAVERSE_SORT);

	/*if 2D draw in declared order. Otherwise, if no alpha or node is a layer, draw directly*/
	if (!eff->camera->is_3D || !is_shape || !appear_has_alpha(eff, node_to_draw)) {
		eff->traversing_mode = TRAVERSE_RENDER;
		/*layout/form clipper, set it in world coords only*/
		if (eff->has_clip) {
			VS3D_PushMatrix(eff->surface);
			VS3D_ResetMatrix(eff->surface);
			VS3D_SetClipper2D(eff->surface, eff->clipper);
			VS3D_PopMatrix(eff->surface);
		}
		
		Node_Render(node_to_draw, eff);
	
		/*back to SORT*/
		eff->traversing_mode = TRAVERSE_SORT;

		if (eff->has_clip) VS3D_ResetClipper2D(eff->surface);
		return;
	}


	SAFEALLOC(ctx, sizeof(Draw3DContext));
	ctx->directional_lights = NewChain();
	ctx->node_to_draw = node_to_draw;

	ctx->has_scale = eff->has_scale;
	memcpy(&ctx->model_matrix, &eff->model_matrix, sizeof(M4Matrix));
	ctx->has_cmat = eff->has_cmat;
	if (ctx->has_cmat) memcpy(&ctx->color_mat, &eff->color_mat, sizeof(M4ColorMatrix));

	ctx->is_pixel_metrics = eff->is_pixel_metrics;
	ctx->split_text_idx = eff->split_text_idx;
	
	for (i=0; i<ChainGetCount(eff->local_lights); i++) {
		ol = ChainGetEntry(eff->local_lights, i);
		nl = malloc(sizeof(DLightContext));
		memcpy(nl, ol, sizeof(DLightContext));
		ChainAddEntry(ctx->directional_lights, nl);
	}
	ctx->clipper = eff->clipper;
	ctx->has_clipper = eff->has_clip;
	ctx->cull_flag = eff->cull_flag;

	if ((ctx->num_clip_planes = eff->num_clip_planes))
		memcpy(ctx->clip_planes, eff->clip_planes, sizeof(M4Plane)*MAX_USER_CLIP_PLANES);

	/*and insert from further to closest*/
	b = *bounds;
	m4_mx_apply_bbox(&ctx->model_matrix, &b);
	m4_mx_apply_bbox(&eff->camera->modelview, &b);
	ctx->zmax = b.max_edge.z;

	/*we don't need an exact sorting, as long as we keep transparent nodes above  -note that for
	speed purposes we store in reverse-z transparent nodes*/
	count = ChainGetCount(eff->surface->alpha_nodes_to_draw);
	for (i=0; i<count; i++) {
		Draw3DContext *next = ChainGetEntry(eff->surface->alpha_nodes_to_draw, i);
		if (next->zmax>ctx->zmax) {
			ChainInsertEntry(eff->surface->alpha_nodes_to_draw, ctx, i);
			return;
		}
	}
	ChainAddEntry(eff->surface->alpha_nodes_to_draw, ctx);
}

void VS_FlushContexts(VisualSurface *surf, RenderEffect3D *eff)
{
	u32 i, idx, count;

	eff->traversing_mode = TRAVERSE_RENDER;

	count = ChainGetCount(surf->alpha_nodes_to_draw);
	for (idx=0; idx<count; idx++) {
		Draw3DContext *ctx = ChainGetEntry(surf->alpha_nodes_to_draw, idx);

		VS3D_PushMatrix(surf);

		/*apply directional lights*/
		eff->local_light_on = 1;
		for (i=0; i<ChainGetCount(ctx->directional_lights); i++) {
			DLightContext *dl = ChainGetEntry(ctx->directional_lights, i);
			VS3D_PushMatrix(surf);
			VS3D_MultMatrix(surf, dl->light_matrix.m);
			Node_Render(dl->dlight, eff);
			VS3D_PopMatrix(surf);
		}
		
		/*clipper, set it in world coords only*/
		if (ctx->has_clipper) {
			VS3D_PushMatrix(surf);
			VS3D_ResetMatrix(surf);
			VS3D_SetClipper2D(surf, ctx->clipper);
			VS3D_PopMatrix(surf);
		}

		/*clip planes, set it in world coords only*/
		for (i=0; i<ctx->num_clip_planes; i++)
			VS3D_SetClipPlane(surf, ctx->clip_planes[i]);

		/*restore effect*/
		VS3D_MultMatrix(surf, ctx->model_matrix.m);
		eff->has_scale = ctx->has_scale;
		memcpy(&eff->model_matrix, &ctx->model_matrix, sizeof(M4Matrix));
		if ((eff->has_cmat = ctx->has_cmat)) memcpy(&eff->color_mat, &ctx->color_mat, sizeof(M4ColorMatrix));
		eff->split_text_idx = ctx->split_text_idx;
		eff->is_pixel_metrics = ctx->is_pixel_metrics;
		/*restore cull flag in case we're completely inside (avoids final frustum/AABB tree culling)*/
		eff->cull_flag = ctx->cull_flag;

		Node_Render(ctx->node_to_draw, eff);
		
		/*reset directional lights*/
		eff->local_light_on = 0;
		for (i=ChainGetCount(ctx->directional_lights); i>0; i--) {
			DLightContext *dl = ChainGetEntry(ctx->directional_lights, i-1);
			Node_Render(dl->dlight, eff);
			free(dl);
		}

		if (ctx->has_clipper) VS3D_ResetClipper2D(surf);
		for (i=0; i<ctx->num_clip_planes; i++) VS3D_ResetClipPlane(surf);

		VS3D_PopMatrix(surf);

		/*and destroy*/
		DeleteChain(ctx->directional_lights);
		free(ctx);
	}
	ChainReset(eff->surface->alpha_nodes_to_draw);
}


static void reset_collide_cursor(Render3D *sr)
{
	if (sr->last_cursor == M4CursorCollide) {
		M4Event evt;
		sr->last_cursor = evt.cursor.cursor_type = M4CursorNormal;
		evt.type = M4E_SET_CURSOR;
		sr->compositor->video_out->PushEvent(sr->compositor->video_out, &evt);
	}
}


#define DEBUG_PICKING	0

Bool VS_ExecuteEvent(VisualSurface *surf, RenderEffect3D *eff, M4UserEvent *ev, Chain *node_list)
{
	Fixed x, y;
	SFVec3f start, end;
	SensorHandler *hs, *hs_grabbed;
	SFVec4f res;
	Fixed in_x, in_y;
	Chain *tmp;
	u32 i, count, stype;
	Render3D *sr = surf->render;

	count = 0;
	if ((ev->event_type > M4E_LEFTUP) || sr->nav_is_grabbed) return 0;

	eff->camera = &surf->camera;
	eff->surface = surf;
	eff->backgrounds = surf->back_stack;
	eff->viewpoints = surf->view_stack;
	eff->fogs = surf->fog_stack;
	eff->navigations = surf->navigation_stack;
	eff->min_hsize = INT2FIX(MIN(surf->width, surf->height)) / 2;
	/*setup projection*/
	VS_SetupProjection(eff);

	eff->traversing_mode = TRAVERSE_PICK;
	eff->collect_layer = NULL;
	sr->hit_info.appear = NULL;

	x = INT2FIX(ev->mouse.x); y = INT2FIX(ev->mouse.y);
	/*main surface with AR*/
	if ((sr->surface == surf) && sr->compositor->has_size_info) {
		Fixed scale = m4_divfix(INT2FIX(surf->width), INT2FIX(sr->out_width));
		x = m4_mulfix(x, scale);
		scale = m4_divfix(INT2FIX(surf->height), INT2FIX(sr->out_height));
		y = m4_mulfix(y, scale);
	}

	start.z = surf->camera.z_near;
	end.z = surf->camera.z_far;
	if (!eff->camera->is_3D && !eff->is_pixel_metrics) {
		start.x = end.x = m4_divfix(x, eff->min_hsize); 
		start.y = end.y = m4_divfix(y, eff->min_hsize);
	} else {
		start.x = end.x = x;
		start.y = end.y = y;
	}

	/*unproject to world coords*/
	in_x = 2*x/ (s32) surf->width;
	in_y = 2*y/ (s32) surf->height;
	
	res.x = in_x; res.y = in_y; res.z = -FIX_ONE; res.q = FIX_ONE;
	m4_mx_apply_vec_4x4(&surf->camera.unprojection, &res);
	if (!res.q) return 0;
	start.x = m4_divfix(res.x, res.q); start.y = m4_divfix(res.y, res.q); start.z = m4_divfix(res.z, res.q);

	res.x = in_x; res.y = in_y; res.z = FIX_ONE; res.q = FIX_ONE;
	m4_mx_apply_vec_4x4(&surf->camera.unprojection, &res);
	if (!res.q) return 0;
	end.x = m4_divfix(res.x, res.q); end.y = m4_divfix(res.y, res.q); end.z = m4_divfix(res.z, res.q);

	eff->ray = m4_ray(start, end);
	/*also update hit info world ray in case we have a grabbed sensor with mouse off*/
	sr->hit_info.world_ray = eff->ray;

#if DEBUG_PICKING
	{
	SFVec3f c2d;
	fprintf(stdout, "cast ray\n\tOrigin %.4f %.4f %.4f - End %.4f %.4f %.4f\n\tDir %.4f %.4f %.4f\n", 
		FIX2FLT(eff->ray.orig.x), FIX2FLT(eff->ray.orig.y), FIX2FLT(eff->ray.orig.z),
		FIX2FLT(end.x), FIX2FLT(end.y), FIX2FLT(end.z),
		FIX2FLT(eff->ray.dir.x), FIX2FLT(eff->ray.dir.y), FIX2FLT(eff->ray.dir.z));
	if (R3D_Get2DPlaneIntersection(&eff->ray, &c2d))
		fprintf(stdout, "\tAt z=0: %.4f %.4f %.4f\n", FIX2FLT(c2d.x), FIX2FLT(c2d.y), FIX2FLT(c2d.z));
	}
#endif

	if (!sr->is_grabbed) sr->hs_grab = NULL;

	sr->sq_dist = 0;
	ChainReset(sr->sensors);

	if (!node_list) {
		Node_Render(SG_GetRootNode(sr->compositor->scene), eff);
	} else {
		for (i=0; i<ChainGetCount(node_list); i++) {
			SFNode *child = ChainGetEntry(node_list, i);
			Node_Render(child, eff);
		}
	}
	ChainReset(eff->sensors);

	hs_grabbed = NULL;
	stype = M4CursorNormal;
	count = ChainGetCount(sr->prev_sensors);
	for (i=0; i<count; i++) {
		hs = ChainGetEntry(sr->prev_sensors, i);
		if (ChainFindEntry(sr->sensors, hs) < 0) {
			/*that's a bit ugly but we need coords if pointer out of the shape when sensor grabbed...*/
			if (sr->is_grabbed) {
				hs_grabbed = hs;
				sr->hs_grab = hs;
			} else {
				hs->OnUserEvent(hs, 0, ev->event_type, &sr->hit_info);
			}
		}
	}

	count = ChainGetCount(sr->sensors);
	for (i=0; i<count; i++) {
		hs = ChainGetEntry(sr->sensors, i);
		hs->OnUserEvent(hs, 1, ev->event_type, &sr->hit_info);
		stype = Node_GetTag(hs->owner);
		if (hs==hs_grabbed) hs_grabbed = NULL;
	}
	/*switch sensors*/
	tmp = sr->sensors;
	sr->sensors = sr->prev_sensors;
	sr->prev_sensors = tmp;

	/*check if we have a grabbed sensor*/
	if (hs_grabbed) {
		hs_grabbed->OnUserEvent(hs_grabbed, 0, ev->event_type, &sr->hit_info);
		ChainReset(sr->prev_sensors);
		ChainAddEntry(sr->prev_sensors, hs_grabbed);
		stype = Node_GetTag(hs_grabbed->owner);
	}

	/*composite texture*/
	if (!stype && sr->hit_info.appear) 
		return r3d_handle_composite_event(sr, ev);

	/*and set cursor*/
	if (sr->last_cursor != M4CursorCollide) {
		switch (stype) {
		case TAG_MPEG4_Anchor: 
		case TAG_X3D_Anchor: 
			stype = M4CursorAnchor; 
			break;
		case TAG_MPEG4_PlaneSensor2D:
		case TAG_MPEG4_PlaneSensor: 
		case TAG_X3D_PlaneSensor: 
			stype = M4CursorPlane; break;
		case TAG_MPEG4_CylinderSensor: 
		case TAG_X3D_CylinderSensor: 
		case TAG_MPEG4_DiscSensor:
		case TAG_MPEG4_SphereSensor:
		case TAG_X3D_SphereSensor:
			stype = M4CursorRotate; break;
		case TAG_MPEG4_ProximitySensor2D:
		case TAG_MPEG4_ProximitySensor:
		case TAG_X3D_ProximitySensor:
			stype = M4CursorProximity; break;
		case TAG_MPEG4_TouchSensor: 
		case TAG_X3D_TouchSensor: 
			stype = M4CursorTouch; break;
		default: stype = M4CursorNormal; break;
		}
		if ((stype != M4CursorNormal) || (sr->last_cursor != stype)) {
			M4Event evt;
			evt.type = M4E_SET_CURSOR;
			evt.cursor.cursor_type = stype;
			sr->compositor->video_out->PushEvent(sr->compositor->video_out, &evt);
			sr->last_cursor = stype;
		}
	} else {
		reset_collide_cursor(sr);
	}
	return count ? 1 : 0;
}

void drawable_do_pick(SFNode *n, RenderEffect3D *eff) 
{
	SFVec3f local_pt, world_pt, vdiff;
	SFVec3f hit_normal;
	SFVec2f text_coords;
	u32 i, count;
	Fixed sqdist;
	Bool node_is_over;
	Render3D *sr;
	M4Matrix mx;
	M4Ray r;
	DrawableStack *st = Node_GetPrivate(n);
	u32 cull_bckup = eff->cull_flag;
	if (!st) return;

	count = ChainGetCount(eff->sensors);

	sr = eff->surface->render;

	node_is_over = 0;
	if (!node_cull(eff, &st->mesh->bounds, 0)) {
		eff->cull_flag = cull_bckup;
		return;
	}
	eff->cull_flag = cull_bckup;
	r = eff->ray;
	m4_mx_copy(mx, eff->model_matrix);
	m4_mx_inverse(&mx);
	m4_mx_apply_ray(&mx, &r);

	/*if we already have a hit point don't check anything below...*/
	if (sr->sq_dist && !sr->hs_grab && !eff->collect_layer) {
		M4Plane p;
		SFVec3f hit = sr->hit_info.world_point;
		m4_mx_apply_vec(&mx, &hit);
		p.normal = r.dir;
		p.d = -1 * m4_vec_dot(p.normal, hit);
		if (m4_box_plane_relation(&st->mesh->bounds, &p) == M4_BOX_FRONT) {
#if DEBUG_PICKING
			fprintf(stdout, "bounding box of node %s (DEF %s) below current hit point - skipping\n", Node_GetName(n), Node_GetDefName(n));
#endif
			return;
		}
	}
	if (!st->IntersectWithRay) {
		node_is_over = m4_mesh_intersect_ray(st->mesh, &r, &local_pt, &hit_normal, &text_coords);
	} else {
		node_is_over = st->IntersectWithRay(st->owner, &r, &local_pt, &hit_normal, &text_coords);
	}
	if (!node_is_over) {
		/*store matrices for grabed sensors*/
		if (0 && sr->hs_grab && (ChainFindEntry(eff->sensors, sr->hs_grab)>=0) ) {
			m4_mx_copy(sr->hit_info.world_to_local, eff->model_matrix);
			m4_mx_copy(sr->hit_info.local_to_world, mx);
#if DEBUG_PICKING
			fprintf(stdout, "found grabed sensor - storing matrices\n");
#endif
		}
		return;
	}

	/*check distance from user and keep the closest hitpoint*/
	world_pt = local_pt;
	m4_mx_apply_vec(&eff->model_matrix, &world_pt);

	for (i=0; i<eff->num_clip_planes; i++) {
		if (m4_plane_get_distance(&eff->clip_planes[i], &world_pt) < 0) {
#if DEBUG_PICKING
			fprintf(stdout, "node %s (def %s) is not in clipper half space\n", Node_GetName(n), Node_GetDefName(n));
#endif
			return;
		}
	}

	m4_vec_diff(vdiff, world_pt, eff->ray.orig);
	sqdist = m4_vec_lensq(vdiff);
	if (sr->sq_dist && (sr->sq_dist+FIX_EPSILON<sqdist)) {
#if DEBUG_PICKING
		fprintf(stdout, "node %s (def %s) is farther (%g) than current pick (%g)\n", Node_GetName(n), Node_GetDefName(n), FIX2FLT(sqdist), FIX2FLT(sr->sq_dist));
#endif
		return;
	}

	sr->sq_dist = sqdist;
	ChainReset(sr->sensors);
	for (i=0; i<count; i++) {
		ChainAddEntry(sr->sensors, ChainGetEntry(eff->sensors, i));
	}

	m4_mx_copy(sr->hit_info.world_to_local, eff->model_matrix);
	m4_mx_copy(sr->hit_info.local_to_world, mx);
	sr->hit_info.local_point = local_pt;
	sr->hit_info.world_point = world_pt;
	sr->hit_info.world_ray = eff->ray;
	sr->hit_info.hit_normal = hit_normal;
	sr->hit_info.hit_texcoords = text_coords;
	if (r3d_has_composite_texture(eff->appear)) {
		sr->hit_info.appear = eff->appear;
	} else {
		sr->hit_info.appear = NULL;
	}
#if DEBUG_PICKING
	fprintf(stdout, "node %s (def %s) is under mouse - hit %g %g %g\n", Node_GetName(n), Node_GetDefName(n),
			FIX2FLT(world_pt.x), FIX2FLT(world_pt.y), FIX2FLT(world_pt.z));
#endif

}


#define DEBUG_COLLISION		0

void VS_DoCollisions(RenderEffect3D *eff, Chain *node_list)
{
	SFVec3f n, pos, dir;
	Bool go;
	u32 i;
	Fixed diff, pos_diff;

	assert(eff->surface && eff->camera);
	/*don't collide on VP animations or when modes discard collision*/
	if ((eff->camera->anim_len && !eff->camera->jumping) || !eff->surface->render->collide_mode || (eff->camera->navigate_mode>=M4_NavExamine)) {
		/*reset ground flag*/
		eff->camera->last_had_ground = 0;
		/*and avoid reseting move at next collision change*/
		eff->camera->last_pos = eff->camera->position;
		return;
	}
	/*don't collide if not moved*/
	if (m4_vec_equal(eff->camera->position, eff->camera->last_pos)) {
		reset_collide_cursor(eff->surface->render);
		return;
	}
	eff->traversing_mode = TRAVERSE_COLLIDE;
	eff->camera->collide_flags = 0;
	eff->camera->collide_dist = FIX_MAX;
	eff->camera->ground_dist = FIX_MAX;
	if ((eff->camera->navigate_mode==M4_NavWalk) && eff->surface->render->gravity_on) eff->camera->collide_flags |= CF_DO_GRAVITY;

	m4_vec_diff(dir, eff->camera->position, eff->camera->last_pos);
	pos_diff = m4_vec_len(dir);
	m4_vec_norm(&dir);
	pos = eff->camera->position;
	
	diff = 0;
	go = 1;
	eff->camera->last_had_col = 0;
	/*some explanation: the current collision detection algo only checks closest distance
	to objects, but doesn't attempt to track object cross during a move. If we step more than
	the collision detection size, we may cross an object without detecting collision. we thus
	break the move in max collision size moves*/
	while (go) {
		if (pos_diff>eff->camera->avatar_size.x) {
			pos_diff-=eff->camera->avatar_size.x;
			diff += eff->camera->avatar_size.x;
		} else {
			diff += pos_diff;
			go = 0;
		}
		n = m4_vec_scale(dir, diff);	
		m4_vec_add(eff->camera->position, eff->camera->last_pos, n);	
		if (!node_list) {
			Node_Render(SG_GetRootNode(eff->surface->render->compositor->scene), eff);
		} else {
			for (i=0; i<ChainGetCount(node_list); i++) {
				SFNode *child = ChainGetEntry(node_list, i);
				Node_Render(child, eff);
			}
		}
		if (eff->camera->collide_flags & CF_COLLISION) break;
		eff->camera->collide_flags &= ~CF_DO_GRAVITY;
	}

	/*gravity*/
	if (eff->camera->collide_flags & CF_GRAVITY) {
		diff = eff->camera->ground_dist - eff->camera->avatar_size.y;
		if (eff->camera->last_had_ground && (-diff>eff->camera->avatar_size.z)) {
#if DEBUG_COLLISION
			fprintf(stdout, "Obstacle detected - too high (dist %g)\n", diff);
#endif
			eff->camera->position = eff->camera->last_pos;
			eff->camera->flags |= CAM_IS_DIRTY;
		} else {
			if ((eff->camera->jumping && fabs(diff)>eff->camera->dheight) 
				|| (!eff->camera->jumping && (ABS(diff)>FIX_ONE/1000) )) {
				eff->camera->last_had_ground = 1;
				n = m4_vec_scale(eff->camera->up, -diff);
#if DEBUG_COLLISION
				fprintf(stdout, "Ground detected camera position: %g %g %g - offset: %g %g %g (dist %g)\n", eff->camera->position.x, eff->camera->position.y, eff->camera->position.z, n.x, n.y, n.z, diff);
#endif

				m4_vec_add(eff->camera->position, eff->camera->position, n);
				m4_vec_add(eff->camera->target, eff->camera->target, n);
				m4_vec_add(eff->camera->last_pos, eff->camera->position, n);
				eff->camera->flags |= CAM_IS_DIRTY;
			}
		}
	}
	/*collsion found*/
	if (eff->camera->collide_flags & CF_COLLISION) {
		if (eff->surface->render->last_cursor != M4CursorCollide) {
			M4Event evt;
			eff->camera->last_had_col = 1;
			evt.type = M4E_SET_CURSOR;
			eff->surface->render->last_cursor = evt.cursor.cursor_type = M4CursorCollide;
			eff->surface->render->compositor->video_out->PushEvent(eff->surface->render->compositor->video_out, &evt);
		}

		/*regular collision*/
		if (eff->surface->render->collide_mode==M4_Collide) {
			eff->camera->position = eff->camera->last_pos;
			eff->camera->flags |= CAM_IS_DIRTY;
#if DEBUG_COLLISION
			fprintf(stdout, "Collision detected - restoring previous pos\n");
#endif
		} else {
			/*camera displacement collision*/
			if (eff->camera->collide_dist) {
#if DEBUG_COLLISION
				if (eff->camera->collide_dist>=eff->camera->avatar_size.x)
					fprintf(stdout, "WARNING: Collision distance %g greater than avatar collide size %g\n", eff->camera->collide_dist, eff->camera->avatar_size.x);
#endif
				/*safety check due to precision, always stay below collide dist*/
				if (eff->camera->collide_dist>=eff->camera->avatar_size.x) eff->camera->collide_dist = eff->camera->avatar_size.x;

				m4_vec_diff(n, eff->camera->position, eff->camera->collide_point);
				m4_vec_norm(&n);
				n = m4_vec_scale(n, eff->camera->avatar_size.x - eff->camera->collide_dist);
#if DEBUG_COLLISION
				fprintf(stdout, "offseting camera: position: %g %g %g - offset: %g %g %g\n", eff->camera->position.x, eff->camera->position.y, eff->camera->position.z, n.x, n.y, n.z);
#endif
				m4_vec_add(eff->camera->position, eff->camera->position, n);
				m4_vec_add(eff->camera->target, eff->camera->target, n);
			} else {
#if DEBUG_COLLISION
				fprintf(stdout, "Collision detected and camera on hit point - restoring previous pos\n");
#endif
				eff->camera->position = eff->camera->last_pos;
			}
			eff->camera->last_pos = eff->camera->position;
			eff->camera->flags |= CAM_IS_DIRTY;
		}
	} else {
		reset_collide_cursor(eff->surface->render);
		eff->camera->last_pos = eff->camera->position;
#if DEBUG_COLLISION
		fprintf(stdout, "no collision found\n");
#endif
	}

	if (eff->camera->flags & CAM_IS_DIRTY) VS_SetupProjection(eff);
}

void drawable_do_collide(SFNode *node, RenderEffect3D *eff)
{
	SFVec3f pos, v1, v2, collide_pt, last_pos;
	Fixed dist, m_dist;
	M4Matrix mx;
	u32 ntag, cull_backup;
	DrawableStack *st = Node_GetPrivate(node);
	if (!st) return;

	/*no collision with lines & points*/
	if (st->mesh->mesh_type != MESH_TRIANGLES) return;
	/*no collision with text (vrml)*/
	ntag = Node_GetTag(node);
	if ((ntag==TAG_MPEG4_Text) || (ntag==TAG_X3D_Text)) return;


	/*cull but don't use near plane to detect objects behind us*/
	cull_backup = eff->cull_flag;
	if (!node_cull(eff, &st->mesh->bounds, 1)) {
		eff->cull_flag = cull_backup;
		return;
	}
	eff->cull_flag = cull_backup;

	/*use up & front to get an average size of the collision dist in this space*/
	pos = eff->camera->position;
	last_pos = eff->camera->last_pos;
	v1 = camera_get_target_dir(eff->camera);
	v1 = m4_vec_scale(v1, eff->camera->avatar_size.x);
	m4_vec_add(v1, v1, pos);
	v2 = camera_get_right_dir(eff->camera);
	v2 = m4_vec_scale(v2, eff->camera->avatar_size.x);
	m4_vec_add(v2, v2, pos);

	m4_mx_copy(mx, eff->model_matrix);
	m4_mx_inverse(&mx);

	m4_mx_apply_vec(&mx, &pos);
	m4_mx_apply_vec(&mx, &last_pos);
	m4_mx_apply_vec(&mx, &v1);
	m4_mx_apply_vec(&mx, &v2);

	m4_vec_diff(v1, v1, pos);
	m4_vec_diff(v2, v2, pos);
	dist = m4_vec_len(v1);
	m_dist = m4_vec_len(v2);
	if (dist<m_dist) m_dist = dist;

	/*check for any collisions*/
	if (m4_mesh_closest_face(st->mesh, pos, m_dist, &collide_pt)) {
#if DEBUG_COLLISION
		m4_vec_diff(v1, pos, collide_pt);
		m4_vec_norm(&v1);
#endif
		/*get transformed hit*/
		m4_mx_apply_vec(&eff->model_matrix, &collide_pt);
		m4_vec_diff(v2, eff->camera->position, collide_pt);
		dist = m4_vec_len(v2);
		if (dist<eff->camera->collide_dist) {
			eff->camera->collide_dist = dist;
			eff->camera->collide_flags |= CF_COLLISION;
			eff->camera->collide_point = collide_pt;
#if DEBUG_COLLISION
			fprintf(stdout, "Collision at %g %g %g (WC) - dist (%g) - local normal %g %g %g\n", 
				eff->camera->collide_point.x, eff->camera->collide_point.y, eff->camera->collide_point.z, 
				dist, 
				v1.x, v1.y, v1.z);
#endif
		} 
		
#if DEBUG_COLLISION
		else fprintf(stdout, "Existing collision (dist %g) closer than current collsion (dist %g)\n", eff->camera->collide_dist, dist);
#endif
	}

	if (eff->camera->collide_flags & CF_DO_GRAVITY) {
		M4Ray r;
		Bool intersect;
		r.orig = eff->camera->position;
		r.dir = m4_vec_scale(eff->camera->up, -1);
		m4_mx_apply_ray(&mx, &r);

		if (!st->IntersectWithRay) {
			intersect = m4_mesh_intersect_ray(st->mesh, &r, &collide_pt, &v1, NULL);
		} else {
			intersect = st->IntersectWithRay(st->owner, &r, &collide_pt, &v1, NULL);
		}

		if (intersect) {
			m4_mx_apply_vec(&eff->model_matrix, &collide_pt);
			m4_vec_diff(v2, eff->camera->position, collide_pt);
			dist = m4_vec_len(v2);
			if (dist<eff->camera->ground_dist) {
				eff->camera->ground_dist = dist;
				eff->camera->collide_flags |= CF_GRAVITY;
				eff->camera->ground_point = collide_pt;
#if DEBUG_COLLISION
				fprintf(stdout, "Ground found at %g %g %g (WC) - dist %g - local normal %g %g %g\n", 
					eff->camera->ground_point.x, eff->camera->ground_point.y, eff->camera->ground_point.z, 
					dist, 
					v1.x, v1.y, v1.z);
#endif
			} 
#if DEBUG_COLLISION
			else fprintf(stdout, "Existing ground (dist %g) closer than current (dist %g)\n", eff->camera->ground_dist, dist);
#endif
		} 
	}
}

