mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-12 08:11:10 +02:00
1759 lines
47 KiB
C++
1759 lines
47 KiB
C++
/*
|
|
===========================================================================
|
|
|
|
IceTech GPL Source Code
|
|
Copyright (C) 2026 Justin Marshall
|
|
|
|
This file is part of the IceTech GPL Source Code (?IceTech Source Code?).
|
|
|
|
IceTech 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.
|
|
|
|
IceTech 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 IceTech Source Code. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
In addition, the IceTech 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 IceTech 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 Justin Marshall, justinmarshall20@gmail.com
|
|
|
|
===========================================================================
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
#pragma hdrstop
|
|
|
|
#include "tr_local.h"
|
|
|
|
/*
|
|
|
|
|
|
All that is done in these functions is the creation of viewLights
|
|
and viewEntitys for the lightDefs and entityDefs that are visible
|
|
in the portal areas that can be seen from the current viewpoint.
|
|
|
|
*/
|
|
|
|
|
|
// if we hit this many planes, we will just stop cropping the
|
|
// view down, which is still correct, just conservative
|
|
const int MAX_PORTAL_PLANES = 20;
|
|
|
|
typedef struct portalStack_s {
|
|
portal_t* p;
|
|
const struct portalStack_s* next;
|
|
|
|
idScreenRect rect;
|
|
|
|
int numPortalPlanes;
|
|
idPlane portalPlanes[MAX_PORTAL_PLANES + 1];
|
|
// positive side is outside the visible frustum
|
|
} portalStack_t;
|
|
|
|
|
|
//====================================================================
|
|
|
|
// DXR world visibility is deliberately driven by its own portal-shape flood.
|
|
//
|
|
// Raster visibility uses FlowViewThroughPortals() with the camera frustum as the
|
|
// initial portal stack, which is correct for viewEntitys/viewLights but too
|
|
// aggressive for ray tracing: reflections, GI, glossy rays, and shadow rays can
|
|
// need static world geometry that is outside the camera FOV.
|
|
//
|
|
// A plain connectedAreas flood is too broad because it ignores portal shape and
|
|
// line-of-sight through portals, often making the whole connected map visible.
|
|
//
|
|
// The DXR path below uses the middle ground:
|
|
// * hide all static world TLAS instances at the start of the view
|
|
// * flood through portals from the view origin with ZERO initial FOV planes
|
|
// * still clip recursively by the actual portal windings / portal planes
|
|
// * show the static world TLAS ranges for areas reached by that flood
|
|
|
|
static idRenderWorldLocal* s_dxrVisibilityWorld = NULL;
|
|
static int* s_dxrWorldModelFirstForArea = NULL;
|
|
static int* s_dxrWorldModelCountForArea = NULL;
|
|
static bool* s_dxrWorldModelShownForArea = NULL;
|
|
static bool s_dxrUseAllLocalModelDXRIndexing = false;
|
|
|
|
static int DXR_AreaNumForWorldModelName(const char* name) {
|
|
if (!name) {
|
|
return -1;
|
|
}
|
|
|
|
// Static proc world models are normally named _area0, _area1, ... by AddWorldModelEntities().
|
|
if (name[0] != '_' || name[1] != 'a' || name[2] != 'r' || name[3] != 'e' || name[4] != 'a') {
|
|
return -1;
|
|
}
|
|
|
|
const char* p = name + 5;
|
|
if (*p < '0' || *p > '9') {
|
|
return -1;
|
|
}
|
|
|
|
int areaNum = 0;
|
|
for (; *p; p++) {
|
|
if (*p < '0' || *p > '9') {
|
|
return -1;
|
|
}
|
|
areaNum = areaNum * 10 + (*p - '0');
|
|
}
|
|
|
|
return areaNum;
|
|
}
|
|
|
|
static int DXR_AreaNumForWorldModel(const idRenderModel* model) {
|
|
if (!model) {
|
|
return -1;
|
|
}
|
|
return DXR_AreaNumForWorldModelName(model->Name());
|
|
}
|
|
|
|
static bool DXR_SurfaceCreatesWorldTopAccelStruct(const modelSurface_t* surf) {
|
|
if (!surf || !surf->shader) {
|
|
return false;
|
|
}
|
|
|
|
// Must match the ParseModel() creation filter exactly:
|
|
// if ( !surf.shader->IsSky() && surf.shader->IsLitMaterial() )
|
|
return !surf->shader->IsSky() && surf->shader->IsLitMaterial();
|
|
}
|
|
|
|
static bool DXR_ModelContributesToWorldDXRList(idRenderWorldLocal* world, const idRenderModel* model) {
|
|
if (!world || !model) {
|
|
return false;
|
|
}
|
|
|
|
if (s_dxrUseAllLocalModelDXRIndexing) {
|
|
return true;
|
|
}
|
|
|
|
const int areaNum = DXR_AreaNumForWorldModel(model);
|
|
return areaNum >= 0 && areaNum < world->numPortalAreas;
|
|
}
|
|
|
|
static void DXR_SetWorldTopAccelStructVisible(idRenderWorldLocal* world, uint32_t topAccelStruct, bool visible) {
|
|
if (!world || !topAccelStruct) {
|
|
return;
|
|
}
|
|
|
|
glSetTopLevelAccelStructureVisible(world->dxrWorldId, topAccelStruct, visible ? 1 : 0);
|
|
}
|
|
|
|
static void DXR_HideAllWorldTopAccelStructs(idRenderWorldLocal* world) {
|
|
if (!world) {
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < world->worldDXRmodels.Num(); i++) {
|
|
DXR_SetWorldTopAccelStructVisible(world, world->worldDXRmodels[i].topAccelStruct, false);
|
|
}
|
|
}
|
|
|
|
static void DXR_ShowAllWorldTopAccelStructs(idRenderWorldLocal* world) {
|
|
if (!world) {
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < world->worldDXRmodels.Num(); i++) {
|
|
DXR_SetWorldTopAccelStructVisible(world, world->worldDXRmodels[i].topAccelStruct, true);
|
|
}
|
|
}
|
|
|
|
static int DXR_CountFilteredSurfacesForModel(const idRenderModel* model) {
|
|
if (!model) {
|
|
return 0;
|
|
}
|
|
|
|
int count = 0;
|
|
for (int surfaceNum = 0; surfaceNum < model->NumSurfaces(); surfaceNum++) {
|
|
const modelSurface_t* surf = model->Surface(surfaceNum);
|
|
if (DXR_SurfaceCreatesWorldTopAccelStruct(surf)) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static void DXR_SelectWorldDXRIndexingMode(idRenderWorldLocal* world) {
|
|
s_dxrUseAllLocalModelDXRIndexing = false;
|
|
|
|
if (!world) {
|
|
return;
|
|
}
|
|
|
|
int areaModelSurfaceCount = 0;
|
|
int allLocalModelSurfaceCount = 0;
|
|
|
|
for (int modelIndex = 0; modelIndex < world->localModels.Num(); modelIndex++) {
|
|
idRenderModel* model = world->localModels[modelIndex];
|
|
if (!model) {
|
|
continue;
|
|
}
|
|
|
|
const int modelSurfaceCount = DXR_CountFilteredSurfacesForModel(model);
|
|
allLocalModelSurfaceCount += modelSurfaceCount;
|
|
|
|
const int areaNum = DXR_AreaNumForWorldModel(model);
|
|
if (areaNum >= 0 && areaNum < world->numPortalAreas) {
|
|
areaModelSurfaceCount += modelSurfaceCount;
|
|
}
|
|
}
|
|
|
|
// The original Doom 3 proc-world path normally creates worldDXRmodels only
|
|
// from _areaN models. If the count does not match but all local lit surfaces
|
|
// do match, use all-local indexing as a compatibility fallback for builds
|
|
// where the DXR parser added more model types to worldDXRmodels.
|
|
if (world->worldDXRmodels.Num() != areaModelSurfaceCount &&
|
|
world->worldDXRmodels.Num() == allLocalModelSurfaceCount) {
|
|
s_dxrUseAllLocalModelDXRIndexing = true;
|
|
}
|
|
}
|
|
|
|
static void DXR_BuildWorldTopAccelStructAreaMap(idRenderWorldLocal* world) {
|
|
s_dxrVisibilityWorld = world;
|
|
s_dxrWorldModelFirstForArea = NULL;
|
|
s_dxrWorldModelCountForArea = NULL;
|
|
s_dxrWorldModelShownForArea = NULL;
|
|
s_dxrUseAllLocalModelDXRIndexing = false;
|
|
|
|
if (!world || world->numPortalAreas <= 0) {
|
|
return;
|
|
}
|
|
|
|
DXR_SelectWorldDXRIndexingMode(world);
|
|
|
|
s_dxrWorldModelFirstForArea = (int*)R_FrameAlloc(world->numPortalAreas * sizeof(s_dxrWorldModelFirstForArea[0]));
|
|
s_dxrWorldModelCountForArea = (int*)R_FrameAlloc(world->numPortalAreas * sizeof(s_dxrWorldModelCountForArea[0]));
|
|
s_dxrWorldModelShownForArea = (bool*)R_FrameAlloc(world->numPortalAreas * sizeof(s_dxrWorldModelShownForArea[0]));
|
|
|
|
for (int i = 0; i < world->numPortalAreas; i++) {
|
|
s_dxrWorldModelFirstForArea[i] = -1;
|
|
s_dxrWorldModelCountForArea[i] = 0;
|
|
s_dxrWorldModelShownForArea[i] = false;
|
|
}
|
|
|
|
int dxrIndex = 0;
|
|
for (int modelIndex = 0; modelIndex < world->localModels.Num(); modelIndex++) {
|
|
idRenderModel* model = world->localModels[modelIndex];
|
|
if (!model) {
|
|
continue;
|
|
}
|
|
|
|
const int areaNum = DXR_AreaNumForWorldModel(model);
|
|
const bool validAreaModel = areaNum >= 0 && areaNum < world->numPortalAreas;
|
|
const bool contributesToDXRList = DXR_ModelContributesToWorldDXRList(world, model);
|
|
const int firstForThisModel = dxrIndex;
|
|
int countForThisArea = 0;
|
|
|
|
for (int surfaceNum = 0; surfaceNum < model->NumSurfaces(); surfaceNum++) {
|
|
const modelSurface_t* surf = model->Surface(surfaceNum);
|
|
if (!DXR_SurfaceCreatesWorldTopAccelStruct(surf)) {
|
|
continue;
|
|
}
|
|
|
|
if (contributesToDXRList) {
|
|
if (validAreaModel && dxrIndex < world->worldDXRmodels.Num()) {
|
|
countForThisArea++;
|
|
}
|
|
dxrIndex++;
|
|
}
|
|
}
|
|
|
|
if (validAreaModel && countForThisArea > 0) {
|
|
s_dxrWorldModelFirstForArea[areaNum] = firstForThisModel;
|
|
s_dxrWorldModelCountForArea[areaNum] = countForThisArea;
|
|
}
|
|
}
|
|
|
|
// Conservative fallback for maps/builds where the DXR list is one entry per
|
|
// portal area but the _areaN surface mapping above could not identify a range.
|
|
if (world->worldDXRmodels.Num() == world->numPortalAreas) {
|
|
for (int areaNum = 0; areaNum < world->numPortalAreas; areaNum++) {
|
|
if (s_dxrWorldModelFirstForArea[areaNum] < 0) {
|
|
s_dxrWorldModelFirstForArea[areaNum] = areaNum;
|
|
s_dxrWorldModelCountForArea[areaNum] = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void DXR_BeginWorldTopAccelStructVisibilityPass(idRenderWorldLocal* world) {
|
|
DXR_BuildWorldTopAccelStructAreaMap(world);
|
|
DXR_HideAllWorldTopAccelStructs(world);
|
|
}
|
|
|
|
static void DXR_ShowWorldTopAccelStructsForArea(idRenderWorldLocal* world, int areaNum) {
|
|
if (!world || world != s_dxrVisibilityWorld ||
|
|
!s_dxrWorldModelFirstForArea || !s_dxrWorldModelCountForArea || !s_dxrWorldModelShownForArea) {
|
|
return;
|
|
}
|
|
if (areaNum < 0 || areaNum >= world->numPortalAreas) {
|
|
return;
|
|
}
|
|
if (s_dxrWorldModelShownForArea[areaNum]) {
|
|
return;
|
|
}
|
|
|
|
const int first = s_dxrWorldModelFirstForArea[areaNum];
|
|
const int count = s_dxrWorldModelCountForArea[areaNum];
|
|
if (first < 0 || count <= 0) {
|
|
return;
|
|
}
|
|
|
|
s_dxrWorldModelShownForArea[areaNum] = true;
|
|
|
|
const int end = first + count;
|
|
for (int i = first; i < end && i < world->worldDXRmodels.Num(); i++) {
|
|
DXR_SetWorldTopAccelStructVisible(world, world->worldDXRmodels[i].topAccelStruct, true);
|
|
}
|
|
}
|
|
|
|
static int DXR_FirstWorldDXRIndexForModel(idRenderWorldLocal* world, const idRenderModel* wantedModel) {
|
|
if (!world || !wantedModel) {
|
|
return -1;
|
|
}
|
|
|
|
int dxrIndex = 0;
|
|
|
|
for (int modelIndex = 0; modelIndex < world->localModels.Num(); modelIndex++) {
|
|
const idRenderModel* model = world->localModels[modelIndex];
|
|
if (!model) {
|
|
continue;
|
|
}
|
|
|
|
const bool contributesToDXRList = DXR_ModelContributesToWorldDXRList(world, model);
|
|
const bool isWantedModel = (model == wantedModel);
|
|
int firstForModel = -1;
|
|
|
|
for (int surfaceNum = 0; surfaceNum < model->NumSurfaces(); surfaceNum++) {
|
|
const modelSurface_t* surf = model->Surface(surfaceNum);
|
|
if (!DXR_SurfaceCreatesWorldTopAccelStruct(surf)) {
|
|
continue;
|
|
}
|
|
|
|
if (!contributesToDXRList) {
|
|
continue;
|
|
}
|
|
|
|
if (isWantedModel && firstForModel < 0) {
|
|
firstForModel = dxrIndex;
|
|
}
|
|
|
|
dxrIndex++;
|
|
}
|
|
|
|
if (isWantedModel) {
|
|
return firstForModel;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static int DXR_WorldDXRCountForModel(idRenderWorldLocal* world, const idRenderModel* model) {
|
|
if (!world || !model || !DXR_ModelContributesToWorldDXRList(world, model)) {
|
|
return 0;
|
|
}
|
|
|
|
return DXR_CountFilteredSurfacesForModel(model);
|
|
}
|
|
|
|
static void DXR_ShowWorldTopAccelStructsForModel(idRenderWorldLocal* world, const idRenderModel* model) {
|
|
if (!world || !model) {
|
|
return;
|
|
}
|
|
|
|
const int first = DXR_FirstWorldDXRIndexForModel(world, model);
|
|
const int count = DXR_WorldDXRCountForModel(world, model);
|
|
|
|
if (first < 0 || count <= 0) {
|
|
return;
|
|
}
|
|
|
|
const int end = first + count;
|
|
for (int i = first; i < end && i < world->worldDXRmodels.Num(); i++) {
|
|
DXR_SetWorldTopAccelStructVisible(world, world->worldDXRmodels[i].topAccelStruct, true);
|
|
}
|
|
}
|
|
|
|
static void DXR_ShowWorldTopAccelStructsReferencedByArea(idRenderWorldLocal* world, int areaNum) {
|
|
if (!world) {
|
|
return;
|
|
}
|
|
|
|
if (areaNum < 0 || areaNum >= world->numPortalAreas) {
|
|
return;
|
|
}
|
|
|
|
portalArea_t* area = &world->portalAreas[areaNum];
|
|
|
|
for (areaReference_t* ref = area->entityRefs.areaNext; ref != &area->entityRefs; ref = ref->areaNext) {
|
|
idRenderEntityLocal* entity = ref->entity;
|
|
if (!entity) {
|
|
continue;
|
|
}
|
|
|
|
idRenderModel* model = entity->parms.hModel;
|
|
if (!model) {
|
|
continue;
|
|
}
|
|
|
|
DXR_ShowWorldTopAccelStructsForModel(world, model);
|
|
}
|
|
}
|
|
|
|
static void DXR_ShowWorldTopAccelStructsForAreaRobust(idRenderWorldLocal* world, int areaNum) {
|
|
if (!world) {
|
|
return;
|
|
}
|
|
|
|
if (areaNum < 0 || areaNum >= world->numPortalAreas) {
|
|
return;
|
|
}
|
|
|
|
// For DXR this means "area already processed this view". The fast area
|
|
// range path and entity-ref fallback are both attempted before the flag is set.
|
|
if (s_dxrWorldModelShownForArea && s_dxrWorldModelShownForArea[areaNum]) {
|
|
return;
|
|
}
|
|
|
|
// Fast _areaN range path.
|
|
DXR_ShowWorldTopAccelStructsForArea(world, areaNum);
|
|
|
|
// Correctness fallback path using the actual render models referenced by this area.
|
|
DXR_ShowWorldTopAccelStructsReferencedByArea(world, areaNum);
|
|
|
|
if (s_dxrWorldModelShownForArea) {
|
|
s_dxrWorldModelShownForArea[areaNum] = true;
|
|
}
|
|
}
|
|
|
|
static void DXR_FloodWorldTopAccelStructVisibilityThroughArea_r(idRenderWorldLocal* world,
|
|
const idVec3 origin, int areaNum, const portalStack_t* ps) {
|
|
portal_t* p;
|
|
float d;
|
|
portalArea_t* area;
|
|
const portalStack_t* check;
|
|
portalStack_t newStack;
|
|
int i, j;
|
|
idVec3 v1, v2;
|
|
int addPlanes;
|
|
idFixedWinding w;
|
|
|
|
if (!world) {
|
|
return;
|
|
}
|
|
if (areaNum < 0 || areaNum >= world->numPortalAreas) {
|
|
return;
|
|
}
|
|
|
|
area = &world->portalAreas[areaNum];
|
|
|
|
// This area is visible to DXR from the view origin through the current portal chain.
|
|
DXR_ShowWorldTopAccelStructsForAreaRobust(world, areaNum);
|
|
|
|
for (p = area->portals; p; p = p->next) {
|
|
// Closed doors / blocked portals should still stop DXR visibility.
|
|
if (p->doublePortal->blockingBits & PS_BLOCK_VIEW) {
|
|
continue;
|
|
}
|
|
|
|
// Geometric portal facing test, not camera-FOV culling.
|
|
d = p->plane.Distance(origin);
|
|
if (d < -0.1f) {
|
|
continue;
|
|
}
|
|
|
|
// Avoid infinite portal recursion.
|
|
for (check = ps; check; check = check->next) {
|
|
if (check->p == p) {
|
|
break;
|
|
}
|
|
}
|
|
if (check) {
|
|
continue;
|
|
}
|
|
|
|
// If very close to the portal plane, avoid numerical clipping issues.
|
|
if (d < 1.0f) {
|
|
newStack = *ps;
|
|
newStack.p = p;
|
|
newStack.next = ps;
|
|
DXR_FloodWorldTopAccelStructVisibilityThroughArea_r(world, origin, p->intoArea, &newStack);
|
|
continue;
|
|
}
|
|
|
|
// Clip this portal against the current portal visibility cone.
|
|
w = *p->w;
|
|
for (j = 0; j < ps->numPortalPlanes; j++) {
|
|
if (!w.ClipInPlace(-ps->portalPlanes[j], 0)) {
|
|
break;
|
|
}
|
|
}
|
|
if (!w.GetNumPoints()) {
|
|
continue;
|
|
}
|
|
|
|
// Do NOT call PortalIsFoggedOut() here. Fog is a raster visibility
|
|
// optimization; it should not hide TLAS geometry needed by rays.
|
|
|
|
newStack.p = p;
|
|
newStack.next = ps;
|
|
|
|
addPlanes = w.GetNumPoints();
|
|
if (addPlanes > MAX_PORTAL_PLANES) {
|
|
addPlanes = MAX_PORTAL_PLANES;
|
|
}
|
|
|
|
newStack.numPortalPlanes = 0;
|
|
for (i = 0; i < addPlanes; i++) {
|
|
j = i + 1;
|
|
if (j == w.GetNumPoints()) {
|
|
j = 0;
|
|
}
|
|
|
|
v1 = origin - w[i].ToVec3();
|
|
v2 = origin - w[j].ToVec3();
|
|
|
|
newStack.portalPlanes[newStack.numPortalPlanes].Normal().Cross(v2, v1);
|
|
|
|
if (newStack.portalPlanes[newStack.numPortalPlanes].Normalize() < 0.01f) {
|
|
continue;
|
|
}
|
|
|
|
newStack.portalPlanes[newStack.numPortalPlanes].FitThroughPoint(origin);
|
|
newStack.numPortalPlanes++;
|
|
}
|
|
|
|
// The last stack plane is the portal plane, matching the raster flood.
|
|
newStack.portalPlanes[newStack.numPortalPlanes] = p->plane;
|
|
newStack.numPortalPlanes++;
|
|
|
|
DXR_FloodWorldTopAccelStructVisibilityThroughArea_r(world, origin, p->intoArea, &newStack);
|
|
}
|
|
}
|
|
|
|
static void DXR_FlowWorldTopAccelStructVisibilityThroughPortals(idRenderWorldLocal* world) {
|
|
portalStack_t ps;
|
|
|
|
if (!world || !tr.viewDef) {
|
|
return;
|
|
}
|
|
|
|
// If portal culling is disabled, match raster debug behavior and show all.
|
|
if (!r_usePortals.GetBool()) {
|
|
DXR_ShowAllWorldTopAccelStructs(world);
|
|
return;
|
|
}
|
|
|
|
// If outside the world, match original FlowViewThroughPortals behavior and show all.
|
|
if (tr.viewDef->areaNum < 0) {
|
|
DXR_ShowAllWorldTopAccelStructs(world);
|
|
return;
|
|
}
|
|
|
|
// Honor r_singleArea debug mode.
|
|
if (r_singleArea.GetBool()) {
|
|
DXR_ShowWorldTopAccelStructsForAreaRobust(world, tr.viewDef->areaNum);
|
|
return;
|
|
}
|
|
|
|
memset(&ps, 0, sizeof(ps));
|
|
ps.next = NULL;
|
|
ps.p = NULL;
|
|
|
|
// Critical difference from raster:
|
|
// zero initial planes means "do not camera-FOV cull DXR".
|
|
// The recursive portal cone still prevents showing the whole connected map.
|
|
ps.numPortalPlanes = 0;
|
|
ps.rect = tr.viewDef->scissor;
|
|
|
|
DXR_FloodWorldTopAccelStructVisibilityThroughArea_r(world,
|
|
tr.viewDef->renderView.vieworg, tr.viewDef->areaNum, &ps);
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
idRenderWorldLocal::ScreenRectForWinding
|
|
===================
|
|
*/
|
|
idScreenRect idRenderWorldLocal::ScreenRectFromWinding(const idWinding* w, viewEntity_t* space) {
|
|
idScreenRect r;
|
|
int i;
|
|
idVec3 v;
|
|
idVec3 ndc;
|
|
float windowX, windowY;
|
|
|
|
r.Clear();
|
|
for (i = 0; i < w->GetNumPoints(); i++) {
|
|
R_LocalPointToGlobal(space->modelMatrix, (*w)[i].ToVec3(), v);
|
|
R_GlobalToNormalizedDeviceCoordinates(v, ndc);
|
|
|
|
windowX = 0.5f * (1.0f + ndc[0]) * (tr.viewDef->viewport.x2 - tr.viewDef->viewport.x1);
|
|
windowY = 0.5f * (1.0f + ndc[1]) * (tr.viewDef->viewport.y2 - tr.viewDef->viewport.y1);
|
|
|
|
r.AddPoint(windowX, windowY);
|
|
}
|
|
|
|
r.Expand();
|
|
|
|
return r;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
PortalIsFoggedOut
|
|
===================
|
|
*/
|
|
bool idRenderWorldLocal::PortalIsFoggedOut(const portal_t* p) {
|
|
idRenderLightLocal* ldef;
|
|
const idWinding* w;
|
|
int i;
|
|
idPlane forward;
|
|
|
|
ldef = p->doublePortal->fogLight;
|
|
if (!ldef) {
|
|
return false;
|
|
}
|
|
|
|
// find the current density of the fog
|
|
const idMaterial* lightShader = ldef->lightShader;
|
|
int size = sizeof(float) * lightShader->GetNumRegisters();
|
|
float* regs = (float*)_alloca(size);
|
|
|
|
lightShader->EvaluateRegisters(regs, ldef->parms.shaderParms, tr.viewDef, ldef->parms.referenceSound);
|
|
|
|
const shaderStage_t* stage = lightShader->GetStage(0);
|
|
|
|
float alpha = regs[stage->color.registers[3]];
|
|
|
|
|
|
// if they left the default value on, set a fog distance of 500
|
|
float a;
|
|
|
|
if (alpha <= 1.0f) {
|
|
a = -0.5f / DEFAULT_FOG_DISTANCE;
|
|
}
|
|
else {
|
|
// otherwise, distance = alpha color
|
|
a = -0.5f / alpha;
|
|
}
|
|
|
|
forward[0] = a * tr.viewDef->worldSpace.modelViewMatrix[2];
|
|
forward[1] = a * tr.viewDef->worldSpace.modelViewMatrix[6];
|
|
forward[2] = a * tr.viewDef->worldSpace.modelViewMatrix[10];
|
|
forward[3] = a * tr.viewDef->worldSpace.modelViewMatrix[14];
|
|
|
|
w = p->w;
|
|
for (i = 0; i < w->GetNumPoints(); i++) {
|
|
float d;
|
|
|
|
d = forward.Distance((*w)[i].ToVec3());
|
|
if (d < 0.5f) {
|
|
return false; // a point not clipped off
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool R_ScreenRectContains( const idScreenRect& outer, const idScreenRect& inner ) {
|
|
if ( inner.IsEmpty() ) {
|
|
return true;
|
|
}
|
|
if ( outer.IsEmpty() ) {
|
|
return false;
|
|
}
|
|
return outer.x1 <= inner.x1 && outer.y1 <= inner.y1 && outer.x2 >= inner.x2 && outer.y2 >= inner.y2;
|
|
}
|
|
|
|
static int R_ScreenRectPixelArea( const idScreenRect& rect ) {
|
|
if ( rect.IsEmpty() ) {
|
|
return 0;
|
|
}
|
|
return ( rect.x2 - rect.x1 + 1 ) * ( rect.y2 - rect.y1 + 1 );
|
|
}
|
|
|
|
static int R_ScreenRectExpansionArea( const idScreenRect& outer, const idScreenRect& inner ) {
|
|
if ( inner.IsEmpty() ) {
|
|
return 0;
|
|
}
|
|
if ( outer.IsEmpty() ) {
|
|
return R_ScreenRectPixelArea( inner );
|
|
}
|
|
idScreenRect expanded = outer;
|
|
expanded.Union( inner );
|
|
return R_ScreenRectPixelArea( expanded ) - R_ScreenRectPixelArea( outer );
|
|
}
|
|
|
|
static bool R_ScreenRectLightExpansionIsTiny( const idScreenRect& outer, const idScreenRect& inner ) {
|
|
const int expansionArea = R_ScreenRectExpansionArea( outer, inner );
|
|
const int coveredArea = R_ScreenRectPixelArea( outer );
|
|
return expansionArea <= Max( 1024, coveredArea / 24 );
|
|
}
|
|
|
|
static const int MAX_CONTAINED_AREA_REFLOODS = 2;
|
|
static byte* s_containedAreaRefloods = NULL;
|
|
|
|
/*
|
|
===================
|
|
FloodViewThroughArea_r
|
|
===================
|
|
*/
|
|
void idRenderWorldLocal::FloodViewThroughArea_r(const idVec3 origin, int areaNum,
|
|
const struct portalStack_s* ps) {
|
|
portal_t* p;
|
|
float d;
|
|
portalArea_t* area;
|
|
const portalStack_t* check;
|
|
portalStack_t newStack;
|
|
int i, j;
|
|
idVec3 v1, v2;
|
|
int addPlanes;
|
|
idFixedWinding w; // we won't overflow because MAX_PORTAL_PLANES = 20
|
|
|
|
area = &portalAreas[areaNum];
|
|
|
|
const bool areaAlreadySeen = area->viewCount == tr.viewCount;
|
|
const bool rectAlreadyCovered = areaAlreadySeen && R_ScreenRectContains( areaScreenRect[areaNum], ps->rect );
|
|
const bool tinyLightExpansion = areaAlreadySeen && !rectAlreadyCovered && R_ScreenRectLightExpansionIsTiny( areaScreenRect[areaNum], ps->rect );
|
|
|
|
// Synthetic newmap portals can create many equivalent paths into the same
|
|
// area. A contained rect still has a different plane stack, so let it add
|
|
// entities for correctness, but avoid repeatedly adding lights for duplicate
|
|
// covered screen space.
|
|
if ( rectAlreadyCovered ) {
|
|
AddAreaEntityRefs(areaNum, ps);
|
|
if ( !s_containedAreaRefloods || s_containedAreaRefloods[areaNum] >= MAX_CONTAINED_AREA_REFLOODS ) {
|
|
return;
|
|
}
|
|
s_containedAreaRefloods[areaNum]++;
|
|
} else if ( tinyLightExpansion ) {
|
|
AddAreaEntityRefs(areaNum, ps);
|
|
areaScreenRect[areaNum].Union(ps->rect);
|
|
return;
|
|
} else {
|
|
// cull models and lights to the current collection of planes
|
|
AddAreaRefs(areaNum, ps);
|
|
}
|
|
|
|
if (areaScreenRect[areaNum].IsEmpty()) {
|
|
areaScreenRect[areaNum] = ps->rect;
|
|
}
|
|
else {
|
|
areaScreenRect[areaNum].Union(ps->rect);
|
|
}
|
|
|
|
// go through all the portals
|
|
for (p = area->portals; p; p = p->next) {
|
|
// an enclosing door may have sealed the portal off
|
|
if (p->doublePortal->blockingBits & PS_BLOCK_VIEW) {
|
|
continue;
|
|
}
|
|
|
|
// make sure this portal is facing away from the view
|
|
d = p->plane.Distance(origin);
|
|
if (d < -0.1f) {
|
|
continue;
|
|
}
|
|
|
|
// make sure the portal isn't in our stack trace,
|
|
// which would cause an infinite loop
|
|
for (check = ps; check; check = check->next) {
|
|
if (check->p == p) {
|
|
break; // don't recursively enter a stack
|
|
}
|
|
}
|
|
if (check) {
|
|
continue; // already in stack
|
|
}
|
|
|
|
// if we are very close to the portal surface, don't bother clipping
|
|
// it, which tends to give epsilon problems that make the area vanish
|
|
if (d < 1.0f) {
|
|
|
|
// go through this portal
|
|
newStack = *ps;
|
|
newStack.p = p;
|
|
newStack.next = ps;
|
|
FloodViewThroughArea_r(origin, p->intoArea, &newStack);
|
|
continue;
|
|
}
|
|
|
|
// clip the portal winding to all of the planes
|
|
w = *p->w;
|
|
for (j = 0; j < ps->numPortalPlanes; j++) {
|
|
if (!w.ClipInPlace(-ps->portalPlanes[j], 0)) {
|
|
break;
|
|
}
|
|
}
|
|
if (!w.GetNumPoints()) {
|
|
continue; // portal not visible
|
|
}
|
|
|
|
// see if it is fogged out
|
|
if (PortalIsFoggedOut(p)) {
|
|
continue;
|
|
}
|
|
|
|
// go through this portal
|
|
newStack.p = p;
|
|
newStack.next = ps;
|
|
|
|
// find the screen pixel bounding box of the remaining portal
|
|
// so we can scissor things outside it
|
|
newStack.rect = ScreenRectFromWinding(&w, &tr.identitySpace);
|
|
newStack.rect.Expand();
|
|
|
|
// slop might have spread it a pixel outside, so trim it back
|
|
newStack.rect.Intersect(ps->rect);
|
|
|
|
// generate a set of clipping planes that will further restrict
|
|
// the visible view beyond just the scissor rect
|
|
|
|
addPlanes = w.GetNumPoints();
|
|
if (addPlanes > MAX_PORTAL_PLANES) {
|
|
addPlanes = MAX_PORTAL_PLANES;
|
|
}
|
|
|
|
newStack.numPortalPlanes = 0;
|
|
for (i = 0; i < addPlanes; i++) {
|
|
j = i + 1;
|
|
if (j == w.GetNumPoints()) {
|
|
j = 0;
|
|
}
|
|
|
|
v1 = origin - w[i].ToVec3();
|
|
v2 = origin - w[j].ToVec3();
|
|
|
|
newStack.portalPlanes[newStack.numPortalPlanes].Normal().Cross(v2, v1);
|
|
|
|
// if it is degenerate, skip the plane
|
|
if (newStack.portalPlanes[newStack.numPortalPlanes].Normalize() < 0.01f) {
|
|
continue;
|
|
}
|
|
newStack.portalPlanes[newStack.numPortalPlanes].FitThroughPoint(origin);
|
|
|
|
newStack.numPortalPlanes++;
|
|
}
|
|
|
|
// the last stack plane is the portal plane
|
|
newStack.portalPlanes[newStack.numPortalPlanes] = p->plane;
|
|
newStack.numPortalPlanes++;
|
|
|
|
FloodViewThroughArea_r(origin, p->intoArea, &newStack);
|
|
}
|
|
}
|
|
|
|
/*
|
|
=======================
|
|
FlowViewThroughPortals
|
|
|
|
Finds viewLights and viewEntities by flowing from an origin through the visible portals.
|
|
origin point can see into. The planes array defines a volume (positive
|
|
sides facing in) that should contain the origin, such as a view frustum or a point light box.
|
|
Zero planes assumes an unbounded volume.
|
|
=======================
|
|
*/
|
|
void idRenderWorldLocal::FlowViewThroughPortals(const idVec3 origin, int numPlanes, const idPlane* planes) {
|
|
portalStack_t ps;
|
|
int i;
|
|
|
|
ps.next = NULL;
|
|
ps.p = NULL;
|
|
|
|
for (i = 0; i < numPlanes; i++) {
|
|
ps.portalPlanes[i] = planes[i];
|
|
}
|
|
|
|
ps.numPortalPlanes = numPlanes;
|
|
ps.rect = tr.viewDef->scissor;
|
|
|
|
if (tr.viewDef->areaNum < 0) {
|
|
s_containedAreaRefloods = NULL;
|
|
|
|
for (i = 0; i < numPortalAreas; i++) {
|
|
areaScreenRect[i] = tr.viewDef->scissor;
|
|
}
|
|
|
|
// if outside the world, mark everything
|
|
for (i = 0; i < numPortalAreas; i++) {
|
|
AddAreaRefs(i, &ps);
|
|
}
|
|
}
|
|
else {
|
|
|
|
s_containedAreaRefloods = (byte*)R_FrameAlloc(numPortalAreas * sizeof(s_containedAreaRefloods[0]));
|
|
memset(s_containedAreaRefloods, 0, numPortalAreas * sizeof(s_containedAreaRefloods[0]));
|
|
|
|
for (i = 0; i < numPortalAreas; i++) {
|
|
areaScreenRect[i].Clear();
|
|
}
|
|
|
|
// flood out through portals, setting area viewCount
|
|
FloodViewThroughArea_r(origin, tr.viewDef->areaNum, &ps);
|
|
}
|
|
}
|
|
|
|
//==================================================================================================
|
|
|
|
|
|
/*
|
|
===================
|
|
FloodLightThroughArea_r
|
|
===================
|
|
*/
|
|
void idRenderWorldLocal::FloodLightThroughArea_r(idRenderLightLocal* light, int areaNum,
|
|
const struct portalStack_s* ps) {
|
|
portal_t* p;
|
|
float d;
|
|
portalArea_t* area;
|
|
const portalStack_t* check, * firstPortalStack;
|
|
portalStack_t newStack;
|
|
int i, j;
|
|
idVec3 v1, v2;
|
|
int addPlanes;
|
|
idFixedWinding w; // we won't overflow because MAX_PORTAL_PLANES = 20
|
|
|
|
area = &portalAreas[areaNum];
|
|
|
|
// add an areaRef
|
|
AddLightRefToArea(light, area);
|
|
|
|
// go through all the portals
|
|
for (p = area->portals; p; p = p->next) {
|
|
// make sure this portal is facing away from the view
|
|
d = p->plane.Distance(light->globalLightOrigin);
|
|
if (d < -0.1f) {
|
|
continue;
|
|
}
|
|
|
|
// make sure the portal isn't in our stack trace,
|
|
// which would cause an infinite loop
|
|
for (check = ps; check; check = check->next) {
|
|
firstPortalStack = check;
|
|
if (check->p == p) {
|
|
break; // don't recursively enter a stack
|
|
}
|
|
}
|
|
if (check) {
|
|
continue; // already in stack
|
|
}
|
|
|
|
// if we are very close to the portal surface, don't bother clipping
|
|
// it, which tends to give epsilon problems that make the area vanish
|
|
if (d < 1.0f) {
|
|
// go through this portal
|
|
newStack = *ps;
|
|
newStack.p = p;
|
|
newStack.next = ps;
|
|
FloodLightThroughArea_r(light, p->intoArea, &newStack);
|
|
continue;
|
|
}
|
|
|
|
// clip the portal winding to all of the planes
|
|
w = *p->w;
|
|
for (j = 0; j < ps->numPortalPlanes; j++) {
|
|
if (!w.ClipInPlace(-ps->portalPlanes[j], 0)) {
|
|
break;
|
|
}
|
|
}
|
|
if (!w.GetNumPoints()) {
|
|
continue; // portal not visible
|
|
}
|
|
// also always clip to the original light planes, because they aren't
|
|
// necessarily extending to infinitiy like a view frustum
|
|
for (j = 0; j < firstPortalStack->numPortalPlanes; j++) {
|
|
if (!w.ClipInPlace(-firstPortalStack->portalPlanes[j], 0)) {
|
|
break;
|
|
}
|
|
}
|
|
if (!w.GetNumPoints()) {
|
|
continue; // portal not visible
|
|
}
|
|
|
|
// go through this portal
|
|
newStack.p = p;
|
|
newStack.next = ps;
|
|
|
|
// generate a set of clipping planes that will further restrict
|
|
// the visible view beyond just the scissor rect
|
|
|
|
addPlanes = w.GetNumPoints();
|
|
if (addPlanes > MAX_PORTAL_PLANES) {
|
|
addPlanes = MAX_PORTAL_PLANES;
|
|
}
|
|
|
|
newStack.numPortalPlanes = 0;
|
|
for (i = 0; i < addPlanes; i++) {
|
|
j = i + 1;
|
|
if (j == w.GetNumPoints()) {
|
|
j = 0;
|
|
}
|
|
|
|
v1 = light->globalLightOrigin - w[i].ToVec3();
|
|
v2 = light->globalLightOrigin - w[j].ToVec3();
|
|
|
|
newStack.portalPlanes[newStack.numPortalPlanes].Normal().Cross(v2, v1);
|
|
|
|
// if it is degenerate, skip the plane
|
|
if (newStack.portalPlanes[newStack.numPortalPlanes].Normalize() < 0.01f) {
|
|
continue;
|
|
}
|
|
newStack.portalPlanes[newStack.numPortalPlanes].FitThroughPoint(light->globalLightOrigin);
|
|
|
|
newStack.numPortalPlanes++;
|
|
}
|
|
|
|
FloodLightThroughArea_r(light, p->intoArea, &newStack);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
=======================
|
|
FlowLightThroughPortals
|
|
|
|
Adds an arearef in each area that the light center flows into.
|
|
This can only be used for shadow casting lights that have a generated
|
|
prelight, because shadows are cast from back side which may not be in visible areas.
|
|
=======================
|
|
*/
|
|
void idRenderWorldLocal::FlowLightThroughPortals(idRenderLightLocal* light) {
|
|
portalStack_t ps;
|
|
int i;
|
|
const idVec3 origin = light->globalLightOrigin;
|
|
|
|
// if the light origin areaNum is not in a valid area,
|
|
// the light won't have any area refs
|
|
if (light->areaNum == -1) {
|
|
return;
|
|
}
|
|
|
|
memset(&ps, 0, sizeof(ps));
|
|
|
|
ps.numPortalPlanes = 6;
|
|
for (i = 0; i < 6; i++) {
|
|
ps.portalPlanes[i] = light->frustum[i];
|
|
}
|
|
|
|
FloodLightThroughArea_r(light, light->areaNum, &ps);
|
|
}
|
|
|
|
//======================================================================================================
|
|
|
|
/*
|
|
===================
|
|
idRenderWorldLocal::FloodFrustumAreas_r
|
|
===================
|
|
*/
|
|
areaNumRef_t* idRenderWorldLocal::FloodFrustumAreas_r(const idFrustum& frustum, const int areaNum, const idBounds& bounds, areaNumRef_t* areas) {
|
|
portal_t* p;
|
|
portalArea_t* portalArea;
|
|
idBounds newBounds;
|
|
areaNumRef_t* a;
|
|
|
|
portalArea = &portalAreas[areaNum];
|
|
|
|
// go through all the portals
|
|
for (p = portalArea->portals; p; p = p->next) {
|
|
|
|
// check if we already visited the area the portal leads to
|
|
for (a = areas; a; a = a->next) {
|
|
if (a->areaNum == p->intoArea) {
|
|
break;
|
|
}
|
|
}
|
|
if (a) {
|
|
continue;
|
|
}
|
|
|
|
// the frustum origin must be at the front of the portal plane
|
|
if (p->plane.Side(frustum.GetOrigin(), 0.1f) == SIDE_BACK) {
|
|
continue;
|
|
}
|
|
|
|
// the frustum must cross the portal plane
|
|
if (frustum.PlaneSide(p->plane, 0.0f) != PLANESIDE_CROSS) {
|
|
continue;
|
|
}
|
|
|
|
// get the bounds for the portal winding projected in the frustum
|
|
frustum.ProjectionBounds(*p->w, newBounds);
|
|
|
|
newBounds.IntersectSelf(bounds);
|
|
|
|
if (newBounds[0][0] > newBounds[1][0] || newBounds[0][1] > newBounds[1][1] || newBounds[0][2] > newBounds[1][2]) {
|
|
continue;
|
|
}
|
|
|
|
newBounds[1][0] = frustum.GetFarDistance();
|
|
|
|
a = areaNumRefAllocator.Alloc();
|
|
a->areaNum = p->intoArea;
|
|
a->next = areas;
|
|
areas = a;
|
|
|
|
areas = FloodFrustumAreas_r(frustum, p->intoArea, newBounds, areas);
|
|
}
|
|
|
|
return areas;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
idRenderWorldLocal::FloodFrustumAreas
|
|
|
|
Retrieves all the portal areas the frustum floods into where the frustum starts in the given areas.
|
|
All portals are assumed to be open.
|
|
===================
|
|
*/
|
|
areaNumRef_t* idRenderWorldLocal::FloodFrustumAreas(const idFrustum& frustum, areaNumRef_t* areas) {
|
|
idBounds bounds;
|
|
areaNumRef_t* a;
|
|
|
|
// bounds that cover the whole frustum
|
|
bounds[0].Set(frustum.GetNearDistance(), -1.0f, -1.0f);
|
|
bounds[1].Set(frustum.GetFarDistance(), 1.0f, 1.0f);
|
|
|
|
for (a = areas; a; a = a->next) {
|
|
areas = FloodFrustumAreas_r(frustum, a->areaNum, bounds, areas);
|
|
}
|
|
|
|
return areas;
|
|
}
|
|
|
|
|
|
/*
|
|
=======================================================================
|
|
|
|
R_FindViewLightsAndEntities
|
|
|
|
=======================================================================
|
|
*/
|
|
|
|
/*
|
|
================
|
|
CullEntityByPortals
|
|
|
|
Return true if the entity reference bounds do not intersect the current portal chain.
|
|
================
|
|
*/
|
|
bool idRenderWorldLocal::CullEntityByPortals(const idRenderEntityLocal* entity, const portalStack_t* ps) {
|
|
|
|
if (!r_useEntityCulling.GetBool()) {
|
|
return false;
|
|
}
|
|
|
|
// try to cull the entire thing using the reference bounds.
|
|
// we do not yet do callbacks or dynamic model creation,
|
|
// because we want to do all touching of the model after
|
|
// we have determined all the lights that may effect it,
|
|
// which optimizes cache usage
|
|
if (R_CullLocalBox(entity->referenceBounds, entity->modelMatrix,
|
|
ps->numPortalPlanes, ps->portalPlanes)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
AddAreaEntityRefs
|
|
|
|
Any models that are visible through the current portalStack will
|
|
have their scissor
|
|
===================
|
|
*/
|
|
void idRenderWorldLocal::AddAreaEntityRefs(int areaNum, const portalStack_t* ps) {
|
|
areaReference_t* ref;
|
|
idRenderEntityLocal* entity;
|
|
portalArea_t* area;
|
|
viewEntity_t* vEnt;
|
|
idBounds b;
|
|
|
|
area = &portalAreas[areaNum];
|
|
|
|
for (ref = area->entityRefs.areaNext; ref != &area->entityRefs; ref = ref->areaNext) {
|
|
entity = ref->entity;
|
|
|
|
// debug tool to allow viewing of only one entity at a time
|
|
if (r_singleEntity.GetInteger() >= 0 && r_singleEntity.GetInteger() != entity->index) {
|
|
continue;
|
|
}
|
|
|
|
// remove decals that are completely faded away
|
|
R_FreeEntityDefFadedDecals(entity, tr.viewDef->renderView.time);
|
|
|
|
// check for completely suppressing the model
|
|
if (!r_skipSuppress.GetBool()) {
|
|
if (entity->parms.suppressSurfaceInViewID
|
|
&& entity->parms.suppressSurfaceInViewID == tr.viewDef->renderView.viewID) {
|
|
continue;
|
|
}
|
|
if (entity->parms.allowSurfaceInViewID
|
|
&& entity->parms.allowSurfaceInViewID != tr.viewDef->renderView.viewID) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// cull reference bounds
|
|
if (CullEntityByPortals(entity, ps)) {
|
|
// we are culled out through this portal chain, but it might
|
|
// still be visible through others
|
|
continue;
|
|
}
|
|
|
|
vEnt = R_SetEntityDefViewEntity(entity);
|
|
|
|
// possibly expand the scissor rect
|
|
vEnt->scissorRect.Union(ps->rect);
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
CullLightByPortals
|
|
|
|
Return true if the light frustum does not intersect the current portal chain.
|
|
The last stack plane is not used because lights are not near clipped.
|
|
================
|
|
*/
|
|
bool idRenderWorldLocal::CullLightByPortals(const idRenderLightLocal* light, const portalStack_t* ps) {
|
|
int i, j;
|
|
const srfTriangles_t* tri;
|
|
float d;
|
|
idFixedWinding w; // we won't overflow because MAX_PORTAL_PLANES = 20
|
|
|
|
if (r_useLightCulling.GetInteger() == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (r_useLightCulling.GetInteger() >= 2) {
|
|
// exact clip of light faces against all planes
|
|
for (i = 0; i < 6; i++) {
|
|
// the light frustum planes face out from the light,
|
|
// so the planes that have the view origin on the negative
|
|
// side will be the "back" faces of the light, which must have
|
|
// some fragment inside the portalStack to be visible
|
|
if (light->frustum[i].Distance(tr.viewDef->renderView.vieworg) >= 0) {
|
|
continue;
|
|
}
|
|
|
|
// get the exact winding for this side
|
|
const idWinding* ow = light->frustumWindings[i];
|
|
|
|
// projected lights may have one of the frustums degenerated
|
|
if (!ow) {
|
|
continue;
|
|
}
|
|
|
|
w = *ow;
|
|
|
|
// now check the winding against each of the portalStack planes
|
|
for (j = 0; j < ps->numPortalPlanes - 1; j++) {
|
|
if (!w.ClipInPlace(-ps->portalPlanes[j])) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (w.GetNumPoints()) {
|
|
// part of the winding is visible through the portalStack,
|
|
// so the light is not culled
|
|
return false;
|
|
}
|
|
}
|
|
// none of the light surfaces were visible
|
|
return true;
|
|
|
|
}
|
|
else {
|
|
|
|
// simple point check against each plane
|
|
tri = light->frustumTris;
|
|
|
|
// check against frustum planes
|
|
for (i = 0; i < ps->numPortalPlanes - 1; i++) {
|
|
for (j = 0; j < tri->numVerts; j++) {
|
|
d = ps->portalPlanes[i].Distance(tri->verts[j].xyz);
|
|
if (d < 0.0f) {
|
|
break; // point is inside this plane
|
|
}
|
|
}
|
|
if (j == tri->numVerts) {
|
|
// all points were outside one of the planes
|
|
tr.pc.c_box_cull_out++;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
===================
|
|
AddAreaLightRefs
|
|
|
|
This is the only point where lights get added to the viewLights list
|
|
===================
|
|
*/
|
|
void idRenderWorldLocal::AddAreaLightRefs(int areaNum, const portalStack_t* ps) {
|
|
areaReference_t* lref;
|
|
portalArea_t* area;
|
|
idRenderLightLocal* light;
|
|
viewLight_t* vLight;
|
|
|
|
area = &portalAreas[areaNum];
|
|
|
|
for (lref = area->lightRefs.areaNext; lref != &area->lightRefs; lref = lref->areaNext) {
|
|
light = lref->light;
|
|
|
|
// debug tool to allow viewing of only one light at a time
|
|
if (r_singleLight.GetInteger() >= 0 && r_singleLight.GetInteger() != light->index) {
|
|
continue;
|
|
}
|
|
|
|
// check for being closed off behind a door
|
|
// a light that doesn't cast shadows will still light even if it is behind a door
|
|
if (r_useLightCulling.GetInteger() >= 3 &&
|
|
!light->parms.noShadows && light->lightShader->LightCastsShadows()
|
|
&& light->areaNum != -1 && !tr.viewDef->connectedAreas[light->areaNum]) {
|
|
continue;
|
|
}
|
|
|
|
// cull frustum
|
|
if (CullLightByPortals(light, ps)) {
|
|
// we are culled out through this portal chain, but it might
|
|
// still be visible through others
|
|
continue;
|
|
}
|
|
|
|
vLight = R_SetLightDefViewLight(light);
|
|
|
|
// expand the scissor rect
|
|
vLight->scissorRect.Union(ps->rect);
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
AddAreaRefs
|
|
|
|
This may be entered multiple times with different planes
|
|
if more than one portal sees into the area
|
|
===================
|
|
*/
|
|
void idRenderWorldLocal::AddAreaRefs(int areaNum, const portalStack_t* ps) {
|
|
// mark the viewCount, so r_showPortals can display the
|
|
// considered portals
|
|
portalAreas[areaNum].viewCount = tr.viewCount;
|
|
|
|
// Do not drive DXR visibility from here.
|
|
// This function is camera-FOV portal visibility for raster only.
|
|
// DXR static world visibility is handled once in FindViewLightsAndEntities()
|
|
// with a separate no-initial-FOV portal-shape flood.
|
|
|
|
// add the models and lights, using more precise culling to the planes
|
|
AddAreaEntityRefs(areaNum, ps);
|
|
AddAreaLightRefs(areaNum, ps);
|
|
}
|
|
|
|
/*
|
|
===================
|
|
BuildConnectedAreas_r
|
|
===================
|
|
*/
|
|
void idRenderWorldLocal::BuildConnectedAreas_r(int areaNum) {
|
|
portalArea_t* area;
|
|
portal_t* portal;
|
|
|
|
if (tr.viewDef->connectedAreas[areaNum]) {
|
|
return;
|
|
}
|
|
|
|
tr.viewDef->connectedAreas[areaNum] = true;
|
|
|
|
// flood through all non-blocked portals
|
|
area = &portalAreas[areaNum];
|
|
for (portal = area->portals; portal; portal = portal->next) {
|
|
if (!(portal->doublePortal->blockingBits & PS_BLOCK_VIEW)) {
|
|
BuildConnectedAreas_r(portal->intoArea);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
BuildConnectedAreas
|
|
|
|
This is only valid for a given view, not all views in a frame
|
|
===================
|
|
*/
|
|
void idRenderWorldLocal::BuildConnectedAreas(void) {
|
|
int i;
|
|
|
|
tr.viewDef->connectedAreas = (bool*)R_FrameAlloc(numPortalAreas
|
|
* sizeof(tr.viewDef->connectedAreas[0]));
|
|
|
|
// if we are outside the world, we can see all areas
|
|
if (tr.viewDef->areaNum == -1) {
|
|
for (i = 0; i < numPortalAreas; i++) {
|
|
tr.viewDef->connectedAreas[i] = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// start with none visible, and flood fill from the current area
|
|
memset(tr.viewDef->connectedAreas, 0, numPortalAreas * sizeof(tr.viewDef->connectedAreas[0]));
|
|
BuildConnectedAreas_r(tr.viewDef->areaNum);
|
|
}
|
|
|
|
/*
|
|
=============
|
|
FindViewLightsAndEntites
|
|
|
|
All the modelrefs and lightrefs that are in visible areas
|
|
will have viewEntitys and viewLights created for them.
|
|
|
|
The scissorRects on the viewEntitys and viewLights may be empty if
|
|
they were considered, but not actually visible.
|
|
=============
|
|
*/
|
|
void idRenderWorldLocal::FindViewLightsAndEntities(void) {
|
|
// clear the visible lightDef and entityDef lists
|
|
tr.viewDef->viewLights = NULL;
|
|
tr.viewDef->viewEntitys = NULL;
|
|
|
|
// find the area to start the portal flooding in
|
|
if (!r_usePortals.GetBool()) {
|
|
// debug tool to force no portal culling
|
|
tr.viewDef->areaNum = -1;
|
|
}
|
|
else {
|
|
tr.viewDef->areaNum = PointInArea(tr.viewDef->initialViewAreaOrigin);
|
|
}
|
|
|
|
// determine all possible connected areas for
|
|
// light-behind-door culling
|
|
BuildConnectedAreas();
|
|
|
|
// The DXR static-world portal flood is useful, but it dirties many TLAS
|
|
// instances when crossing areas. Keep it opt-in until the update path is
|
|
// proven stable on all maps.
|
|
if ( r_pathTracePortalWorldCulling.GetBool() ) {
|
|
DXR_BeginWorldTopAccelStructVisibilityPass(this);
|
|
DXR_FlowWorldTopAccelStructVisibilityThroughPortals(this);
|
|
} else {
|
|
DXR_ShowAllWorldTopAccelStructs(this);
|
|
}
|
|
|
|
// bump the view count, invalidating all
|
|
// visible areas
|
|
tr.viewCount++;
|
|
|
|
// flow through all the portals and add models / lights
|
|
if (r_singleArea.GetBool()) {
|
|
// if debugging, only mark this area
|
|
// if we are outside the world, don't draw anything
|
|
if (tr.viewDef->areaNum >= 0) {
|
|
portalStack_t ps;
|
|
int i;
|
|
static int lastPrintedAreaNum;
|
|
|
|
if (tr.viewDef->areaNum != lastPrintedAreaNum) {
|
|
lastPrintedAreaNum = tr.viewDef->areaNum;
|
|
common->Printf("entering portal area %i\n", tr.viewDef->areaNum);
|
|
}
|
|
|
|
for (i = 0; i < 5; i++) {
|
|
ps.portalPlanes[i] = tr.viewDef->frustum[i];
|
|
}
|
|
ps.numPortalPlanes = 5;
|
|
ps.rect = tr.viewDef->scissor;
|
|
|
|
AddAreaRefs(tr.viewDef->areaNum, &ps);
|
|
}
|
|
}
|
|
else {
|
|
// note that the center of projection for flowing through portals may
|
|
// be a different point than initialViewAreaOrigin for subviews that
|
|
// may have the viewOrigin in a solid/invalid area
|
|
FlowViewThroughPortals(tr.viewDef->renderView.vieworg, 5, tr.viewDef->frustum);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
NumPortals
|
|
==============
|
|
*/
|
|
int idRenderWorldLocal::NumPortals(void) const {
|
|
return numInterAreaPortals;
|
|
}
|
|
|
|
/*
|
|
==============
|
|
FindPortal
|
|
|
|
Game code uses this to identify which portals are inside doors.
|
|
Returns 0 if no portal contacts the bounds
|
|
==============
|
|
*/
|
|
qhandle_t idRenderWorldLocal::FindPortal(const idBounds& b) const {
|
|
int i, j;
|
|
idBounds wb;
|
|
doublePortal_t* portal;
|
|
idWinding* w;
|
|
int firstPortal;
|
|
int numMatches;
|
|
|
|
firstPortal = 0;
|
|
numMatches = 0;
|
|
for (i = 0; i < numInterAreaPortals; i++) {
|
|
portal = &doublePortals[i];
|
|
w = portal->portals[0]->w;
|
|
|
|
wb.Clear();
|
|
for (j = 0; j < w->GetNumPoints(); j++) {
|
|
wb.AddPoint((*w)[j].ToVec3());
|
|
}
|
|
if (wb.IntersectsBounds(b)) {
|
|
if ( firstPortal == 0 ) {
|
|
firstPortal = i + 1;
|
|
}
|
|
numMatches++;
|
|
}
|
|
}
|
|
|
|
if ( numMatches <= 1 ) {
|
|
return firstPortal;
|
|
}
|
|
|
|
for ( i = 0 ; i < portalHandleGroups.Num() ; i++ ) {
|
|
if ( portalHandleGroups[i].bounds.Compare( b, 0.1f ) ) {
|
|
return numInterAreaPortals + i + 1;
|
|
}
|
|
}
|
|
|
|
portalHandleGroup_t group;
|
|
group.bounds = b;
|
|
for ( i = 0 ; i < numInterAreaPortals ; i++ ) {
|
|
portal = &doublePortals[i];
|
|
w = portal->portals[0]->w;
|
|
|
|
wb.Clear();
|
|
for ( j = 0 ; j < w->GetNumPoints() ; j++ ) {
|
|
wb.AddPoint( ( *w )[j].ToVec3() );
|
|
}
|
|
if ( wb.IntersectsBounds( b ) ) {
|
|
group.portals.Append( i + 1 );
|
|
}
|
|
}
|
|
portalHandleGroups.Append( group );
|
|
common->Printf( "FindPortal grouped %i generated portals for bounds\n", group.portals.Num() );
|
|
return numInterAreaPortals + portalHandleGroups.Num();
|
|
}
|
|
|
|
/*
|
|
=============
|
|
FloodConnectedAreas
|
|
=============
|
|
*/
|
|
void idRenderWorldLocal::FloodConnectedAreas(portalArea_t* area, int portalAttributeIndex) {
|
|
if (area->connectedAreaNum[portalAttributeIndex] == connectedAreaNum) {
|
|
return;
|
|
}
|
|
area->connectedAreaNum[portalAttributeIndex] = connectedAreaNum;
|
|
|
|
for (portal_t* p = area->portals; p; p = p->next) {
|
|
if (!(p->doublePortal->blockingBits & (1 << portalAttributeIndex))) {
|
|
FloodConnectedAreas(&portalAreas[p->intoArea], portalAttributeIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
void idRenderWorldLocal::RebuildConnectedAreasForAttribute( int portalAttributeIndex ) {
|
|
for ( int i = 0 ; i < numPortalAreas ; i++ ) {
|
|
connectedAreaNum++;
|
|
FloodConnectedAreas( &portalAreas[i], portalAttributeIndex );
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
AreasAreConnected
|
|
|
|
==============
|
|
*/
|
|
bool idRenderWorldLocal::AreasAreConnected(int areaNum1, int areaNum2, portalConnection_t connection) {
|
|
if (areaNum1 == -1 || areaNum2 == -1) {
|
|
return false;
|
|
}
|
|
if (areaNum1 > numPortalAreas || areaNum2 > numPortalAreas || areaNum1 < 0 || areaNum2 < 0) {
|
|
common->Error("idRenderWorldLocal::AreAreasConnected: bad parms: %i, %i", areaNum1, areaNum2);
|
|
}
|
|
|
|
int attribute = 0;
|
|
|
|
int intConnection = (int)connection;
|
|
|
|
while (intConnection > 1) {
|
|
attribute++;
|
|
intConnection >>= 1;
|
|
}
|
|
if (attribute >= NUM_PORTAL_ATTRIBUTES || (1 << attribute) != (int)connection) {
|
|
common->Error("idRenderWorldLocal::AreasAreConnected: bad connection number: %i\n", (int)connection);
|
|
}
|
|
|
|
return portalAreas[areaNum1].connectedAreaNum[attribute] == portalAreas[areaNum2].connectedAreaNum[attribute];
|
|
}
|
|
|
|
|
|
/*
|
|
==============
|
|
SetPortalState
|
|
|
|
doors explicitly close off portals when shut
|
|
==============
|
|
*/
|
|
void idRenderWorldLocal::SetPortalState(qhandle_t portal, int blockTypes) {
|
|
if (portal == 0) {
|
|
return;
|
|
}
|
|
|
|
if ( portal > numInterAreaPortals ) {
|
|
int groupIndex = portal - numInterAreaPortals - 1;
|
|
if ( groupIndex < 0 || groupIndex >= portalHandleGroups.Num() ) {
|
|
common->Error( "SetPortalState: bad grouped portal number %i", portal );
|
|
}
|
|
|
|
int changedBits = 0;
|
|
for ( int i = 0 ; i < portalHandleGroups[groupIndex].portals.Num() ; i++ ) {
|
|
int portalIndex = portalHandleGroups[groupIndex].portals[i] - 1;
|
|
int old = doublePortals[portalIndex].blockingBits;
|
|
if ( old == blockTypes ) {
|
|
continue;
|
|
}
|
|
changedBits |= old ^ blockTypes;
|
|
doublePortals[portalIndex].blockingBits = blockTypes;
|
|
}
|
|
|
|
for ( int i = 0 ; i < NUM_PORTAL_ATTRIBUTES ; i++ ) {
|
|
if ( changedBits & ( 1 << i ) ) {
|
|
RebuildConnectedAreasForAttribute( i );
|
|
}
|
|
}
|
|
|
|
if ( session->writeDemo ) {
|
|
session->writeDemo->WriteInt( DS_RENDER );
|
|
session->writeDemo->WriteInt( DC_SET_PORTAL_STATE );
|
|
session->writeDemo->WriteInt( portal );
|
|
session->writeDemo->WriteInt( blockTypes );
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (portal < 1 || portal > numInterAreaPortals) {
|
|
common->Error("SetPortalState: bad portal number %i", portal);
|
|
}
|
|
int old = doublePortals[portal - 1].blockingBits;
|
|
if (old == blockTypes) {
|
|
return;
|
|
}
|
|
doublePortals[portal - 1].blockingBits = blockTypes;
|
|
|
|
// leave the connectedAreaGroup the same on one side,
|
|
// then flood fill from the other side with a new number for each changed attribute
|
|
for (int i = 0; i < NUM_PORTAL_ATTRIBUTES; i++) {
|
|
if ((old ^ blockTypes) & (1 << i)) {
|
|
connectedAreaNum++;
|
|
FloodConnectedAreas(&portalAreas[doublePortals[portal - 1].portals[1]->intoArea], i);
|
|
}
|
|
}
|
|
|
|
if (session->writeDemo) {
|
|
session->writeDemo->WriteInt(DS_RENDER);
|
|
session->writeDemo->WriteInt(DC_SET_PORTAL_STATE);
|
|
session->writeDemo->WriteInt(portal);
|
|
session->writeDemo->WriteInt(blockTypes);
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============
|
|
GetPortalState
|
|
==============
|
|
*/
|
|
int idRenderWorldLocal::GetPortalState(qhandle_t portal) {
|
|
if (portal == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if ( portal > numInterAreaPortals ) {
|
|
int groupIndex = portal - numInterAreaPortals - 1;
|
|
if ( groupIndex < 0 || groupIndex >= portalHandleGroups.Num() || portalHandleGroups[groupIndex].portals.Num() == 0 ) {
|
|
common->Error( "GetPortalState: bad grouped portal number %i", portal );
|
|
}
|
|
return doublePortals[portalHandleGroups[groupIndex].portals[0] - 1].blockingBits;
|
|
}
|
|
|
|
if (portal < 1 || portal > numInterAreaPortals) {
|
|
common->Error("GetPortalState: bad portal number %i", portal);
|
|
}
|
|
|
|
return doublePortals[portal - 1].blockingBits;
|
|
}
|
|
|
|
/*
|
|
=====================
|
|
idRenderWorldLocal::ShowPortals
|
|
|
|
Debugging tool, won't work correctly with SMP or when mirrors are present
|
|
=====================
|
|
*/
|
|
void idRenderWorldLocal::ShowPortals() {
|
|
int i, j;
|
|
portalArea_t* area;
|
|
portal_t* p;
|
|
idWinding* w;
|
|
|
|
// flood out through portals, setting area viewCount
|
|
for (i = 0; i < numPortalAreas; i++) {
|
|
area = &portalAreas[i];
|
|
if (area->viewCount != tr.viewCount) {
|
|
continue;
|
|
}
|
|
for (p = area->portals; p; p = p->next) {
|
|
w = p->w;
|
|
if (!w) {
|
|
continue;
|
|
}
|
|
|
|
if (portalAreas[p->intoArea].viewCount != tr.viewCount) {
|
|
// red = can't see
|
|
glColor3f(1, 0, 0);
|
|
}
|
|
else {
|
|
// green = see through
|
|
glColor3f(0, 1, 0);
|
|
}
|
|
|
|
glBegin(GL_LINE_LOOP);
|
|
for (j = 0; j < w->GetNumPoints(); j++) {
|
|
glVertex3fv((*w)[j].ToFloatPtr());
|
|
}
|
|
glEnd();
|
|
}
|
|
}
|
|
}
|