Files
sbox-public/engine/Sandbox.Engine/Systems/SceneSystem/SceneCamera.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

952 lines
24 KiB
C#

using Sandbox.Rendering;
using Sandbox.VR;
namespace Sandbox;
/// <summary>
/// Represents a camera and holds render hooks. This camera can be used to draw tool windows and scene panels.
/// </summary>
[Expose]
public sealed partial class SceneCamera : IDisposable, IManagedCamera
{
// Right now these are 1:1 from the engine. Our intention should be to convert
// these systems into systems that are configurable per camera and from addon code.
// For example, volumetricFog should hold the render state. The addon code should hold
// the volumes. Tonemapping should hold the render state for this camera only.
internal ToneMapping ToneMapping = Application.IsHeadless ? null : new ToneMapping();
internal VolumetricFog VolumetricFogImpl = null;
/// <summary>
/// This is a c++ object with a ton of useful shit.
/// Don't access it directly because it might be dirty.
/// </summary>
CFrustum _frustum;
internal Matrix ProjectionMatrix => Frustum.GetProj();
public RenderAttributes Attributes { get; }
/// <summary>
/// The name of this camera.. for debugging purposes.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Scene objects with any of these tags won't be rendered by this camera.
/// </summary>
public ITagSet ExcludeTags { get; private set; } = new TokenBasedTagSet();
/// <summary>
/// Only scene objects with one of these tags will be rendered by this camera.
/// </summary>
public ITagSet RenderTags { get; private set; } = new TokenBasedTagSet();
/// <summary>
/// Keep hidden! CommandBuffers only!!
/// </summary>
internal Action<Rendering.Stage, SceneCamera> OnRenderStageHook;
/// <summary>
/// Called when rendering the post process pass
/// </summary>
[Obsolete]
public Action OnRenderPostProcess { get; set; }
/// <summary>
/// Called when rendering the transparent pass
/// </summary>
[Obsolete]
public Action OnRenderOpaque { get; set; }
/// <summary>
/// Called when rendering the transparent pass
/// </summary>
[Obsolete]
public Action OnRenderTransparent { get; set; }
public Action OnRenderOverlay { get; set; }
public Action OnRenderUI { get; set; }
/// <summary>
/// The size of the screen. Allows us to work out aspect ratio.
/// For now will get updated automatically on render.
/// </summary>
public Vector2 Size
{
get => _size;
set
{
if ( _size == value )
return;
_size = value;
FrustumDirty = true;
}
}
Vector2 _size = new Vector2( 512, 512 );
/// <summary>
/// Control volumetric fog parameters, expect this to take 1-2ms of your GPU frame time.
/// </summary>
public VolumetricFogParameters VolumetricFog { get; init; } = new();
/// <summary>
/// Control fog based on an image.
/// </summary>
public CubemapFogController CubemapFog { get; init; } = new();
/// <summary>
/// Define the rotations for each of the 6 cube faces (right, left, up, down, front, back)
/// </summary>
internal static readonly Rotation[] CubeRotations =
{
Rotation.LookAt(Vector3.Backward, Vector3.Right), // Negative Z face
Rotation.LookAt(Vector3.Forward, Vector3.Right), // Positive Z face
Rotation.LookAt(Vector3.Right, Vector3.Up), // Positive X face
Rotation.LookAt(Vector3.Left, Vector3.Down), // Negative X face
Rotation.LookAt(Vector3.Down, Vector3.Right), // Negative Y face
Rotation.LookAt(Vector3.Up, Vector3.Right) // Positive Y face
};
public SceneCamera( string name = "Unnamed" )
{
Attributes = new RenderAttributes();
Name = name;
InitCommon();
}
~SceneCamera()
{
Dispose( disposing: false );
}
private bool disposedValue;
private void Dispose( bool disposing )
{
if ( !disposedValue )
{
if ( _frustum.IsValid )
{
_frustum.Delete();
_frustum = default;
}
if ( VolumetricFogImpl != null )
{
// We may have a view queued to render with this
EngineLoop.DisposeAtFrameEnd( VolumetricFogImpl );
VolumetricFogImpl = null;
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose( disposing: true );
GC.SuppressFinalize( this );
}
internal int _cameraId;
internal void InitCommon()
{
_cameraId = (this as IManagedCamera).AllocateCameraId();
// TODO: lets expose these as properties
Attributes.Set( "renderOpaque", true );
Attributes.Set( "renderTranslucent", true );
Attributes.Set( "directLighting", true );
Attributes.Set( "indirectLighting", true );
Attributes.Set( "renderSun", true );
Attributes.Set( "drawShadows", true );
FieldOfView = 70.0f;
Tonemap = new TonemapSystem( this );
Bloom = new BloomAccessor( this );
}
CFrustum Frustum
{
get
{
if ( !_frustum.IsValid )
{
_frustum = CFrustum.Create();
}
if ( FrustumDirty )
{
if ( Size.y <= 0.0f )
Size = new Vector2( 1, 1 );
var aspect = Size.x / Size.y;
if ( Ortho )
{
float orthoWidth = Size.x * (OrthoHeight / Size.y);
_frustum.InitOrthoCamera( Position, Angles, ZNear, ZFar, orthoWidth, OrthoHeight );
}
else
{
_frustum.InitCamera( Position, Angles, ZNear, ZFar, FieldOfView, aspect );
_frustum.SetCameraWidthHeight( Size.x, Size.y );
}
FrustumDirty = false;
}
return _frustum;
}
}
bool FrustumDirty { get; set; } = true;
public override string ToString() => $"SceneCamera:{Name}";
HashSet<SceneWorld> _worlds = new HashSet<SceneWorld>();
SceneWorld _world;
/// <summary>
/// The world we're going to render.
/// </summary>
public SceneWorld World //PaintDay: MainWorld?
{
get => _world;
set
{
if ( _world == value ) return;
_worlds?.Remove( _world );
_world = value;
if ( _world.IsValid() )
{
_worlds?.Add( _world );
}
}
}
/// <summary>
/// Your camera can render multiple worlds.
/// </summary>
public HashSet<SceneWorld> Worlds => _worlds;
Transform _transform = new Transform();
/// <summary>
/// The position of the scene's camera.
/// </summary>
public Vector3 Position
{
get => _transform.Position;
set
{
_transform.Position = value;
FrustumDirty = true;
}
}
/// <summary>
/// The rotation of the scene's camera.
/// </summary>
public Rotation Rotation
{
get => _transform.Rotation;
set
{
_transform.Rotation = value;
FrustumDirty = true;
}
}
/// <summary>
/// The rotation of the scene's camera.
/// </summary>
public Angles Angles
{
get => _transform.Rotation;
set
{
_transform.Rotation = value;
FrustumDirty = true;
}
}
float _fov = 90;
/// <summary>
/// The horizontal field of view of the Camera in degrees.
/// </summary>
public float FieldOfView
{
get => _fov;
set
{
_fov = value;
FrustumDirty = true;
}
}
float _zfar = 100000;
/// <summary>
/// The camera's zFar distance. This is the furthest distance this camera will be able to render.
/// This value totally depends on the game you're making. Shorter the better, sensible ranges would be
/// between about 1000 and 30000, but if you want it to be further out you can balance that out by making
/// znear larger.
/// </summary>
public float ZFar
{
get => _zfar;
set
{
_zfar = value;
FrustumDirty = true;
}
}
float _znear = 1;
/// <summary>
/// The camera's zNear distance. This is the closest distance this camera will be able to render.
/// A good value for this is about 5. Below 5 and particularly below 1 you're going to start to see
/// a lot of artifacts like z-fighting.
/// </summary>
public float ZNear
{
get => _znear;
set
{
_znear = value;
FrustumDirty = true;
}
}
bool _ortho;
/// <summary>
/// Whether to use orthographic projection.
/// </summary>
public bool Ortho
{
get => _ortho;
set
{
_ortho = value;
FrustumDirty = true;
}
}
/// <summary>
/// Height of the ortho when <see cref="Ortho"/> is enabled.
/// </summary>
public float OrthoHeight
{
get => Attributes.GetFloat( "cam.orthoHeight" );
set
{
Attributes.Set( "cam.orthoHeight", value );
FrustumDirty = true;
}
}
/// <summary>
/// Render this camera using a different render mode
/// </summary>
public SceneCameraDebugMode DebugMode
{
get => (SceneCameraDebugMode)Attributes.GetInt( "ToolsVisMode" );
set => Attributes.Set( "ToolsVisMode", (int)value );
}
/// <summary>
/// Render this camera using a wireframe view.
/// </summary>
public bool WireframeMode
{
get => Attributes.GetInt( "Wireframe" ) > 1;
set => Attributes.Set( "Wireframe", value ? 1 : 0 );
}
/// <summary>
/// What kind of clearing should we do before we begin?
/// </summary>
public ClearFlags ClearFlags { get; set; } = ClearFlags.All;
private Rect rect = new Rect( Vector2.Zero, Vector2.One );
/// <summary>
/// The rect of the screen to render to. This is normalized, between 0 and 1.
/// </summary>
public Rect Rect
{
get => rect;
set
{
var newRect = value;
newRect.Left = newRect.Left.Clamp( 0, 1 );
newRect.Top = newRect.Top.Clamp( 0, 1 );
rect = newRect;
}
}
/// <summary>
/// Color the scene camera clears the render target to.
/// </summary>
public Color BackgroundColor { get; set; }
/// <summary>
/// The color of the ambient light. Set it to black for no ambient light, alpha is used for lerping between IBL and constant color.
/// </summary>
public Color AmbientLightColor
{
get;
set;
} = Color.Transparent;
/// <summary>
/// Enable or disable anti-aliasing for this render.
/// </summary>
public bool AntiAliasing
{
get => Attributes.GetInt( "msaa", 4 ) == 4;
set => Attributes.Set( "msaa", value ? 4 : 0 ); // 4 = RENDER_MULTISAMPLE_8X
}
/// <summary>
/// Toggle all post processing effects for this camera. The default is on.
/// </summary>
public bool EnablePostProcessing { get; set; } = true;
/// <summary>
/// Should this camera render engine overlays, you'd only want this on the main camera.
/// </summary>
internal bool EnableEngineOverlays { get; set; } = false;
/// <summary>
/// The HMD eye that this camera is targeting.
/// Use <see cref="StereoTargetEye.None"/> for the user's monitor (i.e. the companion window).
/// </summary>
public StereoTargetEye TargetEye { get; set; } = StereoTargetEye.None;
/// <summary>
/// Set this to false if you don't want the stereo renderer to submit this camera's texture to the compositor.
/// This option isn't considered if <see cref="TargetEye"/> is <see cref="StereoTargetEye.None"/>.
/// </summary>
public bool WantsStereoSubmit { get; set; } = false;
/// <summary>
/// Enable or disable direct lighting
/// </summary>
public bool EnableDirectLighting
{
get => Attributes.GetBool( "directLighting" );
set => Attributes.Set( "directLighting", value );
}
/// <summary>
/// Enable or disable indirect lighting
/// </summary>
public bool EnableIndirectLighting
{
get => Attributes.GetBool( "indirectLighting" );
set => Attributes.Set( "indirectLighting", value );
}
/// <summary>
/// Should be called before a render
/// </summary>
internal void OnPreRender( Vector2 size )
{
Size = size;
ConfigureView( default );
}
/// <summary>
/// Configure the view immediately before rendering. This will set the camera position
/// etc on the local camera renderer. This should be called immediately before adding
/// the views etc.
/// </summary>
void ConfigureView( in ViewSetup config )
{
if ( _world.IsValid() && !Graphics.IsActive )
{
_world.UpdateObjectsForRendering( Position, ZFar );
}
}
void IManagedCamera.OnRenderStage( Rendering.Stage renderStage )
{
// legacy stuff isn't thread safe
if ( ThreadSafe.IsMainThread )
{
switch ( renderStage )
{
case Rendering.Stage.AfterPostProcess:
{
OnRenderOverlay?.Invoke();
break;
}
case Rendering.Stage.AfterUI:
{
OnRenderUI?.Invoke();
break;
}
}
}
// new stuff is commandlist based, so is total thread safe
OnRenderStageHook?.InvokeWithWarning( renderStage, this );
}
/// <summary>
/// Given a pixel rect return a frustum on the current camera.
/// </summary>
public Frustum GetFrustum( Rect pixelRect ) => GetFrustum( pixelRect, Size );
/// <summary>
/// Given a pixel rect return a frustum on the current camera. Pass in 1 to ScreenSize to use normalized screen coords.
/// </summary>
public Frustum GetFrustum( Rect pixelRect, Vector3 screenSize )
{
if ( Ortho )
{
var min = Vector2.Min( pixelRect.TopLeft, pixelRect.BottomRight );
var max = Vector2.Max( pixelRect.TopLeft, pixelRect.BottomRight );
return Sandbox.Frustum.FromOrtho( min, max, screenSize, Position, Rotation, OrthoHeight, ZNear, ZFar );
}
else
{
var c1 = pixelRect.TopLeft;
var c2 = pixelRect.BottomRight;
var tl = GetRay( new Vector2( MathF.Min( c1.x, c2.x ), MathF.Min( c1.y, c2.y ) ), screenSize );
var tr = GetRay( new Vector2( MathF.Max( c1.x, c2.x ), MathF.Min( c1.y, c2.y ) ), screenSize );
var bl = GetRay( new Vector2( MathF.Min( c1.x, c2.x ), MathF.Max( c1.y, c2.y ) ), screenSize );
var br = GetRay( new Vector2( MathF.Max( c1.x, c2.x ), MathF.Max( c1.y, c2.y ) ), screenSize );
return Sandbox.Frustum.FromCorners( tl, tr, br, bl, ZNear, ZFar );
}
}
/// <summary>
/// Given a cursor position get a scene aiming ray.
/// </summary>
public Ray GetRay( Vector3 cursorPosition ) => GetRay( cursorPosition, Size );
/// <summary>
/// Given a cursor position get a scene aiming ray.
/// </summary>
public Ray GetRay( Vector2 cursorPosition, Vector3 screenSize )
{
if ( !Ortho )
{
var aspect = screenSize.x / screenSize.y;
var posNormalized = new Vector2( (2.0f * cursorPosition.x / screenSize.x) - 1, (2.0f * cursorPosition.y / screenSize.y) - 1 ) * -1.0f;
float halfWidth = MathF.Tan( FieldOfView * MathF.PI / 360.0f );
float halfHeight = halfWidth / aspect;
var ray = new Vector3( 1.0f, posNormalized.x / (1.0f / halfWidth), posNormalized.y / (1.0f / halfHeight) ) * Rotation;
return new Ray( Position, ray.Normal );
}
else
{
var screenX = cursorPosition.x;
var screenY = cursorPosition.y;
var halfScreenHeight = OrthoHeight / 2f;
var halfScreenWidth = halfScreenHeight * screenSize.x / screenSize.y;
var orthoX = (2f * screenX / screenSize.x - 1f) * halfScreenWidth;
var orthoY = (1f - 2f * screenY / screenSize.y) * halfScreenHeight;
var forward = Rotation.Forward;
return new Ray
{
Position = Position + Rotation.Right * orthoX + Rotation.Up * orthoY + forward * ZNear,
Forward = forward
};
}
}
/// <summary>
/// Convert from world coords to screen coords. The results for x and y will be from 0 to <see cref="Size"/>.
/// </summary>
public Vector2 ToScreen( Vector3 world )
{
Frustum.WorldToView( world, out var result );
result.x = result.x.Remap( -1, 1, 0, Size.x );
result.y = result.y.Remap( -1, 1, Size.y, 0 );
return result;
}
/// <summary>
/// Convert from world coords to screen coords. The results for x and y will be from 0 to <see cref="Size"/>.
/// </summary>
public bool ToScreen( Vector3 world, out Vector2 screen )
{
var v = ToScreenWithDirection( world );
screen = new Vector2( v.x, v.y ) * Size;
return v.z > 0.0f;
}
/// <summary>
/// Projects a line in world space to screen coords, returning null if the line is
/// fully behind the camera.
/// </summary>
internal Line2D? ToScreen( Line worldLine )
{
var forward = Rotation.Forward;
// Clip line to be fully in front of near plane
if ( worldLine.Clip( new Plane( Position + forward * ZNear, forward ) ) is not { } clipped )
{
return null;
}
Frustum.WorldToView( clipped.Start, out var start );
Frustum.WorldToView( clipped.End, out var end );
return new Line2D( start, end )
.Remap(
new Rect( -1f, -1f, 2f, 2f ),
new Rect( 0f, Size.y, Size.x, -Size.y ) );
}
/// <summary>
/// Convert from world coords to normal screen corrds. The results will be between 0 and 1
/// </summary>
public Vector2 ToScreenNormal( Vector3 world )
{
Frustum.WorldToView( world, out var result );
result.x = result.x.Remap( -1, 1, 0, 1 );
result.y = result.y.Remap( -1, 1, 1, 0 );
return result;
}
/// <summary>
/// Convert from world coords to screen coords but the Z component stores whether this vector
/// is behind the screen (&lt;0) or in front of it (&gt;0). The X and Y components are normalized
/// from 0 to 1.
/// </summary>
/// <param name="world"></param>
/// <returns></returns>
internal Vector3 ToScreenWithDirection( Vector3 world )
{
var behind = Frustum.ScreenTransform( world, out var result );
if ( !Ortho )
{
result.x = (result.x + 1f) / 2f;
result.y = ((result.y * -1f) + 1f) / 2f;
result.z = behind ? -1f : 1f;
}
else
{
result.x = (result.x + 2f) / 2f;
result.y = ((result.y - 2f) / 2f) * -1f;
if ( result.x <= 0f || result.x >= 1f || result.y <= 0f || result.y >= 1f )
result.z = -1f;
else
result.z = 1f;
}
return result;
}
/// <summary>
/// Convert from screen coords to world coords on the near frustum plane.
/// </summary>
public Vector3 ToWorld( Vector2 screen )
{
screen.x = screen.x.Remap( 0, Size.x, -1, 1 );
screen.y = screen.y.Remap( Size.y, 0, -1, 1 );
Frustum.ViewToWorld( screen, out var result );
return result;
}
internal void GatherVolumetricFog( RenderAttributes attributes )
{
if ( !VolumetricFog.Enabled )
{
if ( VolumetricFogImpl != null )
{
MainThread.QueueDispose( VolumetricFogImpl );
VolumetricFogImpl = null;
}
return;
}
if ( VolumetricFogImpl == null )
{
VolumetricFogImpl = new();
}
VolumetricFogImpl.Update( VolumetricFog );
attributes.SetPointer( "IVolumetricFog", VolumetricFogImpl.native );
}
internal void GatherTonemapper( RenderAttributes attributes )
{
attributes.SetPointer( "ITonemapSystem", Tonemap.Enabled ? ToneMapping.GetNative() : IntPtr.Zero );
}
internal void AddToRenderList( SwapChainHandle_t swapChain, Vector2? size )
{
ViewSetup setup = default;
setup.ViewHash = HashCode.Combine( this );
if ( WantsToRenderInStereo() )
{
RenderStereo( setup );
}
else
{
Render( swapChain, size, setup );
}
}
private bool WantsToRenderInStereo()
{
return VRSystem.IsActive && TargetEye > StereoTargetEye.None;
}
private void Render( SwapChainHandle_t swapChain, Vector2? size, in ViewSetup config = default )
{
if ( swapChain.self == default )
return;
var renderSize = size ?? Size;
if ( renderSize.x <= 0 ) return;
if ( renderSize.y <= 0 ) return;
OnPreRender( renderSize );
var setup = new CameraRenderer( "RenderToSwapChain", _cameraId );
setup.Configure( this, config );
setup.Native.Render( swapChain );
}
internal void RenderToTexture( Texture texture, Vector2? size, in ViewSetup config )
{
if ( texture is null || texture.native.IsNull )
return;
var renderSize = size ?? texture.Size;
if ( renderSize.x <= 0 ) return;
if ( renderSize.y <= 0 ) return;
ConfigureView( in config );
var setup = new CameraRenderer( "RenderToTexture", _cameraId );
setup.Configure( this, config );
setup.Native.RenderToTexture( texture.native, Graphics.SceneView );
}
internal void RenderToBitmap( Bitmap bitmap, in ViewSetup config = default )
{
if ( !bitmap.IsValid() )
return;
var renderSize = bitmap.Size;
if ( renderSize.x <= 1 ) return;
if ( renderSize.y <= 1 ) return;
OnPreRender( renderSize );
var setup = new CameraRenderer( "RenderToBitmap", _cameraId );
setup.Configure( this, config );
unsafe
{
setup.Native.RenderToBitmap( (IntPtr)bitmap.GetPointer(), bitmap.Width, bitmap.Height, bitmap.BytesPerPixel );
}
}
/// <summary>
/// Renders the scene from the camera position to a cube texture, capturing all 6 directions.
/// </summary>
internal void RenderToCubeTexture( Texture texture, in ViewSetup config = default )
{
if ( texture is null || texture.native.IsNull )
return;
if ( texture.Depth != 6 ) throw new Exception( "Expected a texture with 6 depth slices for RenderToCubeTexture" );
var renderSize = texture.Size;
if ( renderSize.x <= 0 ) return;
if ( renderSize.y <= 0 ) return;
OnPreRender( renderSize );
//
// Adds the views to the scene system
//
var setup = new CameraRenderer( "RenderToCubeTexture", _cameraId );
setup.Configure( this, config );
for ( int i = 0; i < CubeRotations.Length; i++ )
{
// Override rotation for each face
setup.Native.CameraRotation = (Rotation * CubeRotations[i]).Angles();
setup.Native.RenderToCubeTexture( texture.native, i );
}
}
private void RenderStereo( in ViewSetup config = default )
{
if ( !TargetEye.Contains( StereoTargetEye.LeftEye ) && !TargetEye.Contains( StereoTargetEye.RightEye ) )
{
Log.Warning( $"Called {nameof( RenderStereo )} but neither eyes were present in {nameof( TargetEye )}?" );
return;
}
var setup = new CameraRenderer( "RenderStereo", _cameraId );
setup.Configure( this, config );
var n = setup.Native;
for ( int iEye = 0; iEye < 2; ++iEye )
{
bool isLeftEye = iEye == 0;
var stereoTargetEye = isLeftEye ? StereoTargetEye.LeftEye : StereoTargetEye.RightEye;
var eye = isLeftEye ? VREye.Left : VREye.Right;
// Check flags to see if we should render using this eye...
if ( !TargetEye.Contains( stereoTargetEye ) )
continue;
// PreRender
OnPreRender( VRNative.EyeRenderTargetSize );
// Save off clip planes, used for depth submit
VRNative.ClipPlanes.ZNear = ZNear;
VRNative.ClipPlanes.ZFar = ZFar;
// Grab overrides for this eye
var transform = VRNative.GetTransformForEye( n.CameraPosition, n.CameraRotation, eye );
n.CameraPosition = transform.Position;
n.CameraRotation = transform.Rotation;
// Save off middle eye position for things like skybox
n.MiddleEyePosition = Position;
n.MiddleEyeRotation = Rotation.Angles();
n.OverrideProjection = VRNative.GetProjectionMatrix( ZNear, ZFar, eye );
n.HasOverrideProjection = true;
n.FieldOfView = 0f; // Let clip bounds drive projection
n.ClipSpaceBounds = VRNative.GetClipForEye( eye );
// Render
var submitThisEye = WantsStereoSubmit && eye == VREye.Right;
n.RenderStereo( iEye, (int)VRNative.EyeRenderTargetSize.x, (int)VRNative.EyeRenderTargetSize.y, submitThisEye );
}
VRSystem.IsRendering = WantsStereoSubmit;
// Done here, clean up / reset overrides
n.HasOverrideProjection = false;
n.ClearSceneWorlds();
}
/// <summary>
/// Allows specifying a custom projection matrix for this camera
/// </summary>
public Matrix? CustomProjectionMatrix { get; set; }
}
/// <summary>
/// Flags for clearing a RT before rendering a scene using a SceneCamera
/// </summary>
[Flags, Expose]
public enum ClearFlags
{
None = 0x00,
[Icon( "palette" )]
[Description( "The color buffer (the stuff you can see)" )]
Color = 0xFF,
[Icon( "table_rows" )]
[Description( "The depth buffer" )]
Depth = 0x100,
[Icon( "interests" )]
[Description( "The stencil" )]
Stencil = 0x200,
All = Color | Depth | Stencil
}
[Expose]
public enum SceneCameraDebugMode
{
[Title( "Lit" ), Icon( "image" )]
Normal = 0,
[Title( "Full Bright" ), Icon( "lightbulb" )]
FullBright = 1,
[Title( "World-Space Normals" ), Icon( "shuffle" )]
NormalMap = 21,
[Title( "Albedo" ), Icon( "palette" )]
Albedo = 10,
[Title( "Roughness" ), Icon( "texture" )]
Roughness = 12,
[Title( "Diffuse" ), Icon( "cloud" )]
Diffuse = 2,
[Title( "Reflect" ), Icon( "flare" )]
Reflect = 3,
[Title( "Transmission" ), Icon( "deblur" )]
Transmission = 4,
[Title( "UV Maps" ), Icon( "gradient" )]
ShowUV = 6,
[Title( "Shader IDs" ), Icon( "sell" )]
ShaderIDColor = 16,
[Title( "Tiled Rendering Lights" ), Icon( "view_module" )]
TiledRenderingQuads = 50,
[Title( "Quad Overdraw" ), Icon( "signal_cellular_null" )]
QuadOverdraw = 100,
[Title( "Overdraw" ), Icon( "layers" )]
Overdraw = 101,
[Title( "Ambient Occlusion" ), Icon( "radio_button_checked" )]
AmbientOcclusion = 14,
}