mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 16:28:36 -04:00
https://sbox.game/dev/doc/gameplay/inventory-weapons/ Added InventoryComponent - a slot based inventory, hotbar or shared bucket slots, with a starting loadout BaseInventoryItem - base class for inventory items, with hooks to veto switching, holstering and dropping BaseWeapon - a GMod style base weapon - fire modes, magazines and reserve ammo, reloading, dry fire, ballistics, spread, crosshair and HUD BaseWeaponModel - view/world model base with muzzle flash, brass ejection, tracers and animation hooks Ammo types are resources (.ammo) - weapons share reserve pools by type, clamped to a max reserve BaseWeapon.ShootBullets / ShootBullet - bullet tracing with damage, impulse and hitbox tags Weapon hold types editor Animgraph enum parameters can be set by option name - Renderer.Set( "holdtype", "pistol" ) NpcUsage on BaseWeapon - engagement range and burst behaviour so NPCs can use any weapon ICameraModifier - cameras compose their view through an ordered modifier chain, read the result from CameraComponent.View CameraEffectSystem - screen shake, directional punches and tilts, with world epicenter falloff - camera.AddShake / AddPunch / AddTilt EnvShake component - shakes the view, rumbles controllers, kicks physics TracerEffect - an animated tracer line, ITargetedEffect for effects that fly at a target SceneAnchor - a world position that can follow an object or bone SkinnedModelRenderer.GetClosestBone( worldPosition ) Vector3.WithAimCone can take a custom Random for reproducible spread Improve/Fix PlayerController searches parents for IPressable when pressing PlayerController camera runs through the modifier chain - IEvents.PostCameraSetup is obsolete SoundPlayer preview uses shortcuts - Space toggles playback, Ctrl+Space restarts Fixed Radius( 0 ) traces missing everything Fixed NRE in AssetPreviewWidget Removed Removed CameraComponent.ISceneCameraSetup (obsolete since October, nothing uses)
897 lines
25 KiB
C#
897 lines
25 KiB
C#
|
|
using Sandbox.Rendering;
|
|
using Sandbox.UI;
|
|
using System.Drawing;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace Sandbox;
|
|
|
|
/// <summary>
|
|
/// Every scene should have at least one Camera.
|
|
/// </summary>
|
|
[Expose]
|
|
[Title( "Camera" )]
|
|
[Category( "Rendering" )]
|
|
[Icon( "videocam" )]
|
|
[EditorHandle( "materials/gizmo/camera.png" )]
|
|
public sealed partial class CameraComponent : Component, Component.ExecuteInEditor, Component.ISceneStage
|
|
{
|
|
SceneCamera sceneCamera;
|
|
|
|
/// <summary>
|
|
/// The clear flags for this camera.
|
|
/// </summary>
|
|
[Property]
|
|
public ClearFlags ClearFlags { get; set; } = ClearFlags.Color | ClearFlags.Stencil | ClearFlags.Depth;
|
|
|
|
/// <summary>
|
|
/// The background color of this camera's view if there's no 2D Sky in the scene.
|
|
/// </summary>
|
|
[Property]
|
|
public Color BackgroundColor { get; set; } = "#557685";
|
|
|
|
bool _isMainCamera = true;
|
|
int _priority = 1;
|
|
|
|
/// <summary>
|
|
/// Returns true if this is the main game camera.
|
|
/// </summary>
|
|
[Property]
|
|
public bool IsMainCamera
|
|
{
|
|
get => _isMainCamera;
|
|
set
|
|
{
|
|
if ( _isMainCamera == value )
|
|
return;
|
|
|
|
_isMainCamera = value;
|
|
Scene.UpdateMainCamera();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The axis to use for the field of view.
|
|
/// </summary>
|
|
[Property, Title( "FOV Axis" ), HideIf( nameof( Orthographic ), true )]
|
|
public Axis FovAxis { get; set; } = Axis.Horizontal;
|
|
|
|
/// <summary>
|
|
/// The field of view of this camera.
|
|
/// </summary>
|
|
[Property, Range( 1, 179 ), HideIf( nameof( Orthographic ), true )]
|
|
public float FieldOfView { get; set; } = 60;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[Property, Range( 1, 1000 ), Step( 1 )]
|
|
public float ZNear { get; set; } = 10;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[Property, Range( 1, 100000 ), Step( 1 )]
|
|
public float ZFar { get; set; } = 10000;
|
|
|
|
/// <summary>
|
|
/// The priority of this camera. Dictates which camera gets rendered on top of another. Higher means it'll be rendered on top.
|
|
/// </summary>
|
|
[Property, Range( 1, 16 )]
|
|
public int Priority
|
|
{
|
|
get => _priority;
|
|
set
|
|
{
|
|
if ( _priority == value ) return;
|
|
|
|
_priority = value;
|
|
Scene.UpdateMainCamera();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether or not to use orthographic projection instead of perspective.
|
|
/// </summary>
|
|
[Property]
|
|
public bool Orthographic { get; set; }
|
|
|
|
/// <summary>
|
|
/// The orthographic size for this camera while <see cref="Orthographic"/> is set to true.
|
|
/// </summary>
|
|
[Property]
|
|
public float OrthographicHeight { get; set; } = 1204;
|
|
|
|
/// <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>
|
|
[Property]
|
|
public StereoTargetEye TargetEye { get; set; } = StereoTargetEye.None;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[Property]
|
|
public TagSet RenderTags { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// A list of tags that will be checked to exclude specific game objects when rendering this camera.
|
|
/// </summary>
|
|
[Property]
|
|
public TagSet RenderExcludeTags { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// The size of the camera represented on the screen. Normalized between 0 and 1.
|
|
/// </summary>
|
|
[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
|
|
|
|
/// <summary>
|
|
/// If specified, this camera will render to this RenderTexture instead of the screen.
|
|
/// This can then be used in other stuff like materials.
|
|
/// </summary>
|
|
[Property]
|
|
public RenderTextureAsset RenderTexture;
|
|
|
|
/// <summary>
|
|
/// Since we are rendering to a texture, better to generate mipmaps for it.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// The texture to draw this camera to.
|
|
/// Requires <see cref="Texture.CreateRenderTarget()"/>
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Render this camera using a different render mode
|
|
/// </summary>
|
|
[JsonIgnore, Hide]
|
|
public SceneCameraDebugMode DebugMode { get; set; }
|
|
|
|
/// <summary>
|
|
/// Render this camera using a wireframe view.
|
|
/// </summary>
|
|
[JsonIgnore, Hide]
|
|
public bool WireframeMode { get; set; }
|
|
|
|
/// <summary>
|
|
/// Accessor for getting this Camera Component's SceneCamera
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// The view this camera composed this frame - the base transform reshaped by every
|
|
/// <see cref="ICameraModifier"/>, before transient render effects. Query
|
|
/// <see cref="CameraView.FieldOfView"/> from here for zoom-aware logic like scaling look
|
|
/// sensitivity while scoped. Final from the camera stage of the tick (after Update, before
|
|
/// PreRender) - reading it during Update gives the previous frame's composition.
|
|
/// </summary>
|
|
public CameraView View { get; private set; }
|
|
|
|
bool _composed;
|
|
CameraView _composedBase;
|
|
CameraView _renderView;
|
|
|
|
/// <summary>
|
|
/// Compose this camera's view - the base transform reshaped by every <see cref="ICameraModifier"/>
|
|
/// (published on <see cref="View"/>), then transient effects baked on top for the render. The
|
|
/// scene composes every camera once per tick, after Update and bone merging and before PreRender,
|
|
/// so anything in PreRender positions against a settled camera. Call it yourself only when you're rendering
|
|
/// without ticking the scene and want the composition - otherwise a camera moved after composing
|
|
/// just renders from its raw transform.
|
|
/// </summary>
|
|
public void ComposeView()
|
|
{
|
|
ComposeView( Scene.IsEditor ? null : GatherModifiers( Scene ) );
|
|
}
|
|
|
|
// The scene-wide modifier set, sorted - gathered once per tick and shared by every camera
|
|
// composing that tick (see Scene.UpdateCameraViews). Null when modifiers don't apply (editor).
|
|
internal static ICameraModifier[] GatherModifiers( Scene scene )
|
|
=> scene.GetAll<ICameraModifier>().OrderBy( x => x.CameraOrder ).ToArray();
|
|
|
|
internal void ComposeView( ICameraModifier[] modifiers )
|
|
{
|
|
var view = RawView;
|
|
|
|
// The modifier chain reshapes the view in order (scope zoom, vehicle roll), then gets the
|
|
// final view to place things against (view models).
|
|
if ( modifiers is not null )
|
|
{
|
|
foreach ( var modifier in modifiers )
|
|
{
|
|
modifier.ModifyCamera( this, ref view );
|
|
}
|
|
|
|
View = view;
|
|
|
|
foreach ( var modifier in modifiers )
|
|
{
|
|
modifier.PostCameraSetup( this, view );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
View = view;
|
|
}
|
|
|
|
_renderView = view;
|
|
|
|
// Transient camera effects (screen shake, punches) bake into the render view only - the
|
|
// composed view and component transform are never touched, so they can't accumulate or move
|
|
// the player's aim.
|
|
if ( CameraEffectSystem.Get( Scene ) is { } effects )
|
|
{
|
|
effects.QueryOffsets( this, out var shakeOffset, out var shakeAngles, out var shakeFov );
|
|
|
|
_renderView.Position += _renderView.Rotation * shakeOffset;
|
|
_renderView.Rotation *= shakeAngles;
|
|
_renderView.FieldOfView = (_renderView.FieldOfView + shakeFov).Clamp( 1f, 179f );
|
|
}
|
|
|
|
// What the component looked like when composition finished. A camera changed after this
|
|
// stomps the composition (see UpdateSceneCameraTransform).
|
|
_composedBase = RawView;
|
|
|
|
_composed = true;
|
|
}
|
|
|
|
// The camera's own values, untouched by modifiers or effects.
|
|
CameraView RawView => new()
|
|
{
|
|
Position = WorldPosition,
|
|
Rotation = WorldRotation,
|
|
FieldOfView = FieldOfView,
|
|
ZNear = ZNear,
|
|
ZFar = ZFar
|
|
};
|
|
|
|
internal void UpdateSceneCameraTransform( SceneCamera camera )
|
|
{
|
|
// The view composes once, at the camera stage of the tick - that's the only time modifiers
|
|
// and effects apply. A camera that never composed, or was changed since composing (tools,
|
|
// paused scenes, moviemakers - place a camera, render, done), stomps the composition and
|
|
// renders from its raw values.
|
|
var raw = RawView;
|
|
|
|
if ( !_composed || raw != _composedBase )
|
|
{
|
|
_renderView = raw;
|
|
}
|
|
|
|
camera.Position = _renderView.Position;
|
|
camera.Rotation = _renderView.Rotation;
|
|
camera.ZNear = _renderView.ZNear;
|
|
camera.ZFar = _renderView.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( _renderView.FieldOfView );
|
|
else
|
|
camera.FieldOfView = _renderView.FieldOfView;
|
|
}
|
|
|
|
internal void UpdateSceneCameraStereo( SceneCamera camera )
|
|
{
|
|
camera.TargetEye = TargetEye;
|
|
camera.WantsStereoSubmit = !Scene.IsEditor;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update a SceneCamera with the settings from this component
|
|
/// </summary>
|
|
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<VolumetricFogVolume>().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<VolumetricFogController>().FirstOrDefault()?.BakedFogTexture;
|
|
}
|
|
|
|
//
|
|
// 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<CubemapFog>() )
|
|
{
|
|
if ( cubemapFog.Tags.HasAny( RenderExcludeTags ) )
|
|
continue;
|
|
|
|
cubemapFog.SetupCamera( this, camera );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the SceneCamera UI with the settings from this component
|
|
/// </summary>
|
|
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<SceneCamera> renderEffect ) => null;
|
|
|
|
/// <summary>
|
|
/// Obsolete 09/06/2025
|
|
/// </summary>
|
|
[Obsolete( "Use CommandList" )]
|
|
public IDisposable AddHookAfterTransparent( string debugName, int order, Action<SceneCamera> renderEffect ) => null;
|
|
|
|
/// <summary>
|
|
/// Obsolete 09/06/2025
|
|
/// </summary>
|
|
[Obsolete( "Use CommandList" )]
|
|
public IDisposable AddHookBeforeOverlay( string debugName, int order, Action<SceneCamera> renderEffect ) => null;
|
|
|
|
/// <summary>
|
|
/// Obsolete 02/10/2025
|
|
/// </summary>
|
|
[Obsolete( "Use CommandList" )]
|
|
public IDisposable AddHookAfterUI( string debugName, int order, Action<SceneCamera> renderEffect ) => null;
|
|
|
|
private void OnCameraRenderUI( SceneCamera camera )
|
|
{
|
|
if ( Scene is null )
|
|
return;
|
|
|
|
foreach ( var c in Scene.GetAll<ScreenPanel>().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();
|
|
}
|
|
}
|
|
|
|
internal bool IsSceneEditorCamera;
|
|
|
|
internal void InitializeRendering( bool renderUI = true )
|
|
{
|
|
using ( Scene.Push() )
|
|
{
|
|
EnsureSceneCameraCreated();
|
|
|
|
if ( IsSceneEditorCamera )
|
|
{
|
|
UpdateSceneCamera( sceneCamera );
|
|
Scene.Camera?.CopyPostProcessing( sceneCamera );
|
|
}
|
|
else
|
|
{
|
|
UpdateSceneCamera( sceneCamera );
|
|
}
|
|
|
|
UpdateSceneCameraUI( sceneCamera );
|
|
sceneCamera.RenderUI = renderUI;
|
|
UpdateSceneCameraStereo( sceneCamera );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This should only be called when creating render lists!!
|
|
/// </summary>
|
|
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, CustomSize ?? size, default );
|
|
}
|
|
else
|
|
{
|
|
SceneCamera.AddToRenderList( swapChain, CustomSize ?? size );
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The size of the viewport, in screen coordinates
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a BBox in world space, will return the screen space rect that totally contains the box.
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert from screen coords to world coords on the near frustum plane.
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the view frustum of the current screen rect.
|
|
/// </summary>
|
|
public Frustum GetFrustum()
|
|
{
|
|
UpdateSceneCameraTransform( sceneCamera );
|
|
return sceneCamera.GetFrustum( ScreenRect );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a pixel rect return a frustum on the current camera.
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
|
|
/// <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 screenRect, Vector3 screenSize )
|
|
{
|
|
// Same as above
|
|
UpdateSceneCameraTransform( sceneCamera );
|
|
return sceneCamera.GetFrustum( screenRect, screenSize );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Render scene to a texture from this camera's point of view
|
|
/// </summary>
|
|
public bool RenderToTexture( Texture target, in ViewSetup config = default )
|
|
{
|
|
if ( target is null || target.native.IsNull )
|
|
return false;
|
|
|
|
// TODO: we need to get rid of this guard, it's a hack
|
|
if ( !Graphics.IsActive )
|
|
{
|
|
Scene.PreCameraRender();
|
|
// TODO: not sure why we call this again PreCameraRender should init all cameras
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows specifying a custom projection matrix for this camera
|
|
/// </summary>
|
|
public Matrix? CustomProjectionMatrix
|
|
{
|
|
get => SceneCamera.CustomProjectionMatrix;
|
|
set => SceneCamera.CustomProjectionMatrix = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows specifying a custom aspect ratio for this camera.
|
|
/// By default (or when null) the camera size is screen size or render target size.
|
|
/// </summary>
|
|
[JsonIgnore, Hide]
|
|
public Vector2? CustomSize { get; set; }
|
|
|
|
/// <summary>
|
|
/// Get frustum projection matrix.
|
|
/// </summary>
|
|
public Matrix ProjectionMatrix => sceneCamera.ProjectionMatrix;
|
|
|
|
/// <summary>
|
|
/// Calculates a projection matrix with an oblique clip-plane defined in world space.
|
|
/// </summary>
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Fits the view within the x-axis.
|
|
/// </summary>
|
|
[Icon( "panorama_horizontal" )]
|
|
Horizontal,
|
|
|
|
/// <summary>
|
|
/// Fits the view within the y-axis.
|
|
/// </summary>
|
|
[Icon( "panorama_vertical" )]
|
|
Vertical
|
|
}
|
|
|
|
/// <summary>
|
|
/// Render this camera to the target bitmap.
|
|
/// </summary>
|
|
public void RenderToBitmap( Bitmap targetBitmap ) => RenderToBitmap( targetBitmap, true );
|
|
|
|
/// <summary>
|
|
/// Render this camera to the target bitmap, with the option to include or exclude the drawn UI.
|
|
/// </summary>
|
|
public void RenderToBitmap( Bitmap targetBitmap, bool renderUI )
|
|
{
|
|
if ( targetBitmap == null || targetBitmap.Width <= 1 || targetBitmap.Height <= 1 )
|
|
return;
|
|
|
|
if ( !this.IsValid() )
|
|
return;
|
|
|
|
using ( Scene.Push() )
|
|
{
|
|
Scene.PreCameraRender();
|
|
InitializeRendering( renderUI );
|
|
|
|
// Resize the UI to the target bitmap size
|
|
if ( renderUI )
|
|
ResizeUI( targetBitmap.Size );
|
|
|
|
SceneCamera.OnPreRender( targetBitmap.Size );
|
|
SceneCamera.RenderToBitmap( targetBitmap );
|
|
|
|
// Resize the UI back to the screen size
|
|
if ( renderUI )
|
|
ResizeUI( new Vector2( Screen.Width, Screen.Height ) );
|
|
}
|
|
}
|
|
|
|
private void ResizeUI( Vector2 size )
|
|
{
|
|
if ( Scene is null )
|
|
return;
|
|
|
|
foreach ( var panel in Scene.GetAll<ScreenPanel>() )
|
|
{
|
|
if ( !panel.IsValid() || !panel.Active )
|
|
continue;
|
|
|
|
var target = panel.TargetCamera ?? (IsMainCamera ? this : null);
|
|
if ( target != this )
|
|
continue;
|
|
|
|
if ( RenderExcludeTags.HasAny( panel.GameObject.Tags ) )
|
|
continue;
|
|
|
|
if ( panel.GetPanel() is not RootPanel rootPanel || !rootPanel.IsValid() )
|
|
continue;
|
|
|
|
var screenRect = new Rect( 0, 0, size.x, size.y );
|
|
|
|
rootPanel.PreLayout( screenRect );
|
|
rootPanel.CalculateLayout();
|
|
rootPanel.PostLayout();
|
|
|
|
rootPanel.BuildCommandList();
|
|
}
|
|
}
|
|
}
|