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 _variables = new .() ~ delete _; private TextureCollection _textureCollection ~ delete _; private BufferCollection _bufferCollection ~ _?.ReleaseRef(); public Dictionary 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 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 SetTexture(StringView name, AssetHandle texture, int32? groupTargetIndex = null) { return _textureCollection.SetTexture(name, texture, groupTargetIndex); } public Result, TextureCollection.SetTextureError> GetTexture(StringView name, out int32? groupTargetIndex) { return _textureCollection.GetTexture(name, out groupTargetIndex); } public Result 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 SetVariableRaw(StringView name, ShaderVariableType elementType, int32 rows, int32 columns, int32 arrayLength, Span 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 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(StringView name, out T value) where T : struct { Debug.Profiler.ProfileRendererFunction!(); if(_variables.TryGetValue(name, let entry)) { entry.EnsureTypeMatch(); // 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 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 _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, int32? groupTargetIndex = null, TextureFlags flags = .None) { _entries.Add(new String(name), TextureEntry() { TextureHandle = texture, groupTarget = groupTargetIndex, Dimension = dimension, TextureSlot = slot, Flags = flags }); } public Result SetTexture(StringView name, AssetHandle 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, 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 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 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 SetTexturePlatform(); public Dictionary.Enumerator GetEnumerator() { return _entries.GetEnumerator(); } }