UI/ShipSelect/ShipPortrait3d.cs
using Sandbox.UI;
/// <summary>
/// Renders a 3D preview of a ship model inside a ScenePanel using its built-in RenderScene.
/// Pass a prefab path via PrefabPath; the model is extracted automatically.
/// </summary>
public sealed class ShipPortrait3d : Panel
{
private ScenePanel _scenePanel;
private GameObject _cameraGo;
private CameraComponent _camera;
private GameObject _modelGo;
private string _prefabPath;
private string _materialSkin;
public string PrefabPath
{
get => _prefabPath;
set
{
if ( _prefabPath == value ) return;
_prefabPath = value;
LoadModelFromPrefab( value );
}
}
public string MaterialSkin
{
get => _materialSkin;
set { _materialSkin = value; UpdateSkin(); }
}
// 45° above-and-behind: equal parts backward and up
private static readonly Vector3 CamDir = (Vector3.Backward + Vector3.Up).Normal;
public ShipPortrait3d()
{
_scenePanel = new ScenePanel();
var scene = _scenePanel.RenderScene;
// Camera — 45° elevated, looking down at origin
_cameraGo = scene.CreateObject();
_camera = _cameraGo.Components.Create<CameraComponent>();
_camera.FieldOfView = 45f;
_camera.ZNear = 1f;
_camera.ZFar = 10000f;
_camera.BackgroundColor = Color.Transparent;
_cameraGo.WorldPosition = CamDir * 250f;
_cameraGo.WorldRotation = Rotation.LookAt( -CamDir );
// Three-point lighting
AddLight( scene, Vector3.Up * 100f + Vector3.Right * 80f, Color.White * 3f, 500f );
AddLight( scene, Vector3.Up * 100f + Vector3.Left * 80f + Vector3.Forward * 40f, new Color( 0.4f, 0.6f, 1f ) * 1.5f, 500f );
AddLight( scene, Vector3.Down * 50f, new Color( 1f, 0.8f, 0.5f ), 300f );
_scenePanel.Style.Width = Length.Percent( 100 );
_scenePanel.Style.Height = Length.Percent( 100 );
AddChild( _scenePanel );
}
private static void AddLight( Scene scene, Vector3 position, Color color, float radius )
{
var go = scene.CreateObject();
go.WorldPosition = position;
var light = go.Components.Create<PointLight>();
light.LightColor = color;
light.Radius = radius;
}
private void LoadModelFromPrefab( string prefabPath )
{
_modelGo?.Destroy();
_modelGo = null;
if ( string.IsNullOrEmpty( prefabPath ) ) return;
var prefabFile = ResourceLibrary.Get<PrefabFile>( prefabPath );
if ( prefabFile == null ) return;
// Briefly clone into the game scene to read the model, then discard
var tempGo = SceneUtility.GetPrefabScene( prefabFile )?.Clone();
if ( tempGo == null ) return;
var mr = tempGo.Components.Get<ModelRenderer>( FindMode.EverythingInSelfAndDescendants );
var model = mr?.Model;
tempGo.Destroy();
if ( model == null ) return;
_modelGo = _scenePanel.RenderScene.CreateObject();
_modelGo.WorldScale = Vector3.One * 0.5f;
var renderer = _modelGo.Components.Create<ModelRenderer>();
renderer.Model = model;
renderer.SceneObject?.Attributes.Set( "damagefade", 0f );
UpdateSkin();
// Frame camera to fit model, keeping 45° angle
var size = renderer.Bounds.Size.Length;
_cameraGo.WorldPosition = CamDir * MathF.Max( size * 1.6f, 80f );
_cameraGo.WorldRotation = Rotation.LookAt( -CamDir );
}
private void UpdateSkin()
{
if ( _modelGo == null ) return;
var renderer = _modelGo.Components.Get<ModelRenderer>();
if ( renderer == null ) return;
renderer.MaterialOverride = !string.IsNullOrEmpty( _materialSkin )
? Material.Load( _materialSkin )
: null;
}
public override void Tick()
{
base.Tick();
if ( _modelGo == null ) return;
_modelGo.WorldRotation *= Rotation.FromYaw( 0.5f );
}
}