From 10a42a2a078ecd51b32d35a5cc873ed8176f5a07 Mon Sep 17 00:00:00 2001
From: divverent <divverent@d7cf8633-e32d-0410-b094-e92efae38249>
Date: Sun, 28 Feb 2010 19:35:36 +0000
Subject: [PATCH] WarpZones: - entities with a camera_transform function field 
  arguments: vector origin, vector angles   global input: v_forward, v_right,
 v_up = camera orientation   return value: new origin   global output:
 v_forward, v_right, v_up = new camera orientation   global output:
 trace_endpos = origin on "remote side" for culling purposes - on server, only
 the trace_endpos return value is used, and '0 0 0' angles are passed, and
 only the trace_endpos is used (this may be subject to change) - a warpzone is
 an above described entity that contains a surface of a shader that uses the
 keywords dp_camera AND dp_refract - a camera is an above described entity
 that contains a surface of a shader that JUST uses the keyword dp_camera - in
 case of a camera, preferably the camera_transform function shall not use the
 input values (except for use for certain special effects) - rendering of
 these warpzones is not recursive (yet) - the warpzones do not have any impact
 on gameplay - QC support code is needed both on client and server - example
 support code will be committed to Nexuiz SVN - the interface is not stable
 yet and may be subject to change

git-svn-id: svn://svn.icculus.org/twilight/trunk/darkplaces@10035 d7cf8633-e32d-0410-b094-e92efae38249
::stable-branch::merge=f2467d484c38a13d23bb92f91c19a9d6a71f7bec
---
 cl_screen.c    |   4 +-
 csprogs.c      |  53 ++++++++++++++++-
 csprogs.h      |   2 +
 gl_rmain.c     | 159 +++++++++++++++++++++++++++++++++++++++++--------
 gl_rsurf.c     |  17 ++++--
 model_brush.c  |   4 +-
 model_brush.h  |   2 +
 model_shared.c |   8 ++-
 model_shared.h |   2 +
 progsvm.h      |   2 +
 prvm_edict.c   |   2 +
 quakedef.h     |   4 +-
 r_shadow.c     |  40 ++++++++-----
 render.h       |   3 +
 sv_main.c      |  86 +++++++++++++++++++++++++-
 sv_phys.c      |  19 +++---
 16 files changed, 341 insertions(+), 66 deletions(-)

diff --git a/cl_screen.c b/cl_screen.c
index 8e727ecf..f561a457 100644
--- a/cl_screen.c
+++ b/cl_screen.c
@@ -1353,8 +1353,8 @@ static void R_Envmap_f (void)
 	r_refdef.view.useperspective = true;
 	r_refdef.view.isoverlay = false;
 
-	r_refdef.view.frustum_x = tan(90 * M_PI / 360.0);
-	r_refdef.view.frustum_y = tan(90 * M_PI / 360.0);
+	r_refdef.view.frustum_x = 1; // tan(45 * M_PI / 180.0);
+	r_refdef.view.frustum_y = 1; // tan(45 * M_PI / 180.0);
 
 	buffer1 = (unsigned char *)Mem_Alloc(tempmempool, size * size * 3);
 	buffer2 = (unsigned char *)Mem_Alloc(tempmempool, size * size * 3);
diff --git a/csprogs.c b/csprogs.c
index 966a12ce..d8a39675 100644
--- a/csprogs.c
+++ b/csprogs.c
@@ -174,7 +174,7 @@ qboolean CSQC_AddRenderEdict(prvm_edict_t *ed, int edictnum)
 			return false;
 		entrender = cl.csqcrenderentities + edictnum;
 		r_refdef.scene.entities[r_refdef.scene.numentities++] = entrender;
-		entrender->entitynumber = edictnum;
+		entrender->entitynumber = edictnum + MAX_EDICTS;
 		//entrender->shadertime = 0; // shadertime was set by spawn()
 		entrender->flags = 0;
 		entrender->alpha = 1;
@@ -1045,3 +1045,54 @@ qboolean CL_VM_GetEntitySoundOrigin(int entnum, vec3_t out)
 
 	return r;
 }
