using Sandbox.Rendering; using Sandbox.VR; namespace Sandbox; /// /// Represents a camera and holds render hooks. This camera can be used to draw tool windows and scene panels. /// [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; /// /// This is a c++ object with a ton of useful shit. /// Don't access it directly because it might be dirty. /// CFrustum _frustum; internal Matrix ProjectionMatrix => Frustum.GetProj(); public RenderAttributes Attributes { get; } /// /// The name of this camera.. for debugging purposes. /// public string Name { get; set; } /// /// Scene objects with any of these tags won't be rendered by this camera. /// public ITagSet ExcludeTags { get; private set; } = new TokenBasedTagSet(); /// /// Only scene objects with one of these tags will be rendered by this camera. /// public ITagSet RenderTags { get; private set; } = new TokenBasedTagSet(); /// /// Keep hidden! CommandBuffers only!! /// internal Action OnRenderStageHook; /// /// Called when rendering the post process pass /// [Obsolete] public Action OnRenderPostProcess { get; set; } /// /// Called when rendering the transparent pass /// [Obsolete] public Action OnRenderOpaque { get; set; } /// /// Called when rendering the transparent pass /// [Obsolete] public Action OnRenderTransparent { get; set; } public Action OnRenderOverlay { get; set; } public Action OnRenderUI { get; set; } /// /// The size of the screen. Allows us to work out aspect ratio. /// For now will get updated automatically on render. /// public Vector2 Size { get => _size; set { if ( _size == value ) return; _size = value; FrustumDirty = true; } } Vector2 _size = new Vector2( 512, 512 ); /// /// Control volumetric fog parameters, expect this to take 1-2ms of your GPU frame time. /// public VolumetricFogParameters VolumetricFog { get; init; } = new(); /// /// Control fog based on an image. /// public CubemapFogController CubemapFog { get; init; } = new(); /// /// Define the rotations for each of the 6 cube faces (right, left, up, down, front, back) /// 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 _worlds = new HashSet(); SceneWorld _world; /// /// The world we're going to render. /// public SceneWorld World //PaintDay: MainWorld? { get => _world; set { if ( _world == value ) return; _worlds?.Remove( _world ); _world = value; if ( _world.IsValid() ) { _worlds?.Add( _world ); } } } /// /// Your camera can render multiple worlds. /// public HashSet Worlds => _worlds; Transform _transform = new Transform(); /// /// The position of the scene's camera. /// public Vector3 Position { get => _transform.Position; set { _transform.Position = value; FrustumDirty = true; } } /// /// The rotation of the scene's camera. /// public Rotation Rotation { get => _transform.Rotation; set { _transform.Rotation = value; FrustumDirty = true; } } /// /// The rotation of the scene's camera. /// public Angles Angles { get => _transform.Rotation; set { _transform.Rotation = value; FrustumDirty = true; } } float _fov = 90; /// /// The horizontal field of view of the Camera in degrees. /// public float FieldOfView { get => _fov; set { _fov = value; FrustumDirty = true; } } float _zfar = 100000; /// /// 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. /// public float ZFar { get => _zfar; set { _zfar = value; FrustumDirty = true; } } float _znear = 1; /// /// 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. /// public float ZNear { get => _znear; set { _znear = value; FrustumDirty = true; } } bool _ortho; /// /// Whether to use orthographic projection. /// public bool Ortho { get => _ortho; set { _ortho = value; FrustumDirty = true; } } /// /// Height of the ortho when is enabled. /// public float OrthoHeight { get => Attributes.GetFloat( "cam.orthoHeight" ); set { Attributes.Set( "cam.orthoHeight", value ); FrustumDirty = true; } } /// /// Render this camera using a different render mode /// public SceneCameraDebugMode DebugMode { get => (SceneCameraDebugMode)Attributes.GetInt( "ToolsVisMode" ); set => Attributes.Set( "ToolsVisMode", (int)value ); } /// /// Render this camera using a wireframe view. /// public bool WireframeMode { get => Attributes.GetInt( "Wireframe" ) > 1; set => Attributes.Set( "Wireframe", value ? 1 : 0 ); } /// /// What kind of clearing should we do before we begin? /// public ClearFlags ClearFlags { get; set; } = ClearFlags.All; private Rect rect = new Rect( Vector2.Zero, Vector2.One ); /// /// The rect of the screen to render to. This is normalized, between 0 and 1. /// 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; } } /// /// Color the scene camera clears the render target to. /// public Color BackgroundColor { get; set; } /// /// The color of the ambient light. Set it to black for no ambient light, alpha is used for lerping between IBL and constant color. /// public Color AmbientLightColor { get; set; } = Color.Transparent; /// /// Enable or disable anti-aliasing for this render. /// public bool AntiAliasing { get => Attributes.GetInt( "msaa", 4 ) == 4; set => Attributes.Set( "msaa", value ? 4 : 0 ); // 4 = RENDER_MULTISAMPLE_8X } /// /// Toggle all post processing effects for this camera. The default is on. /// public bool EnablePostProcessing { get; set; } = true; /// /// Should this camera render engine overlays, you'd only want this on the main camera. /// internal bool EnableEngineOverlays { get; set; } = false; /// /// The HMD eye that this camera is targeting. /// Use for the user's monitor (i.e. the companion window). /// public StereoTargetEye TargetEye { get; set; } = StereoTargetEye.None; /// /// 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 is . /// public bool WantsStereoSubmit { get; set; } = false; /// /// Enable or disable direct lighting /// public bool EnableDirectLighting { get => Attributes.GetBool( "directLighting" ); set => Attributes.Set( "directLighting", value ); } /// /// Enable or disable indirect lighting /// public bool EnableIndirectLighting { get => Attributes.GetBool( "indirectLighting" ); set => Attributes.Set( "indirectLighting", value ); } /// /// Should be called before a render /// internal void OnPreRender( Vector2 size ) { Size = size; ConfigureView( default ); } /// /// 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. /// 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 ); } /// /// Given a pixel rect return a frustum on the current camera. /// public Frustum GetFrustum( Rect pixelRect ) => GetFrustum( pixelRect, Size ); /// /// 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 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 ); } } /// /// Given a cursor position get a scene aiming ray. /// public Ray GetRay( Vector3 cursorPosition ) => GetRay( cursorPosition, Size ); /// /// Given a cursor position get a scene aiming ray. /// 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 }; } } /// /// Convert from world coords to screen coords. The results for x and y will be from 0 to . /// 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; } /// /// Convert from world coords to screen coords. The results for x and y will be from 0 to . /// 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; } /// /// Projects a line in world space to screen coords, returning null if the line is /// fully behind the camera. /// 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 ) ); } /// /// Convert from world coords to normal screen corrds. The results will be between 0 and 1 /// 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; } /// /// Convert from world coords to screen coords but the Z component stores whether this vector /// is behind the screen (<0) or in front of it (>0). The X and Y components are normalized /// from 0 to 1. /// /// /// 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; } /// /// Convert from screen coords to world coords on the near frustum plane. /// 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 ); } } /// /// Renders the scene from the camera position to a cube texture, capturing all 6 directions. /// 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(); } /// /// Allows specifying a custom projection matrix for this camera /// public Matrix? CustomProjectionMatrix { get; set; } } /// /// Flags for clearing a RT before rendering a scene using a SceneCamera /// [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, }