dhewm3/neo/renderer/tr_subview.cpp
Daniel Gibson 161a0b39b9 Better support for mirrors seeing other mirrors, #367
It used to crash because after recursing into R_MirrorPoint(), which
calls tr.CropRenderSize(), about MAX_RENDER_CROPS times,
CropRenderSize() wrote behind the end of tr.renderCrops[] and damaged
a pointer.
Even if it didn't crash because of that, endlessly recursing is a bad
idea, so it's limited now. The limit can be configued with the new
r_maxMirrorRecursion CVar, but the hard limit is MAX_RENDER_CROPS-3 = 5

Another problem was that mirrors and other subviews render into
a texture, specifically globalImages->scratchImage ("_scratch"), so
it could happen that it got written to several times before being used.
Now there are 8 scratch images (+ the original "_scratch" which is also
used by gamecode) that the subview code cycles through to minimize the
chance of overwriting something that's still in use.
2025-01-23 03:34:05 +01:00

616 lines
18 KiB
C++

/*
===========================================================================
Doom 3 GPL Source Code
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.
This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").
Doom 3 Source Code 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 3 of the License, or
(at your option) any later version.
Doom 3 Source Code 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 Doom 3 Source Code. If not, see <http://www.gnu.org/licenses/>.
In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code. If not, please request a copy in writing from id Software at the address below.
If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
===========================================================================
*/
#include "sys/platform.h"
#include "renderer/tr_local.h"
typedef struct {
idVec3 origin;
idMat3 axis;
} orientation_t;
static idCVar r_maxMirrorRecursion( "r_maxMirrorRecursion", "2", CVAR_RENDERER | /*CVAR_ARCHIVE |*/ CVAR_INTEGER,
"how deep subviews with mirrors can watch each other", 0, MAX_RENDER_CROPS-3 );
/*
=================
R_MirrorPoint
=================
*/
static void R_MirrorPoint( const idVec3 in, orientation_t *surface, orientation_t *camera, idVec3 &out ) {
int i;
idVec3 local;
idVec3 transformed;
float d;
local = in - surface->origin;
transformed = vec3_origin;
for ( i = 0 ; i < 3 ; i++ ) {
d = local * surface->axis[i];
transformed += d * camera->axis[i];
}
out = transformed + camera->origin;
}
/*
=================
R_MirrorVector
=================
*/
static void R_MirrorVector( const idVec3 in, orientation_t *surface, orientation_t *camera, idVec3 &out ) {
int i;
float d;
out = vec3_origin;
for ( i = 0 ; i < 3 ; i++ ) {
d = in * surface->axis[i];
out += d * camera->axis[i];
}
}
/*
=============
R_PlaneForSurface
Returns the plane for the first triangle in the surface
FIXME: check for degenerate triangle?
=============
*/
static void R_PlaneForSurface( const srfTriangles_t *tri, idPlane &plane ) {
idDrawVert *v1, *v2, *v3;
v1 = tri->verts + tri->indexes[0];
v2 = tri->verts + tri->indexes[1];
v3 = tri->verts + tri->indexes[2];
plane.FromPoints( v1->xyz, v2->xyz, v3->xyz );
}
/*
=========================
R_PreciseCullSurface
Check the surface for visibility on a per-triangle basis
for cases when it is going to be VERY expensive to draw (subviews)
If not culled, also returns the bounding box of the surface in
Normalized Device Coordinates, so it can be used to crop the scissor rect.
OPTIMIZE: we could also take exact portal passing into consideration
=========================
*/
bool R_PreciseCullSurface( const drawSurf_t *drawSurf, idBounds &ndcBounds ) {
const srfTriangles_t *tri;
idPlane clip, eye;
int i, j;
unsigned int pointOr;
unsigned int pointAnd;
idVec3 localView;
idFixedWinding w;
tri = drawSurf->geo;
pointOr = 0;
pointAnd = (unsigned int)~0;
// get an exact bounds of the triangles for scissor cropping
ndcBounds.Clear();
for ( i = 0; i < tri->numVerts; i++ ) {
int j;
unsigned int pointFlags;
R_TransformModelToClip( tri->verts[i].xyz, drawSurf->space->modelViewMatrix,
tr.viewDef->projectionMatrix, eye, clip );
pointFlags = 0;
for ( j = 0; j < 3; j++ ) {
if ( clip[j] >= clip[3] ) {
pointFlags |= (1 << (j*2));
} else if ( clip[j] <= -clip[3] ) {
pointFlags |= ( 1 << (j*2+1));
}
}
pointAnd &= pointFlags;
pointOr |= pointFlags;
}
// trivially reject
if ( pointAnd ) {
return true;
}
// backface and frustum cull
R_GlobalPointToLocal( drawSurf->space->modelMatrix, tr.viewDef->renderView.vieworg, localView );
for ( i = 0; i < tri->numIndexes; i += 3 ) {
idVec3 dir, normal;
float dot;
idVec3 d1, d2;
const idVec3 &v1 = tri->verts[tri->indexes[i]].xyz;
const idVec3 &v2 = tri->verts[tri->indexes[i+1]].xyz;
const idVec3 &v3 = tri->verts[tri->indexes[i+2]].xyz;
// this is a hack, because R_GlobalPointToLocal doesn't work with the non-normalized
// axis that we get from the gui view transform. It doesn't hurt anything, because
// we know that all gui generated surfaces are front facing
if ( tr.guiRecursionLevel == 0 ) {
// we don't care that it isn't normalized,
// all we want is the sign
d1 = v2 - v1;
d2 = v3 - v1;
normal = d2.Cross( d1 );
dir = v1 - localView;
dot = normal * dir;
if ( dot >= 0.0f ) {
return true;
}
}
// now find the exact screen bounds of the clipped triangle
w.SetNumPoints( 3 );
R_LocalPointToGlobal( drawSurf->space->modelMatrix, v1, w[0].ToVec3() );
R_LocalPointToGlobal( drawSurf->space->modelMatrix, v2, w[1].ToVec3() );
R_LocalPointToGlobal( drawSurf->space->modelMatrix, v3, w[2].ToVec3() );
w[0].s = w[0].t = w[1].s = w[1].t = w[2].s = w[2].t = 0.0f;
for ( j = 0; j < 4; j++ ) {
if ( !w.ClipInPlace( -tr.viewDef->frustum[j], 0.1f ) ) {
break;
}
}
for ( j = 0; j < w.GetNumPoints(); j++ ) {
idVec3 screen;
R_GlobalToNormalizedDeviceCoordinates( w[j].ToVec3(), screen );
ndcBounds.AddPoint( screen );
}
}
// if we don't enclose any area, return
if ( ndcBounds.IsCleared() ) {
return true;
}
return false;
}
/*
========================
R_MirrorViewBySurface
========================
*/
static viewDef_t *R_MirrorViewBySurface( drawSurf_t *drawSurf ) {
viewDef_t *parms;
orientation_t surface, camera;
idPlane originalPlane, plane;
// copy the viewport size from the original
parms = (viewDef_t *)R_FrameAlloc( sizeof( *parms ) );
*parms = *tr.viewDef;
parms->renderView.viewID = 0; // clear to allow player bodies to show up, and suppress view weapons
parms->isSubview = true;
parms->isMirror = true;
// create plane axis for the portal we are seeing
R_PlaneForSurface( drawSurf->geo, originalPlane );
R_LocalPlaneToGlobal( drawSurf->space->modelMatrix, originalPlane, plane );
surface.origin = plane.Normal() * -plane[3];
surface.axis[0] = plane.Normal();
surface.axis[0].NormalVectors( surface.axis[1], surface.axis[2] );
surface.axis[2] = -surface.axis[2];
camera.origin = surface.origin;
camera.axis[0] = -surface.axis[0];
camera.axis[1] = surface.axis[1];
camera.axis[2] = surface.axis[2];
// set the mirrored origin and axis
R_MirrorPoint( tr.viewDef->renderView.vieworg, &surface, &camera, parms->renderView.vieworg );
R_MirrorVector( tr.viewDef->renderView.viewaxis[0], &surface, &camera, parms->renderView.viewaxis[0] );
R_MirrorVector( tr.viewDef->renderView.viewaxis[1], &surface, &camera, parms->renderView.viewaxis[1] );
R_MirrorVector( tr.viewDef->renderView.viewaxis[2], &surface, &camera, parms->renderView.viewaxis[2] );
// make the view origin 16 units away from the center of the surface
idVec3 viewOrigin = ( drawSurf->geo->bounds[0] + drawSurf->geo->bounds[1] ) * 0.5;
viewOrigin += ( originalPlane.Normal() * 16 );
R_LocalPointToGlobal( drawSurf->space->modelMatrix, viewOrigin, parms->initialViewAreaOrigin );
// set the mirror clip plane
parms->numClipPlanes = 1;
parms->clipPlanes[0] = -camera.axis[0];
parms->clipPlanes[0][3] = -( camera.origin * parms->clipPlanes[0].Normal() );
return parms;
}
/*
========================
R_XrayViewBySurface
========================
*/
static viewDef_t *R_XrayViewBySurface( drawSurf_t *drawSurf ) {
viewDef_t *parms;
idPlane originalPlane, plane;
// copy the viewport size from the original
parms = (viewDef_t *)R_FrameAlloc( sizeof( *parms ) );
*parms = *tr.viewDef;
parms->renderView.viewID = 0; // clear to allow player bodies to show up, and suppress view weapons
parms->isSubview = true;
parms->isXraySubview = true;
return parms;
}
/*
===============
R_RemoteRender
===============
*/
static void R_RemoteRender( drawSurf_t *surf, textureStage_t *stage ) {
viewDef_t *parms;
// remote views can be reused in a single frame
if ( stage->dynamicFrameCount == tr.frameCount ) {
return;
}
// if the entity doesn't have a remoteRenderView, do nothing
if ( !surf->space->entityDef->parms.remoteRenderView ) {
return;
}
// copy the viewport size from the original
parms = (viewDef_t *)R_FrameAlloc( sizeof( *parms ) );
*parms = *tr.viewDef;
parms->isSubview = true;
parms->isMirror = false;
parms->renderView = *surf->space->entityDef->parms.remoteRenderView;
parms->renderView.viewID = 0; // clear to allow player bodies to show up, and suppress view weapons
parms->initialViewAreaOrigin = parms->renderView.vieworg;
tr.CropRenderSize( stage->width, stage->height, true );
parms->renderView.x = 0;
parms->renderView.y = 0;
parms->renderView.width = SCREEN_WIDTH;
parms->renderView.height = SCREEN_HEIGHT;
tr.RenderViewToViewport( &parms->renderView, &parms->viewport );
parms->scissor.x1 = 0;
parms->scissor.y1 = 0;
parms->scissor.x2 = parms->viewport.x2 - parms->viewport.x1;
parms->scissor.y2 = parms->viewport.y2 - parms->viewport.y1;
parms->superView = tr.viewDef;
parms->subviewSurface = surf;
// generate render commands for it
R_RenderView(parms);
// copy this rendering to the image
stage->dynamicFrameCount = tr.frameCount;
if (!stage->image) {
stage->image = globalImages->GetNextScratchImage();
}
tr.CaptureRenderToImage( stage->image->imgName );
tr.UnCrop();
}
/*
=================
R_MirrorRender
=================
*/
void R_MirrorRender( drawSurf_t *surf, textureStage_t *stage, idScreenRect scissor ) {
viewDef_t *parms;
// remote views can be reused in a single frame
if ( stage->dynamicFrameCount == tr.frameCount ) {
return;
}
// issue a new view command
parms = R_MirrorViewBySurface( surf );
if ( !parms ) {
return;
}
tr.CropRenderSize( stage->width, stage->height, true );
parms->renderView.x = 0;
parms->renderView.y = 0;
parms->renderView.width = SCREEN_WIDTH;
parms->renderView.height = SCREEN_HEIGHT;
tr.RenderViewToViewport( &parms->renderView, &parms->viewport );
parms->scissor.x1 = 0;
parms->scissor.y1 = 0;
parms->scissor.x2 = parms->viewport.x2 - parms->viewport.x1;
parms->scissor.y2 = parms->viewport.y2 - parms->viewport.y1;
parms->superView = tr.viewDef;
parms->subviewSurface = surf;
// triangle culling order changes with mirroring
parms->isMirror = ( ( (int)parms->isMirror ^ (int)tr.viewDef->isMirror ) != 0 );
// generate render commands for it
R_RenderView( parms );
// copy this rendering to the image
stage->dynamicFrameCount = tr.frameCount;
stage->image = globalImages->GetNextScratchImage();
tr.CaptureRenderToImage( stage->image->imgName );
tr.UnCrop();
}
/*
=================
R_XrayRender
=================
*/
void R_XrayRender( drawSurf_t *surf, textureStage_t *stage, idScreenRect scissor ) {
viewDef_t *parms;
// remote views can be reused in a single frame
if ( stage->dynamicFrameCount == tr.frameCount ) {
return;
}
// issue a new view command
parms = R_XrayViewBySurface( surf );
if ( !parms ) {
return;
}
tr.CropRenderSize( stage->width, stage->height, true );
parms->renderView.x = 0;
parms->renderView.y = 0;
parms->renderView.width = SCREEN_WIDTH;
parms->renderView.height = SCREEN_HEIGHT;
tr.RenderViewToViewport( &parms->renderView, &parms->viewport );
parms->scissor.x1 = 0;
parms->scissor.y1 = 0;
parms->scissor.x2 = parms->viewport.x2 - parms->viewport.x1;
parms->scissor.y2 = parms->viewport.y2 - parms->viewport.y1;
parms->superView = tr.viewDef;
parms->subviewSurface = surf;
// triangle culling order changes with mirroring
parms->isMirror = ( ( (int)parms->isMirror ^ (int)tr.viewDef->isMirror ) != 0 );
// generate render commands for it
R_RenderView( parms );
// copy this rendering to the image
stage->dynamicFrameCount = tr.frameCount;
stage->image = globalImages->GetNextScratchImage();
tr.CaptureRenderToImage( stage->image->imgName );
tr.UnCrop();
}
/*
==================
R_GenerateSurfaceSubview
==================
*/
bool R_GenerateSurfaceSubview( drawSurf_t *drawSurf ) {
idBounds ndcBounds;
viewDef_t *parms;
const idMaterial *shader;
// for testing the performance hit
if ( r_skipSubviews.GetBool() ) {
return false;
}
if ( R_PreciseCullSurface( drawSurf, ndcBounds ) ) {
return false;
}
shader = drawSurf->material;
// never recurse through a subview surface that we are
// already seeing through
for ( parms = tr.viewDef ; parms ; parms = parms->superView ) {
if ( parms->subviewSurface
&& parms->subviewSurface->geo == drawSurf->geo
&& parms->subviewSurface->space->entityDef == drawSurf->space->entityDef ) {
break;
}
}
if ( parms ) {
return false;
}
// crop the scissor bounds based on the precise cull
idScreenRect scissor;
idScreenRect *v = &tr.viewDef->viewport;
scissor.x1 = v->x1 + (int)( (v->x2 - v->x1 + 1 ) * 0.5f * ( ndcBounds[0][0] + 1.0f ));
scissor.y1 = v->y1 + (int)( (v->y2 - v->y1 + 1 ) * 0.5f * ( ndcBounds[0][1] + 1.0f ));
scissor.x2 = v->x1 + (int)( (v->x2 - v->x1 + 1 ) * 0.5f * ( ndcBounds[1][0] + 1.0f ));
scissor.y2 = v->y1 + (int)( (v->y2 - v->y1 + 1 ) * 0.5f * ( ndcBounds[1][1] + 1.0f ));
// nudge a bit for safety
scissor.Expand();
scissor.Intersect( tr.viewDef->scissor );
if ( scissor.IsEmpty() ) {
// cropped out
return false;
}
// DG: r_lockSurfaces needs special treatment
if ( r_lockSurfaces.GetBool() && tr.viewDef == tr.primaryView ) {
// we need the scissor for the "real" viewDef (actual camera position etc)
// so mirrors don't "float around" when looking around with r_lockSurfaces enabled
// So do the same calculation as before, but with real viewDef (but don't replace
// calculation above, so the whole mirror or whatever is skipped if not visible in
// locked view!)
viewDef_t* origViewDef = tr.viewDef;
tr.viewDef = &tr.lockSurfacesRealViewDef;
R_PreciseCullSurface( drawSurf, ndcBounds );
idScreenRect origScissor = scissor;
idScreenRect *v2 = &tr.viewDef->viewport;
scissor.x1 = v2->x1 + (int)( (v2->x2 - v2->x1 + 1 ) * 0.5f * ( ndcBounds[0][0] + 1.0f ));
scissor.y1 = v2->y1 + (int)( (v2->y2 - v2->y1 + 1 ) * 0.5f * ( ndcBounds[0][1] + 1.0f ));
scissor.x2 = v2->x1 + (int)( (v2->x2 - v2->x1 + 1 ) * 0.5f * ( ndcBounds[1][0] + 1.0f ));
scissor.y2 = v2->y1 + (int)( (v2->y2 - v2->y1 + 1 ) * 0.5f * ( ndcBounds[1][1] + 1.0f ));
// nudge a bit for safety
scissor.Expand();
scissor.Intersect( tr.viewDef->scissor );
// TBH I'm not 100% happy with how this is handled - you won't get reliable information
// on what's rendered in a mirror this way. Intersecting with the orig. scissor looks "best".
// For handling this "properly" we'd need the whole "locked viewDef vs real viewDef" thing
// for every subview (instead of just once for the primaryView) which would be a lot of
// work for a corner case...
scissor.Intersect( origScissor );
tr.viewDef = origViewDef;
} // DG end
// see what kind of subview we are making
if ( shader->GetSort() != SS_SUBVIEW ) {
for ( int i = 0 ; i < shader->GetNumStages() ; i++ ) {
const shaderStage_t *stage = shader->GetStage( i );
switch ( stage->texture.dynamic ) {
case DI_REMOTE_RENDER:
R_RemoteRender( drawSurf, const_cast<textureStage_t *>(&stage->texture) );
break;
case DI_MIRROR_RENDER: {
// DG: prevent mirrors from mirroring each other to infinity
// and also prevent tr.renderCrops from overflowing (with some extra wiggle
// room in case other cropping things are involved)
int maxDepth = Min(r_maxMirrorRecursion.GetInteger(), MAX_RENDER_CROPS-3);
if ( tr.currentRenderCrop >= maxDepth ) {
const_cast<textureStage_t &>(stage->texture).image = NULL;
return false;
}
R_MirrorRender( drawSurf, const_cast<textureStage_t *>(&stage->texture), scissor );
break;
}
case DI_XRAY_RENDER:
R_XrayRender( drawSurf, const_cast<textureStage_t *>(&stage->texture), scissor );
break;
}
}
return true;
}
// issue a new view command
parms = R_MirrorViewBySurface( drawSurf );
if ( !parms ) {
return false;
}
parms->scissor = scissor;
parms->superView = tr.viewDef;
parms->subviewSurface = drawSurf;
// triangle culling order changes with mirroring
parms->isMirror = ( ( (int)parms->isMirror ^ (int)tr.viewDef->isMirror ) != 0 );
// generate render commands for it
R_RenderView( parms );
return true;
}
/*
================
R_GenerateSubViews
If we need to render another view to complete the current view,
generate it first.
It is important to do this after all drawSurfs for the current
view have been generated, because it may create a subview which
would change tr.viewCount.
================
*/
bool R_GenerateSubViews( void ) {
drawSurf_t *drawSurf;
int i;
bool subviews;
const idMaterial *shader;
// for testing the performance hit
if ( r_skipSubviews.GetBool() ) {
return false;
}
subviews = false;
// scan the surfaces until we either find a subview, or determine
// there are no more subview surfaces.
for ( i = 0 ; i < tr.viewDef->numDrawSurfs ; i++ ) {
drawSurf = tr.viewDef->drawSurfs[i];
shader = drawSurf->material;
if ( !shader || !shader->HasSubview() ) {
continue;
}
if ( R_GenerateSurfaceSubview( drawSurf ) ) {
subviews = true;
}
}
return subviews;
}