+
+qboolean CL_VM_TransformView(int entnum, matrix4x4_t *viewmatrix, mplane_t *clipplane, vec3_t visorigin)
+{
+	qboolean ret = false;
+	prvm_edict_t *ed;
+	prvm_eval_t *val, *valforward, *valright, *valup, *valendpos;
+	vec3_t forward, left, up, origin, ang;
+	matrix4x4_t mat, matq;
+
+	CSQC_BEGIN
+		ed = PRVM_EDICT_NUM(entnum);
+		// camera:
+		//   camera_transform
+		if((val = PRVM_EDICTFIELDVALUE(ed, prog->fieldoffsets.camera_transform)) && val->function)
+		{
+			ret = true;
+			if(viewmatrix || clipplane || visorigin)
+			{
+				valforward = PRVM_GLOBALFIELDVALUE(prog->globaloffsets.v_forward);
+				valright = PRVM_GLOBALFIELDVALUE(prog->globaloffsets.v_right);
+				valup = PRVM_GLOBALFIELDVALUE(prog->globaloffsets.v_up);
+				valendpos = PRVM_GLOBALFIELDVALUE(prog->globaloffsets.trace_endpos);
+				if(valforward && valright && valup && valendpos)
+				{
+					Matrix4x4_ToVectors(viewmatrix, forward, left, up, origin);
+					AnglesFromVectors(ang, forward, up, false);
+					prog->globals.client->time = cl.time;
+					prog->globals.client->self = entnum;
+					VectorCopy(origin, PRVM_G_VECTOR(OFS_PARM0));
+					VectorCopy(ang, PRVM_G_VECTOR(OFS_PARM1));
+					VectorCopy(forward, valforward->vector);
+					VectorScale(left, -1, valright->vector);
+					VectorCopy(up, valup->vector);
+					VectorCopy(origin, valendpos->vector);
+					PRVM_ExecuteProgram(val->function, "QC function e.camera_transform is missing");
+					VectorCopy(PRVM_G_VECTOR(OFS_RETURN), origin);
+					VectorCopy(valforward->vector, forward);
+					VectorScale(valright->vector, -1, left);
+					VectorCopy(valup->vector, up);
+					VectorCopy(valendpos->vector, visorigin);
+					Matrix4x4_Invert_Full(&mat, viewmatrix);
+					Matrix4x4_FromVectors(viewmatrix, forward, left, up, origin);
+					Matrix4x4_Concat(&matq, viewmatrix, &mat);
+					Matrix4x4_TransformPositivePlane(&matq, clipplane->normal[0], clipplane->normal[1], clipplane->normal[2], clipplane->dist, &clipplane->normal[0]);
+				}
+			}
+		}
+	CSQC_END
+
+	return ret;
+}
diff --git a/csprogs.h b/csprogs.h
index 6e2e517c..bb0a3fb8 100644
--- a/csprogs.h
+++ b/csprogs.h
@@ -64,4 +64,6 @@ qboolean MakeDownloadPacket(const char *filename, unsigned char *data, size_t le
 
 qboolean CL_VM_GetEntitySoundOrigin(int entnum, vec3_t out);
 
+qboolean CL_VM_TransformView(int entnum, matrix4x4_t *viewmatrix, mplane_t *clipplane, vec3_t visorigin);
+
 #endif
diff --git a/gl_rmain.c b/gl_rmain.c
index 23b4035d..ccc6e54d 100644
--- a/gl_rmain.c
+++ b/gl_rmain.c
@@ -25,6 +25,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 #include "polygon.h"
 #include "image.h"
 #include "ft2.h"
+#include "csprogs.h"
 
 mempool_t *r_main_mempool;
 rtexturepool_t *r_main_texturepool;
@@ -75,6 +76,7 @@ cvar_t r_showdisabledepthtest = {0, "r_showdisabledepthtest", "0", "disables dep
 cvar_t r_drawportals = {0, "r_drawportals", "0", "shows portals (separating polygons) in world interior in quake1 maps"};
 cvar_t r_drawentities = {0, "r_drawentities","1", "draw entities (doors, players, projectiles, etc)"};
 cvar_t r_drawviewmodel = {0, "r_drawviewmodel","1", "draw your weapon model"};
+cvar_t r_drawexteriormodel = {0, "r_drawexteriormodel","1", "draw your player model (e.g. in chase cam, reflections)"};
 cvar_t r_cullentities_trace = {0, "r_cullentities_trace", "1", "probabistically cull invisible entities"};
 cvar_t r_cullentities_trace_samples = {0, "r_cullentities_trace_samples", "2", "number of samples to test for entity culling (in addition to center sample)"};
 cvar_t r_cullentities_trace_tempentitysamples = {0, "r_cullentities_trace_tempentitysamples", "-1", "number of samples to test for entity culling of temp entities (including all CSQC entities), -1 disables trace culling on these entities to prevent flicker (pvs still applies)"};
@@ -4481,8 +4483,13 @@ void R_SetupShader_Surface(const vec3_t lightcolorbase, qboolean modellighting,
 		// distorted background
 		if (rsurface.texture->currentmaterialflags & MATERIALFLAG_WATERSHADER)
 			mode = SHADERMODE_WATER;
-		else
+		else if (rsurface.texture->currentmaterialflags & MATERIALFLAG_REFRACTION)
 			mode = SHADERMODE_REFRACTION;
+		else
+		{
+			mode = SHADERMODE_GENERIC;
+			permutation |= SHADERPERMUTATION_DIFFUSE;
+		}
 		R_Mesh_TexCoordPointer(0, 2, rsurface.texcoordtexture2f, rsurface.texcoordtexture2f_bufferobject, rsurface.texcoordtexture2f_bufferoffset);
 		R_Mesh_TexCoordPointer(1, 3, rsurface.svector3f, rsurface.svector3f_bufferobject, rsurface.svector3f_bufferoffset);
 		R_Mesh_TexCoordPointer(2, 3, rsurface.tvector3f, rsurface.tvector3f_bufferobject, rsurface.tvector3f_bufferoffset);
@@ -6336,6 +6343,7 @@ void GL_Main_Init(void)
 	Cvar_RegisterVariable(&r_cullentities_trace_enlarge);
 	Cvar_RegisterVariable(&r_cullentities_trace_delay);
 	Cvar_RegisterVariable(&r_drawviewmodel);
+	Cvar_RegisterVariable(&r_drawexteriormodel);
 	Cvar_RegisterVariable(&r_speeds);
 	Cvar_RegisterVariable(&r_fullbrights);
 	Cvar_RegisterVariable(&r_wateralpha);
@@ -6892,6 +6900,8 @@ static void R_View_UpdateEntityVisible (void)
 		:                                                          RENDER_EXTERIORMODEL;
 	if (!r_drawviewmodel.integer)
 		renderimask |= RENDER_VIEWMODEL;
+	if (!r_drawexteriormodel.integer)
+		renderimask |= RENDER_EXTERIORMODEL;
 	if (r_refdef.scene.worldmodel && r_refdef.scene.worldmodel->brush.BoxTouchingVisibleLeafs)
 	{
 		// worldmodel can check visibility
@@ -6904,7 +6914,8 @@ static void R_View_UpdateEntityVisible (void)
 			if ((ent->flags & (RENDER_NODEPTHTEST | RENDER_VIEWMODEL)) || r_refdef.scene.worldmodel->brush.BoxTouchingVisibleLeafs(r_refdef.scene.worldmodel, r_refdef.viewcache.world_leafvisible, ent->mins, ent->maxs))
 				r_refdef.viewcache.entityvisible[i] = true;
 		}
-		if(r_cullentities_trace.integer && r_refdef.scene.worldmodel->brush.TraceLineOfSight)
+		if(r_cullentities_trace.integer && r_refdef.scene.worldmodel->brush.TraceLineOfSight && !r_refdef.view.useclipplane)
+			// sorry, this check doesn't work for portal/reflection/refraction renders as the view origin is not useful for culling
 		{
 			for (i = 0;i < r_refdef.scene.numentities;i++)
 			{
@@ -7297,13 +7308,29 @@ void R_ResetViewRendering3D(void)
 	GL_CullFace(r_refdef.view.cullface_back);
 }
 
+/*
+================
+R_RenderView_UpdateViewVectors
+================
+*/
+static void R_RenderView_UpdateViewVectors(void)
+{
+	// break apart the view matrix into vectors for various purposes
+	// it is important that this occurs outside the RenderScene function because that can be called from reflection renders, where the vectors come out wrong
+	// however the r_refdef.view.origin IS updated in RenderScene intentionally - otherwise the sky renders at the wrong origin, etc
+	Matrix4x4_ToVectors(&r_refdef.view.matrix, r_refdef.view.forward, r_refdef.view.left, r_refdef.view.up, r_refdef.view.origin);
+	VectorNegate(r_refdef.view.left, r_refdef.view.right);
+	// make an inverted copy of the view matrix for tracking sprites
+	Matrix4x4_Invert_Simple(&r_refdef.view.inverse_matrix, &r_refdef.view.matrix);
+}
+
 void R_RenderScene(void);
 void R_RenderWaterPlanes(void);
 
 static void R_Water_StartFrame(void)
 {
 	int i;
-	int waterwidth, waterheight, texturewidth, textureheight;
+	int waterwidth, waterheight, texturewidth, textureheight, camerawidth, cameraheight;
 	r_waterstate_waterplane_t *p;
 
 	if (vid.width > (int)vid.maxtexturesize_2d || vid.height > (int)vid.maxtexturesize_2d)
@@ -7327,20 +7354,24 @@ static void R_Water_StartFrame(void)
 	// calculate desired texture sizes
 	// can't use water if the card does not support the texture size
 	if (!r_water.integer || r_showsurfaces.integer)
-		texturewidth = textureheight = waterwidth = waterheight = 0;
+		texturewidth = textureheight = waterwidth = waterheight = camerawidth = cameraheight = 0;
 	else if (vid.support.arb_texture_non_power_of_two)
 	{
 		texturewidth = waterwidth;
 		textureheight = waterheight;
+		camerawidth = waterwidth;
+		cameraheight = waterheight;
 	}
 	else
 	{
 		for (texturewidth   = 1;texturewidth   < waterwidth ;texturewidth   *= 2);
 		for (textureheight  = 1;textureheight  < waterheight;textureheight  *= 2);
+		for (camerawidth    = 1;camerawidth   <= waterwidth; camerawidth    *= 2); camerawidth  /= 2;
+		for (cameraheight   = 1;cameraheight  <= waterheight;cameraheight   *= 2); cameraheight /= 2;
 	}
 
 	// allocate textures as needed
-	if (r_waterstate.texturewidth != texturewidth || r_waterstate.textureheight != textureheight)
+	if (r_waterstate.texturewidth != texturewidth || r_waterstate.textureheight != textureheight || r_waterstate.camerawidth != camerawidth || r_waterstate.cameraheight != cameraheight)
 	{
 		r_waterstate.maxwaterplanes = MAX_WATERPLANES;
 		for (i = 0, p = r_waterstate.waterplanes;i < r_waterstate.maxwaterplanes;i++, p++)
@@ -7351,10 +7382,15 @@ static void R_Water_StartFrame(void)
 			if (p->texture_reflection)
 				R_FreeTexture(p->texture_reflection);
 			p->texture_reflection = NULL;
+			if (p->texture_camera)
+				R_FreeTexture(p->texture_camera);
+			p->texture_camera = NULL;
 		}
 		memset(&r_waterstate, 0, sizeof(r_waterstate));
 		r_waterstate.texturewidth = texturewidth;
 		r_waterstate.textureheight = textureheight;
+		r_waterstate.camerawidth = camerawidth;
+		r_waterstate.cameraheight = cameraheight;
 	}
 
 	if (r_waterstate.texturewidth)
@@ -7384,8 +7420,13 @@ void R_Water_AddWaterPlane(msurface_t *surface)
 	vec3_t normal;
 	vec3_t center;
 	mplane_t plane;
+	int cam_ent;
 	r_waterstate_waterplane_t *p;
 	texture_t *t = R_GetCurrentTexture(surface->texture);
+	cam_ent = t->camera_entity;
+	if(!(t->currentmaterialflags & MATERIALFLAG_CAMERA))
+		cam_ent = 0;
+
 	// just use the first triangle with a valid normal for any decisions
 	VectorClear(normal);
 	for (triangleindex = 0, e = rsurface.modelelement3i + surface->num_firsttriangle * 3;triangleindex < surface->num_triangles;triangleindex++, e += 3)
@@ -7415,8 +7456,9 @@ void R_Water_AddWaterPlane(msurface_t *surface)
 
 	// find a matching plane if there is one
 	for (planeindex = 0, p = r_waterstate.waterplanes;planeindex < r_waterstate.numwaterplanes;planeindex++, p++)
-		if (fabs(PlaneDiff(vert[0], &p->plane)) < 1 && fabs(PlaneDiff(vert[1], &p->plane)) < 1 && fabs(PlaneDiff(vert[2], &p->plane)) < 1)
-			break;
+		if(p->camera_entity == t->camera_entity)
+			if (fabs(PlaneDiff(vert[0], &p->plane)) < 1 && fabs(PlaneDiff(vert[1], &p->plane)) < 1 && fabs(PlaneDiff(vert[2], &p->plane)) < 1)
+				break;
 	if (planeindex >= r_waterstate.maxwaterplanes)
 		return; // nothing we can do, out of planes
 
@@ -7429,16 +7471,20 @@ void R_Water_AddWaterPlane(msurface_t *surface)
 		// clear materialflags and pvs
 		p->materialflags = 0;
 		p->pvsvalid = false;
+		p->camera_entity = t->camera_entity;
 	}
 	// merge this surface's materialflags into the waterplane
 	p->materialflags |= t->currentmaterialflags;
-	// merge this surface's PVS into the waterplane
-	VectorMAM(0.5f, surface->mins, 0.5f, surface->maxs, center);
-	if (p->materialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION) && r_refdef.scene.worldmodel && r_refdef.scene.worldmodel->brush.FatPVS
-	 && r_refdef.scene.worldmodel->brush.PointInLeaf && r_refdef.scene.worldmodel->brush.PointInLeaf(r_refdef.scene.worldmodel, center)->clusterindex >= 0)
+	if(!(p->materialflags & MATERIALFLAG_CAMERA))
 	{
-		r_refdef.scene.worldmodel->brush.FatPVS(r_refdef.scene.worldmodel, center, 2, p->pvsbits, sizeof(p->pvsbits), p->pvsvalid);
-		p->pvsvalid = true;
+		// merge this surface's PVS into the waterplane
+		VectorMAM(0.5f, surface->mins, 0.5f, surface->maxs, center);
+		if (p->materialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION | MATERIALFLAG_CAMERA) && r_refdef.scene.worldmodel && r_refdef.scene.worldmodel->brush.FatPVS
+		 && r_refdef.scene.worldmodel->brush.PointInLeaf && r_refdef.scene.worldmodel->brush.PointInLeaf(r_refdef.scene.worldmodel, center)->clusterindex >= 0)
+		{
+			r_refdef.scene.worldmodel->brush.FatPVS(r_refdef.scene.worldmodel, center, 2, p->pvsbits, sizeof(p->pvsbits), p->pvsvalid);
+			p->pvsvalid = true;
+		}
 	}
 }
 
