1251 lines
50 KiB
C++
1251 lines
50 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "LightingSystem.h"
|
|
#include "MonteCarlo.h"
|
|
#include "Raster.h"
|
|
#include "HAL/PlatformTime.h"
|
|
|
|
namespace Lightmass
|
|
{
|
|
|
|
struct FVisibilitySamplePos
|
|
{
|
|
/**
|
|
* Min and max world space heights of a single sample on a triangle.
|
|
* This is necessary because supersampling is used during rasterization.
|
|
*/
|
|
FVector2f HeightRange;
|
|
};
|
|
|
|
struct FCellHeights
|
|
{
|
|
/** Last triangle index that rasterized to this cell. */
|
|
uint64 TriangleIndex;
|
|
/** World space X and Y position of this cell. */
|
|
FVector2f Position;
|
|
/** Array of triangle hits on this cell. */
|
|
TArray<FVisibilitySamplePos> HitTriangles;
|
|
};
|
|
|
|
class FCellToHeightsMap
|
|
{
|
|
public:
|
|
|
|
/** Initialization constructor. */
|
|
FCellToHeightsMap(int32 InSizeX,int32 InSizeY):
|
|
Data(InSizeX * InSizeY),
|
|
SizeX(InSizeX),
|
|
SizeY(InSizeY)
|
|
{
|
|
// Clear the map to zero.
|
|
for(int32 Y = 0;Y < SizeY;Y++)
|
|
{
|
|
for(int32 X = 0;X < SizeX;X++)
|
|
{
|
|
FCellHeights& CurrentCell = (*this)(X,Y);
|
|
FMemory::Memzero(&CurrentCell,sizeof(FCellHeights));
|
|
CurrentCell.HitTriangles.Empty(50);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Accessors.
|
|
FCellHeights& operator()(int32 X,int32 Y)
|
|
{
|
|
const uint32 TexelIndex = Y * SizeX + X;
|
|
return Data(TexelIndex);
|
|
}
|
|
const FCellHeights& operator()(int32 X,int32 Y) const
|
|
{
|
|
const int32 TexelIndex = Y * SizeX + X;
|
|
return Data(TexelIndex);
|
|
}
|
|
|
|
int32 GetSizeX() const { return SizeX; }
|
|
int32 GetSizeY() const { return SizeY; }
|
|
SIZE_T GetAllocatedSize() const { return Data.GetAllocatedSize(); }
|
|
|
|
private:
|
|
|
|
/** The mapping data. */
|
|
TChunkedArray<FCellHeights> Data;
|
|
|
|
/** The width of the mapping data. */
|
|
int32 SizeX;
|
|
|
|
/** The height of the mapping data. */
|
|
int32 SizeY;
|
|
};
|
|
|
|
class FCellPlacementRasterPolicy
|
|
{
|
|
public:
|
|
|
|
typedef FVector4f InterpolantType;
|
|
|
|
/** Initialization constructor. */
|
|
FCellPlacementRasterPolicy(
|
|
FCellToHeightsMap& InHeightsMap,
|
|
const FScene& InScene,
|
|
FStaticLightingSystem& InSystem,
|
|
const FBoxSphereBounds3f& InPrecomputedVisibilityBounds,
|
|
float InCellSize)
|
|
:
|
|
HeightsMap(InHeightsMap),
|
|
Scene(InScene),
|
|
System(InSystem),
|
|
PrecomputedVisibilityBounds(InPrecomputedVisibilityBounds),
|
|
CellSize(InCellSize)
|
|
{
|
|
}
|
|
|
|
void SetTriangleIndex(uint64 InTriangleIndex)
|
|
{
|
|
TriangleIndex = InTriangleIndex;
|
|
}
|
|
|
|
protected:
|
|
|
|
// FTriangleRasterizer policy interface.
|
|
|
|
int32 GetMinX() const { return 0; }
|
|
int32 GetMaxX() const { return HeightsMap.GetSizeX(); }
|
|
int32 GetMinY() const { return 0; }
|
|
int32 GetMaxY() const { return HeightsMap.GetSizeY(); }
|
|
|
|
void ProcessPixel(int32 X,int32 Y,const InterpolantType& Interpolant,bool BackFacing);
|
|
|
|
private:
|
|
|
|
uint64 TriangleIndex;
|
|
FCellToHeightsMap& HeightsMap;
|
|
const FScene& Scene;
|
|
FStaticLightingSystem& System;
|
|
FBoxSphereBounds3f PrecomputedVisibilityBounds;
|
|
float CellSize;
|
|
};
|
|
|
|
void FCellPlacementRasterPolicy::ProcessPixel(int32 X,int32 Y,const InterpolantType& WorldPosition,bool BackFacing)
|
|
{
|
|
if (Scene.IsPointInVisibilityVolume(WorldPosition))
|
|
{
|
|
FCellHeights& Cell = HeightsMap(X, Y);
|
|
|
|
if (Cell.TriangleIndex != TriangleIndex)
|
|
{
|
|
// If this is the first hit on this cell from the current triangle, add a new sample
|
|
FVisibilitySamplePos Sample;
|
|
const FVector3f GridPosition = PrecomputedVisibilityBounds.Origin - PrecomputedVisibilityBounds.BoxExtent + FVector3f(X, Y, 0) * CellSize;
|
|
Sample.HeightRange = FVector2f(WorldPosition.Z, WorldPosition.Z);
|
|
Cell.HitTriangles.Add(Sample);
|
|
Cell.Position = FVector2f(GridPosition.X, GridPosition.Y);
|
|
Cell.TriangleIndex = TriangleIndex;
|
|
}
|
|
else
|
|
{
|
|
// If this is not the first hit on this cell from the current triangle, expand the sample min and max
|
|
FVisibilitySamplePos& TriangleEntry = Cell.HitTriangles.Last();
|
|
TriangleEntry.HeightRange.X = FMath::Min(TriangleEntry.HeightRange.X, WorldPosition.Z);
|
|
TriangleEntry.HeightRange.Y = FMath::Max(TriangleEntry.HeightRange.Y, WorldPosition.Z);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32 FStaticLightingSystem::GetGroupCellIndex(FVector3f BoxCenter) const
|
|
{
|
|
const FVector3f GridPosition = FVector3f(GroupVisibilityGridSizeXY, GroupVisibilityGridSizeXY, GroupVisibilityGridSizeZ) * (BoxCenter - VisibilityGridBounds.Min) / (VisibilityGridBounds.Max - VisibilityGridBounds.Min);
|
|
const int32 GridX = FMath::TruncToInt(GridPosition.X);
|
|
const int32 GridY = FMath::TruncToInt(GridPosition.Y);
|
|
const int32 GridZ = FMath::TruncToInt(GridPosition.Z);
|
|
const int32 CellIndex = GridZ * GroupVisibilityGridSizeXY * GroupVisibilityGridSizeZ + GridY * GroupVisibilityGridSizeXY + GridX;
|
|
|
|
if (GridX > 0 && GridX < GroupVisibilityGridSizeXY
|
|
&& GridY > 0 && GridY < GroupVisibilityGridSizeXY
|
|
&& GridZ > 0 && GridZ < GroupVisibilityGridSizeZ)
|
|
{
|
|
return CellIndex;
|
|
}
|
|
else
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
class FVisibilityMeshSortInfo
|
|
{
|
|
public:
|
|
float Distance;
|
|
int32 Index;
|
|
FBox3f Bounds;
|
|
|
|
FVisibilityMeshSortInfo() :
|
|
Bounds(FBox3f(ForceInit))
|
|
{}
|
|
};
|
|
|
|
/** Determines visibility cell placement, called once at startup. */
|
|
void FStaticLightingSystem::SetupPrecomputedVisibility()
|
|
{
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
FLMRandomStream RandomStream(0);
|
|
|
|
const FBoxSphereBounds3f PrecomputedVisibilityBounds = Scene.GetVisibilityVolumeBounds();
|
|
const FVector4f VolumeSizes = PrecomputedVisibilityBounds.BoxExtent * 2.0f / PrecomputedVisibilitySettings.CellSize;
|
|
const int32 SizeX = FMath::TruncToInt(VolumeSizes.X + DELTA) + 1;
|
|
const int32 SizeY = FMath::TruncToInt(VolumeSizes.Y + DELTA) + 1;
|
|
|
|
if (!PrecomputedVisibilitySettings.bPlaceCellsOnlyAlongCameraTracks)
|
|
{
|
|
FCellToHeightsMap HeightsMap(SizeX, SizeY);
|
|
FTriangleRasterizer<FCellPlacementRasterPolicy> Rasterizer(
|
|
FCellPlacementRasterPolicy(
|
|
HeightsMap,
|
|
Scene,
|
|
*this,
|
|
PrecomputedVisibilityBounds,
|
|
PrecomputedVisibilitySettings.CellSize));
|
|
|
|
check(Meshes.Num() == AllMappings.Num());
|
|
|
|
uint64 NextTriangleIndex = 1;
|
|
|
|
// Rasterize the scene to determine potential cell heights
|
|
for (int32 MappingIndex = 0; MappingIndex < AllMappings.Num(); MappingIndex++)
|
|
{
|
|
const FStaticLightingMapping* CurrentMapping = AllMappings[MappingIndex];
|
|
const FStaticLightingMesh* CurrentMesh = CurrentMapping->Mesh;
|
|
|
|
const uint32 GeoMeshLODIndex = CurrentMesh->GetLODIndices() & 0xFFFF;
|
|
const uint32 GeoHLODTreeIndex = (CurrentMesh->GetLODIndices() & 0xFFFF0000) >> 16;
|
|
const uint32 GeoHLODRange = CurrentMesh->GetHLODRange();
|
|
const uint32 GeoHLODRangeStart = GeoHLODRange & 0xFFFF;
|
|
const uint32 GeoHLODRangeEnd = (GeoHLODRange & 0xFFFF0000) >> 16;
|
|
|
|
bool bMeshBelongsToLOD0 = GeoMeshLODIndex == 0;
|
|
|
|
if (GeoHLODTreeIndex > 0)
|
|
{
|
|
bMeshBelongsToLOD0 = GeoHLODRangeStart == GeoHLODRangeEnd;
|
|
}
|
|
|
|
// Only process meshes whose bounding box intersects a PVS volume
|
|
if (Scene.DoesBoxIntersectVisibilityVolume(CurrentMesh->BoundingBox) && bMeshBelongsToLOD0)
|
|
{
|
|
// Whether mesh wants to be fully opaque for visibility step
|
|
const bool bOpaqueMesh = CurrentMesh->IsAlwaysOpaqueForVisibility();
|
|
|
|
// Rasterize all triangles in the mesh
|
|
for (int32 TriangleIndex = 0; TriangleIndex < CurrentMesh->NumTriangles; TriangleIndex++)
|
|
{
|
|
FStaticLightingVertex Vertices[3];
|
|
int32 ElementIndex;
|
|
CurrentMesh->GetTriangle(TriangleIndex, Vertices[0], Vertices[1], Vertices[2], ElementIndex);
|
|
|
|
// Only place cells on opaque surfaces if requested, which can save some memory for foliage maps
|
|
if (!PrecomputedVisibilitySettings.bPlaceCellsOnOpaqueOnly || bOpaqueMesh
|
|
|| (!CurrentMesh->IsMasked(ElementIndex) && !CurrentMesh->IsTranslucent(ElementIndex)))
|
|
{
|
|
FVector2f XYPositions[3];
|
|
for (int32 VertIndex = 0; VertIndex < 3; VertIndex++)
|
|
{
|
|
// Transform world space positions from [PrecomputedVisibilityBounds.Origin - PrecomputedVisibilityBounds.BoxExtent, PrecomputedVisibilityBounds.Origin + PrecomputedVisibilityBounds.BoxExtent] into [0,1]
|
|
const FVector4f TransformedPosition = (Vertices[VertIndex].WorldPosition - PrecomputedVisibilityBounds.Origin + PrecomputedVisibilityBounds.BoxExtent) / (2.0f * PrecomputedVisibilityBounds.BoxExtent);
|
|
// Project positions onto the XY plane
|
|
XYPositions[VertIndex] = FVector2f(TransformedPosition.X * SizeX, TransformedPosition.Y * SizeY);
|
|
}
|
|
|
|
const FVector4f TriangleNormal = (Vertices[2].WorldPosition - Vertices[0].WorldPosition) ^ (Vertices[1].WorldPosition - Vertices[0].WorldPosition);
|
|
|
|
// Only rasterize upward facing triangles
|
|
if (TriangleNormal.Z > 0.0f)
|
|
{
|
|
Rasterizer.SetTriangleIndex(NextTriangleIndex);
|
|
|
|
FVector2f SubsamplePositions[9];
|
|
SubsamplePositions[0] = FVector2f(.5f, .5f);
|
|
SubsamplePositions[1] = FVector2f(0, .5f);
|
|
SubsamplePositions[2] = FVector2f(.5f, 0);
|
|
SubsamplePositions[3] = FVector2f(1, .5f);
|
|
SubsamplePositions[4] = FVector2f(.5f, 1);
|
|
SubsamplePositions[5] = FVector2f(1, 1);
|
|
SubsamplePositions[6] = FVector2f(0, 1);
|
|
SubsamplePositions[7] = FVector2f(1, 0);
|
|
SubsamplePositions[8] = FVector2f(0, 0);
|
|
|
|
const float EdgePullback = .1f;
|
|
|
|
for (int32 SampleIndex = 0; SampleIndex < UE_ARRAY_COUNT(SubsamplePositions); SampleIndex++)
|
|
{
|
|
const FVector2f SamplePosition = SubsamplePositions[SampleIndex] * (1 - 2 * EdgePullback) + FVector2f(EdgePullback, EdgePullback);
|
|
|
|
Rasterizer.DrawTriangle(
|
|
Vertices[0].WorldPosition,
|
|
Vertices[1].WorldPosition,
|
|
Vertices[2].WorldPosition,
|
|
XYPositions[0] - SamplePosition,
|
|
XYPositions[1] - SamplePosition,
|
|
XYPositions[2] - SamplePosition,
|
|
false
|
|
);
|
|
}
|
|
|
|
NextTriangleIndex++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AllPrecomputedVisibilityCells.Empty(SizeX * SizeY * 2);
|
|
|
|
TArray<FVector2f> PlacedHeightRanges;
|
|
for (int32 Y = 0; Y < SizeY; Y++)
|
|
{
|
|
for (int32 X = 0; X < SizeX; X++)
|
|
{
|
|
FCellHeights& Cell = HeightsMap(X, Y);
|
|
const FVector2f CurrentPosition = Cell.Position;
|
|
|
|
// Sort the heights from smallest to largest
|
|
struct FCompareSampleZ
|
|
{
|
|
FORCEINLINE bool operator()(const FVisibilitySamplePos& A, const FVisibilitySamplePos& B) const
|
|
{
|
|
return A.HeightRange.Y < B.HeightRange.Y;
|
|
}
|
|
};
|
|
Cell.HitTriangles.Sort(FCompareSampleZ());
|
|
float LastSampleHeight = -FLT_MAX;
|
|
|
|
PlacedHeightRanges.Reset();
|
|
|
|
// Pass 1 - only place cells in the largest holes which are most likely to be where the play area is
|
|
// Place the bottom slightly above the surface, since cells that clip through the floor often have poor occlusion culling
|
|
for (int32 HeightIndex = 0; HeightIndex < Cell.HitTriangles.Num(); HeightIndex++)
|
|
{
|
|
const float CurrentMaxHeight = Cell.HitTriangles[HeightIndex].HeightRange.Y;
|
|
|
|
// Place a new cell if this is the highest height
|
|
if (HeightIndex + 1 == Cell.HitTriangles.Num()
|
|
// Or if there's a gap above this height of size PlayAreaHeight
|
|
|| ((Cell.HitTriangles[HeightIndex + 1].HeightRange.Y - CurrentMaxHeight) > PrecomputedVisibilitySettings.PlayAreaHeight
|
|
// And this height is not within a cell that was just placed
|
|
&& CurrentMaxHeight - LastSampleHeight > PrecomputedVisibilitySettings.PlayAreaHeight))
|
|
{
|
|
FPrecomputedVisibilityCell NewCell;
|
|
NewCell.Bounds = FBox3f(
|
|
FVector4f(
|
|
CurrentPosition.X - PrecomputedVisibilitySettings.CellSize / 2,
|
|
CurrentPosition.Y - PrecomputedVisibilitySettings.CellSize / 2,
|
|
CurrentMaxHeight),
|
|
FVector4f(
|
|
CurrentPosition.X + PrecomputedVisibilitySettings.CellSize / 2,
|
|
CurrentPosition.Y + PrecomputedVisibilitySettings.CellSize / 2,
|
|
CurrentMaxHeight + PrecomputedVisibilitySettings.PlayAreaHeight));
|
|
|
|
AllPrecomputedVisibilityCells.Add(NewCell);
|
|
LastSampleHeight = CurrentMaxHeight;
|
|
PlacedHeightRanges.Add(FVector2f(NewCell.Bounds.Min.Z, NewCell.Bounds.Max.Z));
|
|
}
|
|
}
|
|
|
|
// Fractions of PrecomputedVisibilitySettings.PlayAreaHeight to guarantee have cell coverage
|
|
float TestHeights[3] = {.4f, .6f, .8f};
|
|
|
|
// Pass 2 - make sure the space above every triangle is covered by precomputed visibility cells, even if the cells are placed poorly (intersecting the floor)
|
|
for (int32 HeightIndex = 0; HeightIndex < Cell.HitTriangles.Num() - 1; HeightIndex++)
|
|
{
|
|
for (int32 ExtremaIndex = 0; ExtremaIndex < 2; ExtremaIndex++)
|
|
{
|
|
const float CurrentMaxHeight = ExtremaIndex == 0 ? Cell.HitTriangles[HeightIndex].HeightRange.X : Cell.HitTriangles[HeightIndex].HeightRange.Y;
|
|
const float CompareHeight = CurrentMaxHeight + .5f * PrecomputedVisibilitySettings.PlayAreaHeight;
|
|
|
|
for (int32 TestIndex = 0; TestIndex < UE_ARRAY_COUNT(TestHeights); TestIndex++)
|
|
{
|
|
const float TestHeight = CurrentMaxHeight + TestHeights[TestIndex] * PrecomputedVisibilitySettings.PlayAreaHeight;
|
|
|
|
int32 ClosestCellInZIndex = -1;
|
|
float ClosestCellInZDistance = FLT_MAX;
|
|
bool bInsideCell = false;
|
|
|
|
for (int32 PlacedHeightIndex = 0; PlacedHeightIndex < PlacedHeightRanges.Num(); PlacedHeightIndex++)
|
|
{
|
|
FVector2f CellHeightRange = PlacedHeightRanges[PlacedHeightIndex];
|
|
|
|
if (TestHeight > CellHeightRange.X && TestHeight < CellHeightRange.Y)
|
|
{
|
|
bInsideCell = true;
|
|
break;
|
|
}
|
|
|
|
float AbsDistance = FMath::Min(FMath::Abs(CompareHeight - CellHeightRange.X), FMath::Abs(CompareHeight - CellHeightRange.Y));
|
|
|
|
if (AbsDistance < ClosestCellInZDistance)
|
|
{
|
|
ClosestCellInZDistance = AbsDistance;
|
|
ClosestCellInZIndex = PlacedHeightIndex;
|
|
}
|
|
}
|
|
|
|
// Place a cell if TestHeight was not inside any existing cells
|
|
if (!bInsideCell)
|
|
{
|
|
FPrecomputedVisibilityCell NewCell;
|
|
float DesiredCellBottom = CurrentMaxHeight;
|
|
|
|
if (ClosestCellInZIndex >= 0)
|
|
{
|
|
const FVector2f NearestCellHeightRange = PlacedHeightRanges[ClosestCellInZIndex];
|
|
const float NearestCellCompareHeight = (NearestCellHeightRange.X + NearestCellHeightRange.Y) / 2;
|
|
|
|
// Move the bottom of the cell to be placed such that it doesn't overlap the nearest cell
|
|
// This makes use of the cell's full height to cover space
|
|
if (CompareHeight < NearestCellCompareHeight)
|
|
{
|
|
DesiredCellBottom = FMath::Min(DesiredCellBottom, NearestCellHeightRange.X - PrecomputedVisibilitySettings.PlayAreaHeight);
|
|
}
|
|
else if (CompareHeight > NearestCellCompareHeight)
|
|
{
|
|
DesiredCellBottom = FMath::Max(DesiredCellBottom, NearestCellHeightRange.Y);
|
|
}
|
|
}
|
|
|
|
NewCell.Bounds = FBox3f(
|
|
FVector4f(
|
|
CurrentPosition.X - PrecomputedVisibilitySettings.CellSize / 2,
|
|
CurrentPosition.Y - PrecomputedVisibilitySettings.CellSize / 2,
|
|
DesiredCellBottom),
|
|
FVector4f(
|
|
CurrentPosition.X + PrecomputedVisibilitySettings.CellSize / 2,
|
|
CurrentPosition.Y + PrecomputedVisibilitySettings.CellSize / 2,
|
|
DesiredCellBottom + PrecomputedVisibilitySettings.PlayAreaHeight));
|
|
|
|
AllPrecomputedVisibilityCells.Add(NewCell);
|
|
PlacedHeightRanges.Add(FVector2f(NewCell.Bounds.Min.Z, NewCell.Bounds.Max.Z));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Place cells along camera tracks
|
|
const int32 NumCellsPlacedOnSurfaces = AllPrecomputedVisibilityCells.Num();
|
|
for (int32 CameraPositionIndex = 0; CameraPositionIndex < Scene.CameraTrackPositions.Num(); CameraPositionIndex++)
|
|
{
|
|
const FVector4f& CurrentPosition = Scene.CameraTrackPositions[CameraPositionIndex];
|
|
bool bInsideCell = false;
|
|
for (int32 CellIndex = 0; CellIndex < AllPrecomputedVisibilityCells.Num(); CellIndex++)
|
|
{
|
|
if (AllPrecomputedVisibilityCells[CellIndex].Bounds.IsInside(CurrentPosition))
|
|
{
|
|
bInsideCell = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bInsideCell)
|
|
{
|
|
FPrecomputedVisibilityCell NewCell;
|
|
|
|
// Snap the cell min to the nearest factor of CellSize from the visibility bounds min + CellSize / 2
|
|
// The CellSize / 2 offset is necessary to match up with cells produced by the rasterizer, since pixels are rasterized at cell centers
|
|
const FVector4f PreSnapTranslation = FVector3f(PrecomputedVisibilitySettings.CellSize / 2) + PrecomputedVisibilityBounds.Origin - PrecomputedVisibilityBounds.BoxExtent;
|
|
const FVector4f TranslatedPosition = CurrentPosition - PreSnapTranslation;
|
|
// FMath::Fmod gives the offset to round up for negative numbers, when we always want the offset to round down
|
|
const float XOffset = TranslatedPosition.X > 0.0f ?
|
|
FMath::Fmod(TranslatedPosition.X, PrecomputedVisibilitySettings.CellSize) :
|
|
PrecomputedVisibilitySettings.CellSize - FMath::Fmod(-TranslatedPosition.X, PrecomputedVisibilitySettings.CellSize);
|
|
const float YOffset = TranslatedPosition.Y > 0.0f ?
|
|
FMath::Fmod(TranslatedPosition.Y, PrecomputedVisibilitySettings.CellSize) :
|
|
PrecomputedVisibilitySettings.CellSize - FMath::Fmod(-TranslatedPosition.Y, PrecomputedVisibilitySettings.CellSize);
|
|
const FVector4f SnappedPosition(CurrentPosition.X - XOffset, CurrentPosition.Y - YOffset, CurrentPosition.Z);
|
|
|
|
NewCell.Bounds = FBox3f(
|
|
FVector4f(
|
|
SnappedPosition.X,
|
|
SnappedPosition.Y,
|
|
SnappedPosition.Z - .5f * PrecomputedVisibilitySettings.PlayAreaHeight),
|
|
FVector4f(
|
|
SnappedPosition.X + PrecomputedVisibilitySettings.CellSize,
|
|
SnappedPosition.Y + PrecomputedVisibilitySettings.CellSize,
|
|
SnappedPosition.Z + .5f * PrecomputedVisibilitySettings.PlayAreaHeight));
|
|
|
|
AllPrecomputedVisibilityCells.Add(NewCell);
|
|
|
|
// Verify that the camera track position is inside the placed cell
|
|
checkSlow(NewCell.Bounds.IsInside(CurrentPosition));
|
|
}
|
|
}
|
|
|
|
{
|
|
TArray<FVisibilityMeshSortInfo> SortMeshes;
|
|
SortMeshes.Empty(VisibilityMeshes.Num());
|
|
|
|
FVector3f CenterPosition(0, 0, 0);
|
|
|
|
if (VisibilityMeshes.Num() > 0)
|
|
{
|
|
// Initialize to first mesh position so we can handle lighting stuff away from the origin
|
|
CenterPosition = VisibilityMeshes[0].Meshes[0]->BoundingBox.GetCenter() / VisibilityMeshes.Num();
|
|
}
|
|
|
|
for (int32 VisibilityMeshIndex = 0; VisibilityMeshIndex < VisibilityMeshes.Num(); VisibilityMeshIndex++)
|
|
{
|
|
FVisibilityMeshSortInfo NewInfo;
|
|
|
|
for (int32 OriginalMeshIndex = 0; OriginalMeshIndex < VisibilityMeshes[VisibilityMeshIndex].Meshes.Num(); OriginalMeshIndex++)
|
|
{
|
|
NewInfo.Bounds += VisibilityMeshes[VisibilityMeshIndex].Meshes[OriginalMeshIndex]->BoundingBox;
|
|
}
|
|
|
|
// First mesh already contributed
|
|
if (VisibilityMeshIndex > 0)
|
|
{
|
|
CenterPosition += NewInfo.Bounds.GetCenter() / VisibilityMeshes.Num();
|
|
}
|
|
|
|
NewInfo.Index = VisibilityMeshIndex;
|
|
SortMeshes.Add(NewInfo);
|
|
}
|
|
|
|
FVector3f CubeCorners[8];
|
|
CubeCorners[0] = FVector3f(1, 1, 1);
|
|
CubeCorners[1] = FVector3f(-1, 1, 1);
|
|
CubeCorners[2] = FVector3f(1, -1, 1);
|
|
CubeCorners[3] = FVector3f(-1, -1, 1);
|
|
CubeCorners[4] = FVector3f(1, 1, -1);
|
|
CubeCorners[5] = FVector3f(-1, 1, -1);
|
|
CubeCorners[6] = FVector3f(1, -1, -1);
|
|
CubeCorners[7] = FVector3f(-1, -1, -1);
|
|
|
|
for (int32 MeshIndex = 0; MeshIndex < SortMeshes.Num(); MeshIndex++)
|
|
{
|
|
const FVector3f BoxCenter = SortMeshes[MeshIndex].Bounds.GetCenter();
|
|
const FVector3f BoxExtent = SortMeshes[MeshIndex].Bounds.GetExtent();
|
|
float LocalDistance = 0;
|
|
|
|
for (int32 CornerIndex = 0; CornerIndex < UE_ARRAY_COUNT(CubeCorners); CornerIndex++)
|
|
{
|
|
// Calculate max distance to a corner of the bounds as a measure of how much this mesh will expand the grid bounds
|
|
LocalDistance = FMath::Max(LocalDistance, (BoxCenter + BoxExtent * CubeCorners[CornerIndex]).SizeSquared());
|
|
}
|
|
|
|
SortMeshes[MeshIndex].Distance = LocalDistance;
|
|
}
|
|
|
|
struct FCompareMeshesByDistance
|
|
{
|
|
FORCEINLINE bool operator()( const FVisibilityMeshSortInfo& A, const FVisibilityMeshSortInfo& B ) const
|
|
{
|
|
return A.Distance < B.Distance;
|
|
}
|
|
};
|
|
SortMeshes.Sort(FCompareMeshesByDistance());
|
|
|
|
VisibilityGridBounds = FBox3f(ForceInit);
|
|
// Drop last 10% of meshes which will expand the grid bounds
|
|
// This is to handle distant skybox type meshes
|
|
const int32 MaxMeshIndex = FMath::Min(FMath::Max(FMath::TruncToInt(.9f * SortMeshes.Num()), 1), SortMeshes.Num() - 1);
|
|
|
|
for (int32 MeshIndex = 0; MeshIndex < MaxMeshIndex; MeshIndex++)
|
|
{
|
|
VisibilityGridBounds += SortMeshes[MeshIndex].Bounds;
|
|
}
|
|
|
|
//@todo - expose
|
|
const float TargetNumGroupsAsFractionOfMeshes = .3f;
|
|
const int32 ZDimensionDivisor = 4;
|
|
// Determine grid X and Y size using SizeX * SizeY * SizeZ = NumMeshes * TargetNumGroupsAsFractionOfMeshes
|
|
GroupVisibilityGridSizeXY = FMath::Max(FMath::TruncToInt(FMath::Pow((ZDimensionDivisor * TargetNumGroupsAsFractionOfMeshes * VisibilityMeshes.Num()), 1.0f / 3.0f)), 1);
|
|
GroupVisibilityGridSizeZ = FMath::Max(GroupVisibilityGridSizeXY / ZDimensionDivisor, 1);
|
|
|
|
const int32 GridSizeXY = GroupVisibilityGridSizeXY;
|
|
const int32 GridSizeZ = GroupVisibilityGridSizeZ;
|
|
|
|
const FVector3f CellSize = VisibilityGridBounds.GetExtent() / FVector3f(GroupVisibilityGridSizeXY, GroupVisibilityGridSizeXY, GroupVisibilityGridSizeZ);
|
|
const float GridCellBoundingRadius = CellSize.Size();
|
|
const float MeshGroupingCellRadiusThreshold = .5f;
|
|
|
|
GroupGrid.Empty(GridSizeXY * GridSizeXY * GridSizeZ);
|
|
|
|
// Initialize grid group indices to invalid
|
|
for (int32 GridEntry = 0; GridEntry < GridSizeXY * GridSizeXY * GridSizeZ; GridEntry++)
|
|
{
|
|
GroupGrid.Add(-1);
|
|
}
|
|
|
|
for (int32 VisibilityMeshIndex = 0; VisibilityMeshIndex < VisibilityMeshes.Num(); VisibilityMeshIndex++)
|
|
{
|
|
FBox3f MeshBounds(ForceInit);
|
|
|
|
for (int32 OriginalMeshIndex = 0; OriginalMeshIndex < VisibilityMeshes[VisibilityMeshIndex].Meshes.Num(); OriginalMeshIndex++)
|
|
{
|
|
MeshBounds += VisibilityMeshes[VisibilityMeshIndex].Meshes[OriginalMeshIndex]->BoundingBox;
|
|
}
|
|
|
|
const float MeshBoundingRadiusSqr = MeshBounds.GetExtent().SizeSquared();
|
|
|
|
// Only put the mesh in a group if its radius is small enough to keep the group effective
|
|
bool bPutInGroup = MeshBoundingRadiusSqr < FMath::Square(GridCellBoundingRadius * MeshGroupingCellRadiusThreshold);
|
|
|
|
if (bPutInGroup)
|
|
{
|
|
const int32 CellIndex = GetGroupCellIndex(MeshBounds.GetCenter());
|
|
|
|
if (CellIndex >= 0)
|
|
{
|
|
int32 GroupIndex = GroupGrid[CellIndex];
|
|
|
|
if (GroupIndex == -1)
|
|
{
|
|
// Add a new group if needed
|
|
VisibilityGroups.Add(FVisibilityMeshGroup());
|
|
GroupIndex = VisibilityGroups.Num() - 1;
|
|
GroupGrid[CellIndex] = GroupIndex;
|
|
}
|
|
|
|
// Add to list of meshes in the group
|
|
VisibilityGroups[GroupIndex].VisibilityIds.Add(VisibilityMeshIndex);
|
|
}
|
|
else
|
|
{
|
|
// Mesh was not inside grid
|
|
bPutInGroup = false;
|
|
}
|
|
}
|
|
|
|
// Mark whether the mesh was put into a group so we can look it up during visibility tracing
|
|
VisibilityMeshes[VisibilityMeshIndex].bInGroup = bPutInGroup;
|
|
|
|
if (!bPutInGroup)
|
|
{
|
|
Stats.NumPrecomputedVisibilityMeshesExcludedFromGroups++;
|
|
}
|
|
}
|
|
|
|
// Build group bounds
|
|
for (int32 GroupIndex = 0; GroupIndex < VisibilityGroups.Num(); GroupIndex++)
|
|
{
|
|
FVisibilityMeshGroup& Group = VisibilityGroups[GroupIndex];
|
|
Group.GroupBounds = FBox3f(ForceInit);
|
|
|
|
for (int32 EntryIndex = 0; EntryIndex < Group.VisibilityIds.Num(); EntryIndex++)
|
|
{
|
|
const int32 VisibilityId = Group.VisibilityIds[EntryIndex];
|
|
|
|
for (int32 MeshIndex = 0; MeshIndex < VisibilityMeshes[VisibilityId].Meshes.Num(); MeshIndex++)
|
|
{
|
|
Group.GroupBounds += VisibilityMeshes[VisibilityId].Meshes[MeshIndex]->BoundingBox;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const SIZE_T NumVisDataBytes = static_cast<SIZE_T>(AllPrecomputedVisibilityCells.Num()) * VisibilityMeshes.Num() / 8;
|
|
Stats.NumPrecomputedVisibilityCellsTotal = AllPrecomputedVisibilityCells.Num();
|
|
Stats.NumPrecomputedVisibilityCellsCamaraTracks = AllPrecomputedVisibilityCells.Num() - NumCellsPlacedOnSurfaces;
|
|
Stats.NumPrecomputedVisibilityMeshes = VisibilityMeshes.Num();
|
|
Stats.PrecomputedVisibilityDataBytes = NumVisDataBytes;
|
|
Stats.PrecomputedVisibilitySetupTime = FPlatformTime::Seconds() - StartTime;
|
|
|
|
if (AllPrecomputedVisibilityCells.Num() > 0)
|
|
{
|
|
LogSolverMessage(FString::Printf(TEXT("Setup precomputed visibility %.1fs, %u meshes, %u Cells"), FPlatformTime::Seconds() - StartTime, Stats.NumPrecomputedVisibilityMeshes, Stats.NumPrecomputedVisibilityCellsTotal));
|
|
}
|
|
}
|
|
|
|
static bool IsMeshVisible(const TArray<uint8>& VisibilityData, int32 MeshId)
|
|
{
|
|
return (VisibilityData[MeshId / 8] & 1 << (MeshId % 8)) != 0;
|
|
}
|
|
|
|
static void SetMeshVisible(TArray<uint8>& VisibilityData, int32 MeshId)
|
|
{
|
|
VisibilityData[MeshId / 8] |= 1 << (MeshId % 8);
|
|
}
|
|
|
|
class FAxisAlignedCellFace
|
|
{
|
|
public:
|
|
|
|
FAxisAlignedCellFace() {}
|
|
|
|
FAxisAlignedCellFace(const FVector4f& InFaceDirection, const FVector4f& InFaceMin, const FVector4f& InFaceExtent) :
|
|
FaceDirection(InFaceDirection),
|
|
FaceMin(InFaceMin),
|
|
FaceExtent(InFaceExtent)
|
|
{}
|
|
|
|
FVector4f FaceDirection;
|
|
FVector4f FaceMin;
|
|
FVector4f FaceExtent;
|
|
};
|
|
|
|
/** Stores information about a single query sample between a visibility cell and a mesh. */
|
|
struct FVisibilityQuerySample
|
|
{
|
|
/** Sample position generated from the mesh. */
|
|
FVector4f MeshPosition;
|
|
|
|
/** Sample position generated from the cell. */
|
|
FVector4f CellPosition;
|
|
|
|
/** Position of the intersection with the scene between the sample positions. */
|
|
FVector4f IntersectionPosition;
|
|
|
|
/** Distance along the vector perpendicular to the mesh->cell vector. */
|
|
float PerpendicularDistance;
|
|
};
|
|
|
|
/**
|
|
* Computes visibility from a cell to a box, returns true if the box is visible from the cell.
|
|
* The TArrays are passed in to avoid allocations between cells, the same arrays are reused.
|
|
*/
|
|
bool ComputeBoxVisibility(
|
|
FStaticLightingAggregateMeshType& AggregateMesh,
|
|
FPrecomputedVisibilitySettings& PrecomputedVisibilitySettings,
|
|
FPrecomputedVisibilityCell& CurrentCell,
|
|
FAxisAlignedCellFace* CellFaces,
|
|
const FBox3f& MeshBox,
|
|
FStaticLightingMappingContext& MappingContext,
|
|
FLMRandomStream& RandomStream,
|
|
TArray<int32>& VisibleCellFaces,
|
|
TArray<float>& VisibleCellFacePDFs,
|
|
TArray<float>& VisibleCellFaceCDFs,
|
|
TArray<int32>& VisibleMeshFaces,
|
|
TArray<FVisibilityQuerySample>& SamplePositions,
|
|
TArray<const FVisibilityQuerySample*>& FurthestSamples,
|
|
TArray<FDebugStaticLightingRay>& DebugVisibilityRays,
|
|
bool bDebugThisCell,
|
|
bool bDebugThisMesh,
|
|
bool bGroupQuery)
|
|
{
|
|
const double SampleGenerationStartTime = FPlatformTime::Seconds();
|
|
|
|
const FVector4f CenterCellPosition = .5f * (CurrentCell.Bounds.Min + CurrentCell.Bounds.Max);
|
|
const FVector4f MeshToCellCenter = CenterCellPosition - MeshBox.GetCenter();
|
|
const float Distance = MeshToCellCenter.Size3();
|
|
const FVector4f MeshBoxExtent = MeshBox.GetExtent() * 2;
|
|
|
|
FAxisAlignedCellFace MeshBoxFaces[6];
|
|
MeshBoxFaces[0] = FAxisAlignedCellFace(FVector4f(-1, 0, 0), FVector4f(MeshBox.Min.X, MeshBox.Min.Y, MeshBox.Min.Z), FVector4f(0, MeshBoxExtent.Y, MeshBoxExtent.Z));
|
|
MeshBoxFaces[1] = FAxisAlignedCellFace(FVector4f(1, 0, 0), FVector4f(MeshBox.Min.X + MeshBoxExtent.X, MeshBox.Min.Y, MeshBox.Min.Z), FVector4f(0, MeshBoxExtent.Y, MeshBoxExtent.Z));
|
|
MeshBoxFaces[2] = FAxisAlignedCellFace(FVector4f(0, -1, 0), FVector4f(MeshBox.Min.X, MeshBox.Min.Y, MeshBox.Min.Z), FVector4f(MeshBoxExtent.X, 0, MeshBoxExtent.Z));
|
|
MeshBoxFaces[3] = FAxisAlignedCellFace(FVector4f(0, 1, 0), FVector4f(MeshBox.Min.X, MeshBox.Min.Y + MeshBoxExtent.Y, MeshBox.Min.Z), FVector4f(MeshBoxExtent.X, 0, MeshBoxExtent.Z));
|
|
MeshBoxFaces[4] = FAxisAlignedCellFace(FVector4f(0, 0, -1), FVector4f(MeshBox.Min.X, MeshBox.Min.Y, MeshBox.Min.Z), FVector4f(MeshBoxExtent.X, MeshBoxExtent.Y, 0));
|
|
MeshBoxFaces[5] = FAxisAlignedCellFace(FVector4f(0, 0, 1), FVector4f(MeshBox.Min.X, MeshBox.Min.Y, MeshBox.Min.Z + MeshBoxExtent.Z), FVector4f(MeshBoxExtent.X, MeshBoxExtent.Y, 0));
|
|
|
|
VisibleCellFaces.Reset();
|
|
VisibleCellFacePDFs.Reset();
|
|
for (int32 i = 0; i < 6; i++)
|
|
{
|
|
const float DotProduct = -Dot3((MeshToCellCenter / Distance), CellFaces[i].FaceDirection);
|
|
if (DotProduct > 0.0f)
|
|
{
|
|
VisibleCellFaces.Add(i);
|
|
VisibleCellFacePDFs.Add(DotProduct);
|
|
}
|
|
}
|
|
|
|
// Ensure that some of the faces will be sampled
|
|
if (VisibleCellFacePDFs.Num() == 0)
|
|
{
|
|
for (int32 i = 0; i < 6; i++)
|
|
{
|
|
VisibleCellFaces.Add(i);
|
|
VisibleCellFacePDFs.Add(i);
|
|
}
|
|
}
|
|
|
|
float UnnormalizedIntegral;
|
|
CalculateStep1dCDF(VisibleCellFacePDFs, VisibleCellFaceCDFs, UnnormalizedIntegral);
|
|
|
|
VisibleMeshFaces.Reset();
|
|
for (int32 i = 0; i < 6; i++)
|
|
{
|
|
if (Dot3(MeshToCellCenter, MeshBoxFaces[i].FaceDirection) > 0.0f)
|
|
{
|
|
VisibleMeshFaces.Add(i);
|
|
}
|
|
}
|
|
|
|
if (bGroupQuery)
|
|
{
|
|
MappingContext.Stats.NumPrecomputedVisibilityGroupQueries++;
|
|
}
|
|
else
|
|
{
|
|
MappingContext.Stats.NumPrecomputedVisibilityQueries++;
|
|
}
|
|
|
|
const float MeshSize = MeshBox.GetExtent().Size();
|
|
const float SizeRatio = MeshSize / Distance;
|
|
// Use MaxMeshSamples for meshes with a large projected angle, and MinMeshSamples for meshes with a small projected angle
|
|
// Meshes with a large projected angle require more samples to determine visibility accurately
|
|
const int32 NumMeshSamples = FMath::Clamp(FMath::TruncToInt(SizeRatio * PrecomputedVisibilitySettings.MaxMeshSamples), PrecomputedVisibilitySettings.MinMeshSamples, PrecomputedVisibilitySettings.MaxMeshSamples);
|
|
|
|
// Treat meshes whose projected angle is greater than 90 degrees as visible, since it becomes overly costly to determine if these are visible
|
|
// (consider a large close mesh that only has a tiny part visible)
|
|
bool bVisible = SizeRatio > 1.0f;
|
|
|
|
if (bVisible)
|
|
{
|
|
MappingContext.Stats.NumQueriesVisibleByDistanceRatio++;
|
|
}
|
|
|
|
if (!bVisible)
|
|
{
|
|
const FVector4f PerpendicularVector = MeshToCellCenter ^ FVector4f(0, 0, 1);
|
|
SamplePositions.Reset();
|
|
|
|
// Generate samples for explicit visibility sampling of the mesh
|
|
for (int32 CellSampleIndex = 0; CellSampleIndex < PrecomputedVisibilitySettings.NumCellSamples; CellSampleIndex++)
|
|
{
|
|
for (int32 MeshSampleIndex = 0; MeshSampleIndex < NumMeshSamples; MeshSampleIndex++)
|
|
{
|
|
FVisibilityQuerySample NewSample;
|
|
{
|
|
float PDF;
|
|
float Sample;
|
|
// Generate a sample on the visible faces of the cell, picking a face with probability proportional to the projected angle of the cell face onto the mesh's origin
|
|
//@todo - weight by face area, since cells have a different height from their x and y sizes
|
|
Sample1dCDF(VisibleCellFacePDFs, VisibleCellFaceCDFs, UnnormalizedIntegral, RandomStream, PDF, Sample);
|
|
const int32 ChosenCellFaceIndex = FMath::TruncToInt(Sample * VisibleCellFaces.Num());
|
|
const FAxisAlignedCellFace& ChosenFace = CellFaces[VisibleCellFaces[ChosenCellFaceIndex]];
|
|
NewSample.CellPosition = ChosenFace.FaceMin + ChosenFace.FaceExtent * FVector4f(RandomStream.GetFraction(), RandomStream.GetFraction(), RandomStream.GetFraction());
|
|
}
|
|
{
|
|
// Generate a sample on the visible faces of the mesh
|
|
const int32 ChosenFaceIndex = FMath::TruncToInt(RandomStream.GetFraction() * VisibleMeshFaces.Num());
|
|
const FAxisAlignedCellFace& ChosenFace = MeshBoxFaces[VisibleMeshFaces[ChosenFaceIndex]];
|
|
NewSample.MeshPosition = ChosenFace.FaceMin + ChosenFace.FaceExtent * FVector4f(RandomStream.GetFraction(), RandomStream.GetFraction(), RandomStream.GetFraction());
|
|
}
|
|
const FVector4f HalfPosition = .5f * (NewSample.CellPosition + NewSample.MeshPosition);
|
|
NewSample.PerpendicularDistance = Dot3(HalfPosition, PerpendicularVector);
|
|
SamplePositions.Add(NewSample);
|
|
}
|
|
}
|
|
|
|
// Sorts two samples based on perpendicular distance.
|
|
struct FCompareSampleDistance
|
|
{
|
|
FORCEINLINE bool operator()(const FVisibilityQuerySample& A, const FVisibilityQuerySample& B) const
|
|
{
|
|
return A.PerpendicularDistance < B.PerpendicularDistance;
|
|
}
|
|
};
|
|
// Sort the samples to make them more coherent in kDOP tree traversals, which provides a small speedup
|
|
SamplePositions.Sort(FCompareSampleDistance());
|
|
|
|
const double SampleGenerationEndTime = FPlatformTime::Seconds();
|
|
MappingContext.Stats.PrecomputedVisibilitySampleSetupThreadTime += SampleGenerationEndTime - SampleGenerationStartTime;
|
|
|
|
float FurthestDistanceSquared = 0;
|
|
// Early out if any sample finds the mesh visible, unless we are debugging this cell and want to see all the samples
|
|
for (int32 CellSampleIndex = 0; (CellSampleIndex < PrecomputedVisibilitySettings.NumCellSamples) && (!bVisible || bDebugThisCell); CellSampleIndex++)
|
|
{
|
|
for (int32 MeshSampleIndex = 0; MeshSampleIndex < NumMeshSamples; MeshSampleIndex++)
|
|
{
|
|
FVisibilityQuerySample& CurrentSample = SamplePositions[CellSampleIndex * NumMeshSamples + MeshSampleIndex];
|
|
const FVector4f CellSamplePosition = CurrentSample.CellPosition;
|
|
const FVector4f MeshSamplePosition = CurrentSample.MeshPosition;
|
|
|
|
FLightRay Ray(
|
|
CellSamplePosition,
|
|
MeshSamplePosition,
|
|
NULL,
|
|
NULL,
|
|
// Masked materials often have small holes which increase visibility errors
|
|
// This also allows us to use boolean visibility traces which are much faster than first hit traces
|
|
// Only intersect with static objects since they will not move in game
|
|
LIGHTRAY_STATIC_AND_OPAQUEONLY
|
|
);
|
|
|
|
FLightRayIntersection Intersection;
|
|
// Use boolean visibility traces
|
|
AggregateMesh.IntersectLightRay(Ray, false, false, false, MappingContext.RayCache, Intersection);
|
|
|
|
MappingContext.Stats.NumPrecomputedVisibilityRayTraces++;
|
|
|
|
//Note: using intersection position even though we used a boolean ray trace, so the position may not be the closest
|
|
CurrentSample.IntersectionPosition = Intersection.IntersectionVertex.WorldPosition;
|
|
|
|
const float DistanceSquared = (CellSamplePosition - Intersection.IntersectionVertex.WorldPosition).SizeSquared3();
|
|
FurthestDistanceSquared = FMath::Max(FurthestDistanceSquared, DistanceSquared);
|
|
|
|
if (bDebugThisMesh)
|
|
{
|
|
// Draw all the rays from the debug cell to the debug mesh
|
|
FDebugStaticLightingRay DebugRay(CellSamplePosition, MeshSamplePosition, Intersection.bIntersects, false);
|
|
if (Intersection.bIntersects)
|
|
{
|
|
DebugRay.End = Intersection.IntersectionVertex.WorldPosition;
|
|
}
|
|
DebugVisibilityRays.Add(DebugRay);
|
|
}
|
|
|
|
if (!Intersection.bIntersects)
|
|
{
|
|
MappingContext.Stats.NumQueriesVisibleExplicitSampling++;
|
|
bVisible = true;
|
|
if (!bDebugThisCell)
|
|
{
|
|
// The mesh is visible from the current cell, move on to the next mesh
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const double RayTraceEndTime = FPlatformTime::Seconds();
|
|
MappingContext.Stats.PrecomputedVisibilityRayTraceThreadTime += RayTraceEndTime - SampleGenerationEndTime;
|
|
|
|
// If the meshes has not been determined to be visible by explicit sampling,
|
|
// Do importance sampling to try and find visible meshes through cracks that have a low probability of being detected by explicit sampling.
|
|
if (!bVisible)
|
|
{
|
|
FurthestSamples.Reset();
|
|
|
|
// Create an array of all the longest rays toward the mesh
|
|
const float DistanceThreshold = FMath::Sqrt(FurthestDistanceSquared) * 7.0f / 8.0f;
|
|
const float DistanceThresholdSq = DistanceThreshold * DistanceThreshold;
|
|
for (int32 SampleIndex = 0; SampleIndex < SamplePositions.Num(); SampleIndex++)
|
|
{
|
|
const FVisibilityQuerySample& CurrentSample = SamplePositions[SampleIndex];
|
|
const float DistanceSquared = (CurrentSample.CellPosition - CurrentSample.IntersectionPosition).SizeSquared3();
|
|
if (DistanceSquared > DistanceThresholdSq)
|
|
{
|
|
FurthestSamples.Add(&CurrentSample);
|
|
}
|
|
}
|
|
|
|
// Trace importance sampled rays to try and find visible meshes through small cracks
|
|
// This is only slightly effective, but doesn't cost much compared to explicit sampling due to the small number of rays
|
|
for (int32 ImportanceSampleIndex = 0; ImportanceSampleIndex < PrecomputedVisibilitySettings.NumImportanceSamples && !bVisible && FurthestSamples.Num() > 0; ImportanceSampleIndex++)
|
|
{
|
|
// Pick one of the furthest samples with uniform probability
|
|
const int32 SampleIndex = FMath::TruncToInt(RandomStream.GetFraction() * FurthestSamples.Num());
|
|
const FVisibilityQuerySample& CurrentSample = *FurthestSamples[SampleIndex];
|
|
const float VectorLength = (CurrentSample.CellPosition - CurrentSample.MeshPosition).Size3();
|
|
const FVector4f CurrentDirection = (CurrentSample.MeshPosition - CurrentSample.CellPosition).GetSafeNormal();
|
|
|
|
FVector4f XAxis;
|
|
FVector4f YAxis;
|
|
GenerateCoordinateSystem(CurrentDirection, XAxis, YAxis);
|
|
|
|
// Generate a new direction in a cone 2 degrees from the original direction, to find cracks nearby
|
|
const FVector4f SampleDirection = UniformSampleCone(
|
|
RandomStream,
|
|
FMath::Cos(2.0f * PI / 180.0f),
|
|
XAxis,
|
|
YAxis,
|
|
CurrentDirection);
|
|
|
|
const FVector4f EndPoint = CurrentSample.CellPosition + SampleDirection * VectorLength;
|
|
|
|
FLightRay Ray(
|
|
CurrentSample.CellPosition,
|
|
EndPoint,
|
|
NULL,
|
|
NULL,
|
|
LIGHTRAY_STATIC_AND_OPAQUEONLY
|
|
);
|
|
|
|
FLightRayIntersection Intersection;
|
|
AggregateMesh.IntersectLightRay(Ray, false, false, false, MappingContext.RayCache, Intersection);
|
|
|
|
MappingContext.Stats.NumPrecomputedVisibilityRayTraces++;
|
|
|
|
if (bDebugThisMesh)
|
|
{
|
|
// Draw all the rays from the debug cell to the debug mesh
|
|
FDebugStaticLightingRay DebugRay(CurrentSample.CellPosition, EndPoint, Intersection.bIntersects, true);
|
|
if (Intersection.bIntersects)
|
|
{
|
|
DebugRay.End = Intersection.IntersectionVertex.WorldPosition;
|
|
}
|
|
DebugVisibilityRays.Add(DebugRay);
|
|
}
|
|
|
|
if (!Intersection.bIntersects)
|
|
{
|
|
MappingContext.Stats.NumQueriesVisibleImportanceSampling++;
|
|
bVisible = true;
|
|
if (!bDebugThisCell)
|
|
{
|
|
// The mesh is visible from the current cell, move on to the next mesh
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
MappingContext.Stats.PrecomputedVisibilityImportanceSampleThreadTime += FPlatformTime::Seconds() - RayTraceEndTime;
|
|
}
|
|
}
|
|
|
|
return bVisible;
|
|
}
|
|
|
|
void AddMeshDebugLines(TArray<FDebugStaticLightingRay>& DebugVisibilityRays, const TArray<FStaticLightingMesh*>& Meshes, const FBox3f& MeshBox)
|
|
{
|
|
// Draw the bounding boxes of each mesh and the combined bounds
|
|
if (Meshes.Num() > 1)
|
|
{
|
|
for (int32 OriginalMeshIndex = 0; OriginalMeshIndex < Meshes.Num(); OriginalMeshIndex++)
|
|
{
|
|
const FVector4f Min = Meshes[OriginalMeshIndex]->BoundingBox.Min;
|
|
const FVector4f Max = Meshes[OriginalMeshIndex]->BoundingBox.Max;
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Min.Z), FVector4f(Min.X, Max.Y, Min.Z), false));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Max.Y, Min.Z), FVector4f(Min.X, Max.Y, Max.Z), false));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Min.Z), FVector4f(Min.X, Min.Y, Max.Z), false));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Max.Z), FVector4f(Min.X, Max.Y, Max.Z), false));
|
|
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Min.Y, Min.Z), FVector4f(Max.X, Max.Y, Min.Z), false));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Max.Y, Min.Z), FVector4f(Max.X, Max.Y, Max.Z), false));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Min.Y, Min.Z), FVector4f(Max.X, Min.Y, Max.Z), false));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Min.Y, Max.Z), FVector4f(Max.X, Max.Y, Max.Z), false));
|
|
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Min.Z), FVector4f(Max.X, Min.Y, Min.Z), false));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Max.Z), FVector4f(Max.X, Min.Y, Max.Z), false));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Max.Y, Min.Z), FVector4f(Max.X, Max.Y, Min.Z), false));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Max.Y, Max.Z), FVector4f(Max.X, Max.Y, Max.Z), false));
|
|
}
|
|
}
|
|
|
|
const FVector4f Min = MeshBox.Min;
|
|
const FVector4f Max = MeshBox.Max;
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Min.Z), FVector4f(Min.X, Max.Y, Min.Z), true));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Max.Y, Min.Z), FVector4f(Min.X, Max.Y, Max.Z), true));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Min.Z), FVector4f(Min.X, Min.Y, Max.Z), true));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Max.Z), FVector4f(Min.X, Max.Y, Max.Z), true));
|
|
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Min.Y, Min.Z), FVector4f(Max.X, Max.Y, Min.Z), true));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Max.Y, Min.Z), FVector4f(Max.X, Max.Y, Max.Z), true));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Min.Y, Min.Z), FVector4f(Max.X, Min.Y, Max.Z), true));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Min.Y, Max.Z), FVector4f(Max.X, Max.Y, Max.Z), true));
|
|
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Min.Z), FVector4f(Max.X, Min.Y, Min.Z), true));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Max.Z), FVector4f(Max.X, Min.Y, Max.Z), true));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Max.Y, Min.Z), FVector4f(Max.X, Max.Y, Min.Z), true));
|
|
DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Max.Y, Max.Z), FVector4f(Max.X, Max.Y, Max.Z), true));
|
|
}
|
|
|
|
/** Calculates visibility for a given group of cells, called from all threads. */
|
|
void FStaticLightingSystem::CalculatePrecomputedVisibility(int32 BucketIndex)
|
|
{
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
check(BucketIndex >= 0 && BucketIndex < PrecomputedVisibilitySettings.NumCellDistributionBuckets);
|
|
// Create a new link for the output of this task
|
|
TList<FPrecomputedVisibilityData>* DataLink = new TList<FPrecomputedVisibilityData>(FPrecomputedVisibilityData(),NULL);
|
|
DataLink->Element.Guid = Scene.VisibilityBucketGuids[BucketIndex];
|
|
|
|
// Determine the range of cells to process from the bucket index
|
|
const int32 StartCellIndex = BucketIndex * AllPrecomputedVisibilityCells.Num() / PrecomputedVisibilitySettings.NumCellDistributionBuckets;
|
|
const int32 MaxCellIndex = BucketIndex + 1 == PrecomputedVisibilitySettings.NumCellDistributionBuckets ?
|
|
// Last bucket processes up to the end of the array
|
|
AllPrecomputedVisibilityCells.Num() :
|
|
(BucketIndex + 1) * AllPrecomputedVisibilityCells.Num() / PrecomputedVisibilitySettings.NumCellDistributionBuckets;
|
|
|
|
DataLink->Element.PrecomputedVisibilityCells.Empty(MaxCellIndex - StartCellIndex);
|
|
|
|
FStaticLightingMappingContext MappingContext(NULL, *this);
|
|
|
|
// These are re-used across operations on the same thread to reduce reallocations
|
|
TArray<int32> VisibleCellFaces;
|
|
TArray<float> VisibleCellFacePDFs;
|
|
TArray<float> VisibleCellFaceCDFs;
|
|
TArray<int32> VisibleMeshFaces;
|
|
TArray<FVisibilityQuerySample> SamplePositions;
|
|
TArray<const FVisibilityQuerySample*> FurthestSamples;
|
|
TArray<bool> GroupVisibility;
|
|
|
|
TArray<const FPrecomputedVisibilityOverrideVolume*> AffectingOverrideVolumes;
|
|
for (int32 CellIndex = StartCellIndex; CellIndex < MaxCellIndex; CellIndex++)
|
|
{
|
|
const double StartSampleTime = FPlatformTime::Seconds();
|
|
|
|
// Seed by absolute cell index for deterministic results regardless of how cell tasks are distributed
|
|
FLMRandomStream RandomStream(CellIndex);
|
|
|
|
// Reset cached information for this cell so that traces won't be affected by what previous cells did
|
|
MappingContext.RayCache.Clear();
|
|
|
|
DataLink->Element.PrecomputedVisibilityCells.Add(AllPrecomputedVisibilityCells[CellIndex]);
|
|
FPrecomputedVisibilityCell& CurrentCell = DataLink->Element.PrecomputedVisibilityCells.Last();
|
|
|
|
const bool bDebugThisCell = CurrentCell.Bounds.IsInside(Scene.DebugInput.CameraPosition) && PrecomputedVisibilitySettings.bVisualizePrecomputedVisibility;
|
|
|
|
AffectingOverrideVolumes.Reset();
|
|
for (int32 VolumeIndex = 0; VolumeIndex < Scene.PrecomputedVisibilityOverrideVolumes.Num(); VolumeIndex++)
|
|
{
|
|
if (Scene.PrecomputedVisibilityOverrideVolumes[VolumeIndex].Bounds.Intersect(CurrentCell.Bounds))
|
|
{
|
|
AffectingOverrideVolumes.Add(&Scene.PrecomputedVisibilityOverrideVolumes[VolumeIndex]);
|
|
}
|
|
}
|
|
|
|
CurrentCell.VisibilityData.Empty(VisibilityMeshes.Num() / 8 + 1);
|
|
CurrentCell.VisibilityData.AddZeroed(VisibilityMeshes.Num() / 8 + 1);
|
|
|
|
FAxisAlignedCellFace CellFaces[6];
|
|
const FVector3f CellBoundsSize = CurrentCell.Bounds.GetSize();
|
|
CellFaces[0] = FAxisAlignedCellFace(FVector4f(-1, 0, 0), FVector4f(CurrentCell.Bounds.Min.X, CurrentCell.Bounds.Min.Y, CurrentCell.Bounds.Min.Z), FVector4f(0, CellBoundsSize.Y, CellBoundsSize.Z));
|
|
CellFaces[1] = FAxisAlignedCellFace(FVector4f(1, 0, 0), FVector4f(CurrentCell.Bounds.Max.X, CurrentCell.Bounds.Min.Y, CurrentCell.Bounds.Min.Z), FVector4f(0, CellBoundsSize.Y, CellBoundsSize.Z));
|
|
CellFaces[2] = FAxisAlignedCellFace(FVector4f(0, -1, 0), FVector4f(CurrentCell.Bounds.Min.X, CurrentCell.Bounds.Min.Y, CurrentCell.Bounds.Min.Z), FVector4f(CellBoundsSize.X, 0, CellBoundsSize.Z));
|
|
CellFaces[3] = FAxisAlignedCellFace(FVector4f(0, 1, 0), FVector4f(CurrentCell.Bounds.Min.X, CurrentCell.Bounds.Max.Y, CurrentCell.Bounds.Min.Z), FVector4f(CellBoundsSize.X, 0, CellBoundsSize.Z));
|
|
CellFaces[4] = FAxisAlignedCellFace(FVector4f(0, 0, -1), FVector4f(CurrentCell.Bounds.Min.X, CurrentCell.Bounds.Min.Y, CurrentCell.Bounds.Min.Z), FVector4f(CellBoundsSize.X, CellBoundsSize.Y, 0));
|
|
CellFaces[5] = FAxisAlignedCellFace(FVector4f(0, 0, 1), FVector4f(CurrentCell.Bounds.Min.X, CurrentCell.Bounds.Min.Y, CurrentCell.Bounds.Max.Z), FVector4f(CellBoundsSize.X, CellBoundsSize.Y, 0));
|
|
|
|
GroupVisibility.Reset();
|
|
|
|
// First determine group visibility using the combined bounds, so we can skip lots of mesh queries later (if they are all invisible)
|
|
for (int32 GroupIndex = 0; GroupIndex < VisibilityGroups.Num(); GroupIndex++)
|
|
{
|
|
const FVisibilityMeshGroup& Group = VisibilityGroups[GroupIndex];
|
|
bool bDebugThisMesh = false;
|
|
|
|
bool bVisible = ComputeBoxVisibility(
|
|
*AggregateMesh,
|
|
PrecomputedVisibilitySettings,
|
|
CurrentCell,
|
|
CellFaces,
|
|
Group.GroupBounds,
|
|
MappingContext,
|
|
RandomStream,
|
|
VisibleCellFaces,
|
|
VisibleCellFacePDFs,
|
|
VisibleCellFaceCDFs,
|
|
VisibleMeshFaces,
|
|
SamplePositions,
|
|
FurthestSamples,
|
|
DataLink->Element.DebugVisibilityRays,
|
|
bDebugThisCell,
|
|
bDebugThisMesh,
|
|
true);
|
|
|
|
GroupVisibility.Add(bVisible);
|
|
}
|
|
|
|
for (int32 VisibilityMeshIndex = 0; VisibilityMeshIndex < VisibilityMeshes.Num(); VisibilityMeshIndex++)
|
|
{
|
|
const FVisibilityMesh& VisibilityMesh = VisibilityMeshes[VisibilityMeshIndex];
|
|
|
|
FBox3f OriginalMeshBounds(ForceInit);
|
|
// Combine mesh bounds, usually only BSP has multiple meshes per VisibilityId
|
|
//@todo - could explicitly sample each bounds separately, but they tend to be pretty close together in world space
|
|
for (int32 OriginalMeshIndex = 0; OriginalMeshIndex < VisibilityMesh.Meshes.Num(); OriginalMeshIndex++)
|
|
{
|
|
OriginalMeshBounds += VisibilityMesh.Meshes[OriginalMeshIndex]->BoundingBox;
|
|
}
|
|
|
|
const FBox3f MeshBox(
|
|
OriginalMeshBounds.GetCenter() - OriginalMeshBounds.GetExtent() * PrecomputedVisibilitySettings.MeshBoundsScale,
|
|
OriginalMeshBounds.GetCenter() + OriginalMeshBounds.GetExtent() * PrecomputedVisibilitySettings.MeshBoundsScale);
|
|
|
|
const bool bDebugThisMesh = VisibilityMeshIndex == Scene.DebugInput.DebugVisibilityId && bDebugThisCell;
|
|
|
|
if (bDebugThisMesh)
|
|
{
|
|
AddMeshDebugLines(DataLink->Element.DebugVisibilityRays, VisibilityMesh.Meshes, MeshBox);
|
|
}
|
|
|
|
bool bVisible = false;
|
|
bool bForceInvisible = false;
|
|
|
|
// Apply override volumes first in case they can save us some work
|
|
for (int32 VolumeIndex = 0; VolumeIndex < AffectingOverrideVolumes.Num(); VolumeIndex++)
|
|
{
|
|
if (AffectingOverrideVolumes[VolumeIndex]->OverrideVisibilityIds.Contains(VisibilityMeshIndex))
|
|
{
|
|
bVisible = true;
|
|
break;
|
|
}
|
|
// This means forced visibility will override forced invisibility
|
|
// Something to keep in mind when an LD complains that an actor
|
|
// they put into the OverrideInvisibility list is still showing up!
|
|
if (AffectingOverrideVolumes[VolumeIndex]->OverrideInvisibilityIds.Contains(VisibilityMeshIndex))
|
|
{
|
|
bVisible = false;
|
|
bForceInvisible = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bVisible && !bForceInvisible)
|
|
{
|
|
const int32 GroupCellIndex = GetGroupCellIndex(OriginalMeshBounds.GetCenter());
|
|
|
|
bool bGroupVisible = true;
|
|
|
|
// Lookup group visibility, if this mesh was put into a group
|
|
if (GroupCellIndex >= 0 && VisibilityMesh.bInGroup)
|
|
{
|
|
const int32 GroupId = GroupGrid[GroupCellIndex];
|
|
bGroupVisible = GroupVisibility[GroupGrid[GroupCellIndex]];
|
|
}
|
|
|
|
// Only determine mesh visibility if the group containing the mesh was also visible
|
|
if (bGroupVisible)
|
|
{
|
|
bVisible = ComputeBoxVisibility(
|
|
*AggregateMesh,
|
|
PrecomputedVisibilitySettings,
|
|
CurrentCell,
|
|
CellFaces,
|
|
MeshBox,
|
|
MappingContext,
|
|
RandomStream,
|
|
VisibleCellFaces,
|
|
VisibleCellFacePDFs,
|
|
VisibleCellFaceCDFs,
|
|
VisibleMeshFaces,
|
|
SamplePositions,
|
|
FurthestSamples,
|
|
DataLink->Element.DebugVisibilityRays,
|
|
bDebugThisCell,
|
|
bDebugThisMesh,
|
|
false);
|
|
}
|
|
else
|
|
{
|
|
MappingContext.Stats.NumPrecomputedVisibilityMeshQueriesSkipped++;
|
|
}
|
|
}
|
|
|
|
if (bVisible)
|
|
{
|
|
SetMeshVisible(CurrentCell.VisibilityData, VisibilityMeshIndex);
|
|
}
|
|
}
|
|
|
|
if (bDebugThisCell)
|
|
{
|
|
// Draw the bounds of each cell processed
|
|
const FVector4f Min = CurrentCell.Bounds.Min;
|
|
const FVector4f Max = CurrentCell.Bounds.Max;
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Min.Z), FVector4f(Min.X, Max.Y, Min.Z), false));
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Max.Y, Min.Z), FVector4f(Min.X, Max.Y, Max.Z), false));
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Min.Z), FVector4f(Min.X, Min.Y, Max.Z), false));
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Max.Z), FVector4f(Min.X, Max.Y, Max.Z), false));
|
|
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Min.Y, Min.Z), FVector4f(Max.X, Max.Y, Min.Z), false));
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Max.Y, Min.Z), FVector4f(Max.X, Max.Y, Max.Z), false));
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Min.Y, Min.Z), FVector4f(Max.X, Min.Y, Max.Z), false));
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Max.X, Min.Y, Max.Z), FVector4f(Max.X, Max.Y, Max.Z), false));
|
|
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Min.Z), FVector4f(Max.X, Min.Y, Min.Z), false));
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Min.Y, Max.Z), FVector4f(Max.X, Min.Y, Max.Z), false));
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Max.Y, Min.Z), FVector4f(Max.X, Max.Y, Min.Z), false));
|
|
DataLink->Element.DebugVisibilityRays.Add(FDebugStaticLightingRay(FVector4f(Min.X, Max.Y, Max.Z), FVector4f(Max.X, Max.Y, Max.Z), false));
|
|
}
|
|
}
|
|
|
|
MappingContext.Stats.PrecomputedVisibilityThreadTime = FPlatformTime::Seconds() - StartTime;
|
|
MappingContext.Stats.NumPrecomputedVisibilityCellsProcessed = MaxCellIndex - StartCellIndex;
|
|
CompleteVisibilityTaskList.AddElement(DataLink);
|
|
}
|
|
|
|
}
|