using static Sandbox.SceneCubemap;
namespace Sandbox;
///
/// A cubemap probe that captures the environment around it.
///
[Expose]
[Title( "Envmap Probe" )]
[Category( "Light" )]
[Icon( "radio_button_unchecked" )]
[EditorHandle( "materials/gizmo/envmap.png" )]
[Alias( "EnvmapComponent" )]
public sealed class EnvmapProbe : Component, Component.ExecuteInEditor
{
SceneCubemap _sceneObject;
[Property, MakeDirty] public SceneCubemap.ProjectionMode Projection { get; set; }
[Property, MakeDirty] public Color TintColor { get; set; } = Color.White;
[Property, MakeDirty] public BBox Bounds { get; set; } = BBox.FromPositionAndSize( 0, 1024 );
[Property, Range( -32.0f, 32.0f ), MakeDirty] public float Feathering { get; set; } = 8.0f;
internal int ArrayIndex;
internal int Priority;
///
/// If this is set, the EnvmapProbe will use a custom cubemap texture instead of rendering dynamically
///
[Property, MakeDirty]
[ShowIf( nameof( RenderDynamically ), false )]
public Texture Texture { get; set; }
Texture _dynamicTexture;
public bool Dirty;
int BouncesLeft;
///
/// Cubemaps in Source 2 have an inverted Y axis, for rendering them dynamically it uses correct axis
/// We used to invert-Y but since we are rendering directly to cubemaps (and can't manipulate Y projection matrix
/// without breaking culling ), we invert the matrix of the cubemap being drawn
///
internal bool NeedsInvertedAxis => RenderDynamically;
protected override void DrawGizmos()
{
if ( !Gizmo.IsSelected )
return;
Gizmo.Draw.Color = TintColor;
Gizmo.Draw.LineBBox( Bounds );
Gizmo.Draw.Color = TintColor.WithAlpha( 0.1f );
Gizmo.Draw.LineBBox( Bounds.Grow( Feathering ) );
}
protected override void OnEnabled()
{
Assert.True( !_sceneObject.IsValid() );
Assert.NotNull( Scene );
_sceneObject = new SceneCubemap( Scene.SceneWorld, null, Bounds, WorldTransform, TintColor, Priority, Feathering, (int)Projection );
_sceneObject.Tags.SetFrom( Tags );
Transform.OnTransformChanged += OnTransformChanged;
UpdateSceneObject();
}
protected override void OnDisabled()
{
Transform.OnTransformChanged -= OnTransformChanged;
_sceneObject?.Delete();
_sceneObject = null;
_dynamicTexture?.Dispose();
_dynamicTexture = null;
}
protected override async Task OnLoad()
{
if ( Application.IsHeadless )
return;
if ( RenderDynamically && Active )
{
Dirty = true;
}
while ( Dirty )
{
LoadingScreen.Title = "Generating Envmaps";
LoadingScreen.Subtitle = "Creating reflection maps";
await Task.DelayRealtime( 10 );
}
}
protected override void OnDirty()
{
if ( _sceneObject.IsValid() )
{
UpdateSceneObject();
}
}
void OnTransformChanged()
{
if ( !_sceneObject.IsValid() )
return;
UpdateSceneObject();
}
void UpdateSceneObject()
{
if ( !_sceneObject.IsValid() )
return;
var tx = WorldTransform;
var bounds = Bounds;
if ( NeedsInvertedAxis )
{
tx = tx.WithScale( -1 );
bounds = new BBox( -Bounds.Maxs, -Bounds.Mins );
}
_sceneObject.Transform = tx;
_sceneObject.Projection = Projection;
_sceneObject.TintColor = TintColor;
_sceneObject.ProjectionBounds = bounds;
_sceneObject.LocalBounds = _sceneObject.ProjectionBounds;
_sceneObject.Radius = Bounds.Size.Length;
_sceneObject.Feathering = Feathering;
// Update bounce count when strategy or multibounce changes
if ( UpdateStrategy == CubemapDynamicUpdate.OnEnabled )
{
BouncesLeft = MultiBounce ? 4 : 0;
}
// Update texture based on current settings
if ( RenderDynamically )
{
CreateTexture();
_sceneObject.Texture = _dynamicTexture;
}
else
{
// When switching to static texture, dispose dynamic texture
if ( _dynamicTexture != null )
{
_dynamicTexture.Dispose();
_dynamicTexture = null;
}
_sceneObject.Texture = Texture;
}
}
///
/// Tags have been updated - lets update our tags
///
protected override void OnTagsChanged()
{
if ( _sceneObject.IsValid() )
{
_sceneObject.Tags.SetFrom( Tags );
_sceneObject.RenderDirty();
}
}
internal static void InitializeFromLegacy( GameObject go, Sandbox.MapLoader.ObjectEntry kv )
{
var component = go.Components.Create();
var boundsMin = kv.GetValue( "box_mins", new Vector3( -72.0f, -72.0f, -72.0f ) );
var boundsMax = kv.GetValue( "box_maxs", new Vector3( 72.0f, 72.0f, 72.0f ) );
var indoorOutdoorLevel = kv.GetValue( "indoor_outdoor_level" );
var feathering = kv.GetValue( "cubemap_feathering", 0.25f );
component.Bounds = new BBox( boundsMin, boundsMax );
component.Feathering = feathering * 8.0f;
if ( kv.TypeName == "env_combined_light_probe_volume" || kv.TypeName == "env_cubemap_box" )
{
component.Projection = ProjectionMode.Box;
}
else
{
component.Projection = ProjectionMode.Sphere;
}
//
// Because we don't render cubemaps in map compiled anymore, the imported texture is likely BLACK.
// So instead we switch this up to create the texture dynamically, once, on startup
//
component.UpdateStrategy = CubemapDynamicUpdate.OnEnabled;
component.Texture = default;
component.RenderDynamically = true;
}
[Property, ToggleGroup( nameof( RenderDynamically ), Label = "Render Dynamically" ), MakeDirty]
public bool RenderDynamically { get; set; }
///
/// Resolution of the cubemap texture
///
[Property, ToggleGroup( nameof( RenderDynamically ) ), MakeDirty]
public CubemapResolution Resolution { get; set; } = CubemapResolution.Small;
///
/// Only update dynamically if we're this close to it
///
[Property, ToggleGroup( nameof( RenderDynamically ) )]
public float MaxDistance { get; set; } = 512;
[Property, ToggleGroup( nameof( RenderDynamically ) )]
public float ZNear { get; set; } = 16;
[Property, ToggleGroup( nameof( RenderDynamically ) )]
public float ZFar { get; set; } = 4096;
[Property, ToggleGroup( nameof( RenderDynamically ) ), MakeDirty]
public CubemapDynamicUpdate UpdateStrategy { get; set; }
[Property, ToggleGroup( nameof( RenderDynamically ) ), ShowIf( "UpdateStrategy", CubemapDynamicUpdate.TimeInterval ), Range( 0, 10 )]
public float DelayBetweenUpdates { get; set; } = 0.1f;
[Property, ToggleGroup( nameof( RenderDynamically ) ), ShowIf( "UpdateStrategy", CubemapDynamicUpdate.FrameInterval ), Range( 0, 16 )]
public int FrameInterval { get; set; } = 5;
///
/// Minimum amount of reflection bounces to render when first enabled before settling, at cost of extra performance on load
/// Often times you don't need this
///
[Property, ToggleGroup( nameof( RenderDynamically ) ), ShowIf( "UpdateStrategy", CubemapDynamicUpdate.OnEnabled ), MakeDirty]
public bool MultiBounce { get; set; } = false;
protected override void OnUpdate()
{
base.OnUpdate();
TryToDirty();
}
void TryToDirty()
{
if ( !RenderDynamically )
{
// Reset counters when not rendering dynamically
QueuedFrames = 0;
QueuedTime = 0;
return;
}
// Update counters
QueuedFrames++;
QueuedTime += Time.Delta;
if ( !IsReadyToUpdate() )
return;
Dirty = true;
}
int QueuedFrames = 0;
float QueuedTime = 0;
internal bool IsReadyToUpdate()
{
// If it's dirty, always update even if we're render once
if ( _sceneObject?.RequiresUpdate ?? false )
return true;
if ( UpdateStrategy == CubemapDynamicUpdate.EveryFrame )
return true;
if ( UpdateStrategy == CubemapDynamicUpdate.FrameInterval && QueuedFrames > FrameInterval )
return true;
if ( UpdateStrategy == CubemapDynamicUpdate.TimeInterval && QueuedTime > DelayBetweenUpdates )
return true;
if ( UpdateStrategy == CubemapDynamicUpdate.OnEnabled && BouncesLeft > 0 )
return true;
return false;
}
void CreateTexture()
{
if ( !RenderDynamically )
return;
var CubemapSize = (int)Resolution;
var numMips = 7; // Cubemapper is calibrated for 7 mipmaps
// Only create if we don't have a texture or the resolution changed
if ( _dynamicTexture is null || _dynamicTexture.Width != CubemapSize )
{
// Dispose old texture if it exists
_dynamicTexture?.Dispose();
_dynamicTexture = Texture.CreateCube( CubemapSize, CubemapSize )
.WithUAVBinding()
.WithMips( numMips )
.WithFormat( ImageFormat.RGBA16161616F )
.Finish();
}
}
int _renderCount;
internal void RenderCubemap()
{
if ( _dynamicTexture is null )
return;
_renderCount++;
CubemapRendering.GGXFilterType filterType;
if ( UpdateStrategy == CubemapDynamicUpdate.OnEnabled )
{
filterType = CubemapRendering.GGXFilterType.Quality;
}
else
{
filterType = CubemapRendering.GGXFilterType.Fast;
}
CubemapRendering.Render( Scene.SceneWorld, _dynamicTexture, WorldTransform.WithScale( 1 ), ZNear.Clamp( 1, ZFar ), ZFar.Clamp( ZNear, 1024 * 16 ), filterType );
// Just finished rendering, signal to component that we're done
_sceneObject.RequiresUpdate = false;
// Reset counters after rendering
QueuedFrames = 0;
QueuedTime = 0;
if ( BouncesLeft > 0 && UpdateStrategy == CubemapDynamicUpdate.OnEnabled )
BouncesLeft--;
Dirty = false;
}
public enum CubemapResolution
{
[Title( "Small (128²)" )]
Small = 128,
[Title( "Medium (256²)" )]
Medium = 256,
[Title( "Large (512²)" )]
Large = 512
}
public enum CubemapDynamicUpdate
{
///
/// Update once, when the cubemap is enabled
///
OnEnabled,
///
/// Update every frame (slow, not recommended)
///
EveryFrame,
///
/// Update every x frames
///
FrameInterval,
///
/// Update on a time based interval
///
TimeInterval,
}
}