Torque3D/Engine/source/scene/culling/sceneCullingState.cpp
2023-09-26 12:03:51 -05:00

963 lines
32 KiB
C++

//-----------------------------------------------------------------------------
// Copyright (c) 2012 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
#include "platform/platform.h"
#include "scene/culling/sceneCullingState.h"
#include "scene/sceneManager.h"
#include "scene/sceneObject.h"
#include "scene/zones/sceneZoneSpace.h"
#include "math/mathUtils.h"
#include "platform/profiler.h"
#include "terrain/terrData.h"
#include "util/tempAlloc.h"
#include "gfx/sim/debugDraw.h"
extern bool gEditingMission;
bool SceneCullingState::smDisableTerrainOcclusion = true;
bool SceneCullingState::smDisableZoneCulling = false;
U32 SceneCullingState::smMaxOccludersPerZone = 4;
F32 SceneCullingState::smOccluderMinWidthPercentage = 0.1f;
F32 SceneCullingState::smOccluderMinHeightPercentage = 0.1f;
//-----------------------------------------------------------------------------
SceneCullingState::SceneCullingState( SceneManager* sceneManager, const SceneCameraState& viewState )
: mSceneManager( sceneManager ),
mCameraState( viewState ),
mDisableTerrainOcclusion( smDisableTerrainOcclusion ),
mDisableZoneCulling( smDisableZoneCulling )
{
AssertFatal( sceneManager->getZoneManager(), "SceneCullingState::SceneCullingState - SceneManager must have a zone manager!" );
VECTOR_SET_ASSOCIATION( mZoneStates );
VECTOR_SET_ASSOCIATION( mAddedOccluderObjects );
// Allocate zone states.
const U32 numZones = sceneManager->getZoneManager()->getNumZones();
mZoneStates.setSize( numZones );
dMemset( mZoneStates.address(), 0, sizeof( SceneZoneCullingState ) * numZones );
// Allocate the zone visibility flags.
mZoneVisibilityFlags.setSize( numZones );
mZoneVisibilityFlags.clear();
// Culling frustum
mCullingFrustum = mCameraState.getFrustum();
mCullingFrustum.bakeProjectionOffset();
// Construct the root culling volume from
// the culling frustum. Omit the frustum's
// near and far plane so we don't test it repeatedly.
PlaneF* planes = allocateData< PlaneF >( 4 );
planes[ 0 ] = mCullingFrustum.getPlanes()[ Frustum::PlaneLeft ];
planes[ 1 ] = mCullingFrustum.getPlanes()[ Frustum::PlaneRight ];
planes[ 2 ] = mCullingFrustum.getPlanes()[ Frustum::PlaneTop];
planes[ 3 ] = mCullingFrustum.getPlanes()[ Frustum::PlaneBottom ];
mRootVolume = SceneCullingVolume(
SceneCullingVolume::Includer,
PlaneSetF( planes, 4 )
);
clearExtraPlanesCull();
}
//-----------------------------------------------------------------------------
bool SceneCullingState::isWithinVisibleZone( SceneObject* object ) const
{
SceneManager* mgr = object->getSceneManager();
SceneZoneSpaceManager* zm = mgr->getZoneManager();
U32 numZones = 0;
U32* zones = NULL;
SceneZoneSpaceManager::ObjectZoneValueIterator itr, itrEnd;
zm->getObjectZoneValueIterators(object, itr, itrEnd);
for (itr; itr != itrEnd; itr++)
{
if (mZoneVisibilityFlags.test(*itr))
return true;
}
return false;
}
//-----------------------------------------------------------------------------
void SceneCullingState::addOccluder( SceneObject* object )
{
PROFILE_SCOPE( SceneCullingState_addOccluder );
// If the occluder is itself occluded, don't add it.
//
// NOTE: We do allow near plane intersections here. Silhouette extraction
// should take that into account.
if( cullObjects( &object, 1, DontCullRenderDisabled ) != 1 )
return;
// If the occluder has already been added, do nothing. Check this
// after the culling check since the same occluder can be added by
// two separate zones and not be visible in one yet be visible in the
// other.
if( mAddedOccluderObjects.contains( object ) )
return;
mAddedOccluderObjects.push_back( object );
// Let the object build a silhouette. If it doesn't
// return one, abort.
Vector< Point3F > silhouette;
object->buildSilhouette( getCameraState(), silhouette );
if( silhouette.empty() || silhouette.size() < 3 )
return;
// Generate the culling volume.
SceneCullingVolume volume;
if( !createCullingVolume(
silhouette.address(),
silhouette.size(),
SceneCullingVolume::Occluder,
volume ) )
return;
// Add the frustum to all zones that the object is assigned to.
U32 numZones = 0;
U32* zones = NULL;
SceneManager* sm = object->getSceneManager();
SceneZoneSpaceManager* zm = sm->getZoneManager();
SceneZoneSpaceManager::ObjectZoneValueIterator itr, itrEnd;
zm->getObjectZoneValueIterators(object, itr, itrEnd);
for (itr; itr != itrEnd; itr++)
{
addCullingVolumeToZone(*itr, volume);
}
}
//-----------------------------------------------------------------------------
bool SceneCullingState::addCullingVolumeToZone( U32 zoneId, const SceneCullingVolume& volume )
{
PROFILE_SCOPE( SceneCullingState_addCullingVolumeToZone );
AssertFatal( zoneId < mZoneStates.size(), "SceneCullingState::addCullingVolumeToZone - Zone ID out of range" );
SceneZoneCullingState& zoneState = mZoneStates[ zoneId ];
// [rene, 07-Apr-10] I previously used to attempt to merge things here and detect whether
// the visibility state of the zone has changed at all. Since we allow polyhedra to be
// degenerate here and since polyhedra cannot be merged easily like frustums, I have opted
// to remove this for now. I'm also convinced that with the current traversal system it
// adds little benefit.
// Link the volume to the zone state.
typedef SceneZoneCullingState::CullingVolumeLink LinkType;
LinkType* link = reinterpret_cast< LinkType* >( allocateData( sizeof( LinkType ) ) );
link->mVolume = volume;
link->mNext = zoneState.mCullingVolumes;
zoneState.mCullingVolumes = link;
if( volume.isOccluder() )
zoneState.mHaveOccluders = true;
else
zoneState.mHaveIncluders = true;
// Mark sorting state as dirty.
zoneState.mHaveSortedVolumes = false;
// Set the visibility flag for the zone.
if( volume.isIncluder() )
mZoneVisibilityFlags.set( zoneId );
return true;
}
//-----------------------------------------------------------------------------
bool SceneCullingState::addCullingVolumeToZone( U32 zoneId, SceneCullingVolume::Type type, const AnyPolyhedron& polyhedron )
{
// Allocate space on our chunker.
const U32 numPlanes = polyhedron.getNumPlanes();
PlaneF* planes = allocateData< PlaneF >( numPlanes );
// Copy the planes over.
dMemcpy( planes, polyhedron.getPlanes(), numPlanes * sizeof( planes[ 0 ] ) );
// Create a culling volume.
SceneCullingVolume volume(
type,
PlaneSetF( planes, numPlanes )
);
// And add it.
return addCullingVolumeToZone( zoneId, volume );
}
//-----------------------------------------------------------------------------
bool SceneCullingState::createCullingVolume( const Point3F* vertices, U32 numVertices, SceneCullingVolume::Type type, SceneCullingVolume& outVolume )
{
const Point3F& viewPos = getCameraState().getViewPosition();
const Point3F& viewDir = getCameraState().getViewDirection();
const bool isOrtho = getCullingFrustum().isOrtho();
//TODO: check if we need to handle penetration of the near plane for occluders specially
// Allocate space for the clipping planes we generate. Assume the worst case
// of every edge generating a plane and, for includers, all edges meeting at
// steep angles so we need to insert extra planes (the latter is not possible,
// of course, but it makes things less complicated here). For occluders, add
// an extra plane for the near cap.
const U32 maxPlanes = ( type == SceneCullingVolume::Occluder ? numVertices + 1 : numVertices * 2 );
PlaneF* planes = allocateData< PlaneF >( maxPlanes );
// Keep track of the world-space bounds of the polygon. We use this later
// to derive some metrics.
Box3F wsPolyBounds;
wsPolyBounds.minExtents = Point3F( TypeTraits< F32 >::MAX, TypeTraits< F32 >::MAX, TypeTraits< F32 >::MAX );
wsPolyBounds.maxExtents = Point3F( TypeTraits< F32 >::MIN, TypeTraits< F32 >::MIN, TypeTraits< F32 >::MIN );
// For occluders, also keep track of the nearest, and two farthest silhouette points. We use
// this later to construct a near capping plane.
F32 minVertexDistanceSquared = TypeTraits< F32 >::MAX;
U32 leastDistantVert = 0;
F32 maxVertexDistancesSquared[ 2 ] = { TypeTraits< F32 >::MIN, TypeTraits< F32 >::MIN };
U32 mostDistantVertices[ 2 ] = { 0, 0 };
// Generate the extrusion volume. For orthographic projections, extrude
// parallel to the view direction whereas for parallel projections, extrude
// from the viewpoint.
U32 numPlanes = 0;
U32 lastVertex = numVertices - 1;
bool invert = false;
for( U32 i = 0; i < numVertices; lastVertex = i, ++ i )
{
AssertFatal( numPlanes < maxPlanes, "SceneCullingState::createCullingVolume - Did not allocate enough planes!" );
const Point3F& v1 = vertices[ i ];
const Point3F& v2 = vertices[ lastVertex ];
// Keep track of bounds.
wsPolyBounds.minExtents.setMin( v1 );
wsPolyBounds.maxExtents.setMax( v1 );
// Skip the edge if it's length is really short.
const Point3F edgeVector = v2 - v1;
const F32 edgeVectorLenSquared = edgeVector.lenSquared();
if( edgeVectorLenSquared < 0.025f )
continue;
//TODO: might need to do additional checks here for non-planar polygons used by occluders
//TODO: test for colinearity of edge vector with view vector (occluders only)
// Create a plane for the edge.
if( isOrtho )
{
// Compute a plane through the two edge vertices and one
// of the vertices extended along the view direction.
if( !invert )
planes[ numPlanes ] = PlaneF( v1, v1 + viewDir, v2 );
else
planes[ numPlanes ] = PlaneF( v2, v1 + viewDir, v1 );
}
else
{
// Compute a plane going through the viewpoint and the two
// edge vertices.
if( !invert )
planes[ numPlanes ] = PlaneF( v1, viewPos, v2 );
else
planes[ numPlanes ] = PlaneF( v2, viewPos, v1 );
}
numPlanes ++;
// If this is the first plane that we have created, find out whether
// the vertex ordering is giving us the plane orientations that we want
// (facing inside). If not, invert vertex order from now on.
if( numPlanes == 1 )
{
Point3F center( 0, 0, 0 );
for( U32 n = 0; n < numVertices; ++ n )
center += vertices[n];
center /= numVertices;
if( planes[numPlanes - 1].whichSide( center ) == PlaneF::Back )
{
invert = true;
planes[ numPlanes - 1 ].invert();
}
}
// For occluders, keep tabs of the nearest, and two farthest vertices.
if( type == SceneCullingVolume::Occluder )
{
const F32 distSquared = ( v1 - viewPos ).lenSquared();
if( distSquared < minVertexDistanceSquared )
{
minVertexDistanceSquared = distSquared;
leastDistantVert = i;
}
if( distSquared > maxVertexDistancesSquared[ 0 ] )
{
// Move 0 to 1.
maxVertexDistancesSquared[ 1 ] = maxVertexDistancesSquared[ 0 ];
mostDistantVertices[ 1 ] = mostDistantVertices[ 0 ];
// Replace 0.
maxVertexDistancesSquared[ 0 ] = distSquared;
mostDistantVertices[ 0 ] = i;
}
else if( distSquared > maxVertexDistancesSquared[ 1 ] )
{
// Replace 1.
maxVertexDistancesSquared[ 1 ] = distSquared;
mostDistantVertices[ 1 ] = i;
}
}
}
// If the extrusion produced no useful result, abort.
if( numPlanes < 3 )
return false;
// For includers, test the angle of the edges at the current vertex.
// If too steep, add an extra plane to improve culling efficiency.
if( false )//type == SceneCullingVolume::Includer )
{
const U32 numOriginalPlanes = numPlanes;
U32 lastPlaneIndex = numPlanes - 1;
for( U32 i = 0; i < numOriginalPlanes; lastPlaneIndex = i, ++ i )
{
const PlaneF& currentPlane = planes[ i ];
const PlaneF& lastPlane = planes[ lastPlaneIndex ];
// Compute the cosine of the angle between the two plane normals.
const F32 cosAngle = mFabs( mDot( currentPlane, lastPlane ) );
// The planes meet at increasingly steep angles the more they point
// in opposite directions, i.e the closer the angle of their normals
// is to 180 degrees. Skip any two planes that don't get near that.
if( cosAngle > 0.1f )
continue;
Point3F newNormal = currentPlane + lastPlane;//addNormals - mDot( addNormals, crossNormals ) * crossNormals;
//
planes[ numPlanes ] = PlaneF( currentPlane.getPosition(), newNormal );
numPlanes ++;
}
}
// Compute the metrics of the culling volume in relation to the view frustum.
//
// For this, we are short-circuiting things slightly. The correct way (other than doing
// full screen projections) would be to transform all the polygon points into camera
// space, lay an AABB around those points, and then find the X and Z extents on the near plane.
//
// However, while not as accurate, a faster way is to just project the axial vectors
// of the bounding box onto both the camera right and up vector. This gives us a rough
// estimate of the camera-space size of the polygon we're looking at.
const MatrixF& cameraTransform = getCameraState().getViewWorldMatrix();
const Point3F cameraRight = cameraTransform.getRightVector();
const Point3F cameraUp = cameraTransform.getUpVector();
const Point3F wsPolyBoundsExtents = wsPolyBounds.getExtents();
F32 widthEstimate =
getMax( mFabs( wsPolyBoundsExtents.x * cameraRight.x ),
getMax( mFabs( wsPolyBoundsExtents.y * cameraRight.y ),
mFabs( wsPolyBoundsExtents.z * cameraRight.z ) ) );
F32 heightEstimate =
getMax( mFabs( wsPolyBoundsExtents.x * cameraUp.x ),
getMax( mFabs( wsPolyBoundsExtents.y * cameraUp.y ),
mFabs( wsPolyBoundsExtents.z * cameraUp.z ) ) );
// If the current camera is a perspective one, divide the two estimates
// by the distance of the nearest bounding box vertex to the camera
// to account for perspective distortion.
if( !isOrtho )
{
const Point3F nearestVertex = wsPolyBounds.computeVertex(
Box3F::getPointIndexFromOctant( - viewDir )
);
const F32 distance = ( nearestVertex - viewPos ).len();
widthEstimate /= distance;
heightEstimate /= distance;
}
// If we are creating an occluder, check to see if the estimates fit
// our minimum requirements.
if( type == SceneCullingVolume::Occluder )
{
const F32 widthEstimatePercentage = widthEstimate / getCullingFrustum().getWidth();
const F32 heightEstimatePercentage = heightEstimate / getCullingFrustum().getHeight();
if( widthEstimatePercentage < smOccluderMinWidthPercentage ||
heightEstimatePercentage < smOccluderMinHeightPercentage )
return false; // Reject.
}
// Use the area estimate as the volume's sort point.
const F32 sortPoint = widthEstimate * heightEstimate;
// Finally, if it's an occluder, compute a near cap. The near cap prevents objects
// in front of the occluder from testing positive. The same could be achieved by
// manually comparing distances before testing objects but since that would amount
// to the same checks the plane/AABB tests do, it's easier to just add another plane.
// Additionally, it gives the benefit of being able to create more precise culling
// results by angling the plane.
//NOTE: Could consider adding a near cap for includers too when generating a volume
// for the outdoor zone as that may prevent quite a bit of space from being included.
// However, given that this space will most likely just be filled with interior
// stuff anyway, it's probably not worth it.
if( type == SceneCullingVolume::Occluder )
{
const U32 nearCapIndex = numPlanes;
planes[ nearCapIndex ] = PlaneF(
vertices[ mostDistantVertices[ 0 ] ],
vertices[ mostDistantVertices[ 1 ] ],
vertices[ leastDistantVert ] );
// Invert the plane, if necessary.
if( planes[ nearCapIndex ].whichSide( viewPos ) == PlaneF::Front )
planes[ nearCapIndex ].invert();
numPlanes ++;
}
// Create the volume from the planes.
outVolume = SceneCullingVolume(
type,
PlaneSetF( planes, numPlanes )
);
outVolume.setSortPoint( sortPoint );
// Done.
return true;
}
//-----------------------------------------------------------------------------
namespace {
struct ZoneArrayIterator
{
U32 mCurrent;
U32 mNumZones;
const U32* mZones;
ZoneArrayIterator( const U32* zones, U32 numZones )
: mCurrent( 0 ),
mNumZones( numZones ),
mZones( zones ) {}
bool isValid() const
{
return ( mCurrent < mNumZones );
}
ZoneArrayIterator& operator ++()
{
mCurrent ++;
return *this;
}
U32 operator *() const
{
return mZones[ mCurrent ];
}
};
}
template< typename T, typename Iter >
inline SceneZoneCullingState::CullingTestResult SceneCullingState::_testOccludersOnly( const T& bounds, Iter zoneIter ) const
{
// Test the culling states of all zones that the object
// is assigned to.
for( ; zoneIter.isValid(); ++ zoneIter )
{
const SceneZoneCullingState& zoneState = getZoneState( *zoneIter );
// Skip zone if there are no occluders.
if( !zoneState.hasOccluders() )
continue;
// If the object's world bounds overlaps any of the volumes
// for this zone, it's rendered.
if( zoneState.testVolumes( bounds, true ) == SceneZoneCullingState::CullingTestPositiveByOcclusion )
return SceneZoneCullingState::CullingTestPositiveByOcclusion;
}
return SceneZoneCullingState::CullingTestNegative;
}
template< typename T, typename Iter >
inline SceneZoneCullingState::CullingTestResult SceneCullingState::_test( const T& bounds, Iter zoneIter,
const PlaneF& nearPlane, const PlaneF& farPlane ) const
{
// Defer test of near and far plane until we've hit a zone
// which actually has visible space. This prevents us from
// doing near/far tests on objects that were included in the
// potential render list but aren't actually in any visible
// zone.
bool haveTestedNearAndFar = false;
// Test the culling states of all zones that the object
// is assigned to.
for( ; zoneIter.isValid(); ++ zoneIter )
{
const SceneZoneCullingState& zoneState = getZoneState( *zoneIter );
// Skip zone if there are no positive culling volumes.
if( !zoneState.hasIncluders() )
continue;
// If we haven't tested the near and far plane yet, do so
// now.
if( !haveTestedNearAndFar )
{
// Test near plane.
PlaneF::Side nearSide = nearPlane.whichSide( bounds );
if( nearSide == PlaneF::Back )
return SceneZoneCullingState::CullingTestNegative;
// Test far plane.
PlaneF::Side farSide = farPlane.whichSide( bounds );
if( farSide == PlaneF::Back )
return SceneZoneCullingState::CullingTestNegative;
haveTestedNearAndFar = true;
}
// If the object's world bounds overlaps any of the volumes
// for this zone, it's rendered.
SceneZoneCullingState::CullingTestResult result = zoneState.testVolumes( bounds );
if( result == SceneZoneCullingState::CullingTestPositiveByInclusion )
return result;
else if( result == SceneZoneCullingState::CullingTestPositiveByOcclusion )
return result;
}
return SceneZoneCullingState::CullingTestNegative;
}
//-----------------------------------------------------------------------------
template< bool OCCLUDERS_ONLY, typename T >
inline SceneZoneCullingState::CullingTestResult SceneCullingState::_test( const T& bounds, const U32* zones, U32 numZones ) const
{
// If zone culling is disabled, only test against
// the root frustum.
if( disableZoneCulling() )
{
if( !OCCLUDERS_ONLY && !getCullingFrustum().isCulled( bounds ) )
return SceneZoneCullingState::CullingTestPositiveByInclusion;
return SceneZoneCullingState::CullingTestNegative;
}
// Otherwise test each of the zones.
if( OCCLUDERS_ONLY )
{
return _testOccludersOnly(
bounds,
ZoneArrayIterator( zones, numZones )
);
}
else
{
const PlaneF* frustumPlanes = getCullingFrustum().getPlanes();
return _test(
bounds,
ZoneArrayIterator( zones, numZones ),
frustumPlanes[ Frustum::PlaneNear ],
frustumPlanes[ Frustum::PlaneFar ]
);
}
}
//-----------------------------------------------------------------------------
bool SceneCullingState::isCulled( const Box3F& aabb, const U32* zones, U32 numZones ) const
{
SceneZoneCullingState::CullingTestResult result = _test< false >( aabb, zones, numZones );
return ( result == SceneZoneCullingState::CullingTestNegative ||
result == SceneZoneCullingState::CullingTestPositiveByOcclusion );
}
//-----------------------------------------------------------------------------
bool SceneCullingState::isCulled( const OrientedBox3F& obb, const U32* zones, U32 numZones ) const
{
SceneZoneCullingState::CullingTestResult result = _test< false >( obb, zones, numZones );
return ( result == SceneZoneCullingState::CullingTestNegative ||
result == SceneZoneCullingState::CullingTestPositiveByOcclusion );
}
//-----------------------------------------------------------------------------
bool SceneCullingState::isCulled( const SphereF& sphere, const U32* zones, U32 numZones ) const
{
SceneZoneCullingState::CullingTestResult result = _test< false >( sphere, zones, numZones );
return ( result == SceneZoneCullingState::CullingTestNegative ||
result == SceneZoneCullingState::CullingTestPositiveByOcclusion );
}
//-----------------------------------------------------------------------------
bool SceneCullingState::isOccluded( SceneObject* object ) const
{
if( disableZoneCulling() )
return false;
CullingTestResult result = _testOccludersOnly(
object->getWorldBox(),
mSceneManager->getZoneManager()->makeObjectZoneValueIterator(object)
);
return ( result == SceneZoneCullingState::CullingTestPositiveByOcclusion );
}
//-----------------------------------------------------------------------------
bool SceneCullingState::isOccluded( const Box3F& aabb, const U32* zones, U32 numZones ) const
{
return ( _test< true >( aabb, zones, numZones ) == SceneZoneCullingState::CullingTestPositiveByOcclusion );
}
//-----------------------------------------------------------------------------
bool SceneCullingState::isOccluded( const OrientedBox3F& obb, const U32* zones, U32 numZones ) const
{
return ( _test< true >( obb, zones, numZones ) == SceneZoneCullingState::CullingTestPositiveByOcclusion );
}
//-----------------------------------------------------------------------------
bool SceneCullingState::isOccluded( const SphereF& sphere, const U32* zones, U32 numZones ) const
{
return ( _test< true >( sphere, zones, numZones ) == SceneZoneCullingState::CullingTestPositiveByOcclusion );
}
//-----------------------------------------------------------------------------
U32 SceneCullingState::cullObjects( SceneObject** objects, U32 numObjects, U32 cullOptions ) const
{
PROFILE_SCOPE( SceneCullingState_cullObjects );
U32 numRemainingObjects = 0;
// We test near and far planes separately in order to not do the tests
// repeatedly, so fetch the planes now.
const PlaneF& nearPlane = getCullingFrustum().getPlanes()[ Frustum::PlaneNear ];
const PlaneF& farPlane = getCullingFrustum().getPlanes()[ Frustum::PlaneFar ];
SceneZoneSpaceManager* zoneMgr = mSceneManager->getZoneManager();
for( U32 i = 0; i < numObjects; ++ i )
{
SceneObject* object = objects[ i ];
bool isCulled = true;
// If we should respect editor overrides, test that now.
if( !( cullOptions & CullEditorOverrides ) &&
gEditingMission &&
( ( object->isCullingDisabledInEditor() && object->isRenderEnabled() ) || object->isSelected() ) )
{
isCulled = false;
}
// If the object is render-disabled, it gets culled. The only
// way around this is the editor override above.
else if( !( cullOptions & DontCullRenderDisabled ) &&
!object->isRenderEnabled() )
{
isCulled = true;
}
// Global bounds objects are never culled. Note that this means
// that if these objects are to respect zoning, they need to manually
// trigger the respective culling checks for whatever they want to
// batch.
else if( object->isGlobalBounds() )
isCulled = false;
// If terrain occlusion checks are enabled, run them now.
else if( !mDisableTerrainOcclusion &&
object->getWorldBox().minExtents.x > -1e5 &&
isOccludedByTerrain( object ) )
{
// Occluded by terrain.
isCulled = true;
}
// If the object shouldn't be subjected to more fine-grained culling
// or if zone culling is disabled, just test against the root frustum.
else if( !( object->getTypeMask() & CULLING_INCLUDE_TYPEMASK ) ||
( object->getTypeMask() & CULLING_EXCLUDE_TYPEMASK ) ||
disableZoneCulling() )
{
isCulled = getCullingFrustum().isCulled( object->getWorldBox() );
}
// Go through the zones that the object is assigned to and
// test the object against the frustums of each of the zones.
else
{
CullingTestResult result = _test(
object->getWorldBox(),
zoneMgr->makeObjectZoneValueIterator( object ),
nearPlane,
farPlane
);
isCulled = ( result == SceneZoneCullingState::CullingTestNegative ||
result == SceneZoneCullingState::CullingTestPositiveByOcclusion );
}
if( !isCulled )
isCulled = isOccludedWithExtraPlanesCull( object->getWorldBox() );
if( !isCulled )
objects[ numRemainingObjects ++ ] = object;
}
return numRemainingObjects;
}
//-----------------------------------------------------------------------------
bool SceneCullingState::isOccludedByTerrain( SceneObject* object ) const
{
PROFILE_SCOPE( SceneCullingState_isOccludedByTerrain );
// Don't try to occlude globally bounded objects.
if( object->isGlobalBounds() )
return false;
const Vector< SceneObject* >& terrains = getSceneManager()->getContainer()->getTerrains();
const U32 numTerrains = terrains.size();
for( U32 terrainIdx = 0; terrainIdx < numTerrains; ++ terrainIdx )
{
TerrainBlock* terrain = dynamic_cast< TerrainBlock* >( terrains[ terrainIdx ] );
if( !terrain )
continue;
MatrixF terrWorldTransform = terrain->getWorldTransform();
Point3F localCamPos = getCameraState().getViewPosition();
terrWorldTransform.mulP(localCamPos);
F32 height;
terrain->getHeight( Point2F( localCamPos.x, localCamPos.y ), &height );
bool aboveTerrain = ( height <= localCamPos.z );
// Don't occlude if we're below the terrain. This prevents problems when
// looking out from underground bases...
if( !aboveTerrain )
continue;
const Box3F& oBox = object->getObjBox();
F32 minSide = getMin(oBox.len_x(), oBox.len_y());
if (minSide > 85.0f)
continue;
const Box3F& rBox = object->getWorldBox();
Point3F ul(rBox.minExtents.x, rBox.minExtents.y, rBox.maxExtents.z);
Point3F ur(rBox.minExtents.x, rBox.maxExtents.y, rBox.maxExtents.z);
Point3F ll(rBox.maxExtents.x, rBox.minExtents.y, rBox.maxExtents.z);
Point3F lr(rBox.maxExtents.x, rBox.maxExtents.y, rBox.maxExtents.z);
terrWorldTransform.mulP(ul);
terrWorldTransform.mulP(ur);
terrWorldTransform.mulP(ll);
terrWorldTransform.mulP(lr);
Point3F xBaseL0_s = ul - localCamPos;
Point3F xBaseL0_e = lr - localCamPos;
Point3F xBaseL1_s = ur - localCamPos;
Point3F xBaseL1_e = ll - localCamPos;
static F32 checkPoints[3] = {0.75, 0.5, 0.25};
RayInfo rinfo;
for( U32 i = 0; i < 3; i ++ )
{
Point3F start = (xBaseL0_s * checkPoints[i]) + localCamPos;
Point3F end = (xBaseL0_e * checkPoints[i]) + localCamPos;
if (terrain->castRay(start, end, &rinfo))
continue;
terrain->getHeight(Point2F(start.x, start.y), &height);
if ((height <= start.z) == aboveTerrain)
continue;
start = (xBaseL1_s * checkPoints[i]) + localCamPos;
end = (xBaseL1_e * checkPoints[i]) + localCamPos;
if (terrain->castRay(start, end, &rinfo))
continue;
Point3F test = (start + end) * 0.5;
if (terrain->castRay(localCamPos, test, &rinfo) == false)
continue;
return true;
}
}
return false;
}
//-----------------------------------------------------------------------------
void SceneCullingState::debugRenderCullingVolumes() const
{
const ColorI occluderColor( 255, 0, 0, 255 );
const ColorI includerColor( 0, 255, 0, 255 );
const PlaneF& nearPlane = getCullingFrustum().getPlanes()[ Frustum::PlaneNear ];
const PlaneF& farPlane = getCullingFrustum().getPlanes()[ Frustum::PlaneFar ];
DebugDrawer* drawer = DebugDrawer::get();
const SceneZoneSpaceManager* zoneManager = mSceneManager->getZoneManager();
bool haveDebugZone = false;
const U32 numZones = mZoneStates.size();
for( S32 zoneId = numZones - 1; zoneId >= 0; -- zoneId )
{
if( !zoneManager->isValidZoneId( zoneId ) )
continue;
const SceneZoneCullingState& zoneState = mZoneStates[ zoneId ];
if( !zoneManager->getZoneOwner( zoneId )->isSelected() && ( zoneId != SceneZoneSpaceManager::RootZoneId || haveDebugZone ) )
continue;
haveDebugZone = true;
for( SceneZoneCullingState::CullingVolumeIterator iter( zoneState );
iter.isValid(); ++ iter )
{
// Temporarily add near and far plane to culling volume so that
// no matter how it is defined, it has a chance of being properly
// capped.
const U32 numPlanes = iter->getPlanes().getNumPlanes();
const PlaneF* planes = iter->getPlanes().getPlanes();
TempAlloc< PlaneF > tempPlanes( numPlanes + 2 );
tempPlanes[ 0 ] = nearPlane;
tempPlanes[ 1 ] = farPlane;
dMemcpy( &tempPlanes[ 2 ], planes, numPlanes * sizeof( PlaneF ) );
// Build a polyhedron from the plane set.
Polyhedron polyhedron;
polyhedron.buildFromPlanes(
PlaneSetF( tempPlanes, numPlanes + 2 )
);
// If the polyhedron has any renderable data,
// hand it over to the debug drawer.
if( polyhedron.getNumEdges() )
drawer->drawPolyhedron( polyhedron, iter->isOccluder() ? occluderColor : includerColor );
}
}
}