Editor/PbrMaterialPreviewWidget.cs
using System;
using System.Collections.Generic;
using System.IO;
using Editor;
using Sandbox;
using Widget = Editor.Widget;
public sealed class PbrMaterialPreviewWidget : SceneRenderingWidget
{
private GameObject previewObject;
private ModelRenderer previewRenderer;
private Model sphereModel;
private Model planeModel;
private Model activeModel;
private Material previewMaterial;
private Material fallbackMaterial;
private readonly List<string> temporaryPreviewFiles = new();
private readonly string previewSessionId = Guid.NewGuid().ToString( "N" );
private string temporaryPreviewFolder;
private PbrPreviewShape previewShape = PbrPreviewShape.Sphere;
private Vector2 lastCursorPosition;
private Vector2 orbitAngles = new( 135f, 28f );
private Vector3 cameraOrigin = Vector3.Zero;
private float cameraDistance = 150f;
private float actualCameraDistance = 150f;
private float modelRotation;
private bool orbitControl;
private bool zoomControl;
private bool panControl;
private bool hasGeneratedMaterial;
private bool previewPipelineEnabled;
private bool parallaxEnabled;
private string previewWarning;
public Action<string> WarningLogged { get; set; }
private const string PreviewMaterialFileName = "preview_pbr.vmat";
private const string PreviewParallaxHeightScale = "0.020";
private static Vector2 CursorPosition => Editor.Application.UnscaledCursorPosition;
public PbrMaterialPreviewWidget( Widget parent ) : base( parent )
{
MouseTracking = true;
FocusMode = FocusMode.Click;
MinimumSize = new Vector2( 480, 220 );
SetStyles( "background-color: #101215; border: 1px solid #2a3038; border-radius: 6px;" );
ResetView();
}
public void SetPreviewEnabled( bool enabled )
{
if ( previewPipelineEnabled == enabled )
return;
previewPipelineEnabled = enabled;
if ( previewPipelineEnabled )
{
previewWarning = null;
EnsurePreviewScene();
ApplyMaterialToRenderer();
Update();
return;
}
hasGeneratedMaterial = false;
previewWarning = null;
orbitControl = false;
zoomControl = false;
panControl = false;
DisposePreviewResources();
DestroyPreviewScene();
Update();
}
public void SetParallaxEnabled( bool enabled )
{
parallaxEnabled = enabled;
}
public void SetResult( PbrGeneratorResult result )
{
if ( !previewPipelineEnabled )
{
hasGeneratedMaterial = false;
previewWarning = null;
Update();
return;
}
EnsurePreviewScene();
hasGeneratedMaterial = false;
previewWarning = null;
if ( result == null || result.Albedo == null || !result.Albedo.IsValid )
{
DisposePreviewResources();
ApplyMaterialToRenderer();
Update();
return;
}
var material = CreateTemporaryPreviewMaterial( result );
if ( material == null || !material.IsValid )
{
previewMaterial = null;
previewWarning = "Could not load the generated preview material; showing fallback material.";
LogPreviewWarning( previewWarning );
DisposePreviewResources();
ApplyMaterialToRenderer();
Update();
return;
}
previewMaterial = material;
hasGeneratedMaterial = true;
ApplyMaterialToRenderer();
Update();
}
public void SetPreviewShape( PbrPreviewShape shape )
{
var shapeChanged = previewShape != shape;
previewShape = shape;
if ( !previewPipelineEnabled )
return;
EnsurePreviewScene();
var needsModelRefresh = shapeChanged
|| activeModel == null
|| !activeModel.IsValid
|| previewRenderer == null
|| previewRenderer.Model == null
|| !previewRenderer.Model.IsValid;
if ( !needsModelRefresh )
{
UpdatePreviewTransform();
ApplyMaterialToRenderer();
Update();
return;
}
activeModel = GetModelForShape( previewShape );
if ( previewRenderer != null )
previewRenderer.Model = activeModel;
ResetViewDistance();
UpdatePreviewTransform();
ApplyMaterialToRenderer();
Update();
}
public void ResetView()
{
orbitAngles = new Vector2( 135f, 28f );
cameraOrigin = Vector3.Zero;
modelRotation = 0f;
ResetViewDistance();
actualCameraDistance = cameraDistance;
UpdatePreviewTransform();
UpdateCameraPosition();
Update();
}
public override void OnDestroyed()
{
DisposePreviewResources();
DestroyPreviewScene();
base.OnDestroyed();
}
protected override void PreFrame()
{
if ( !previewPipelineEnabled || Scene == null )
return;
using ( Scene.Push() )
{
Scene.EditorTick( RealTime.Now, RealTime.Delta );
UpdateInputControls();
UpdatePreviewTransform();
UpdateCameraPosition();
}
Update();
}
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
if ( !previewPipelineEnabled )
return;
if ( e.LeftMouseButton )
{
orbitControl = true;
lastCursorPosition = CursorPosition;
e.Accepted = true;
return;
}
if ( e.RightMouseButton )
{
zoomControl = true;
lastCursorPosition = CursorPosition;
e.Accepted = true;
return;
}
if ( e.MiddleMouseButton )
{
panControl = true;
lastCursorPosition = CursorPosition;
e.Accepted = true;
}
}
protected override void OnMouseReleased( MouseEvent e )
{
base.OnMouseReleased( e );
if ( !previewPipelineEnabled )
return;
if ( e.LeftMouseButton )
{
orbitControl = false;
e.Accepted = true;
}
if ( e.RightMouseButton )
{
zoomControl = false;
e.Accepted = true;
}
if ( e.MiddleMouseButton )
{
panControl = false;
e.Accepted = true;
}
}
protected override void OnMouseWheel( WheelEvent e )
{
base.OnMouseWheel( e );
if ( !previewPipelineEnabled )
return;
Zoom( e.Delta * -0.12f );
e.Accept();
Update();
}
protected override void OnPaint()
{
if ( !previewPipelineEnabled )
{
var disabledRect = LocalRect;
Paint.ClearPen();
Paint.SetBrush( Theme.WindowBackground );
Paint.DrawRect( disabledRect, 4 );
return;
}
base.OnPaint();
if ( hasGeneratedMaterial && string.IsNullOrWhiteSpace( previewWarning ) )
return;
var rect = LocalRect.Shrink( 12 );
Paint.SetDefaultFont( 12, 500, false, false );
Paint.SetPen( string.IsNullOrWhiteSpace( previewWarning ) ? Theme.TextDisabled : Theme.Yellow );
Paint.DrawText( rect, string.IsNullOrWhiteSpace( previewWarning ) ? "Generate maps to preview the material" : previewWarning, TextFlag.Center );
}
private void EnsurePreviewScene()
{
if ( Scene != null && previewObject.IsValid() && previewRenderer != null )
return;
CleanStalePreviewFolders();
DestroyPreviewScene();
Scene = Scene.CreateEditorScene();
Scene.Name = "Seam-Less PBR Preview";
using ( Scene.Push() )
{
CreateCameraAndLighting();
sphereModel = CreateTessellatedSphereModel();
planeModel = CreatePlaneModel();
fallbackMaterial = Material.Load( "materials/dev/gray_grid_8.vmat" );
CreatePreviewObject();
}
ResetView();
}
private void DestroyPreviewScene()
{
DestroyPreviewObject();
sphereModel = null;
planeModel = null;
activeModel = null;
fallbackMaterial = null;
Camera = null;
Scene?.Destroy();
Scene = null;
}
private void CreateCameraAndLighting()
{
var cameraObject = new GameObject( true, "PBR Preview Camera" );
var camera = cameraObject.AddComponent<CameraComponent>();
camera.BackgroundColor = new Color( 0.075f, 0.082f, 0.09f );
camera.WorldRotation = new Angles( 20f, 225f, 0f );
camera.FieldOfView = 45f;
camera.ZNear = 0.1f;
camera.ZFar = 15000f;
Camera = camera;
var ambientObject = new GameObject( true, "PBR Preview Ambient" );
var ambient = ambientObject.AddComponent<AmbientLight>();
ambient.Color = Color.Cyan * 0.055f;
var sunObject = new GameObject( true, "PBR Preview Sun" );
var sun = sunObject.AddComponent<DirectionalLight>();
sun.Shadows = true;
sun.WorldRotation = new Angles( 50f, 45f, 0f );
sun.LightColor = Color.White * 0.65f;
var envmapObject = new GameObject( true, "PBR Preview Envmap" );
var envmap = envmapObject.AddComponent<EnvmapProbe>();
envmap.Mode = EnvmapProbe.EnvmapProbeMode.CustomTexture;
envmap.Texture = Texture.Load( "textures/cubemaps/default2.vtex" );
envmap.Bounds = BBox.FromPositionAndSize( Vector3.Zero, 100000f );
envmap.WorldPosition = Vector3.Zero;
envmap.TintColor = Color.White * 0.1f;
}
private void CreatePreviewObject()
{
DestroyPreviewObject();
activeModel = GetModelForShape( previewShape );
if ( activeModel == null || !activeModel.IsValid )
{
activeModel = Model.Sphere;
previewWarning = "Preview model failed to load; using default sphere.";
LogPreviewWarning( previewWarning );
}
previewObject = new GameObject( true, "PBR Preview Model" );
previewObject.WorldTransform = Transform.Zero;
previewRenderer = previewObject.AddComponent<ModelRenderer>();
previewRenderer.Model = activeModel;
previewRenderer.MaterialOverride = fallbackMaterial;
UpdatePreviewTransform();
}
private void DestroyPreviewObject()
{
if ( previewObject.IsValid() )
previewObject.Destroy();
previewObject = null;
previewRenderer = null;
}
private void UpdatePreviewTransform()
{
if ( !previewObject.IsValid() )
return;
previewObject.WorldPosition = Vector3.Zero;
previewObject.WorldRotation = Rotation.FromYaw( modelRotation );
previewObject.WorldScale = GetModelScale( previewShape );
}
private void ApplyMaterialToRenderer()
{
if ( previewRenderer == null )
return;
var material = hasGeneratedMaterial && previewMaterial != null && previewMaterial.IsValid
? previewMaterial
: fallbackMaterial;
previewRenderer.MaterialOverride = material != null && material.IsValid ? material : null;
}
private Material CreateTemporaryPreviewMaterial( PbrGeneratorResult result )
{
try
{
temporaryPreviewFolder ??= Path.Combine( GetPreviewCacheRoot(), previewSessionId );
Directory.CreateDirectory( temporaryPreviewFolder );
temporaryPreviewFiles.Clear();
var albedoPath = WriteTemporaryPreviewTexture( result.Albedo, "albedo", "materials/default/default_color.tga" );
var normalPath = WriteTemporaryPreviewTexture( result.Normal, "normal", "materials/default/default_normal.tga" );
var roughnessPath = WriteTemporaryPreviewTexture( result.Roughness, "roughness", "materials/default/default_rough.tga" );
var aoPath = WriteTemporaryPreviewTexture( result.AmbientOcclusion, "ao", "materials/default/default_ao.tga" );
var metallicPath = WriteTemporaryPreviewTexture( result.Metallic, "metallic", "" );
var heightPath = parallaxEnabled ? WriteTemporaryPreviewTexture( result.Height, "height", "" ) : "";
var materialPath = Path.Combine( temporaryPreviewFolder, PreviewMaterialFileName );
var materialContent = BuildTemporaryVmat( albedoPath, normalPath, roughnessPath, aoPath, metallicPath, heightPath );
var materialAsset = default( Editor.Asset );
if ( ShouldWritePreviewMaterial( materialPath, materialContent ) )
{
if ( WritePreviewTextFile( materialPath, materialContent ) )
materialAsset = RegisterAndCompilePreviewFile( materialPath );
}
temporaryPreviewFiles.Add( materialPath );
var material = LoadPreviewMaterial( materialAsset, materialPath );
if ( material != null && material.IsValid )
return material;
}
catch ( Exception ex )
{
LogPreviewWarning( $"Temporary preview material failed: {ex.Message}" );
}
return null;
}
private bool ShouldWritePreviewMaterial( string path, string content )
{
if ( !File.Exists( path ) )
return true;
try
{
return !string.Equals( File.ReadAllText( path ), content, StringComparison.Ordinal );
}
catch
{
return true;
}
}
private string WriteTemporaryPreviewTexture( Bitmap bitmap, string suffix, string fallback )
{
if ( bitmap == null || !bitmap.IsValid )
return fallback;
var path = Path.Combine( temporaryPreviewFolder, $"preview_{suffix}.png" );
if ( !WritePreviewBinaryFile( path, bitmap.ToPng() ) && !File.Exists( path ) )
return fallback;
temporaryPreviewFiles.Add( path );
var asset = RegisterAndCompilePreviewFile( path );
if ( asset != null && !string.IsNullOrWhiteSpace( asset.RelativePath ) )
return NormalizeMaterialPath( asset.RelativePath );
return GetLibraryAssetReferencePath( path );
}
private bool WritePreviewTextFile( string path, string content )
{
var tempPath = GetPreviewTempWritePath( path );
try
{
Directory.CreateDirectory( Path.GetDirectoryName( path ) );
File.WriteAllText( tempPath, content );
return MovePreviewTempFileIntoPlace( tempPath, path );
}
catch ( Exception ex )
{
LogPreviewWarning( $"Could not write preview file {Path.GetFileName( path )}: {ex.Message}" );
DeletePreviewTempFile( tempPath );
return false;
}
}
private bool WritePreviewBinaryFile( string path, byte[] bytes )
{
var tempPath = GetPreviewTempWritePath( path );
try
{
Directory.CreateDirectory( Path.GetDirectoryName( path ) );
File.WriteAllBytes( tempPath, bytes );
return MovePreviewTempFileIntoPlace( tempPath, path );
}
catch ( Exception ex )
{
LogPreviewWarning( $"Could not write preview texture {Path.GetFileName( path )}: {ex.Message}" );
DeletePreviewTempFile( tempPath );
return false;
}
}
private string GetPreviewTempWritePath( string path )
{
var folder = Path.GetDirectoryName( path );
var filename = Path.GetFileName( path );
return Path.Combine( folder, $".{filename}.{Guid.NewGuid():N}.tmp" );
}
private bool MovePreviewTempFileIntoPlace( string tempPath, string path )
{
try
{
if ( File.Exists( path ) )
{
File.Replace( tempPath, path, null, true );
return true;
}
File.Move( tempPath, path );
return true;
}
catch ( Exception ex )
{
LogPreviewWarning( $"Could not swap preview file {Path.GetFileName( path )}: {ex.Message}" );
DeletePreviewTempFile( tempPath );
return false;
}
}
private void DeletePreviewTempFile( string tempPath )
{
try
{
if ( !string.IsNullOrWhiteSpace( tempPath ) && File.Exists( tempPath ) )
File.Delete( tempPath );
}
catch
{
}
}
private string BuildTemporaryVmat( string albedo, string normal, string roughness, string ao, string metallic, string height )
{
var metalnessLine = string.IsNullOrWhiteSpace( metallic )
? "\tg_flMetalness \"0.000\"\n"
: $"\tTextureMetalness \"{metallic}\"\n";
var parallaxBlock = string.IsNullOrWhiteSpace( height )
? ""
: "\tF_PARALLAX_OCCLUSION 1\n" +
$"\tg_flHeightMapScale \"{PreviewParallaxHeightScale}\"\n" +
$"\tTextureHeight \"{height}\"\n";
return
"Layer0\n" +
"{\n" +
"\tshader \"shaders/complex.shader\"\n" +
"\tF_SPECULAR 1\n" +
parallaxBlock +
"\tg_flAmbientOcclusionDirectDiffuse \"0.000\"\n" +
"\tg_flAmbientOcclusionDirectSpecular \"0.000\"\n" +
$"\tTextureAmbientOcclusion \"{ao}\"\n" +
"\tg_flModelTintAmount \"1.000\"\n" +
"\tg_vColorTint \"[1.000000 1.000000 1.000000 0.000000]\"\n" +
$"\tTextureColor \"{albedo}\"\n" +
"\tg_bFogEnabled \"0\"\n" +
metalnessLine +
$"\tTextureNormal \"{normal}\"\n" +
"\tg_flRoughnessScaleFactor \"1.000\"\n" +
$"\tTextureRoughness \"{roughness}\"\n" +
"\tg_vTexCoordOffset \"[0.000 0.000]\"\n" +
"\tg_vTexCoordScale \"[1.000 1.000]\"\n" +
"\tg_vTexCoordScrollSpeed \"[0.000 0.000]\"\n" +
"}\n";
}
private Editor.Asset RegisterAndCompilePreviewFile( string path )
{
try
{
var asset = AssetSystem.RegisterFile( path );
if ( asset != null && asset.CanRecompile )
asset.Compile( false );
return asset;
}
catch ( Exception ex )
{
LogPreviewWarning( $"Preview asset registration failed for {Path.GetFileName( path )}: {ex.Message}" );
return null;
}
}
private Material LoadPreviewMaterial( Editor.Asset materialAsset, string materialPath )
{
var candidates = new List<string>();
if ( materialAsset != null )
{
AddMaterialLoadCandidate( candidates, materialAsset.Path );
AddMaterialLoadCandidate( candidates, materialAsset.RelativePath );
}
AddMaterialLoadCandidate( candidates, GetLibraryAssetReferencePath( materialPath ) );
AddMaterialLoadCandidate( candidates, NormalizeMaterialPath( materialPath ) );
foreach ( var candidate in candidates )
{
try
{
var material = Material.Load( candidate );
if ( material != null && material.IsValid )
return material;
}
catch ( Exception ex )
{
LogPreviewWarning( $"Preview material load failed for {candidate}: {ex.Message}" );
}
}
return null;
}
private void AddMaterialLoadCandidate( List<string> candidates, string value )
{
if ( string.IsNullOrWhiteSpace( value ) )
return;
var normalized = NormalizeMaterialPath( value );
foreach ( var candidate in candidates )
{
if ( string.Equals( candidate, normalized, StringComparison.OrdinalIgnoreCase ) )
return;
}
candidates.Add( normalized );
}
private string GetPreviewCacheRoot()
{
return Path.Combine( GetLibraryFolder(), "Assets", "Preview", "Pbr" );
}
private string GetLibraryAssetsFolder()
{
return Path.Combine( GetLibraryFolder(), "Assets" );
}
private string GetLibraryFolder()
{
var projectRoot = Sandbox.Project.Current?.GetRootPath();
if ( string.IsNullOrWhiteSpace( projectRoot ) )
projectRoot = Environment.CurrentDirectory;
var publishedFolder = Path.Combine( projectRoot, "Libraries", "forsomethings.seamless" );
if ( Directory.Exists( publishedFolder ) )
return publishedFolder;
return Path.Combine( projectRoot, "Libraries", "SeamLess" );
}
private string GetLibraryAssetReferencePath( string path )
{
var fullPath = Path.GetFullPath( path );
var fullAssetsPath = Path.GetFullPath( GetLibraryAssetsFolder() );
var normalizedPath = NormalizeMaterialPath( fullPath );
var normalizedAssetsPath = NormalizeMaterialPath( fullAssetsPath );
if ( !normalizedAssetsPath.EndsWith( "/" ) )
normalizedAssetsPath += "/";
if ( normalizedPath.StartsWith( normalizedAssetsPath, StringComparison.OrdinalIgnoreCase ) )
return normalizedPath.Substring( normalizedAssetsPath.Length );
return normalizedPath;
}
private string NormalizeMaterialPath( string path )
{
return path.Replace( "\\", "/" ).Replace( "\"", "" );
}
private void DisposePreviewResources()
{
previewMaterial = null;
ApplyMaterialToRenderer();
ClearTemporaryPreviewFiles();
}
private void ClearTemporaryPreviewFiles()
{
DeletePreviewFiles( temporaryPreviewFiles );
temporaryPreviewFiles.Clear();
DeletePreviewFolder( temporaryPreviewFolder );
temporaryPreviewFolder = null;
}
private void DeletePreviewFiles( IEnumerable<string> files )
{
foreach ( var file in files )
{
try
{
if ( File.Exists( file ) )
File.Delete( file );
}
catch ( Exception ex )
{
LogPreviewWarning( $"Could not delete preview cache file {Path.GetFileName( file )}: {ex.Message}" );
}
}
}
private void DeletePreviewFolder( string folder )
{
try
{
if ( !string.IsNullOrWhiteSpace( folder ) && Directory.Exists( folder ) )
Directory.Delete( folder, true );
}
catch ( Exception ex )
{
LogPreviewWarning( $"Could not delete preview cache folder {Path.GetFileName( folder )}: {ex.Message}" );
}
}
private void CleanStalePreviewFolders()
{
var cacheRoot = GetPreviewCacheRoot();
if ( !Directory.Exists( cacheRoot ) )
return;
foreach ( var folder in Directory.GetDirectories( cacheRoot ) )
{
var folderName = Path.GetFileName( folder );
if ( string.Equals( folderName, previewSessionId, StringComparison.OrdinalIgnoreCase ) )
continue;
if ( !IsPreviewSessionFolderName( folderName ) )
continue;
DeletePreviewFolder( folder );
}
}
private bool IsPreviewSessionFolderName( string folderName )
{
if ( string.IsNullOrWhiteSpace( folderName ) || folderName.Length != 32 )
return false;
foreach ( var character in folderName )
{
if ( !Uri.IsHexDigit( character ) )
return false;
}
return true;
}
private void UpdateInputControls()
{
var cursorPosition = CursorPosition;
var cursorDelta = cursorPosition - lastCursorPosition;
if ( orbitControl )
{
if ( cursorDelta.Length > 0f )
{
orbitAngles.x += cursorDelta.x * 0.2f;
orbitAngles.y += cursorDelta.y * 0.2f;
orbitAngles.x = orbitAngles.x.NormalizeDegrees();
orbitAngles.y = orbitAngles.y.Clamp( -85f, 85f );
modelRotation -= cursorDelta.x * 0.08f;
}
Editor.Application.UnscaledCursorPosition = lastCursorPosition;
Cursor = CursorShape.Blank;
return;
}
if ( zoomControl )
{
if ( Math.Abs( cursorDelta.y ) > 0f )
Zoom( cursorDelta.y * 0.22f );
Editor.Application.UnscaledCursorPosition = lastCursorPosition;
Cursor = CursorShape.Blank;
return;
}
if ( panControl )
{
if ( cursorDelta.Length > 0f )
Pan( cursorDelta );
Editor.Application.UnscaledCursorPosition = lastCursorPosition;
Cursor = CursorShape.Blank;
return;
}
lastCursorPosition = cursorPosition;
Cursor = CursorShape.None;
}
private void Pan( Vector2 cursorDelta )
{
if ( !Camera.IsValid() )
return;
var scale = Math.Max( 0.08f, cameraDistance * 0.0016f );
cameraOrigin += Camera.WorldRotation.Right * cursorDelta.x * scale;
cameraOrigin += Camera.WorldRotation.Down * cursorDelta.y * scale;
}
private void Zoom( float delta )
{
cameraDistance += delta;
cameraDistance = cameraDistance.Clamp( 18f, 1200f );
}
private void ResetViewDistance()
{
var size = GetScaledModelSize().Length;
cameraDistance = Math.Max( 80f, size * 1.75f );
actualCameraDistance = cameraDistance;
}
private void UpdateCameraPosition()
{
if ( !Camera.IsValid() )
return;
Camera.WorldRotation = new Angles( orbitAngles.y, -orbitAngles.x, 0f );
actualCameraDistance = actualCameraDistance.LerpTo( cameraDistance, RealTime.Delta * 14f );
Camera.WorldPosition = cameraOrigin + GetScaledModelCenter() + Camera.WorldRotation.Backward * actualCameraDistance;
}
private Model GetModelForShape( PbrPreviewShape shape )
{
return shape switch
{
PbrPreviewShape.Cube => Model.Cube,
PbrPreviewShape.Plane => planeModel,
_ => sphereModel
};
}
private Vector3 GetModelScale( PbrPreviewShape shape )
{
if ( shape == PbrPreviewShape.Cube )
{
var cubeSize = Model.Cube?.RenderBounds.Size.Length ?? 0f;
var cubeScale = cubeSize > 0f ? 120f / cubeSize : 1f;
return Vector3.One * cubeScale;
}
return Vector3.One;
}
private Vector3 GetScaledModelSize()
{
if ( activeModel == null || !activeModel.IsValid )
return new Vector3( 64f, 64f, 64f );
return MultiplyVectors( activeModel.RenderBounds.Size, GetModelScale( previewShape ) );
}
private Vector3 GetScaledModelCenter()
{
if ( activeModel == null || !activeModel.IsValid )
return Vector3.Zero;
return MultiplyVectors( activeModel.RenderBounds.Center, GetModelScale( previewShape ) );
}
private Vector3 MultiplyVectors( Vector3 a, Vector3 b )
{
return new Vector3( a.x * b.x, a.y * b.y, a.z * b.z );
}
private void LogPreviewWarning( string message )
{
if ( string.IsNullOrWhiteSpace( message ) )
return;
WarningLogged?.Invoke( message );
Log.Warning( $"Seam-Less PBR Preview: {message}" );
}
private static Model CreatePlaneModel()
{
var material = Material.Load( "materials/dev/gray_grid_8.vmat" );
var mesh = new Mesh( material );
mesh.CreateVertexBuffer( 4, new[]
{
new Vertex( new Vector3( -80f, -80f, 0f ), Vector3.Up, Vector3.Forward, new Vector4( 0f, 0f, 0f, 0f ) ),
new Vertex( new Vector3( 80f, -80f, 0f ), Vector3.Up, Vector3.Forward, new Vector4( 2f, 0f, 0f, 0f ) ),
new Vertex( new Vector3( 80f, 80f, 0f ), Vector3.Up, Vector3.Forward, new Vector4( 2f, 2f, 0f, 0f ) ),
new Vertex( new Vector3( -80f, 80f, 0f ), Vector3.Up, Vector3.Forward, new Vector4( 0f, 2f, 0f, 0f ) )
} );
mesh.CreateIndexBuffer( 6, new[] { 0, 1, 2, 2, 3, 0 } );
mesh.Bounds = BBox.FromPositionAndSize( Vector3.Zero, 160f );
return Model.Builder.AddMesh( mesh ).Create();
}
private static Model CreateTessellatedSphereModel()
{
const int uFacets = 64;
const int vFacets = 64;
const float maxU = 4f;
const float maxV = 4f;
const float radius = 36f;
var material = Material.Load( "materials/dev/gray_grid_8.vmat" );
var mesh = new Mesh( material );
mesh.CreateVertexBuffer<Vertex>( (uFacets + 1) * (vFacets + 1) );
mesh.CreateIndexBuffer( 2 * 3 * uFacets * vFacets );
mesh.Bounds = BBox.FromPositionAndSize( Vector3.Zero, radius * 2f );
mesh.LockVertexBuffer<Vertex>( vertices =>
{
var v = 0.5f;
var index = 0;
var dU = 1f / uFacets;
var dV = 1f / vFacets;
for ( var nV = 0; nV < vFacets + 1; nV++ )
{
var u = 0f;
for ( var nU = 0; nU < uFacets + 1; nU++ )
{
var sinTheta = MathF.Sin( u * MathF.PI );
var cosTheta = MathF.Cos( u * MathF.PI );
var sinPhi = MathF.Sin( v * 2f * MathF.PI );
var cosPhi = MathF.Cos( v * 2f * MathF.PI );
var normal = new Vector3( sinTheta * cosPhi, sinTheta * sinPhi, cosTheta ).Normal;
vertices[index++] = new Vertex
{
Position = radius * normal,
Normal = normal,
Tangent = new Vector4( new Vector3( -sinPhi, cosPhi, 0f ).Normal, -1f ),
TexCoord0 = new Vector2( (v - 0.5f) * maxV, u * maxU ),
TexCoord1 = new Vector2( (v - 0.5f) * maxV, u * maxU ) * -1f,
Color = Color.White
};
u += dU;
}
v += dV;
}
} );
mesh.LockIndexBuffer( indices =>
{
var index = 0;
for ( var v = 0; v < vFacets; v++ )
{
for ( var u = 0; u < uFacets; u++ )
{
indices[index++] = v * (uFacets + 1) + u;
indices[index++] = v * (uFacets + 1) + (u + 1);
indices[index++] = (v + 1) * (uFacets + 1) + u;
indices[index++] = v * (uFacets + 1) + (u + 1);
indices[index++] = (v + 1) * (uFacets + 1) + (u + 1);
indices[index++] = (v + 1) * (uFacets + 1) + u;
}
}
} );
return Model.Builder.AddMesh( mesh ).Create();
}
}