mirror of
https://github.com/aharabada/glitchy-engine-beef.git
synced 2026-09-05 13:01:52 +00:00
476 lines
13 KiB
Beef
476 lines
13 KiB
Beef
using GlitchyEngine.Content;
|
|
using GlitchyEngine.Core;
|
|
using GlitchyEngine.Math;
|
|
using System;
|
|
using System.Collections;
|
|
|
|
using internal GlitchyEngine.Renderer;
|
|
|
|
namespace GlitchyEngine.Renderer;
|
|
|
|
public class Material : Asset
|
|
{
|
|
/**
|
|
* If true, this material is a runtime instance and can be changed by scripts.
|
|
* If false, it is the actual material from the harddrive and should not be modified by scripts.
|
|
* If a script tries to modify it, a runtime instance will be created.
|
|
*/
|
|
private bool _isRuntimeInstance = false;
|
|
|
|
private Material _parent ~ _?.ReleaseRef();
|
|
|
|
private Effect _effect ~ _?.ReleaseRef();
|
|
|
|
private Dictionary<StringView, BufferVariable> _variables = new .() ~ delete _;
|
|
|
|
private TextureCollection _textureCollection ~ delete _;
|
|
|
|
private BufferCollection _bufferCollection ~ _?.ReleaseRef();
|
|
|
|
public Dictionary<StringView, BufferVariable> Variables => _variables;
|
|
public TextureCollection Textures => _textureCollection;
|
|
|
|
public Effect Effect
|
|
{
|
|
get => _effect;
|
|
set
|
|
{
|
|
SetReference!(_effect, value);
|
|
|
|
if (_effect == null)
|
|
return;
|
|
|
|
Init();
|
|
}
|
|
}
|
|
|
|
// TODO: allow changing
|
|
public Material Parent => _parent;
|
|
|
|
public bool IsRuntimeInstance => _isRuntimeInstance;
|
|
|
|
public this()
|
|
{
|
|
|
|
}
|
|
|
|
public this(Effect effect)
|
|
{
|
|
Effect = effect;
|
|
}
|
|
|
|
public this(Material parentMaterial, bool isRuntimeInstance)
|
|
{
|
|
_parent = parentMaterial..AddRef();
|
|
Effect = _parent.Effect;
|
|
_isRuntimeInstance = isRuntimeInstance;
|
|
}
|
|
|
|
private void Init()
|
|
{
|
|
InitBuffers();
|
|
InitTextures();
|
|
}
|
|
|
|
private void InitBuffers()
|
|
{
|
|
_variables.Clear();
|
|
_bufferCollection?.ReleaseRef();
|
|
_bufferCollection = new BufferCollection();
|
|
|
|
void InitVariables(ConstantBuffer buffer)
|
|
{
|
|
for (BufferVariable variable in buffer.Variables)
|
|
{
|
|
if (_variables.ContainsKey(variable.Name))
|
|
{
|
|
// TODO: Handle overlapping variable names?
|
|
Log.EngineLogger.Error("Variable with same name already added to Material, skipping...");
|
|
continue;
|
|
}
|
|
|
|
_variables.Add(variable.Name, variable);
|
|
}
|
|
}
|
|
|
|
BufferCollection parentBuffers = _parent?._bufferCollection ?? _effect.Buffers;
|
|
|
|
for (let (bufferName, engineBufferName, buffer) in parentBuffers)
|
|
{
|
|
if (buffer == null)
|
|
continue;
|
|
|
|
if (ConstantBuffer parentConstBuffer = buffer as ConstantBuffer)
|
|
{
|
|
using (OverridingConstantBuffer childConstBuffer = new OverridingConstantBuffer(parentConstBuffer))
|
|
{
|
|
_bufferCollection.Add(@bufferName.Index, childConstBuffer.Name, engineBufferName, childConstBuffer);
|
|
InitVariables(childConstBuffer);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.EngineLogger.Error("Found buffer in constant buffer collection that is not a constant buffer.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InitTextures()
|
|
{
|
|
delete _textureCollection;
|
|
_textureCollection = new TextureCollection(_parent?._textureCollection);
|
|
|
|
if (_parent == null)
|
|
{
|
|
for(let (name, effectTexture) in _effect.Textures)
|
|
{
|
|
// TODO: We need to be able to define default textures in the shader.
|
|
// At least things like "Black", "White", "Normal"
|
|
// At best whole paths. Shouldn't be that hard to do...
|
|
AssetHandle<Texture> textureHandle = .Invalid;
|
|
int32? groupTarget = null;
|
|
|
|
if (textureHandle.IsValid)
|
|
{
|
|
if (textureHandle.Dimension != effectTexture.TextureDimension)
|
|
textureHandle = .Invalid;
|
|
}
|
|
|
|
int32 textureSlot = -1;
|
|
|
|
// TODO: This is a hack, because PsSlot and VsSlot actually break because they are references to dictionary items which sometimes get reallocated...
|
|
// But it is enough for now to detect if the slot exists.
|
|
if (effectTexture.PsSlot != null)
|
|
{
|
|
textureSlot = (.)_effect.PixelShader.Textures[name].Index;
|
|
}
|
|
else if (effectTexture.VsSlot != null)
|
|
{
|
|
textureSlot = (.)_effect.VertexShader.Textures[name].Index;
|
|
}
|
|
|
|
Log.EngineLogger.AssertDebug(textureSlot != -1);
|
|
|
|
_textureCollection.AddTexture(name, effectTexture.TextureDimension, textureSlot, textureHandle, groupTarget);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(let (name, entry) in _parent._textureCollection.[Friend]_entries)
|
|
{
|
|
TextureCollection.TextureFlags flags = .Unset;
|
|
|
|
if (entry.Flags.HasFlag(.Locked))
|
|
{
|
|
flags |= .Locked | .Readonly;
|
|
}
|
|
|
|
_textureCollection.AddTexture(name, entry.Dimension, entry.TextureSlot, .Invalid, null, flags);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Binds the materials Shaders and Parameters to the given context.
|
|
*/
|
|
public void Bind()
|
|
{
|
|
Debug.Profiler.ProfileRendererFunction!();
|
|
|
|
for (let (bufferName, engineBufferName, buffer) in _bufferCollection)
|
|
{
|
|
if (let cbuffer = buffer as ConstantBuffer)
|
|
{
|
|
TrySilent!(cbuffer.Apply());
|
|
}
|
|
}
|
|
|
|
_effect.Bind();
|
|
|
|
RenderCommand.BindConstantBuffers(_bufferCollection);
|
|
|
|
_textureCollection.Bind();
|
|
|
|
}
|
|
|
|
/** @brief Sets a texture of the material.
|
|
* @param name The name of the texture to set.
|
|
* @param texture The texture to bind to the effect.
|
|
*/
|
|
public Result<void, TextureCollection.SetTextureError> SetTexture(StringView name, AssetHandle<Texture> texture, int32? groupTargetIndex = null)
|
|
{
|
|
return _textureCollection.SetTexture(name, texture, groupTargetIndex);
|
|
}
|
|
|
|
public Result<AssetHandle<Texture>, TextureCollection.SetTextureError> GetTexture(StringView name, out int32? groupTargetIndex)
|
|
{
|
|
return _textureCollection.GetTexture(name, out groupTargetIndex);
|
|
}
|
|
|
|
public Result<void, TextureCollection.SetTextureError> ResetTexture(StringView name)
|
|
{
|
|
return _textureCollection.ResetTexture(name);
|
|
}
|
|
|
|
private mixin SetVariable(StringView name, var value)
|
|
{
|
|
if(_variables.TryGetValue(name, let entry))
|
|
{
|
|
entry.SetData(value);
|
|
}
|
|
}
|
|
|
|
public enum SetVariableError
|
|
{
|
|
VariableNotFound,
|
|
ElementTypeMismatch,
|
|
MatrixDimensionMismatch,
|
|
ProvidedBufferTooShort,
|
|
}
|
|
|
|
internal Result<void, SetVariableError> SetVariableRaw(StringView name, ShaderVariableType elementType, int32 rows, int32 columns, int32 arrayLength, Span<uint8> rawData)
|
|
{
|
|
if(!_variables.TryGetValue(name, let variable))
|
|
{
|
|
return .Err(.VariableNotFound);
|
|
}
|
|
|
|
let matchResult = variable.CheckTypematch(rows, columns, elementType);
|
|
|
|
switch (matchResult)
|
|
{
|
|
case .Ok:
|
|
// Nothing went wrong.
|
|
case .ElementTypeMismatch:
|
|
return .Err(.ElementTypeMismatch);
|
|
case .MatrixDimensionMismatch:
|
|
return .Err(.MatrixDimensionMismatch);
|
|
}
|
|
|
|
if (rawData.Length < variable._sizeInBytes)
|
|
return .Err(.ProvidedBufferTooShort);
|
|
|
|
// TODO: This method is very stupid atm.
|
|
// We could e.g. allow array length mismatches by simply copying the smaller amount of elements.
|
|
variable.SetRawData(rawData.Ptr);
|
|
|
|
return .Ok;
|
|
}
|
|
|
|
public Result<void, SetVariableError> ResetVariable(StringView name)
|
|
{
|
|
if(!_variables.TryGetValue(name, let variable))
|
|
{
|
|
return .Err(.VariableNotFound);
|
|
}
|
|
|
|
variable.IsUnset = true;
|
|
|
|
return .Ok;
|
|
}
|
|
|
|
public void SetVariable(StringView name, bool value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, bool2 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, bool3 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, bool4 value) => SetVariable!(name, value);
|
|
|
|
public void SetVariable(StringView name, int32 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, int2 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, int3 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, int4 value) => SetVariable!(name, value);
|
|
|
|
public void SetVariable(StringView name, uint32 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, uint2 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, uint3 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, uint4 value) => SetVariable!(name, value);
|
|
|
|
public void SetVariable(StringView name, float value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, float2 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, float3 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, float4 value) => SetVariable!(name, value);
|
|
|
|
public void SetVariable(StringView name, Color value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, ColorRGB value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, ColorRGBA value) => SetVariable!(name, value);
|
|
|
|
public void SetVariable(StringView name, Matrix3x3 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, Matrix4x3 value) => SetVariable!(name, value);
|
|
public void SetVariable(StringView name, Matrix value) => SetVariable!(name, value);
|
|
|
|
public void GetVariable<T>(StringView name, out T value) where T : struct
|
|
{
|
|
Debug.Profiler.ProfileRendererFunction!();
|
|
|
|
if(_variables.TryGetValue(name, let entry))
|
|
{
|
|
entry.EnsureTypeMatch<T>();
|
|
|
|
// TODO: This obviously breaks for all cases where a custom SetData was necessary.
|
|
value = *(T*)entry.firstByte;
|
|
}
|
|
else
|
|
{
|
|
value = ?;
|
|
Log.EngineLogger.Assert(false, scope $"The effect doesn't contain a variable named \"{name}\"");
|
|
}
|
|
}
|
|
}
|
|
|
|
public class TextureCollection : IEnumerable<(String key, TextureEntry value)>
|
|
{
|
|
public enum TextureFlags
|
|
{
|
|
None = 0x0,
|
|
/// The texture slot is dirty and needs to be sent do the GPU.
|
|
Dirty = 0x1,
|
|
/// The texture slot is locked, its value cannot be overwritten in child materials.
|
|
Locked = 0x2,
|
|
/// (For inherited textures only) The texture slot doesn't override the value set in the parent material.
|
|
Unset = 0x4,
|
|
/// The texture slots value cannot be changed. (i.e. it is locked in the parent material)
|
|
Readonly = 0x8
|
|
}
|
|
|
|
public struct TextureEntry
|
|
{
|
|
public AssetHandle<Texture> TextureHandle;
|
|
public int32? groupTarget;
|
|
public TextureDimension Dimension;
|
|
public TextureFlags Flags;
|
|
public int32 TextureSlot;
|
|
}
|
|
|
|
protected int _generation = 0;
|
|
protected int _parentGeneration = 0;
|
|
private TextureCollection _parent;
|
|
|
|
private Dictionary<String, TextureEntry> _entries = new .() ~ DeleteDictionaryAndKeys!(_);
|
|
|
|
public enum SetTextureError
|
|
{
|
|
TextureNotFound,
|
|
DimensionMismatch,
|
|
TextureSlotReadonly
|
|
}
|
|
|
|
public this(TextureCollection parent = null)
|
|
{
|
|
_parent = parent;
|
|
}
|
|
|
|
internal void AddTexture(StringView name, TextureDimension dimension, int32 slot, AssetHandle<Texture> texture, int32? groupTargetIndex = null, TextureFlags flags = .None)
|
|
{
|
|
_entries.Add(new String(name),
|
|
TextureEntry() {
|
|
TextureHandle = texture,
|
|
groupTarget = groupTargetIndex,
|
|
Dimension = dimension,
|
|
TextureSlot = slot,
|
|
Flags = flags
|
|
});
|
|
}
|
|
|
|
public Result<void, SetTextureError> SetTexture(StringView name, AssetHandle<Texture> texture, int32? groupTargetIndex = null)
|
|
{
|
|
if (_entries.TryGetRefAlt(name, ?, let entry))
|
|
{
|
|
if (entry.Flags.HasFlag(.Readonly))
|
|
{
|
|
return .Err(.TextureSlotReadonly);
|
|
}
|
|
|
|
Texture textureAsset = texture.Get();
|
|
|
|
if (textureAsset != null && textureAsset.Dimension != entry.Dimension)
|
|
{
|
|
return .Err(.DimensionMismatch);
|
|
}
|
|
|
|
entry.TextureHandle = texture;
|
|
|
|
// TODO: Assert group target index
|
|
// We have to figure out how to properly use/pass them anyway
|
|
|
|
entry.groupTarget = groupTargetIndex;
|
|
|
|
Enum.ClearFlag(ref entry.Flags, .Unset);
|
|
Enum.SetFlag(ref entry.Flags, .Dirty);
|
|
|
|
return .Ok;
|
|
}
|
|
|
|
return .Err(.TextureNotFound);
|
|
}
|
|
|
|
public Result<AssetHandle<Texture>, SetTextureError> GetTexture(StringView name, out int32? groupTargetIndex)
|
|
{
|
|
if (_entries.TryGetRefAlt(name, ?, let entry))
|
|
{
|
|
if (entry.Flags.HasFlag(.Unset) && _parent != null)
|
|
{
|
|
return _parent.GetTexture(name, out groupTargetIndex);
|
|
}
|
|
|
|
groupTargetIndex = entry.groupTarget;
|
|
|
|
return entry.TextureHandle;
|
|
}
|
|
|
|
groupTargetIndex = -1;
|
|
|
|
return .Err(.TextureNotFound);
|
|
}
|
|
|
|
public Result<void, SetTextureError> ResetTexture(StringView name)
|
|
{
|
|
if (_entries.TryGetRefAlt(name, ?, let entry))
|
|
{
|
|
if (entry.Flags.HasFlag(.Readonly))
|
|
{
|
|
return .Err(.TextureSlotReadonly);
|
|
}
|
|
|
|
entry.TextureHandle = .Invalid;
|
|
entry.groupTarget = null;
|
|
|
|
Enum.SetFlag(ref entry.Flags, .Unset | .Dirty);
|
|
|
|
return .Ok;
|
|
}
|
|
|
|
return .Err(.TextureNotFound);
|
|
}
|
|
|
|
public void Bind()
|
|
{
|
|
for (var (name, entry) in _entries)
|
|
{
|
|
AssetHandle<Texture> handle = entry.TextureHandle;
|
|
TextureCollection parentCollection = _parent;
|
|
TextureFlags flags = entry.Flags;
|
|
|
|
// If necessary, look for a texture in the parent collection
|
|
while (flags.HasFlag(.Unset) && parentCollection != null)
|
|
{
|
|
TextureEntry parentEntry = parentCollection._entries[name];
|
|
handle = parentEntry.TextureHandle;
|
|
parentCollection = parentCollection._parent;
|
|
flags = parentEntry.Flags;
|
|
}
|
|
|
|
Texture texture = handle.Get();
|
|
|
|
using (let viewBinding = texture?.GetViewBinding())
|
|
{
|
|
RenderCommand.BindTexture(viewBinding, entry.TextureSlot, .All);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected extern Result<void> SetTexturePlatform();
|
|
|
|
public Dictionary<String, TextureEntry>.Enumerator GetEnumerator()
|
|
{
|
|
return _entries.GetEnumerator();
|
|
}
|
|
} |