Files
sbox-public/engine/Sandbox.Engine/Scene/SceneUtility.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

325 lines
8.8 KiB
C#

using Facepunch.ActionGraphs;
using System.Text.Json.Nodes;
namespace Sandbox;
public static class SceneUtility
{
/// <summary>
/// Find all "__guid" guids, and replace them with new guids. This is used to make GameObject serializations unique,
/// so when you duplicate stuff, it copies over uniquely and keeps associations.
/// </summary>
public static Dictionary<Guid, Guid> MakeIdGuidsUnique( JsonObject json, Guid? rootGuid = null )
{
var translate = CreateUniqueGuidLookup( json, rootGuid );
//
// Find every guid and translate them, but only if they're in our
// guid dictionary.
//
Sandbox.Json.WalkJsonTree( json, ( k, v ) =>
{
if ( !v.TryGetValue<Guid>( out var guid ) ) return v;
if ( !translate.TryGetValue( guid, out var updatedGuid ) ) return v;
return updatedGuid;
} );
return translate;
}
internal static Dictionary<Guid, Guid> CreateUniqueGuidLookup( JsonObject json, Guid? rootGuid = null )
{
Dictionary<Guid, Guid> translate = new();
//
// This is a prefab that is being spawned into the scene. As such, it already has a guid.
// So we need to replace whatever the prefab's guid is with the real id, and make sure
// it doesn't generate a new one for it.. and make sure that it updates all of the children
// so they correctly reference the new guid.
//
if ( rootGuid.HasValue )
{
if ( json.TryGetPropertyValue( "__guid", out var node ) && node is JsonValue jsv && jsv.TryGetValue<Guid>( out var guid ) )
{
translate[guid] = rootGuid.Value;
}
}
//
// We want to skip ActionGraph Guids if they're cached, so we can re-use the cached versions.
//
if ( ActionGraph.SerializationOptions.Cache is { } cache )
{
foreach ( var guid in cache.Guids )
{
translate[guid] = guid;
}
}
//
// Find all guids with "__guid" as their name. Add them to translate
// with a new target value.
//
Sandbox.Json.WalkJsonTree( json, ( k, v ) =>
{
if ( k == GameObject.JsonKeys.Id )
{
if ( v.TryGetValue<Guid>( out var guid ) && !translate.ContainsKey( guid ) )
{
translate[guid] = Guid.NewGuid();
}
}
return v;
},
( k, v ) =>
{
// GameObjects & Components that are part of prefab instances have their id stored in the PrefabToInstanceId lookup in JSON.
// We need to remap those as well in addtion to __guid id's.
if ( k == GameObject.JsonKeys.PrefabIdToInstanceId )
{
foreach ( var (prefabId, instanceID) in v )
{
if ( instanceID.AsValue().TryGetValue<Guid>( out var guid ) && !translate.ContainsKey( guid ) )
{
translate[guid] = Guid.NewGuid();
}
}
}
return v;
} );
return translate;
}
/// <summary>
/// Find all "Id" guids, and replace them with new guids. This is used to make GameObject serializations unique,
/// so when you duplicate stuff, it copies over uniquely and keeps associations.
/// </summary>
[Obsolete( "Use MakeIdGuidsUnique" )]
public static void MakeGameObjectsUnique( JsonObject json, Guid? rootGuid = null )
{
MakeIdGuidsUnique( json, rootGuid );
}
/// <summary>
/// Create a unique copy of the passed in GameObject
/// </summary>
[System.Obsolete( "Please use GameObject.Clone( ... )" )]
public static GameObject Instantiate( GameObject template, Transform transform )
{
Assert.NotNull( Game.ActiveScene, "No Active Scene" );
using var batchGroup = CallbackBatch.Batch();
JsonObject json = null;
if ( template is PrefabScene prefabScene && prefabScene.Source is PrefabFile prefabFile )
{
json = prefabFile.RootObject;
}
else
{
json = template.Serialize();
}
if ( json is not null )
{
MakeIdGuidsUnique( json );
}
var go = new GameObject();
if ( json is not null )
{
go.Deserialize( json );
}
go.LocalTransform = transform.WithScale( transform.Scale * go.LocalScale );
if ( template is PrefabScene prefabScene1 )
{
go.InitPrefabInstance( prefabScene1.Source.ResourcePath, false );
}
go.Parent = Game.ActiveScene;
return go;
}
/// <summary>
/// Create a unique copy of the passed in GameObject
/// </summary>
[System.Obsolete( "Please use GameObject.Clone( ... )" )]
public static GameObject Instantiate( GameObject template ) => Instantiate( template, Transform.Zero );
/// <summary>
/// Create a unique copy of the passed in GameObject
/// </summary>
[System.Obsolete( "Please use GameObject.Clone( ... )" )]
public static GameObject Instantiate( GameObject template, Vector3 position, Rotation rotation )
=> Instantiate( template, new Transform( position, rotation, 1.0f ) );
/// <summary>
/// Create a unique copy of the passed in GameObject
/// </summary>
[System.Obsolete( "Please use GameObject.Clone( ... )" )]
public static GameObject Instantiate( GameObject template, Vector3 position )
=> Instantiate( template, new Transform( position, Rotation.Identity, 1.0f ) );
/// <summary>
/// Get a (cached) scene from a PrefabFile
/// </summary>
public static PrefabScene GetPrefabScene( PrefabFile prefabFile ) => prefabFile.GetScene();
/// <summary>
/// Render a GameObject to a bitmap. This is usually used for easily rendering "previews" of GameObjects,
/// for things like saving thumbnails etc.
/// </summary>
static void RenderToBitmap( Bitmap bitmap, Func<GameObject> func )
{
var scene = Scene.CreateEditorScene();
scene.Name = "RenderGameObjectToBitmap";
try
{
using var sceneScope = scene.Push();
CameraComponent camera = default;
// camera
{
var go = new GameObject( true, "camera" );
camera = go.AddComponent<CameraComponent>();
camera.BackgroundColor = Color.Transparent;
camera.WorldRotation = new Angles( 20, 180 + 45, 0 );
camera.FieldOfView = 50.0f;
camera.ZFar = 15000.0f;
camera.ZNear = 0.1f;
var sharpen = go.AddComponent<Sharpen>( true );
sharpen.Scale = 0.2f;
go.AddComponent<AmbientOcclusion>( true );
go.AddComponent<ScreenSpaceReflections>( true ); // I don't think this'll work?
go.AddComponent<Bloom>( true );
}
// ambient light
{
var go = new GameObject( true, "ambient" );
var c = go.AddComponent<AmbientLight>();
c.Color = Color.White * 0.2f;
}
// sun light
{
var go = new GameObject( true, "sun" );
var sun = go.AddComponent<DirectionalLight>();
sun.Shadows = true;
sun.WorldRotation = new Angles( 60, 45, 0 );
sun.LightColor = Color.White * 0.4f;
}
// envmap
{
var go = new GameObject( true, "envmap" );
var c = go.AddComponent<EnvmapProbe>();
c.Texture = Texture.Load( "textures/cubemaps/default2.vtex" );
c.Bounds = BBox.FromPositionAndSize( Vector3.Zero, 100000 );
}
GameObject o = default;
// prefab spawn
{
o = func();
o.WorldPosition = 0;
}
// tick tick
float t = 0;
for ( int i = 0; i < 8; i++ )
{
scene.EditorTick( t += 0.1f, 0.1f );
}
// place the camera
{
var bounds = o.GetBounds();
var distance = MathX.SphereCameraDistance( bounds.Size.Length * 0.5f, camera.FieldOfView );
var aspect = bitmap.Width / bitmap.Height;
if ( aspect > 1 ) distance *= aspect;
camera.WorldPosition = (o.WorldRotation * bounds.Center) + camera.WorldRotation.Forward * -distance;
}
// render twice, for any temporal shit to kick in
camera.RenderToBitmap( bitmap );
scene.EditorTick( t += 0.1f, 0.1f );
camera.RenderToBitmap( bitmap );
scene.Destroy();
}
finally
{
scene.Destroy();
}
}
/// <summary>
/// Render a GameObject to a bitmap. This is usually used for easily rendering "previews" of GameObjects,
/// for things like saving thumbnails etc.
/// </summary>
public static void RenderGameObjectToBitmap( GameObject objSource, Bitmap bitmap )
{
if ( objSource == null ) return;
if ( bitmap == null ) return;
Sandbox.Rendering.TextureStreaming.ExecuteWithDisabled( () =>
{
RenderToBitmap( bitmap, () =>
{
var o = objSource.Clone();
o.WorldPosition = 0;
return o;
} );
} );
}
/// <summary>
/// Render a Model to a bitmap. This is usually used for easily rendering "previews" of Models for thumbnails
/// </summary>
public static void RenderModelBitmap( Model model, Bitmap bitmap )
{
if ( model == null ) return;
if ( bitmap == null ) return;
Sandbox.Rendering.TextureStreaming.ExecuteWithDisabled( () =>
{
RenderToBitmap( bitmap, () =>
{
var o = new GameObject();
o.AddComponent<ModelRenderer>().Model = model;
return o;
} );
} );
}
/// <summary>
/// Run an action inside a batch group. A batchgroup is used with GameObject and Components to
/// make sure that their OnEnable/OnDisable and other callbacks are called in a deterministic order,
/// and that they can find each other during creation.
/// </summary>
public static void RunInBatchGroup( Action action )
{
using ( CallbackBatch.Isolated() )
{
action?.Invoke();
}
}
}