@@ -7448,6 +7494,7 @@ static void R_Water_ProcessPlanes(void)
 	r_refdef_view_t myview;
 	int planeindex;
 	r_waterstate_waterplane_t *p;
+	vec3_t visorigin;
 
 	originalview = r_refdef.view;
 
@@ -7461,6 +7508,13 @@ static void R_Water_ProcessPlanes(void)
 			if (!p->texture_refraction)
 				goto error;
 		}
+		else if (p->materialflags & MATERIALFLAG_CAMERA)
+		{
+			if (!p->texture_camera)
+				p->texture_camera = R_LoadTexture2D(r_main_texturepool, va("waterplane%i_camera", planeindex), r_waterstate.camerawidth, r_waterstate.cameraheight, NULL, TEXTYPE_COLORBUFFER, TEXF_FORCELINEAR, NULL);
+			if (!p->texture_camera)
+				goto error;
+		}
 
 		if (p->materialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFLECTION))
 		{
@@ -7515,9 +7569,20 @@ static void R_Water_ProcessPlanes(void)
 		{
 			r_waterstate.renderingrefraction = true;
 			r_refdef.view = myview;
+
 			r_refdef.view.clipplane = p->plane;
 			VectorNegate(r_refdef.view.clipplane.normal, r_refdef.view.clipplane.normal);
 			r_refdef.view.clipplane.dist = -r_refdef.view.clipplane.dist;
+
+			if((p->materialflags & MATERIALFLAG_CAMERA) && p->camera_entity)
+			{
+				// we need to perform a matrix transform to render the view... so let's get the transformation matrix
+				r_waterstate.renderingrefraction = false; // we don't want to hide the player model from these ones
+				CL_VM_TransformView(p->camera_entity - MAX_EDICTS, &r_refdef.view.matrix, &r_refdef.view.clipplane, visorigin);
+				R_RenderView_UpdateViewVectors();
+				r_refdef.scene.worldmodel->brush.FatPVS(r_refdef.scene.worldmodel, visorigin, 2, r_refdef.viewcache.world_pvsbits, (r_refdef.viewcache.world_numclusters+7)>>3, false);
+			}
+
 			PlaneClassify(&r_refdef.view.clipplane);
 
 			R_ResetViewRendering3D();
@@ -7528,6 +7593,47 @@ static void R_Water_ProcessPlanes(void)
 			R_Mesh_CopyToTexture(p->texture_refraction, 0, 0, r_refdef.view.viewport.x, r_refdef.view.viewport.y, r_refdef.view.viewport.width, r_refdef.view.viewport.height);
 			r_waterstate.renderingrefraction = false;
 		}
+		else if (p->materialflags & MATERIALFLAG_CAMERA)
+		{
+			r_refdef.view = myview;
+
+			r_refdef.view.clipplane = p->plane;
+			VectorNegate(r_refdef.view.clipplane.normal, r_refdef.view.clipplane.normal);
+			r_refdef.view.clipplane.dist = -r_refdef.view.clipplane.dist;
+
+			r_refdef.view.width = r_waterstate.camerawidth;
+			r_refdef.view.height = r_waterstate.cameraheight;
+			r_refdef.view.frustum_x = 1; // tan(45 * M_PI / 180.0);
+			r_refdef.view.frustum_y = 1; // tan(45 * M_PI / 180.0);
+
+			if(p->camera_entity)
+			{
+				// we need to perform a matrix transform to render the view... so let's get the transformation matrix
+				CL_VM_TransformView(p->camera_entity - MAX_EDICTS, &r_refdef.view.matrix, &r_refdef.view.clipplane, visorigin);
+			}
+
+			// reverse the cullface settings for this render
+			r_refdef.view.cullface_front = GL_FRONT;
+			r_refdef.view.cullface_back = GL_BACK;
+			// also reverse the view matrix
+			Matrix4x4_ConcatScale3(&r_refdef.view.matrix, 1, -1, 1);
+			R_RenderView_UpdateViewVectors();
+			if(p->camera_entity)
+				r_refdef.scene.worldmodel->brush.FatPVS(r_refdef.scene.worldmodel, visorigin, 2, r_refdef.viewcache.world_pvsbits, (r_refdef.viewcache.world_numclusters+7)>>3, false);
+			
+			// camera needs no clipplane
+			r_refdef.view.useclipplane = false;
+
+			PlaneClassify(&r_refdef.view.clipplane);
+
+			R_ResetViewRendering3D();
+			R_ClearScreen(r_refdef.fogenabled);
+			R_View_Update();
+			R_RenderScene();
+
+			R_Mesh_CopyToTexture(p->texture_camera, 0, 0, r_refdef.view.viewport.x, r_refdef.view.viewport.y, r_refdef.view.viewport.width, r_refdef.view.viewport.height);
+			r_waterstate.renderingrefraction = false;
+		}
 
 	}
 	r_waterstate.renderingscene = false;
