using Sandbox.Rendering; using System.Drawing; using System.Text.Json.Serialization; namespace Sandbox; /// /// Every scene should have at least one Camera. /// [Expose] [Title( "Camera" )] [Category( "Camera" )] [Icon( "videocam" )] [EditorHandle( "materials/gizmo/camera.png" )] public sealed partial class CameraComponent : Component, Component.ExecuteInEditor, Component.ISceneStage { SceneCamera sceneCamera; /// /// The clear flags for this camera. /// [Property] public ClearFlags ClearFlags { get; set; } = ClearFlags.Color | ClearFlags.Stencil | ClearFlags.Depth; /// /// The background color of this camera's view if there's no 2D Sky in the scene. /// [Property] public Color BackgroundColor { get; set; } = "#557685"; bool _isMainCamera = true; int _priority = 1; /// /// Returns true if this is the main game camera. /// [Property] public bool IsMainCamera { get => _isMainCamera; set { if ( _isMainCamera == value ) return; _isMainCamera = value; Scene.UpdateMainCamera(); } } /// /// The axis to use for the field of view. /// [Property, Title( "FOV Axis" ), HideIf( nameof( Orthographic ), true )] public Axis FovAxis { get; set; } = Axis.Horizontal; /// /// The field of view of this camera. /// [Property, Range( 1, 179 ), HideIf( nameof( Orthographic ), true )] public float FieldOfView { get; set; } = 60; /// /// The camera's near clip plane 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. /// [Property, Range( 1, 1000 ), Step( 1 )] public float ZNear { get; set; } = 10; /// /// The camera's far clip plane 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. /// [Property, Range( 1, 100000 ), Step( 1 )] public float ZFar { get; set; } = 10000; /// /// The priority of this camera. Dictates which camera gets rendered on top of another. Higher means it'll be rendered on top. /// [Property, Range( 1, 16 )] public int Priority { get => _priority; set { if ( _priority == value ) return; _priority = value; Scene.UpdateMainCamera(); } } /// /// Whether or not to use orthographic projection instead of perspective. /// [Property] public bool Orthographic { get; set; } /// /// The orthographic size for this camera while is set to true. /// [Property] public float OrthographicHeight { get; set; } = 1204; /// /// The HMD eye that this camera is targeting. /// Use for the user's monitor (i.e. the companion window). /// [Property] public StereoTargetEye TargetEye { get; set; } = StereoTargetEye.None; /// /// A list of tags that will be checked to include specific game objects when rendering this camera. /// If none are set, it will include everything. /// [Property] public TagSet RenderTags { get; set; } = new(); /// /// A list of tags that will be checked to exclude specific game objects when rendering this camera. /// [Property] public TagSet RenderExcludeTags { get; set; } = new(); /// /// The size of the camera represented on the screen. Normalized between 0 and 1. /// [Property, Range( 0, 1, slider: false )] public Vector4 Viewport { get; set; } = new( 0.0f, 0.0f, 1.0f, 1.0f ); // todo this should use a rect /// /// If specified, this camera will render to this RenderTexture instead of the screen. /// This can then be used in other stuff like materials. /// [Property] public RenderTextureAsset RenderTexture; /// /// Since we are rendering to a texture, better to generate mipmaps for it. /// private CommandList _renderTextureMipGenCommandList { get { var cmd = new CommandList( "Generate MipMaps For RenderTexture" ); if ( RenderTexture != null && RenderTexture.Texture != null && RenderTexture.Texture.Mips > 1 ) cmd.GenerateMipMaps( RenderTexture.Texture ); return cmd; } } private Texture _renderTarget; /// /// The texture to draw this camera to. /// Requires /// [JsonIgnore, Hide] public Texture RenderTarget { get => RenderTexture?.Texture ?? _renderTarget; set { if ( _renderTarget == value ) return; if ( value is not null ) { if ( !value.Desc.m_nFlags.HasFlag( NativeEngine.RuntimeTextureSpecificationFlags.TSPEC_RENDER_TARGET ) ) throw new Exception( $"{nameof( RenderTarget )} texture needs to be a render target" ); } _renderTarget = value; } } /// /// Render this camera using a different render mode /// [JsonIgnore, Hide] public SceneCameraDebugMode DebugMode { get; set; } /// /// Render this camera using a wireframe view. /// [JsonIgnore, Hide] public bool WireframeMode { get; set; } /// /// Accessor for getting this Camera Component's SceneCamera /// internal SceneCamera SceneCamera { get => sceneCamera; } public override void Reset() { base.Reset(); RenderTags = new(); RenderExcludeTags = new(); Viewport = new( 0.0f, 0.0f, 1.0f, 1.0f ); ClearFlags = ClearFlags.Color | ClearFlags.Stencil | ClearFlags.Depth; } private void EnsureSceneCameraCreated() { if ( sceneCamera is not null ) return; // scene should be scoped in! Assert.NotNull( Scene ); if ( Game.IsPlaying ) { // Only assert active scene in play mode, since we want to create cameras anywhere in the editor Assert.AreEqual( Game.ActiveScene, Scene, "Camera scene is not active scene" ); } // Use the GameObject name so we can identify the camera when debugging sceneCamera = new( GameObject.Name ); sceneCamera.OnRenderStageHook = ExecuteCommandLists; } protected override void OnAwake() { EnsureSceneCameraCreated(); Scene.Cameras.Add( this ); } protected override void OnDestroy() { Scene.Cameras.Remove( this ); sceneCamera?.Dispose(); sceneCamera = null; } protected override void DrawGizmos() { if ( sceneCamera is null ) return; using var scope = Gizmo.Scope( $"{GetHashCode()}" ); Gizmo.Transform = global::Transform.Zero; UpdateSceneCameraTransform( sceneCamera ); var frustum = sceneCamera.GetFrustum( new Rect( 0f, 0f, 1920f, 1080f ), new Vector2( 1920, 1080 ) ); Gizmo.Draw.Color = Color.White.WithAlpha( 0.4f ); Gizmo.Draw.LineFrustum( frustum ); } internal void UpdateSceneCameraTransform( SceneCamera camera ) { camera.Position = WorldPosition; camera.Rotation = WorldRotation; camera.ZNear = ZNear; camera.ZFar = ZFar; camera.Rect = new Rect( Viewport.x, Viewport.y, Viewport.z, Viewport.w ); camera.Size = ScreenRect.Size; camera.Ortho = Orthographic; camera.OrthoHeight = OrthographicHeight; if ( FovAxis == Axis.Vertical ) camera.FieldOfView = Screen.CreateVerticalFieldOfView( FieldOfView ); else camera.FieldOfView = FieldOfView; } internal void UpdateSceneCameraStereo( SceneCamera camera ) { camera.TargetEye = TargetEye; camera.WantsStereoSubmit = !Scene.IsEditor; } /// /// Update a SceneCamera with the settings from this component /// public void UpdateSceneCamera( SceneCamera camera, bool includeTags = true ) { if ( Scene is null ) { Log.Warning( $"Trying to update camera from {this} but has no scene" ); return; } UpdateSceneCameraTransform( camera ); camera.World = Scene.SceneWorld; camera.ClearFlags = ClearFlags; camera.Worlds.Clear(); camera.Worlds.Add( Scene.DebugSceneWorld ); // Also render any active gizmos if ( Gizmo.Active is not null ) camera.Worlds.Add( Gizmo.Active.World ); CopyPostProcessing( camera ); // A camera (such as the editor viewport) might not want to take render tags into account // from an existing camera source if ( includeTags ) { camera.RenderTags.SetFrom( RenderTags ); camera.ExcludeTags.SetFrom( RenderExcludeTags ); } // Merge our global Scene attributes into the camera Scene.RenderAttributes.MergeTo( camera.Attributes ); camera.DebugMode = DebugMode; camera.WireframeMode = WireframeMode; camera.EnablePostProcessing = EnablePostProcessing; camera.BackgroundColor = ClearFlags.Contains( ClearFlags.Color ) ? BackgroundColor : Color.Transparent; } internal void CopyPostProcessing( SceneCamera camera ) { if ( Scene is null ) { Log.Warning( $"Trying to update camera from {this} but has no scene" ); return; } // defaults - let components override camera.CubemapFog.Enabled = false; camera.Bloom.Enabled = false; AutoExposure.Apply( camera ); // Don't set the background colour if we don't clear color // Also don't hook into render overlays, nor volumetric fog stuff. if ( ClearFlags.Contains( ClearFlags.Color ) ) { camera.VolumetricFog.Enabled = Scene.GetAllComponents().Count() > 0; camera.VolumetricFog.DrawDistance = 4096; camera.VolumetricFog.FadeInStart = 64; camera.VolumetricFog.FadeInEnd = 256; camera.VolumetricFog.IndirectStrength = 1.0f; camera.VolumetricFog.Anisotropy = 1; camera.VolumetricFog.Scattering = 1.0f; camera.VolumetricFog.BakedIndirectTexture = Scene.GetAllComponents().FirstOrDefault()?.BakedFogTexture; } #pragma warning disable CS0612 GameObject.RunEvent( x => x.SetupCamera( this, camera ) ); #pragma warning restore CS0612 // // Child camera executes command lists from this camera // camera.OnRenderStageHook = ExecuteCommandLists; // // Hack because I don't want this to have to be on a camera. This // is hidden from users, so we'll figure out how to square it later // foreach ( var cubemapFog in Scene.GetAllComponents() ) { if ( cubemapFog.Tags.HasAny( RenderExcludeTags ) ) continue; cubemapFog.SetupCamera( this, camera ); } } /// /// Update the SceneCamera UI with the settings from this component /// private void UpdateSceneCameraUI( SceneCamera camera ) { if ( Scene is null ) return; camera.OnRenderUI = () => OnCameraRenderUI( camera ); } [Obsolete( "Use CommandList" )] public IDisposable AddHookAfterOpaque( string debugName, int order, Action renderEffect ) => null; /// /// Obsolete 09/06/2025 /// [Obsolete( "Use CommandList" )] public IDisposable AddHookAfterTransparent( string debugName, int order, Action renderEffect ) => null; /// /// Obsolete 09/06/2025 /// [Obsolete( "Use CommandList" )] public IDisposable AddHookBeforeOverlay( string debugName, int order, Action renderEffect ) => null; /// /// Obsolete 02/10/2025 /// [Obsolete( "Use CommandList" )] public IDisposable AddHookAfterUI( string debugName, int order, Action renderEffect ) => null; private void OnCameraRenderUI( SceneCamera camera ) { if ( Scene is null ) return; foreach ( var c in Scene.GetAll().OrderBy( x => x.ZIndex ) ) { if ( !c.Active ) continue; var target = c.TargetCamera ?? (IsMainCamera ? this : null); if ( target != this ) continue; if ( RenderExcludeTags.HasAny( c.GameObject.Tags ) ) continue; c.Render(); } } /// /// Obsolete 02/10/2025 /// [Obsolete] public interface ISceneCameraSetup { void SetupCamera( CameraComponent camera, SceneCamera sceneCamera ); } internal bool IsSceneEditorCamera; internal void InitializeRendering() { using ( Scene.Push() ) { EnsureSceneCameraCreated(); if ( IsSceneEditorCamera ) { UpdateSceneCamera( sceneCamera ); Scene.Camera?.CopyPostProcessing( sceneCamera ); } else { UpdateSceneCamera( sceneCamera ); } UpdateSceneCameraUI( sceneCamera ); UpdateSceneCameraStereo( sceneCamera ); } } /// /// This should only be called when creating render lists!! /// internal void AddToRenderList( SwapChainHandle_t swapChain, Vector2? size ) { if ( !Active ) return; // if width and height are too small, skip it if ( Viewport.z <= 0 ) return; if ( Viewport.w <= 0 ) return; using ( Scene.Push() ) { InitializeRendering(); if ( RenderTarget is not null && RenderTarget.native.IsValid ) { SceneCamera.RenderToTexture( RenderTarget, size ?? CustomSize, default ); } else { SceneCamera.AddToRenderList( swapChain, size ?? CustomSize ); } } } public Vector2 PointToScreenNormal( in Vector3 worldPosition ) { EnsureSceneCameraCreated(); UpdateSceneCameraTransform( sceneCamera ); return sceneCamera.ToScreenNormal( worldPosition ); } public Vector2 PointToScreenPixels( in Vector3 worldPosition ) { var sr = ScreenRect; var v = PointToScreenNormal( worldPosition ); return new Vector2( v.x, v.y ) * sr.Size; } /// /// The size of the viewport, in screen coordinates /// public Rect ScreenRect { get { var ss = CustomSize ?? Screen.Size; return new Rect( ss.x * Viewport.x, ss.y * Viewport.y, ss.x * Viewport.z, ss.y * Viewport.w ); } } /// /// Given a BBox in world space, will return the screen space rect that totally contains the box. /// public Rect BBoxToScreenPixels( BBox bounds, out bool isBehind ) { Vector2[] corners = new Vector2[8]; Vector3 min = bounds.Mins; Vector3 max = bounds.Maxs; isBehind = true; corners[0] = PointToScreenPixels( new Vector3( min.x, min.y, min.z ) ); corners[1] = PointToScreenPixels( new Vector3( max.x, min.y, min.z ) ); corners[2] = PointToScreenPixels( new Vector3( min.x, max.y, min.z ) ); corners[3] = PointToScreenPixels( new Vector3( min.x, min.y, max.z ) ); corners[4] = PointToScreenPixels( new Vector3( max.x, max.y, min.z ) ); corners[5] = PointToScreenPixels( new Vector3( min.x, max.y, max.z ) ); corners[6] = PointToScreenPixels( new Vector3( max.x, min.y, max.z ) ); corners[7] = PointToScreenPixels( new Vector3( max.x, max.y, max.z ) ); Vector2 minScreen = corners[0]; Vector2 maxScreen = corners[0]; for ( int i = 1; i < 8; i++ ) { minScreen = Vector2.Min( minScreen, corners[i] ); maxScreen = Vector2.Max( maxScreen, corners[i] ); } var ss = CustomSize ?? Screen.Size; // off screen if ( maxScreen.x < 0 ) isBehind = true; if ( maxScreen.y < 0 ) isBehind = true; if ( minScreen.x > ss.x ) isBehind = true; if ( minScreen.y > ss.y ) isBehind = true; return Rect.FromPoints( minScreen, maxScreen ); } public Vector2 PointToScreenPixels( Vector3 worldPosition, out bool isBehind ) { EnsureSceneCameraCreated(); UpdateSceneCameraTransform( sceneCamera ); var sr = ScreenRect; var v = sceneCamera.ToScreenWithDirection( worldPosition ); isBehind = v.z <= 0.0f; return new Vector2( v.x, v.y ) * sr.Size; } public Vector2 PointToScreenNormal( Vector3 worldPosition, out bool isBehind ) { EnsureSceneCameraCreated(); UpdateSceneCameraTransform( sceneCamera ); var v = sceneCamera.ToScreenWithDirection( worldPosition ); isBehind = v.z <= 0.0f; return new Vector2( v.x, v.y ); } public Ray ScreenPixelToRay( Vector2 pixelPosition ) { EnsureSceneCameraCreated(); UpdateSceneCameraTransform( sceneCamera ); return sceneCamera.GetRay( pixelPosition, ScreenRect.Size ); } public Ray ScreenNormalToRay( Vector3 normalPosition ) { var pixelPosition = new Vector3( normalPosition.x * ScreenRect.Size.x, normalPosition.y * ScreenRect.Size.y, normalPosition.z ); return ScreenPixelToRay( pixelPosition ); } /// /// Convert from screen coords to world coords on the near frustum plane. /// public Vector3 ScreenToWorld( Vector2 screen ) { EnsureSceneCameraCreated(); UpdateSceneCameraTransform( sceneCamera ); return sceneCamera.ToWorld( screen ); } protected override void OnEnabled() { Scene?.UpdateMainCamera(); AddCommandList( _hudCommandList, Rendering.Stage.AfterPostProcess, 5000 ); AddCommandList( _overlayCommandList, Rendering.Stage.AfterUI, 5000 ); if ( RenderTexture is not null ) AddCommandList( _renderTextureMipGenCommandList, Rendering.Stage.AfterPostProcess, 10000 ); } protected override void OnDisabled() { Scene?.UpdateMainCamera(); RemoveCommandList( _hudCommandList ); RemoveCommandList( _overlayCommandList ); RemoveCommandList( _renderTextureMipGenCommandList ); } /// /// Returns the view frustum of the current screen rect. /// public Frustum GetFrustum() { UpdateSceneCameraTransform( sceneCamera ); return sceneCamera.GetFrustum( ScreenRect ); } /// /// Given a pixel rect return a frustum on the current camera. /// public Frustum GetFrustum( Rect screenRect ) { // Note: Updating every time might cause issues if too much pressure is put on this function. Keep an eye on it. UpdateSceneCameraTransform( sceneCamera ); return sceneCamera.GetFrustum( screenRect ); } /// /// Given a pixel rect return a frustum on the current camera. Pass in 1 to ScreenSize to use normalized screen coords. /// public Frustum GetFrustum( Rect screenRect, Vector3 screenSize ) { // Same as above UpdateSceneCameraTransform( sceneCamera ); return sceneCamera.GetFrustum( screenRect, screenSize ); } /// /// Render scene to a texture from this camera's point of view /// public bool RenderToTexture( Texture target, in ViewSetup config = default ) { if ( target is null || target.native.IsNull ) return false; if ( !Graphics.IsActive ) { Scene.PreCameraRender(); InitializeRendering(); } using var setup = new CameraRenderer( $"{GameObject.Name}.RenderToTexture", sceneCamera._cameraId ); lock ( this ) { setup.Configure( sceneCamera, config ); // // Adds the views to the scene system // setup.Native.RenderToTexture( target.native, Graphics.SceneView ); setup.Native.ClearSceneWorlds(); } return true; } /// /// Allows specifying a custom projection matrix for this camera /// public Matrix? CustomProjectionMatrix { get => SceneCamera.CustomProjectionMatrix; set => SceneCamera.CustomProjectionMatrix = value; } /// /// Allows specifying a custom aspect ratio for this camera. /// By default the camera size is screen size or render target size. /// [JsonIgnore, Hide] public Vector2? CustomSize { get; set; } /// /// Get frustum projection matrix. /// public Matrix ProjectionMatrix => sceneCamera.ProjectionMatrix; /// /// Calculates a projection matrix with an oblique clip-plane defined in world space. /// public Matrix CalculateObliqueMatrix( Plane clipPlane ) { var tx = WorldTransform; var normal = tx.Rotation.Inverse * clipPlane.Normal; normal = new Vector3( normal.y, -normal.z, normal.x ).Normal; System.Numerics.Matrix4x4 m = ProjectionMatrix; Vector4 q = default; q.x = (MathF.Sign( normal.x ) - m.M13) / m.M11; q.y = (MathF.Sign( normal.y ) - m.M23) / m.M22; q.z = 1f; q.w = (1f - m.M33) / m.M34; var plane = new Vector4( normal, Vector3.Dot( tx.Position - clipPlane.Position, clipPlane.Normal ) ); var c = plane * (1.0f / System.Numerics.Vector4.Dot( plane, q )); m.M31 = -c.x; m.M32 = -c.y; m.M33 = -c.z; m.M34 = c.w; return m; } public enum Axis { /// /// Fits the view within the x-axis. /// [Icon( "panorama_horizontal" )] Horizontal, /// /// Fits the view within the y-axis. /// [Icon( "panorama_vertical" )] Vertical } /// /// Render this camera to the target bitmap. /// public void RenderToBitmap( Bitmap targetBitmap ) { if ( targetBitmap == null || targetBitmap.Width <= 1 || targetBitmap.Height <= 1 ) return; if ( !this.IsValid() ) return; using ( Scene.Push() ) { Scene.PreCameraRender(); InitializeRendering(); SceneCamera.OnPreRender( targetBitmap.Size ); SceneCamera.RenderToBitmap( targetBitmap ); } } }