using NativeEngine;
using System.Runtime.CompilerServices;
namespace Sandbox;
///
/// Used to render to the screen using your Graphics Card, or whatever you
/// kids are using in your crazy future computers. Whatever it is I'm sure
/// it isn't fungible and everyone has free money and no-one has to ever work.
///
public static partial class Graphics
{
public enum PrimitiveType
{
Points = NativeEngine.RenderPrimitiveType.RENDER_PRIM_POINTS,
Lines = NativeEngine.RenderPrimitiveType.RENDER_PRIM_LINES,
LinesWithAdjacency = NativeEngine.RenderPrimitiveType.RENDER_PRIM_LINES_WITH_ADJACENCY,
LineStrip = NativeEngine.RenderPrimitiveType.RENDER_PRIM_LINE_STRIP,
LineStripWithAdjacency = NativeEngine.RenderPrimitiveType.RENDER_PRIM_LINE_STRIP_WITH_ADJACENCY,
Triangles = NativeEngine.RenderPrimitiveType.RENDER_PRIM_TRIANGLES,
TrianglesWithAdjacency = NativeEngine.RenderPrimitiveType.RENDER_PRIM_TRIANGLES_WITH_ADJACENCY,
TriangleStrip = NativeEngine.RenderPrimitiveType.RENDER_PRIM_TRIANGLE_STRIP,
TriangleStripWithAdjacency = NativeEngine.RenderPrimitiveType.RENDER_PRIM_TRIANGLE_STRIP_WITH_ADJACENCY,
}
///
/// If true then we're currently rendering and
/// you are safe to use the contents of this class
///
public static bool IsActive => _state.active;
///
/// The current layer type. This is useful to tell whether you're meant to be drawing opaque, transparent or shadow. You mainly
/// don't need to think about this, but when you do, it's here.
///
public static SceneLayerType LayerType => _state.layerType;
struct RenderState
{
public bool active;
public IRenderContext renderContext;
public ISceneLayer sceneLayer;
public ISceneView sceneView;
public SceneLayerType layerType;
public RenderAttributes attributes;
internal SceneSystemPerFrameStats_t stats;
public ImageFormat colorFormat;
public MultisampleAmount msaaLevel;
public Transform cameraTransform;
internal RenderAttributes frameAttributes;
internal RenderTarget renderTarget;
internal float defaultMinZ;
internal float defaultMaxZ;
}
[ThreadStatic]
private static RenderState _state;
internal static IRenderContext Context => _state.renderContext;
internal static ISceneLayer SceneLayer => _state.sceneLayer;
internal static ISceneView SceneView => _state.sceneView;
internal static ImageFormat IdealColorFormat => _state.colorFormat;
internal static MultisampleAmount IdealMsaaLevel => _state.msaaLevel;
internal static SceneSystemPerFrameStats_t Stats => _state.stats;
///
/// When Frame grabbing, we store the result in this pooled render target. It stays checked out
/// until the end of this render scope.
///
[ThreadStatic]
static HashSet grabbedTextures;
///
/// In pixel size, where are we rendering to?
///
public static Rect Viewport
{
get => Context.GetViewport().Rect;
set => Context.SetViewport( value );
}
///
/// Access to the current render context's attributes. These will be used
/// to set attributes in materials/shaders. This is cleared at the end of the render block.
///
public static RenderAttributes Attributes => _state.attributes;
///
/// Access to the current frame's attributes.
/// These will live until the end of the frame.
///
internal static RenderAttributes FrameAttributes => _state.frameAttributes;
///
/// The camera transform of the currently rendering view
///
public static Transform CameraTransform => _state.cameraTransform;
///
/// The camera position of the currently rendering view
///
public static Vector3 CameraPosition => CameraTransform.Position;
///
/// The camera rotation of the currently rendering view
///
public static Rotation CameraRotation => CameraTransform.Rotation;
///
/// The field of view of the currently rendering camera view, in degrees.
///
public static float FieldOfView => _state.sceneView.GetFrustum().GetCameraFOV();
///
/// The frustum of the currently rendering camera view.
///
public static Frustum Frustum
{
get
{
AssertRenderBlock();
var cf = _state.sceneView.GetFrustum();
// Extract planes from native CFrustum
// Plane indices: RIGHT=0, LEFT=1, TOP=2, BOTTOM=3, NEAR=4, FAR=5
cf.GetPlane( 0, out var rn, out var rd );
cf.GetPlane( 1, out var ln, out var ld );
cf.GetPlane( 2, out var tn, out var td );
cf.GetPlane( 3, out var bn, out var bd );
cf.GetPlane( 4, out var nn, out var nd );
cf.GetPlane( 5, out var fn, out var fd );
return new Frustum(
right: new Plane( ln, ld ),
left: new Plane( ln, rd ),
top: new Plane( tn, td ),
bottom: new Plane( bn, bd ),
near: new Plane( nn, nd ),
far: new Plane( fn, fd )
);
}
}
internal static int RenderMultiSampleToNum( RenderMultisampleType msaaLevel )
{
switch ( msaaLevel )
{
case RenderMultisampleType.RENDER_MULTISAMPLE_NONE:
return 1;
case RenderMultisampleType.RENDER_MULTISAMPLE_2X:
return 2;
case RenderMultisampleType.RENDER_MULTISAMPLE_4X:
return 4;
case RenderMultisampleType.RENDER_MULTISAMPLE_6X:
return 6;
case RenderMultisampleType.RENDER_MULTISAMPLE_8X:
return 8;
case RenderMultisampleType.RENDER_MULTISAMPLE_16X:
return 16;
default:
throw new System.Exception( "Unknown multisample amount" );
}
}
///
/// Creates a scope where Graphics is safe to use.
///
internal ref struct Scope
{
RenderState _previous;
public Scope( in ManagedRenderSetup_t setup )
{
_previous = _state;
_state = new RenderState();
var frustum = setup.sceneView.GetFrustum();
_state.active = true;
_state.sceneLayer = setup.sceneLayer;
_state.sceneView = setup.sceneView;
_state.renderContext = setup.renderContext;
_state.layerType = setup.sceneLayer.LayerEnum;
_state.colorFormat = setup.colorImageFormat;
_state.msaaLevel = setup.msaaLevel.FromEngine();
_state.cameraTransform = new Transform( frustum.GetCameraPosition(), frustum.GetCameraAngles() );
_state.stats = setup.stats;
_state.attributes = ObjectPool.Get();
_state.attributes.Set( setup.renderContext.GetAttributesPtrForModify() );
_state.frameAttributes = ObjectPool.Get();
_state.frameAttributes.Set( setup.sceneView.GetRenderAttributesPtr() );
}
public void Dispose()
{
if ( _state.attributes is not null )
{
_state.attributes.Set( default( CRenderAttributes ) );
ObjectPool.Return( _state.attributes );
}
if ( _state.frameAttributes is not null )
{
_state.frameAttributes.Set( default( CRenderAttributes ) );
ObjectPool.Return( _state.frameAttributes );
}
_state = _previous;
if ( grabbedTextures is not null )
{
// return to the pool
foreach ( var tex in grabbedTextures )
{
tex.Dispose();
}
grabbedTextures.Clear();
}
}
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
internal static void AssertRenderBlock()
{
if ( !IsActive )
throw new System.Exception( "Tried to render outside of rendering block" );
}
///
/// Setup the lighting attributes for this current object. Place them in the targetAttributes
///
public static void SetupLighting( SceneObject obj, RenderAttributes targetAttributes = null )
{
targetAttributes ??= Attributes;
Assert.NotNull( targetAttributes );
Assert.IsValid( obj );
NativeEngine.CSceneSystem.SetupPerObjectLighting( targetAttributes.Get(), obj, SceneLayer );
}
///
/// Grabs the current viewport's color texture and stores it in targetName on renderAttributes.
///
public static RenderTarget GrabFrameTexture( string targetName = "FrameTexture", RenderAttributes renderAttributes = null, DownsampleMethod downsampleMethod = DownsampleMethod.None )
{
renderAttributes ??= Attributes;
AssertRenderBlock();
bool withMips = downsampleMethod != DownsampleMethod.None;
var numMips = withMips ? (int)Math.Log2( Math.Max( Viewport.Width, Viewport.Height ) ) : 1;
// Grab a new one - which may very well be the one we just returned
var frameTexture = RenderTarget.GetTemporary( 1, ImageFormat.Default, ImageFormat.None, msaa: MultisampleAmount.MultisampleNone, numMips: numMips, targetName );
RenderTools.ResolveFrameBuffer( Context, frameTexture.ColorTarget.native, Viewport );
// Generate a mip chain if we want one
if ( withMips ) GenerateMipMaps( frameTexture.ColorTarget, downsampleMethod );
grabbedTextures ??= new();
grabbedTextures.Add( frameTexture );
renderAttributes.Set( targetName, frameTexture.ColorTarget );
return frameTexture;
}
[Obsolete( "Use GrabFrameTexture with DownsampleMethod instead" )]
public static void GrabFrameTexture( string targetName, RenderAttributes renderAttributes, bool withMips )
{
GrabFrameTexture( targetName, renderAttributes, withMips ? DownsampleMethod.GaussianBlur : DownsampleMethod.None );
}
///
/// Grabs the current depth texture and stores it in targetName on renderAttributes.
///
public static RenderTarget GrabDepthTexture( string targetName = "DepthTexture", RenderAttributes renderAttributes = null )
{
renderAttributes ??= Attributes;
AssertRenderBlock();
// Grab a new one - which may very well be the one we just returned
var grabbedTexture = RenderTarget.GetTemporary( 1, ImageFormat.None, ImageFormat.Default, msaa: MultisampleAmount.MultisampleScreen, targetName: targetName );
RenderTools.ResolveDepthBuffer( Context, grabbedTexture.DepthTarget.native, Viewport );
grabbedTextures ??= new();
grabbedTextures.Add( grabbedTexture );
renderAttributes.Set( targetName, grabbedTexture.DepthTarget );
return grabbedTexture;
}
///
/// Get or set the current render target. Setting this will bind the render target and change the viewport to match it.
///
public static RenderTarget RenderTarget
{
get => _state.renderTarget;
set
{
if ( _state.renderTarget == value )
return;
// Going from default render target to custom render target
if ( _state.renderTarget == null )
{
var viewport = Context.GetViewport();
// Save off min/max Z values here, so we can restore them later.
_state.defaultMinZ = viewport.MinZ;
_state.defaultMaxZ = viewport.MaxZ;
}
_state.renderTarget = value;
// Resetting to default render target
if ( _state.renderTarget == null )
{
// alex: if we don't restore min/max Z values properly when setting back to
// the default render target, we get a lot of weird depth issues.
// This mainly only applies to things like worldpanels when we render filtered
// elements, but could probably happen in other places too?
var viewport = new RenderViewport( SceneLayer.m_viewport.Rect, _state.defaultMinZ, _state.defaultMaxZ );
Context.SetViewport( viewport );
Context.RestoreRenderTargets( SceneLayer );
return;
}
Context.BindRenderTargets( _state.renderTarget.ColorTarget?.native ?? default, _state.renderTarget.DepthTarget?.native ?? default, SceneLayer );
Viewport = new Rect( 0, 0, _state.renderTarget.Width, _state.renderTarget.Height );
}
}
///
/// Clear the current drawing context to given color.
///
/// Color to clear to.
/// Whether to clear the color buffer at all.
/// Whether to clear the depth buffer.
/// Whether to clear the stencil buffer.
public static void Clear( Color color, bool clearColor = true, bool clearDepth = true, bool clearStencil = true )
{
Context.Clear( color, clearColor, clearDepth, clearStencil );
}
///
/// Clear the current drawing context to given color.
///
/// Whether to clear the color buffer to transparent color.
/// Whether to clear the depth buffer.
public static void Clear( bool clearColor = true, bool clearDepth = true )
{
Clear( Color.Transparent, clearColor, clearDepth, false );
}
///
/// Render this camera to the specified texture target
///
[Obsolete( "Use CameraComponent.RenderToTexture" )]
public static bool RenderToTexture( SceneCamera camera, Texture target )
{
return false;
}
///
/// Copies pixel data from one texture to another on the GPU.
/// This does not automatically resize or scale the texture, format and size should be equal.
///
public static void CopyTexture( Texture srcTexture, Texture dstTexture )
{
if ( srcTexture == null ) throw new ArgumentNullException( nameof( srcTexture ) );
if ( dstTexture == null ) throw new ArgumentNullException( nameof( dstTexture ) );
if ( srcTexture.ImageFormat != dstTexture.ImageFormat ) throw new ArgumentException( "Source and destination texture format must match!" );
if ( srcTexture.Size != dstTexture.Size ) throw new ArgumentException( "Source and destination texture size must match!" );
RenderTools.CopyTexture( Context, srcTexture.native, dstTexture.native, default, 0, 0, 0, 0, 0, 0 );
}
[Obsolete( "Use the CopyTexture overload without 'srcMipLevels' and 'dstMipLevels' parameters instead." )]
public static void CopyTexture( Texture srcTexture, Texture dstTexture, int srcMipSlice = 0, int srcArraySlice = 0, int srcMipLevels = 1, int dstMipSlice = 0, int dstArraySlice = 0, int dstMipLevels = 1 )
{
CopyTexture( srcTexture, dstTexture, srcMipSlice, srcArraySlice, dstMipSlice, dstArraySlice );
}
///
/// Copies pixel data from one texture to another on the GPU.
/// This does not automatically resize or scale the texture, format and size should be equal.
/// This one lets you copy to/from arrays / specific mips.
///
public static void CopyTexture( Texture srcTexture, Texture dstTexture, int srcMipSlice = 0, int srcArraySlice = 0, int dstMipSlice = 0, int dstArraySlice = 0 )
{
if ( srcTexture == null ) throw new ArgumentNullException( nameof( srcTexture ) );
if ( dstTexture == null ) throw new ArgumentNullException( nameof( dstTexture ) );
if ( srcTexture.ImageFormat != dstTexture.ImageFormat ) throw new ArgumentException( "Source and destination texture format must match!" );
if ( srcMipSlice < 0 || srcMipSlice >= srcTexture.Mips ) throw new ArgumentException( $"{nameof( srcMipSlice )} out of bounds" );
if ( dstMipSlice < 0 || dstMipSlice >= dstTexture.Mips ) throw new ArgumentException( $"{nameof( dstMipSlice )} out of bounds" );
var srcDesc = srcTexture.Desc;
var dstDesc = dstTexture.Desc;
if ( srcArraySlice < 0 || srcArraySlice >= srcDesc.ArrayCount ) throw new ArgumentException( $"{nameof( srcArraySlice )} out of bounds" );
if ( dstArraySlice < 0 || dstArraySlice >= dstDesc.ArrayCount ) throw new ArgumentException( $"{nameof( dstArraySlice )} out of bounds" );
var srcMipWidth = Math.Max( 1, srcTexture.Width >> srcMipSlice );
var srcMipHeight = Math.Max( 1, srcTexture.Height >> srcMipSlice );
var dstMipWidth = Math.Max( 1, dstTexture.Width >> dstMipSlice );
var dstMipHeight = Math.Max( 1, dstTexture.Height >> dstMipSlice );
if ( srcMipWidth != dstMipWidth || srcMipHeight != dstMipHeight )
throw new ArgumentException( "Source and destination texture mip level sizes must match!" );
RenderTools.CopyTexture( Context, srcTexture.native, dstTexture.native, default, 0, 0,
(uint)srcMipSlice, (uint)srcArraySlice,
(uint)dstMipSlice, (uint)dstArraySlice );
}
}