@@ -8243,13 +8349,7 @@ void R_RenderView(void)
 
 	r_refdef.view.colorscale = r_hdr_scenebrightness.value;
 
-	// break apart the view matrix into vectors for various purposes
-	// it is important that this occurs outside the RenderScene function because that can be called from reflection renders, where the vectors come out wrong
-	// however the r_refdef.view.origin IS updated in RenderScene intentionally - otherwise the sky renders at the wrong origin, etc
-	Matrix4x4_ToVectors(&r_refdef.view.matrix, r_refdef.view.forward, r_refdef.view.left, r_refdef.view.up, r_refdef.view.origin);
-	VectorNegate(r_refdef.view.left, r_refdef.view.right);
-	// make an inverted copy of the view matrix for tracking sprites
-	Matrix4x4_Invert_Simple(&r_refdef.view.inverse_matrix, &r_refdef.view.matrix);
+	R_RenderView_UpdateViewVectors();
 
 	R_Shadow_UpdateWorldLightSelection();
 
@@ -9074,6 +9174,11 @@ texture_t *R_GetCurrentTexture(texture_t *t)
 	t->update_lastrenderframe = r_textureframe;
 	t->update_lastrenderentity = (void *)ent;
 
+	if(ent && ent->entitynumber >= MAX_EDICTS && ent->entitynumber < 2 * MAX_EDICTS)
+		t->camera_entity = ent->entitynumber;
+	else
+		t->camera_entity = 0;
+
 	// switch to an alternate material if this is a q1bsp animated material
 	{
 		texture_t *texture = t;
@@ -9130,7 +9235,7 @@ texture_t *R_GetCurrentTexture(texture_t *t)
 	if(t->basematerialflags & MATERIALFLAG_WATERSHADER && r_waterstate.enabled && !r_refdef.view.isoverlay)
 		t->currentalpha *= t->r_water_wateralpha;
 	if(!r_waterstate.enabled || r_refdef.view.isoverlay)
-		t->currentmaterialflags &= ~(MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION);
+		t->currentmaterialflags &= ~(MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION | MATERIALFLAG_CAMERA);
 	if (!(rsurface.ent_flags & RENDER_LIGHT))
 		t->currentmaterialflags |= MATERIALFLAG_FULLBRIGHT;
 	else if (rsurface.modeltexcoordlightmap2f == NULL && !(t->currentmaterialflags & MATERIALFLAG_FULLBRIGHT))
@@ -9153,11 +9258,11 @@ texture_t *R_GetCurrentTexture(texture_t *t)
 		t->currentmaterialflags |= MATERIALFLAG_VERTEXTEXTUREBLEND;
 	if (t->currentmaterialflags & MATERIALFLAG_BLENDED)
 	{
-		if (t->currentmaterialflags & (MATERIALFLAG_REFRACTION | MATERIALFLAG_WATERSHADER))
+		if (t->currentmaterialflags & (MATERIALFLAG_REFRACTION | MATERIALFLAG_WATERSHADER | MATERIALFLAG_CAMERA))
 			t->currentmaterialflags &= ~MATERIALFLAG_BLENDED;
 	}
 	else
-		t->currentmaterialflags &= ~(MATERIALFLAG_REFRACTION | MATERIALFLAG_WATERSHADER);
+		t->currentmaterialflags &= ~(MATERIALFLAG_REFRACTION | MATERIALFLAG_WATERSHADER | MATERIALFLAG_CAMERA);
 	if ((t->currentmaterialflags & (MATERIALFLAG_BLENDED | MATERIALFLAG_NODEPTHTEST)) == MATERIALFLAG_BLENDED && r_transparentdepthmasking.integer && !(t->basematerialflags & MATERIALFLAG_BLENDED))
 		t->currentmaterialflags |= MATERIALFLAG_TRANSDEPTH;
 
@@ -10259,6 +10364,8 @@ static void RSurf_BindReflectionForSurface(const msurface_t *surface)
 	bestp = NULL;
 	for (planeindex = 0, p = r_waterstate.waterplanes;planeindex < r_waterstate.numwaterplanes;planeindex++, p++)
 	{
+		if(p->camera_entity != rsurface.texture->camera_entity)
+			continue;
 		d = 0;
 		for (vertexindex = 0, v = rsurface.modelvertex3f + surface->num_firstvertex * 3;vertexindex < surface->num_vertices;vertexindex++, v += 3)
 		{
@@ -10276,11 +10383,13 @@ static void RSurf_BindReflectionForSurface(const msurface_t *surface)
 	case RENDERPATH_CGGL:
 #ifdef SUPPORTCG
 		if (r_cg_permutation->fp_Texture_Refraction) CG_BindTexture(r_cg_permutation->fp_Texture_Refraction, bestp ? bestp->texture_refraction : r_texture_black);CHECKCGERROR
+		else if (r_cg_permutation->fp_Texture_First) CG_BindTexture(r_cg_permutation->fp_Texture_First, bestp ? bestp->texture_camera : r_texture_black);CHECKCGERROR
 		if (r_cg_permutation->fp_Texture_Reflection) CG_BindTexture(r_cg_permutation->fp_Texture_Reflection, bestp ? bestp->texture_reflection : r_texture_black);CHECKCGERROR
 #endif
 		break;
 	case RENDERPATH_GL20:
 		if (r_glsl_permutation->loc_Texture_Refraction >= 0) R_Mesh_TexBind(GL20TU_REFRACTION, bestp ? bestp->texture_refraction : r_texture_black);
+		else if (r_glsl_permutation->loc_Texture_First >= 0) R_Mesh_TexBind(GL20TU_FIRST, bestp ? bestp->texture_camera : r_texture_black);
 		if (r_glsl_permutation->loc_Texture_Reflection >= 0) R_Mesh_TexBind(GL20TU_REFLECTION, bestp ? bestp->texture_reflection : r_texture_black);
 		break;
 	case RENDERPATH_GL13:
@@ -10788,7 +10897,7 @@ extern rtexture_t *r_shadow_prepasslightingdiffusetexture;
 extern rtexture_t *r_shadow_prepasslightingspeculartexture;
 static void R_DrawTextureSurfaceList_GL20(int texturenumsurfaces, const msurface_t **texturesurfacelist, qboolean writedepth, qboolean prepass)
 {
-	if (r_waterstate.renderingscene && (rsurface.texture->currentmaterialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION)))
+	if (r_waterstate.renderingscene && (rsurface.texture->currentmaterialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION | MATERIALFLAG_CAMERA)))
 		return;
 	RSurf_PrepareVerticesForBatch(true, true, texturenumsurfaces, texturesurfacelist);
 	if (prepass)
@@ -10798,7 +10907,7 @@ static void R_DrawTextureSurfaceList_GL20(int texturenumsurfaces, const msurface
 		R_SetupShader_Surface(vec3_origin, (rsurface.texture->currentmaterialflags & MATERIALFLAG_MODELLIGHT) != 0, 1, 1, rsurface.texture->specularscale, RSURFPASS_DEFERREDGEOMETRY);
 		RSurf_DrawBatch_Simple(texturenumsurfaces, texturesurfacelist);
 	}
-	else if ((rsurface.texture->currentmaterialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION)) && !r_waterstate.renderingscene)
+	else if ((rsurface.texture->currentmaterialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_CAMERA)) && !r_waterstate.renderingscene)
 	{
 		// render water or distortion background, then blend surface on top
 		GL_DepthMask(true);
diff --git a/gl_rsurf.c b/gl_rsurf.c
index acd76992..554fb0fa 100644
--- a/gl_rsurf.c
+++ b/gl_rsurf.c
@@ -22,6 +22,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 #include "quakedef.h"
 #include "r_shadow.h"
 #include "portals.h"
+#include "csprogs.h"
 
 cvar_t r_ambient = {0, "r_ambient", "0", "brightens map, value is 0-128"};
 cvar_t r_lockpvs = {0, "r_lockpvs", "0", "disables pvs switching, allows you to walk around and inspect what is visible from a given location in the map (anything not visible from your current location will not be drawn)"};
@@ -544,10 +545,10 @@ void R_Q1BSP_DrawSky(entity_render_t *ent)
 		R_DrawModelSurfaces(ent, true, true, false, false, false);
 }
 
