Introduces a new Ghost.Shader.Concept project implementing a modern, data-oriented material and shader system with: - Global/local keyword bitsets (fast O(1) ops, 64 bytes) - Multi-pass shader program and per-pass render state overrides - Thread-safe, 16-byte aligned material property blocks - Material pooling to reduce GC pressure - Batch renderer for efficient PSO grouping and async variant warmup - Full demo (Program.cs) and extensive documentation (ARCHITECTURE.md, README.md, PROJECT_SUMMARY.md) - Minor integration: new enums, doc updates, and keyword handling in existing code No breaking changes to the existing engine; all new code is isolated. This serves as a reference implementation for high-performance, extensible material/shader architectures.
230 lines
5.8 KiB
C#
230 lines
5.8 KiB
C#
namespace Ghost.Shader.Concept;
|
|
|
|
/// <summary>
|
|
/// Represents a material instance with properties, keywords, and per-pass overrides.
|
|
/// Thread-safe for property updates, optimized for rendering.
|
|
/// </summary>
|
|
public sealed class Material : IDisposable
|
|
{
|
|
private readonly ShaderProgram _shaderProgram;
|
|
private readonly MaterialPropertyBlock _propertyBlock;
|
|
private KeywordSet _localKeywords;
|
|
private readonly RenderState[] _passOverrides;
|
|
private readonly object _lock = new();
|
|
private bool _isDirty = true;
|
|
|
|
public ShaderProgram ShaderProgram => _shaderProgram;
|
|
public bool IsDirty => _isDirty;
|
|
|
|
public Material(ShaderProgram shaderProgram)
|
|
{
|
|
_shaderProgram = shaderProgram;
|
|
_propertyBlock = new MaterialPropertyBlock();
|
|
_localKeywords = new KeywordSet();
|
|
_passOverrides = new RenderState[shaderProgram.Passes.Length];
|
|
|
|
// Initialize pass overrides with shader defaults
|
|
for (int i = 0; i < _passOverrides.Length; i++)
|
|
{
|
|
_passOverrides[i] = shaderProgram.Passes[i].RenderState;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_propertyBlock?.Dispose();
|
|
}
|
|
|
|
#region Property Updates
|
|
|
|
public void SetFloat(string name, float value)
|
|
{
|
|
_propertyBlock.SetFloat(name, value);
|
|
MarkDirty();
|
|
}
|
|
|
|
public void SetVector2(string name, float x, float y)
|
|
{
|
|
_propertyBlock.SetVector2(name, x, y);
|
|
MarkDirty();
|
|
}
|
|
|
|
public void SetVector3(string name, float x, float y, float z)
|
|
{
|
|
_propertyBlock.SetVector3(name, x, y, z);
|
|
MarkDirty();
|
|
}
|
|
|
|
public void SetVector4(string name, float x, float y, float z, float w)
|
|
{
|
|
_propertyBlock.SetVector4(name, x, y, z, w);
|
|
MarkDirty();
|
|
}
|
|
|
|
public void SetInt(string name, int value)
|
|
{
|
|
_propertyBlock.SetInt(name, value);
|
|
MarkDirty();
|
|
}
|
|
|
|
public void SetMatrix4x4(string name, ReadOnlySpan<float> matrix)
|
|
{
|
|
_propertyBlock.SetMatrix4x4(name, matrix);
|
|
MarkDirty();
|
|
}
|
|
|
|
public bool TryGetFloat(string name, out float value)
|
|
{
|
|
return _propertyBlock.TryGetFloat(name, out value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Keyword Management
|
|
|
|
public void EnableKeyword(ShaderKeyword keyword)
|
|
{
|
|
if (keyword.Scope != KeywordScope.Local)
|
|
throw new ArgumentException("Only local keywords can be set on materials", nameof(keyword));
|
|
|
|
lock (_lock)
|
|
{
|
|
_localKeywords.Enable(keyword);
|
|
MarkDirty();
|
|
}
|
|
}
|
|
|
|
public void DisableKeyword(ShaderKeyword keyword)
|
|
{
|
|
if (keyword.Scope != KeywordScope.Local)
|
|
throw new ArgumentException("Only local keywords can be set on materials", nameof(keyword));
|
|
|
|
lock (_lock)
|
|
{
|
|
_localKeywords.Disable(keyword);
|
|
MarkDirty();
|
|
}
|
|
}
|
|
|
|
public bool IsKeywordEnabled(ShaderKeyword keyword)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _localKeywords.IsEnabled(keyword);
|
|
}
|
|
}
|
|
|
|
public KeywordSet GetLocalKeywords()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _localKeywords;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Pass State Overrides
|
|
|
|
public void SetPassRenderState(int passIndex, RenderState state)
|
|
{
|
|
if (passIndex < 0 || passIndex >= _passOverrides.Length)
|
|
throw new ArgumentOutOfRangeException(nameof(passIndex));
|
|
|
|
lock (_lock)
|
|
{
|
|
_passOverrides[passIndex] = state;
|
|
MarkDirty();
|
|
}
|
|
}
|
|
|
|
public void SetPassRenderState(string passName, RenderState state)
|
|
{
|
|
int index = _shaderProgram.GetPassIndex(passName);
|
|
if (index < 0)
|
|
throw new ArgumentException($"Pass '{passName}' not found in shader program", nameof(passName));
|
|
|
|
SetPassRenderState(index, state);
|
|
}
|
|
|
|
public RenderState GetPassRenderState(int passIndex)
|
|
{
|
|
if (passIndex < 0 || passIndex >= _passOverrides.Length)
|
|
throw new ArgumentOutOfRangeException(nameof(passIndex));
|
|
|
|
lock (_lock)
|
|
{
|
|
return _passOverrides[passIndex];
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Pipeline Key Generation
|
|
|
|
/// <summary>
|
|
/// Generates a pipeline key for a specific pass, combining global and local keywords.
|
|
/// </summary>
|
|
public unsafe GraphicsPipelineKey GetPipelineKey(int passIndex, in KeywordSet globalKeywords)
|
|
{
|
|
if (passIndex < 0 || passIndex >= _passOverrides.Length)
|
|
throw new ArgumentOutOfRangeException(nameof(passIndex));
|
|
|
|
KeywordSet combined;
|
|
RenderState state;
|
|
|
|
lock (_lock)
|
|
{
|
|
fixed (KeywordSet* pGlobal = &globalKeywords)
|
|
fixed (KeywordSet* pLocal = &_localKeywords)
|
|
{
|
|
combined = KeywordSet.Merge(pGlobal, pLocal);
|
|
}
|
|
state = _passOverrides[passIndex];
|
|
}
|
|
|
|
var variantKey = _shaderProgram.CreateVariantKey(combined);
|
|
var pipelineKey = new GraphicsPipelineKey(
|
|
variantKey,
|
|
state.ComputeHash(),
|
|
_shaderProgram.Passes[passIndex].PassId);
|
|
|
|
return pipelineKey;
|
|
}
|
|
|
|
#endregion
|
|
|
|
public unsafe void CopyPropertiesTo(byte* destination, int maxSize)
|
|
{
|
|
_propertyBlock.CopyTo(destination, maxSize);
|
|
}
|
|
|
|
public void ClearDirty()
|
|
{
|
|
_isDirty = false;
|
|
}
|
|
|
|
private void MarkDirty()
|
|
{
|
|
_isDirty = true;
|
|
}
|
|
|
|
public Material Clone()
|
|
{
|
|
var clone = new Material(_shaderProgram);
|
|
|
|
lock (_lock)
|
|
{
|
|
clone._propertyBlock.CopyFrom(_propertyBlock);
|
|
clone._localKeywords = _localKeywords;
|
|
|
|
for (int i = 0; i < _passOverrides.Length; i++)
|
|
{
|
|
clone._passOverrides[i] = _passOverrides[i];
|
|
}
|
|
}
|
|
|
|
return clone;
|
|
}
|
|
}
|