-extern void R_Water_AddWaterPlane(msurface_t *surface);
+extern void R_Water_AddWaterPlane(msurface_t *surface, int entno);
 void R_Q1BSP_DrawAddWaterPlanes(entity_render_t *ent)
 {
-	int i, j, flagsmask;
+	int i, j, n, flagsmask;
 	dp_model_t *model = ent->model;
 	msurface_t *surfaces;
 	if (model == NULL)
@@ -559,7 +560,7 @@ void R_Q1BSP_DrawAddWaterPlanes(entity_render_t *ent)
 		RSurf_ActiveModelEntity(ent, false, false, false);
 
 	surfaces = model->data_surfaces;
-	flagsmask = MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION;
+	flagsmask = MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION | MATERIALFLAG_CAMERA;
 
 	// add visible surfaces to draw list
 	if (ent == r_refdef.scene.worldentity)
@@ -569,16 +570,20 @@ void R_Q1BSP_DrawAddWaterPlanes(entity_render_t *ent)
 			j = model->sortedmodelsurfaces[i];
 			if (r_refdef.viewcache.world_surfacevisible[j])
 				if (surfaces[j].texture->basematerialflags & flagsmask)
-					R_Water_AddWaterPlane(surfaces + j);
+					R_Water_AddWaterPlane(surfaces + j, 0);
 		}
 	}
 	else
 	{
+		if(ent->entitynumber >= MAX_EDICTS) // && CL_VM_TransformView(ent->entitynumber - MAX_EDICTS, NULL, NULL, NULL))
+			n = ent->entitynumber;
+		else
+			n = 0;
 		for (i = 0;i < model->nummodelsurfaces;i++)
 		{
 			j = model->sortedmodelsurfaces[i];
 			if (surfaces[j].texture->basematerialflags & flagsmask)
-				R_Water_AddWaterPlane(surfaces + j);
+				R_Water_AddWaterPlane(surfaces + j, n);
 		}
 	}
 	rsurface.entity = NULL; // used only by R_GetCurrentTexture and RSurf_ActiveWorldEntity/RSurf_ActiveModelEntity
@@ -1233,7 +1238,7 @@ void R_Q1BSP_DrawLight(entity_render_t *ent, int numsurfaces, const int *surface
 			// now figure out what to do with this particular range of surfaces
 			if (!(rsurface.texture->currentmaterialflags & MATERIALFLAG_WALL))
 				continue;
-			if (r_waterstate.renderingscene && (rsurface.texture->currentmaterialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION)))
+			if (r_waterstate.renderingscene && (rsurface.texture->currentmaterialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION | MATERIALFLAG_CAMERA)))
 				continue;
 			if (rsurface.texture->currentmaterialflags & MATERIALFLAGMASK_DEPTHSORTED)
 			{
diff --git a/model_brush.c b/model_brush.c
index e51bf83c..92293ef5 100644
--- a/model_brush.c
+++ b/model_brush.c
@@ -3686,7 +3686,7 @@ void Mod_Q1BSP_Load(dp_model_t *mod, void *buffer, void *bufferend)
 				mod->DrawSky = R_Q1BSP_DrawSky;
 
 			for (j = 0, surface = &mod->data_surfaces[mod->firstmodelsurface];j < mod->nummodelsurfaces;j++, surface++)
-				if (surface->texture->basematerialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION))
+				if (surface->texture->basematerialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION | MATERIALFLAG_CAMERA))
 					break;
 			if (j < mod->nummodelsurfaces)
 				mod->DrawAddWaterPlanes = R_Q1BSP_DrawAddWaterPlanes;
@@ -7040,7 +7040,7 @@ void Mod_Q3BSP_Load(dp_model_t *mod, void *buffer, void *bufferend)
 			mod->DrawSky = R_Q1BSP_DrawSky;
 
 		for (j = 0;j < mod->nummodelsurfaces;j++)
-			if (mod->data_surfaces[j + mod->firstmodelsurface].texture->basematerialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION))
+			if (mod->data_surfaces[j + mod->firstmodelsurface].texture->basematerialflags & (MATERIALFLAG_WATERSHADER | MATERIALFLAG_REFRACTION | MATERIALFLAG_REFLECTION | MATERIALFLAG_CAMERA))
 				break;
 		if (j < mod->nummodelsurfaces)
 			mod->DrawAddWaterPlanes = R_Q1BSP_DrawAddWaterPlanes;
diff --git a/model_brush.h b/model_brush.h
index 34519383..ab2a5121 100644
--- a/model_brush.h
+++ b/model_brush.h
@@ -114,6 +114,8 @@ mplane_t;
 #define MATERIALFLAG_CUSTOMSURFACE 16777216
 // causes MATERIALFLAG_BLENDED to render a depth pass before rendering, hiding backfaces and other hidden geometry
 #define MATERIALFLAG_TRANSDEPTH 33554432
+// like refraction, but doesn't distort etc.
+#define MATERIALFLAG_CAMERA 67108864
 // combined mask of all attributes that require depth sorted rendering
 #define MATERIALFLAGMASK_DEPTHSORTED (MATERIALFLAG_BLENDED | MATERIALFLAG_NODEPTHTEST)
 // combined mask of all attributes that cause some sort of transparency
diff --git a/model_shared.c b/model_shared.c
index 3d2b877c..0674e723 100644
--- a/model_shared.c
+++ b/model_shared.c
@@ -1984,6 +1984,10 @@ void Mod_LoadQ3Shaders(void)
 					shader.reflectfactor = atof(parameter[1]);
 					Vector4Set(shader.reflectcolor4f, atof(parameter[2]), atof(parameter[3]), atof(parameter[4]), atof(parameter[5]));
 				}
+				else if (!strcasecmp(parameter[0], "dpcamera"))
+				{
+					shader.textureflags |= Q3TEXTUREFLAG_CAMERA;
+				}
 				else if (!strcasecmp(parameter[0], "dpwater") && numparameters >= 12)
 				{
 					shader.textureflags |= Q3TEXTUREFLAG_WATERSHADER;
@@ -2081,7 +2085,7 @@ void Mod_LoadQ3Shaders(void)
 			}
 			// fix up multiple reflection types
 			if(shader.textureflags & Q3TEXTUREFLAG_WATERSHADER)
-				shader.textureflags &= ~(Q3TEXTUREFLAG_REFRACTION | Q3TEXTUREFLAG_REFLECTION);
+				shader.textureflags &= ~(Q3TEXTUREFLAG_REFRACTION | Q3TEXTUREFLAG_REFLECTION | Q3TEXTUREFLAG_CAMERA);
 
 			Q3Shader_AddToHash (&shader);
 		}
@@ -2167,6 +2171,8 @@ qboolean Mod_LoadTextureFromQ3Shader(texture_t *texture, const char *name, qbool
 			texture->basematerialflags |= MATERIALFLAG_REFLECTION;
 		if (shader->textureflags & Q3TEXTUREFLAG_WATERSHADER)
 			texture->basematerialflags |= MATERIALFLAG_WATERSHADER;
+		if (shader->textureflags & Q3TEXTUREFLAG_CAMERA)
+			texture->basematerialflags |= MATERIALFLAG_CAMERA;
 		texture->customblendfunc[0] = GL_ONE;
 		texture->customblendfunc[1] = GL_ZERO;
 		if (shader->numlayers > 0)
diff --git a/model_shared.h b/model_shared.h
index 40dc7e31..d19c71bf 100644
--- a/model_shared.h
+++ b/model_shared.h
@@ -207,6 +207,7 @@ shadowmesh_t;
 #define Q3TEXTUREFLAG_REFRACTION 256
 #define Q3TEXTUREFLAG_REFLECTION 512
 #define Q3TEXTUREFLAG_WATERSHADER 1024
+#define Q3TEXTUREFLAG_CAMERA 2048
 
 #define Q3PATHLENGTH 64
 #define TEXTURE_MAXFRAMES 64
@@ -551,6 +552,7 @@ typedef struct texture_s
 	float reflectfactor; // amount of reflection distort (1.0 = like the cvar specifies)
 	vec4_t reflectcolor4f; // color tint of reflection (including alpha factor)
 	float r_water_wateralpha; // additional wateralpha to apply when r_water is active
+	int camera_entity; // entity number for use by cameras
 
 	// offsetmapping
 	dpoffsetmapping_technique_t offsetmapping;
diff --git a/progsvm.h b/progsvm.h
index 938f29ea..dc7f192a 100644
--- a/progsvm.h
+++ b/progsvm.h
@@ -266,6 +266,8 @@ typedef struct prvm_prog_fieldoffsets_s
 	int enemy; // ssqc / csqc (physics)
 	int aiment; // ssqc / csqc (physics)
 	int movedir; // ssqc / csqc (physics)
+
+	int camera_transform; // csqc (warpzones)
 }
 prvm_prog_fieldoffsets_t;
 
diff --git a/prvm_edict.c b/prvm_edict.c
index 1959bc6d..0b8072c5 100644
--- a/prvm_edict.c
+++ b/prvm_edict.c
@@ -1654,6 +1654,8 @@ void PRVM_FindOffsets(void)
 	prog->fieldoffsets.jointtype                      = PRVM_ED_FindFieldOffset("jointtype");
 	prog->fieldoffsets.movedir                        = PRVM_ED_FindFieldOffset("movedir");
 
+	prog->fieldoffsets.camera_transform               = PRVM_ED_FindFieldOffset("camera_transform");
+
 	prog->funcoffsets.CSQC_ConsoleCommand             = PRVM_ED_FindFunctionOffset("CSQC_ConsoleCommand");
 	prog->funcoffsets.CSQC_Ent_Remove                 = PRVM_ED_FindFunctionOffset("CSQC_Ent_Remove");
 	prog->funcoffsets.CSQC_Ent_Spawn                  = PRVM_ED_FindFunctionOffset("CSQC_Ent_Spawn");
diff --git a/quakedef.h b/quakedef.h
index c680ccd5..b308f60e 100644
--- a/quakedef.h
+++ b/quakedef.h
@@ -85,6 +85,7 @@ extern char engineversion[128];
 #define	MAX_SAVEGAMES			12
 #define	SAVEGAME_COMMENT_LENGTH	39
 #define	MAX_CLIENTNETWORKEYES	2
+#define	MAX_LEVELNETWORKEYES	0 // no portal support
 #define	MAX_OCCLUSION_QUERIES	256
 
 #define	MAX_WATERPLANES			2
@@ -147,7 +148,8 @@ extern char engineversion[128];
 #define	MAX_DEMONAME			16 ///< max demo name length for demos command
 #define	MAX_SAVEGAMES			12 ///< max savegames listed in savegame menu
 #define	SAVEGAME_COMMENT_LENGTH	39 ///< max comment length of savegame in menu
-#define	MAX_CLIENTNETWORKEYES	2 ///< max number of locations that can be added to pvs when culling network entities (must be at least 2 for prediction)
+#define	MAX_CLIENTNETWORKEYES	8 ///< max number of locations that can be added to pvs when culling network entities (must be at least 2 for prediction)
+#define	MAX_LEVELNETWORKEYES	64 ///< max number of locations that can be added to pvs when culling network entities (must be at least 2 for prediction)
 #define	MAX_OCCLUSION_QUERIES	4096 ///< max number of GL_ARB_occlusion_query objects that can be used in one frame
 
 #define	MAX_WATERPLANES			16 ///< max number of water planes visible (each one causes additional view renders)
diff --git a/r_shadow.c b/r_shadow.c
index 67449fc6..8a23ca0e 100644
--- a/r_shadow.c
+++ b/r_shadow.c
@@ -4520,26 +4520,34 @@ void R_DrawModelShadows(void)
 			{
 				if(ent->entitynumber != 0)
 				{
-					// networked entity - might be attached in some way (then we should use the parent's light direction, to not tear apart attached entities)
-					int entnum, entnum2, recursion;
-					entnum = entnum2 = ent->entitynumber;
-					for(recursion = 32; recursion > 0; --recursion)
+					if(ent->entitynumber >= MAX_EDICTS) // csqc entity
 					{
-						entnum2 = cl.entities[entnum].state_current.tagentity;
-						if(entnum2 >= 1 && entnum2 < cl.num_entities && cl.entities_active[entnum2])
-							entnum = entnum2;
-						else
-							break;
+						// FIXME handle this
+						VectorNegate(ent->modellight_lightdir, relativelightdirection);
 					}
-					if(recursion && recursion != 32) // if we followed a valid non-empty attachment chain
+					else
 					{
-						VectorNegate(cl.entities[entnum].render.modellight_lightdir, relativelightdirection);
-						// transform into modelspace of OUR entity
-						Matrix4x4_Transform3x3(&cl.entities[entnum].render.matrix, relativelightdirection, tmp);
-						Matrix4x4_Transform3x3(&ent->inversematrix, tmp, relativelightdirection);
+						// networked entity - might be attached in some way (then we should use the parent's light direction, to not tear apart attached entities)
+						int entnum, entnum2, recursion;
+						entnum = entnum2 = ent->entitynumber;
+						for(recursion = 32; recursion > 0; --recursion)
+						{
+							entnum2 = cl.entities[entnum].state_current.tagentity;
+							if(entnum2 >= 1 && entnum2 < cl.num_entities && cl.entities_active[entnum2])
+								entnum = entnum2;
+							else
+								break;
+						}
+						if(recursion && recursion != 32) // if we followed a valid non-empty attachment chain
+						{
+							VectorNegate(cl.entities[entnum].render.modellight_lightdir, relativelightdirection);
+							// transform into modelspace of OUR entity
+							Matrix4x4_Transform3x3(&cl.entities[entnum].render.matrix, relativelightdirection, tmp);
+							Matrix4x4_Transform3x3(&ent->inversematrix, tmp, relativelightdirection);
+						}
+						else
+							VectorNegate(ent->modellight_lightdir, relativelightdirection);
 					}
-					else
-						VectorNegate(ent->modellight_lightdir, relativelightdirection);
 				}
 				else
 					VectorNegate(ent->modellight_lightdir, relativelightdirection);
diff --git a/render.h b/render.h
index 31a83672..aa84b231 100644
--- a/render.h
+++ b/render.h
@@ -477,10 +477,12 @@ typedef struct r_waterstate_waterplane_s
 {
 	rtexture_t *texture_refraction;
 	rtexture_t *texture_reflection;
+	rtexture_t *texture_camera;
 	mplane_t plane;
 	int materialflags; // combined flags of all water surfaces on this plane
 	unsigned char pvsbits[(MAX_MAP_LEAFS+7)>>3]; // FIXME: buffer overflow on huge maps
 	qboolean pvsvalid;
+	int camera_entity;
 }
 r_waterstate_waterplane_t;
 
@@ -493,6 +495,7 @@ typedef struct r_waterstate_s
 
 	int waterwidth, waterheight;
 	int texturewidth, textureheight;
+	int camerawidth, cameraheight;
 
 	int maxwaterplanes; // same as MAX_WATERPLANES
 	int numwaterplanes;
diff --git a/sv_main.c b/sv_main.c
index 86436e03..6193888f 100644
--- a/sv_main.c
+++ b/sv_main.c
@@ -1584,6 +1584,85 @@ void SV_MarkWriteEntityStateToClient(entity_state_t *s)
 	sv.sententities[s->number] = sv.sententitiesmark;
 }
 
+#if MAX_LEVELNETWORKEYES > 0
+#define MAX_EYE_RECURSION 1 // increase if recursion gets supported by portals
+void SV_AddCameraEyes(void)
+{
+	int e, i, j, k;
+	prvm_edict_t *ed;
+	int cameras[MAX_LEVELNETWORKEYES];
+	vec3_t camera_origins[MAX_LEVELNETWORKEYES];
+	int eye_levels[MAX_CLIENTNETWORKEYES];
+	int n_cameras = 0;
+	vec3_t mi, ma;
+	prvm_eval_t *valendpos, *val;
+
+	if(!prog->fieldoffsets.camera_transform)
+		return;
+	valendpos = PRVM_GLOBALFIELDVALUE(prog->globaloffsets.trace_endpos);
+	if(!valendpos)
+		return;
+
+	for(i = 0; i < sv.writeentitiestoclient_numeyes; ++i)
+		eye_levels[i] = 0;
+
+	// check line of sight to portal entities and add them to PVS
+	for (e = 1, ed = PRVM_NEXT_EDICT(prog->edicts);e < prog->num_edicts;e++, ed = PRVM_NEXT_EDICT(ed))
+	{
+		if (!ed->priv.server->free)
+		{
+			if((val = PRVM_EDICTFIELDVALUE(ed, prog->fieldoffsets.camera_transform)) && val->function)
+			{
+				prog->globals.server->self = e;
+				prog->globals.server->other = sv.writeentitiestoclient_cliententitynumber;
+				VectorCopy(sv.writeentitiestoclient_eyes[0], valendpos->vector);
+				VectorCopy(sv.writeentitiestoclient_eyes[0], PRVM_G_VECTOR(OFS_PARM0));
+				VectorClear(PRVM_G_VECTOR(OFS_PARM1));
+				PRVM_ExecuteProgram(val->function, "QC function e.camera_transform is missing");
+				if(!VectorCompare(valendpos->vector, sv.writeentitiestoclient_eyes[0]))
+					VectorCopy(valendpos->vector, camera_origins[n_cameras]);
+				cameras[n_cameras] = e;
+				++n_cameras;
+				if(n_cameras >= MAX_LEVELNETWORKEYES)
+					break;
+			}
+		}
+	}
+
+	if(!n_cameras)
+		return;
+
+	// i is loop counter, is reset to 0 when an eye got added
+	// j is camera index to check
+	for(i = 0, j = 0; sv.writeentitiestoclient_numeyes < MAX_CLIENTNETWORKEYES && i < n_cameras; ++i, ++j, j %= n_cameras)
+	{
+		if(!cameras[j])
+			continue;
+		ed = PRVM_EDICT_NUM(cameras[j]);
+		VectorAdd(ed->fields.server->origin, ed->fields.server->mins, mi);
+		VectorAdd(ed->fields.server->origin, ed->fields.server->maxs, ma);
+		for(k = 0; k < sv.writeentitiestoclient_numeyes; ++k)
+		if(eye_levels[k] <= MAX_EYE_RECURSION)
+		{
+			if(SV_CanSeeBox(sv_cullentities_trace_samples.integer, sv_cullentities_trace_enlarge.value, sv.writeentitiestoclient_eyes[k], mi, ma))
+			{
+				eye_levels[sv.writeentitiestoclient_numeyes] = eye_levels[k] + 1;
+				VectorCopy(camera_origins[j], sv.writeentitiestoclient_eyes[sv.writeentitiestoclient_numeyes]);
+				// Con_Printf("added eye %d: %f %f %f because we can see %f %f %f .. %f %f %f from eye %d\n", j, sv.writeentitiestoclient_eyes[sv.writeentitiestoclient_numeyes][0], sv.writeentitiestoclient_eyes[sv.writeentitiestoclient_numeyes][1], sv.writeentitiestoclient_eyes[sv.writeentitiestoclient_numeyes][2], mi[0], mi[1], mi[2], ma[0], ma[1], ma[2], k);
+				sv.writeentitiestoclient_numeyes++;
+				cameras[j] = 0;
+				i = 0;
+				break;
+			}
+		}
+	}
+}
+#else
+void SV_AddCameraEyes(void)
+{
+}
+#endif
+
 void SV_WriteEntitiesToClient(client_t *client, prvm_edict_t *clent, sizebuf_t *msg, int maxsize)
 {
 	qboolean need_empty = false;
@@ -1634,7 +1713,12 @@ void SV_WriteEntitiesToClient(client_t *client, prvm_edict_t *clent, sizebuf_t *
 		//	Con_DPrintf("Trying to walk into solid in a pingtime... not predicting for culling\n");
 	}
 
-	// TODO: check line of sight to portal entities and add them to PVS
+	SV_AddCameraEyes();
+
+	// build PVS from the new eyes
+	if (sv.worldmodel && sv.worldmodel->brush.FatPVS)
+		for(i = 1; i < sv.writeentitiestoclient_numeyes; ++i)
+			sv.writeentitiestoclient_pvsbytes = sv.worldmodel->brush.FatPVS(sv.worldmodel, sv.writeentitiestoclient_eyes[i], 8, sv.writeentitiestoclient_pvs, sizeof(sv.writeentitiestoclient_pvs), sv.writeentitiestoclient_pvsbytes != 0);
 
 	sv.sententitiesmark++;
 
diff --git a/sv_phys.c b/sv_phys.c
index 13a0cc15..d7ae93f4 100644
--- a/sv_phys.c
+++ b/sv_phys.c
@@ -1128,16 +1128,14 @@ qboolean SV_RunThink (prvm_edict_t *ent)
 SV_Impact
 
 Two entities have touched, so run their touch functions
-returns true if the impact kept the origin of the touching entity intact
 ==================
 */
 extern void VM_SetTraceGlobals(const trace_t *trace);
 extern sizebuf_t vm_tempstringsbuf;
-qboolean SV_Impact (prvm_edict_t *e1, trace_t *trace)
+void SV_Impact (prvm_edict_t *e1, trace_t *trace)
 {
 	int restorevm_tempstringsbuf_cursize;
 	int old_self, old_other;
-	vec3_t org;
 	prvm_edict_t *e2 = (prvm_edict_t *)trace->ent;
 	prvm_eval_t *val;
 
@@ -1145,8 +1143,6 @@ qboolean SV_Impact (prvm_edict_t *e1, trace_t *trace)
 	old_other = prog->globals.server->other;
 	restorevm_tempstringsbuf_cursize = vm_tempstringsbuf.cursize;
 
-	VectorCopy(e1->fields.server->origin, org);
-
 	VM_SetTraceGlobals(trace);
 
 	prog->globals.server->time = sv.time;
@@ -1179,8 +1175,6 @@ qboolean SV_Impact (prvm_edict_t *e1, trace_t *trace)
 	prog->globals.server->self = old_self;
 	prog->globals.server->other = old_other;
 	vm_tempstringsbuf.cursize = restorevm_tempstringsbuf_cursize;
-
-	return VectorCompare(e1->fields.server->origin, org);
 }
 
 
@@ -1468,7 +1462,7 @@ static qboolean SV_PushEntity (trace_t *trace, prvm_edict_t *ent, vec3_t push, q
 {
 	int type;
 	int bump;
-	vec3_t original;
+	vec3_t original, original_velocity;
 	vec3_t end;
 
 	VectorCopy(ent->fields.server->origin, original);
@@ -1498,8 +1492,11 @@ static qboolean SV_PushEntity (trace_t *trace, prvm_edict_t *ent, vec3_t push, q
 	if (trace->bmodelstartsolid && failonbmodelstartsolid)
 		return true;
 
-
 	VectorCopy (trace->endpos, ent->fields.server->origin);
+
+	VectorCopy(ent->fields.server->origin, original);
+	VectorCopy(ent->fields.server->velocity, original_velocity);
+
 	SV_LinkEdict(ent);
 
 #if 0
@@ -1514,9 +1511,9 @@ static qboolean SV_PushEntity (trace_t *trace, prvm_edict_t *ent, vec3_t push, q
 		SV_LinkEdict_TouchAreaGrid(ent);
 
 	if((ent->fields.server->solid >= SOLID_TRIGGER && trace->ent && (!((int)ent->fields.server->flags & FL_ONGROUND) || ent->fields.server->groundentity != PRVM_EDICT_TO_PROG(trace->ent))))
-		return SV_Impact (ent, trace);
+		SV_Impact (ent, trace);
 
-	return true;
+	return VectorCompare(ent->fields.server->origin, original) && VectorCompare(ent->fields.server->velocity, original_velocity);
 }
 
 
-- 
2.39.5