port from perforce

This commit is contained in:
2026-04-18 22:31:51 +02:00
commit 8d0ab5b7cc
8409 changed files with 3972376 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
using System;
using System.Xml.Serialization;
using Intromat.ViewModels;
namespace Intromat.Nodes.Textures
{
[XmlRoot("Blend2DColor", Namespace = _namespace)]
public class Blend2DColorModel : DxTextureModelBase
{
public enum EBlendMode
{
Multiply
}
public EBlendMode BlendMode { get; set; }
public override CodeGenNodeViewModel CreateViewModel()
{
return new Blend2DColorNode();
}
}
}

View File

@@ -0,0 +1,129 @@
using System;
using DynamicData;
using Intromat.Model.Compiler;
using Intromat.PersistentModel;
using Intromat.ViewModels;
using Intromat.ViewModels.Editors;
using Intromat.Views;
using NodeNetwork.Toolkit.ValueNode;
using ReactiveUI;
using SharpDX.D3DCompiler;
using SharpDX.Direct3D11;
using Splat;
using Buffer = SharpDX.Direct3D11.Buffer;
namespace Intromat.Nodes.Textures
{
public class Blend2DColorNode : DxTextureFilterNodeBase
{
private Buffer? _constantBuffer;
private ComputeShader? _cs;
private const string _computeShader = @"cbuffer cb
{
int m;
}
Texture2D tSource;
Texture2D tTarget;
SamplerState ss;
RWTexture2D<float4> o;
[numthreads(16,16,1)]
void main(in uint3 i : SV_DispatchThreadID)
{
float2 dim;
o.GetDimensions(dim.x, dim.y);
float2 uv = i.xy / dim;
float4 src = tSource.SampleLevel(ss, uv, 0);
float4 dst = tTarget.SampleLevel(ss, uv, 0);
switch (m)
{
case 0: o[i.xy] = src * dst; break;
}
}";
static Blend2DColorNode()
{
Locator.CurrentMutable.Register(() => new CodeGenNodeView(), typeof(IViewFor<Blend2DColorNode>));
}
public Blend2DColorNode()
{
Name = "Blend";
Inputs.Add(Target = new CodeGenInputViewModel<TextureValue?>(EPortType.Texture)
{
Name = "Target",
Group = Width.Group
});
Inputs.Add(BlendMode = new CodeGenInputViewModel<int>(EPortType.None)
{
Name = "Blend mode",
Group = Width.Group,
Editor = BlendModeEditor
});
}
public override NodeModelBase CreateModel()
{
return new Blend2DColorModel();
}
public override void SaveModel(NodeModelBase model)
{
base.SaveModel(model);
var blend2D = (Blend2DColorModel)model;
blend2D.BlendMode = (Blend2DColorModel.EBlendMode)BlendModeEditor.Value;
}
public override void LoadModel(NodeModelBase model)
{
base.LoadModel(model);
var blend2D = (Blend2DColorModel)model;
BlendModeEditor.Value = (int)blend2D.BlendMode;
}
protected override void CreateDeviceResources(Device device)
{
base.CreateDeviceResources(device);
var bufferDesc = new BufferDescription(16, ResourceUsage.Dynamic, BindFlags.ConstantBuffer, CpuAccessFlags.Write, ResourceOptionFlags.None, 0);
_constantBuffer = new Buffer(device, bufferDesc);
var flags = ShaderFlags.None;
#if DEBUG
flags |= ShaderFlags.Debug;
#endif
_cs = new ComputeShader(device, ShaderBytecode.Compile(_computeShader, "main", "cs_5_0", flags).Bytecode.Data);
}
protected override unsafe void UpdateFrame(Device device, DeviceContext context)
{
base.UpdateFrame(device, context);
var srvSource = Input.Value?.ShaderResourceView;
var srvTarget = Target.Value?.ShaderResourceView;
if (srvSource == null || srvTarget == null)
return;
context.MapSubresource(_constantBuffer, 0, MapMode.WriteDiscard, MapFlags.None, out var stream);
var ints = new Span<int>((void*)stream.DataPointer, 4);
ints[0] = BlendMode.Value;
context.UnmapSubresource(_constantBuffer, 0);
context.ComputeShader.SetConstantBuffer(0, _constantBuffer);
context.ComputeShader.SetShaderResource(0, srvSource);
context.ComputeShader.SetShaderResource(1, srvTarget);
context.ComputeShader.SetSampler(0, _samplerState);
context.ComputeShader.SetShader(_cs, null, 0);
context.ComputeShader.SetUnorderedAccessView(0, _output!.UnorderedAccessView);
context.Dispatch(_texDesc.Width / 16, _texDesc.Height / 16, 1);
context.ComputeShader.SetShaderResource(0, null);
context.ComputeShader.SetShaderResource(1, null);
context.ComputeShader.SetUnorderedAccessView(0, null);
}
public ValueNodeInputViewModel<TextureValue?> Target { get; }
public EnumEditorViewModel BlendModeEditor { get; } = new(typeof(Blend2DColorModel.EBlendMode));
public ValueNodeInputViewModel<int> BlendMode { get; }
}
}

View File

@@ -0,0 +1,39 @@
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq;
using DynamicData;
using Intromat.Model.Compiler;
using Intromat.ViewModels;
using NodeNetwork.Toolkit.ValueNode;
using SharpDX.Direct3D11;
namespace Intromat.Nodes.Textures
{
public abstract class DxTextureFilterNodeBase : DxTextureNodeBase
{
protected SamplerState? _samplerState;
protected DxTextureFilterNodeBase()
{
Inputs.Add(Input = new CodeGenInputViewModel<TextureValue?>(EPortType.Texture)
{
Name = "Input",
Group = Width.Group
});
var inputObservable = Input.ValueChanged;
WidthEditor.InputValueProvider = inputObservable.Select(input => input?.Width ?? 0);
HeightEditor.InputValueProvider = inputObservable.Select(input => input?.Height ?? 0);
}
[MemberNotNull(nameof(_samplerState))]
protected override void CreateDeviceResources(Device device)
{
base.CreateDeviceResources(device);
var samplerDesc = SamplerStateDescription.Default();
samplerDesc.Filter = Filter.MinMagMipLinear;
_samplerState = new SamplerState(device, samplerDesc);
}
public ValueNodeInputViewModel<TextureValue?> Input { get; }
}
}

View File

@@ -0,0 +1,12 @@
using Intromat.Nodes.Code;
using Intromat.PersistentModel;
namespace Intromat.Nodes.Textures
{
public abstract class DxTextureModelBase : NodeModelBase
{
public IntLiteralModel Width { get; set; } = null!;
public IntLiteralModel Height { get; set; } = null!;
}
}

View File

@@ -0,0 +1,157 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq;
using DynamicData;
using Intromat.Graphics;
using Intromat.Model.Compiler;
using Intromat.Nodes.Code;
using Intromat.PersistentModel;
using Intromat.ViewModels;
using Intromat.ViewModels.Editors;
using Intromat.ViewModels.Previews;
using NodeNetwork.Toolkit.ValueNode;
using NodeNetwork.ViewModels;
using ReactiveUI;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using Splat;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using Device = SharpDX.Direct3D11.Device;
namespace Intromat.Nodes.Textures
{
[CategoryOrder("Texture", 0)]
public abstract class DxTextureNodeBase : DxNodeBase
{
protected TextureValue? _output;
protected Texture2DDescription _texDesc;
protected bool _resourcesCreated;
protected DxTextureNodeBase()
{
var textureGroup = new EndpointGroup("Texture");
var parentObservable = this.WhenAnyObservable(vm => vm.ParentChanged).Select(parent => (CodeGenNetworkViewModel?)parent);
Inputs.Add(Width = new CodeGenInputViewModel<ITypedExpression<int>>(EPortType.None)
{
Name = "Width",
Group = textureGroup,
Editor = WidthEditor = new()
{
ParentValueProvider = parentObservable.Select(parent => parent?.DefaultWidth ?? -1),
MinValue = -16,
MaxValue = 16,
CustomValue = 0,
RelativeSource = ERelativeSource.Parent
}
});
Inputs.Add(Height = new CodeGenInputViewModel<ITypedExpression<int>>(EPortType.None)
{
Name = "Height",
Group = textureGroup,
Editor = HeightEditor = new()
{
ParentValueProvider = parentObservable.Select(parent => parent?.DefaultHeight ?? -1),
MinValue = -16,
MaxValue = 16,
CustomValue = 0,
RelativeSource = ERelativeSource.Parent
}
});
Outputs.Add(Output = new CodeGenOutputViewModel<TextureValue?>(EPortType.Texture)
{
Name = "Output",
Group = textureGroup
});
var dxHost = Locator.Current.GetService<DxHost>()!;
Output.Value = this
.WhenAnyObservable(vm => vm.ParentChanged, vm => vm.AnyInputChanged, (parent, _) => parent)
.Where(parent => parent != null)
.Throttle(TimeSpan.FromMilliseconds(1))
.ObserveOn(dxHost.RenderDispatcher)
.Do(_ => { UpdateFrame(dxHost.Device, dxHost.Device.ImmediateContext); })
.Select(_ => new TextureValue(_output!));
var dxTexturePreviewViewModel = new DxTexturePreviewViewModel(this, Output.Value);
Preview = dxTexturePreviewViewModel;
_texDesc = new Texture2DDescription
{
ArraySize = 1,
Format = Format.R16G16B16A16_Float,
BindFlags = BindFlags.ShaderResource | BindFlags.UnorderedAccess,
Usage = ResourceUsage.Default,
SampleDescription = new(1, 0)
};
}
[MemberNotNull(nameof(_output))]
protected bool EnsureBuffers(Device device)
{
return EnsureTexture(device, ref _output);
}
protected bool EnsureTexture(Device device, [NotNull] ref TextureValue? textureValue)
{
var width = ((IntLiteralValue)Width.Value).EvaluateDimension();
var height = ((IntLiteralValue)Height.Value).EvaluateDimension();
if (_texDesc.Width != width || _texDesc.Height != height || textureValue == null)
{
textureValue?.ShaderResourceView?.Resource?.Dispose();
textureValue?.ShaderResourceView?.Dispose();
textureValue?.UnorderedAccessView?.Dispose();
_texDesc.Width = width;
_texDesc.Height = height;
var texture = new Texture2D(device, _texDesc);
textureValue ??= new TextureValue();
textureValue.Width = width;
textureValue.Height = height;
textureValue.ShaderResourceView = new ShaderResourceView(device, texture);
textureValue.UnorderedAccessView = new UnorderedAccessView(device, texture);
return true;
}
return false;
}
protected virtual void UpdateFrame(Device device, DeviceContext deviceContext)
{
if (!_resourcesCreated)
{
_resourcesCreated = true;
CreateDeviceResources(device);
}
EnsureBuffers(device);
}
protected virtual void CreateDeviceResources(Device device)
{
}
public override void SaveModel(NodeModelBase model)
{
base.SaveModel(model);
var textureModel = (DxTextureModelBase)model;
textureModel.Width = WidthEditor.CreateModel();
textureModel.Height = HeightEditor.CreateModel();
}
public override void LoadModel(NodeModelBase model)
{
base.LoadModel(model);
var textureModel = (DxTextureModelBase)model;
WidthEditor.LoadModel(textureModel.Width);
HeightEditor.LoadModel(textureModel.Height);
}
public DimensionEditorViewModel WidthEditor { get; }
public ValueNodeInputViewModel<ITypedExpression<int>> Width { get; }
public DimensionEditorViewModel HeightEditor { get; }
public ValueNodeInputViewModel<ITypedExpression<int>> Height { get; }
public ValueNodeOutputViewModel<TextureValue?> Output { get; }
}
}

View File

@@ -0,0 +1,23 @@
using ReactiveUI;
using SharpDX.Direct3D11;
namespace Intromat.Nodes.Textures
{
public class SamplerDesc : ReactiveObject
{
private Filter _filter = SamplerStateDescription.Default().Filter;
private TextureAddressMode _address = SamplerStateDescription.Default().AddressU;
public Filter Filter
{
get => _filter;
set => this.RaiseAndSetIfChanged(ref _filter, value);
}
public TextureAddressMode Address
{
get => _address;
set => this.RaiseAndSetIfChanged(ref _address, value);
}
}
}

View File

@@ -0,0 +1,16 @@
using System.Xml.Serialization;
using Intromat.Nodes.Code;
using Intromat.ViewModels;
using SharpDX.Direct3D11;
namespace Intromat.Nodes.Textures
{
[XmlRoot("SamplerLiteral", Namespace = _namespace)]
public sealed class SamplerLiteralModel : LiteralModelBase<SamplerDesc>
{
public override CodeGenNodeViewModel? CreateViewModel()
{
return null;
}
}
}

View File

@@ -0,0 +1,10 @@
using Intromat.Nodes.Code;
using SharpDX.Direct3D11;
namespace Intromat.Nodes.Textures
{
public class SamplerLiteralValue : LiteralValueBase<SamplerDesc>
{
public SamplerState? SamplerState { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System.Xml.Serialization;
using Intromat.Nodes.Code;
using Intromat.ViewModels;
namespace Intromat.Nodes.Textures
{
[XmlRoot("ShaderTexture", Namespace = _namespace)]
public sealed class ShaderTextureModel : DxTextureModelBase
{
public StringLiteralModel Source { get; set; } = null!;
public LiteralModelEntry[] InputValues { get; set; } = null!;
public override CodeGenNodeViewModel CreateViewModel()
{
return new ShaderTextureNode();
}
}
}

View File

@@ -0,0 +1,477 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using DynamicData;
using Intromat.Graphics;
using Intromat.Model.Compiler;
using Intromat.Nodes.Code;
using Intromat.PersistentModel;
using Intromat.ViewModels;
using Intromat.ViewModels.Editors;
using Intromat.Views;
using NodeNetwork.ViewModels;
using ReactiveUI;
using SharpDX;
using SharpDX.D3DCompiler;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using Splat;
using Buffer = SharpDX.Direct3D11.Buffer;
namespace Intromat.Nodes.Textures
{
public class ShaderTextureNode : DxTextureNodeBase
{
private readonly List<Buffer> _constantBuffers = new();
private ComputeShader? _cs;
private byte[]? _byteCode;
private TextureValue[]? _customOutputs;
private readonly List<(NodeInputViewModel, InputDesc)> _customInputs = new();
private readonly EndpointGroup _group;
private readonly Dictionary<string, object> _inputValues = new();
public record InputDesc(int ConstantBufferIndex, int Offset, Type VariableType, string Name);
static ShaderTextureNode()
{
Locator.CurrentMutable.Register(() => new CodeGenNodeView(), typeof(IViewFor<ShaderTextureNode>));
}
public ShaderTextureNode()
{
Name = "Shader Texture";
_group = new EndpointGroup("Shader Texture");
Inputs.Add(Source = new CodeGenInputViewModel<ITypedExpression<string>>(EPortType.String)
{
Name = "Source",
Group = _group,
Editor = SourceEditor
});
var dxHost = Locator.Current.GetService<DxHost>()!;
Source.ValueChanged
.ObserveOn(dxHost.RenderDispatcher)
.Subscribe(_ =>
{
_resourcesCreated = false;
UpdateFrame(dxHost.Device, dxHost.Device.ImmediateContext);
});
}
public override NodeModelBase CreateModel()
{
return new ShaderTextureModel();
}
public override void SaveModel(NodeModelBase model)
{
base.SaveModel(model);
var shaderTexture = (ShaderTextureModel)model;
shaderTexture.Source = SourceEditor.CreateModel();
shaderTexture.InputValues = _customInputs
.Where(i => i.Item1.Editor != null)
.Select(i => new LiteralModelEntry()
{
Key = i.Item1.Name,
Literal = CreateLiteralModel((dynamic)i.Item1.Editor)
})
.ToArray();
}
private TModel CreateLiteralModel<T, TValue, TModel>(ExpressionEditorBaseViewModel<T, TValue, TModel> editor) where TValue : LiteralValueBase<T>, new() where TModel : LiteralModelBase<T>, new()
{
return editor.CreateModel();
}
public override void LoadModel(NodeModelBase model)
{
base.LoadModel(model);
var shaderTexture = (ShaderTextureModel)model;
SourceEditor.LoadModel(shaderTexture.Source);
foreach (var inputValue in shaderTexture.InputValues)
{
_inputValues[inputValue.Key] = inputValue.Literal;
}
var dxHost = Locator.Current.GetService<DxHost>()!;
_resourcesCreated = true;
CreateDeviceResources(dxHost.Device);
}
protected override void CreateDeviceResources(Device device)
{
foreach (var cb in _constantBuffers)
cb.Dispose();
_constantBuffers.Clear();
var flags = ShaderFlags.None;
#if DEBUG
flags |= ShaderFlags.Debug;
#endif
var errors = new List<string>();
var shader = Source.Value.Evaluate();
if (string.IsNullOrEmpty(shader))
{
errors.Add("Shader is empty");
}
else
{
try
{
var compilationResult = ShaderBytecode.Compile(shader, "main", "cs_5_0", flags);
if (compilationResult.HasErrors)
{
errors.Add(compilationResult.Message);
}
else
{
_byteCode = compilationResult.Bytecode.Data;
var reflection = new ShaderReflection(_byteCode);
var newInputs = new List<InputDesc>();
var newOutputs = new List<InputDesc>();
for (int i = 0; i < reflection.Description.BoundResources; ++i)
{
var resBindingDesc = reflection.GetResourceBindingDescription(i);
switch (resBindingDesc.Type)
{
case ShaderInputType.ConstantBuffer:
{
var cb = reflection.GetConstantBuffer(resBindingDesc.Name);
var cbIndex = resBindingDesc.BindPoint;
var cbSize = cb.Description.Size;
var bufferDesc = new BufferDescription(cbSize, ResourceUsage.Dynamic, BindFlags.ConstantBuffer, CpuAccessFlags.Write, ResourceOptionFlags.None, 0);
_constantBuffers.Add(new Buffer(device, bufferDesc));
for (int j = 0; j < cb.Description.VariableCount; ++j)
{
var variable = cb.GetVariable(j);
var varTypeDesc = variable.GetVariableType()
.Description;
var type = varTypeDesc.Type switch
{
ShaderVariableType.Float => typeof(float),
ShaderVariableType.Int => typeof(int),
ShaderVariableType.Bool => typeof(bool),
_ => null
};
if (type == null)
{
errors.Add($"Constant buffer {resBindingDesc.Name} at slot {cbIndex} has a variable {variable.Description.Name} with unsupported type {varTypeDesc.Type}");
continue;
}
for (int k = 0; k < varTypeDesc.ColumnCount; ++k)
{
var suffix = "" + 'X' + k;
var inputName = varTypeDesc.ColumnCount == 1 ? variable.Description.Name : $"{variable.Description.Name}.{suffix}";
newInputs.Add(new(cbIndex, variable.Description.StartOffset + k * variable.Description.Size, type, inputName));
}
}
break;
}
case ShaderInputType.Texture:
{
var texName = resBindingDesc.Name;
newInputs.Add(new(-1, resBindingDesc.BindPoint, typeof(TextureValue), texName));
break;
}
case ShaderInputType.Sampler:
{
var texName = resBindingDesc.Name;
newInputs.Add(new(-1, resBindingDesc.BindPoint, typeof(SamplerLiteralValue), texName));
break;
}
case ShaderInputType.UnorderedAccessViewRWTyped:
{
var texName = resBindingDesc.Name;
if (resBindingDesc.Dimension != ShaderResourceViewDimension.Texture2D)
{
errors.Add($"UAV {texName} at slot {resBindingDesc.BindPoint} has unsupported dimension {resBindingDesc.Dimension}");
break;
}
newOutputs.Add(new(-1, resBindingDesc.BindPoint, typeof(TextureValue), texName));
break;
}
default:
{
errors.Add($"Resource {resBindingDesc.Name} at slot {resBindingDesc.BindPoint} has unsupported type {resBindingDesc.Type}");
break;
}
}
}
if (newOutputs.Count == 0)
{
errors.Add("The shader doesn't contain any UAV suitable for the texture output");
}
Inputs.Edit(inputs =>
{
var maxSamplerStateIndex = 0;
for (var i = _customInputs.Count - 1; i >= 0; --i)
{
var existingInput = _customInputs[i];
if (newInputs.All(inputDesc => inputDesc.Name != existingInput.Item1.Name))
{
inputs.Remove(existingInput.Item1);
_customInputs.RemoveAt(i);
}
}
foreach (var newInputDesc in newInputs)
{
if (_customInputs.Any(i => i.Item2 == newInputDesc))
continue;
NodeInputViewModel newInput;
if (newInputDesc.VariableType == typeof(float))
{
newInput = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = newInputDesc.Name,
Group = _group,
Editor = new FloatExpressionEditorViewModel { MaxValue = 1 }
};
if (_inputValues.TryGetValue(newInputDesc.Name, out var inputValue) && inputValue is FloatLiteralModel model)
((FloatExpressionEditorViewModel)newInput.Editor).LoadModel(model);
}
else if (newInputDesc.VariableType == typeof(int))
{
newInput = new CodeGenInputViewModel<ITypedExpression<int>>(EPortType.Integer)
{
Name = newInputDesc.Name,
Group = _group,
Editor = new IntegerExpressionEditorViewModel { MaxValue = 100 }
};
if (_inputValues.TryGetValue(newInputDesc.Name, out var inputValue) && inputValue is IntLiteralModel model)
((IntegerExpressionEditorViewModel)newInput.Editor).LoadModel(model);
}
else if (newInputDesc.VariableType == typeof(bool))
{
newInput = new CodeGenInputViewModel<ITypedExpression<bool>>(EPortType.Boolean)
{
Name = newInputDesc.Name,
Group = _group,
Editor = new BooleanExpressionEditorViewModel()
};
if (_inputValues.TryGetValue(newInputDesc.Name, out var inputValue) && inputValue is BooleanLiteralModel model)
((BooleanExpressionEditorViewModel)newInput.Editor).LoadModel(model);
}
else if (newInputDesc.VariableType == typeof(TextureValue))
{
newInput = new CodeGenInputViewModel<TextureValue>(EPortType.Texture) { Name = newInputDesc.Name };
if (newInputDesc.Offset == 0)
{
var inputObservable = ((CodeGenInputViewModel<TextureValue>)newInput).ValueChanged;
WidthEditor.InputValueProvider = inputObservable.Select(input => input?.Width ?? 0);
HeightEditor.InputValueProvider = inputObservable.Select(input => input?.Height ?? 0);
}
}
else if (newInputDesc.VariableType == typeof(SamplerLiteralValue))
{
maxSamplerStateIndex = Math.Max(maxSamplerStateIndex, newInputDesc.Offset);
newInput = new CodeGenInputViewModel<ITypedExpression<SamplerDesc>>(EPortType.None)
{
Name = newInputDesc.Name,
Group = _group,
Editor = new SamplerEditorViewModel { CustomValue = new SamplerDesc() }
};
if (_inputValues.TryGetValue(newInputDesc.Name, out var inputValue) && inputValue is SamplerLiteralModel model)
((SamplerEditorViewModel)newInput.Editor).LoadModel(model);
}
else
{
continue;
}
_customInputs.Add((newInput, newInputDesc));
inputs.Add(newInput);
}
});
Outputs.Edit(outputs =>
{
if (outputs.Count > newOutputs.Count)
{
if (newOutputs.Count == 0)
{
if (outputs.Count > 1)
outputs.RemoveRange(1, outputs.Count - 1);
outputs[0]
.Name = "Output";
}
else
{
outputs.RemoveRange(newOutputs.Count, outputs.Count - newOutputs.Count);
}
}
var outputIndex = 0;
if (outputIndex < newOutputs.Count)
for (; outputIndex < outputs.Count; ++outputIndex)
outputs[outputIndex]
.Name = newOutputs[outputIndex]
.Name;
for (; outputIndex < newOutputs.Count; ++outputIndex)
{
var newOutputDesc = newOutputs[outputIndex];
NodeOutputViewModel newOutput;
if (newOutputDesc.VariableType == typeof(TextureValue))
newOutput = new CodeGenOutputViewModel<TextureValue>(EPortType.Texture) { Name = newOutputDesc.Name };
else
continue;
outputs.Add(newOutput);
}
_customOutputs = new TextureValue[outputs.Count];
});
}
}
catch (CompilationException e)
{
errors.Add(e.Message);
}
}
_cs?.Dispose();
_cs = errors.Count == 0 ? new ComputeShader(device, _byteCode) : null;
Errors.Edit(e =>
{
e.Clear();
e.AddRange(errors);
});
}
protected override unsafe void UpdateFrame(Device device, DeviceContext context)
{
if (!_resourcesCreated)
{
_resourcesCreated = true;
var dxHost = Locator.Current.GetService<DxHost>()!;
dxHost.MainDispatcher!.Invoke(() => CreateDeviceResources(device));
}
EnsureBuffers(device);
if (_customOutputs == null || _cs == null)
return;
_customOutputs[0] = _output;
for (int i = 1; i < Outputs.Count; ++i)
{
EnsureTexture(device, ref _customOutputs![i]);
}
var cbData = _constantBuffers.Select(cb =>
{
context.MapSubresource(cb, 0, MapMode.WriteDiscard, MapFlags.None, out var stream);
return stream.DataPointer;
}).ToArray();
int maxSrvIndex = 0;
foreach (var input in _customInputs)
{
var desc = input.Item2;
if (desc.ConstantBufferIndex >= 0)
{
var dataPtr = cbData[desc.ConstantBufferIndex] + desc.Offset;
if (desc.VariableType == typeof(float))
{
_inputValues[input.Item1.Name] = ((FloatExpressionEditorViewModel)input.Item1.Editor).CreateModel();
*(float*)dataPtr = ((CodeGenInputViewModel<ITypedExpression<float>>)input.Item1).Value.Evaluate();
}
else if (desc.VariableType == typeof(int))
{
_inputValues[input.Item1.Name] = ((IntegerExpressionEditorViewModel)input.Item1.Editor).CreateModel();
*(int*)dataPtr = ((CodeGenInputViewModel<ITypedExpression<int>>)input.Item1).Value.Evaluate();
}
else if (desc.VariableType == typeof(bool))
{
_inputValues[input.Item1.Name] = ((BooleanExpressionEditorViewModel)input.Item1.Editor).CreateModel();
*(bool*)dataPtr = ((CodeGenInputViewModel<ITypedExpression<bool>>)input.Item1).Value.Evaluate();
}
}
else
{
if (desc.VariableType == typeof(TextureValue))
{
maxSrvIndex = Math.Max(maxSrvIndex, desc.Offset);
context.ComputeShader.SetShaderResource(desc.Offset, ((CodeGenInputViewModel<TextureValue>)input.Item1).Value?.ShaderResourceView);
}
else if (desc.VariableType == typeof(SamplerLiteralValue))
{
_inputValues[input.Item1.Name] = ((SamplerEditorViewModel)input.Item1.Editor).CreateModel();
var samplerValue = (SamplerLiteralValue)((CodeGenInputViewModel<ITypedExpression<SamplerDesc>>)input.Item1).Value;
EnsureSamplerState(device, ref samplerValue);
context.ComputeShader.SetSampler(desc.Offset, samplerValue.SamplerState);
}
}
}
for (var i = 0; i < _customOutputs!.Length; i++)
{
var output = _customOutputs![i];
context.ComputeShader.SetUnorderedAccessView(i, output.UnorderedAccessView);
}
for (int i = 0; i < _constantBuffers.Count; ++i)
{
context.UnmapSubresource(_constantBuffers[i], 0);
context.ComputeShader.SetConstantBuffer(i, _constantBuffers[i]);
}
context.ComputeShader.SetShader(_cs, null, 0);
context.Dispatch(_texDesc.Width / 16, _texDesc.Height / 16, 1);
for (var i = 0; i < _customOutputs!.Length; i++)
{
context.ComputeShader.SetUnorderedAccessView(i, null);
}
for (var i = 0; i <= maxSrvIndex; i++)
{
context.ComputeShader.SetShaderResource(i, null);
}
}
private void EnsureSamplerState(Device device, ref SamplerLiteralValue samplerLiteralValue)
{
if (samplerLiteralValue.SamplerState == null || samplerLiteralValue.SamplerState.Description.AddressU != samplerLiteralValue.Value.Address || samplerLiteralValue.SamplerState.Description.Filter != samplerLiteralValue.Value.Filter)
{
var desc = SamplerStateDescription.Default();
desc.AddressU = desc.AddressV = desc.AddressW = samplerLiteralValue.Value.Address;
desc.Filter = samplerLiteralValue.Value.Filter;
samplerLiteralValue.SamplerState = new SamplerState(device, desc);
}
}
public ISourceList<string> Errors { get; } = new SourceList<string>();
public CodeGenInputViewModel<ITypedExpression<string>> Source { get; }
public StringExpressionEditorViewModel SourceEditor { get; } = new()
{
CustomValue = @"
cbuffer cb
{
float f;
}
Texture2D t0;
SamplerState ss;
RWTexture2D<float4> o;
[numthreads(16,16,1)]
void main(uint3 i : SV_DispatchThreadID)
{
float2 s;
o.GetDimensions(s.x, s.y);
float2 uv = i.xy/s;
float4 c = t0.SampleLevel(ss, uv, 0);
o[i.xy] = float4(c.xyz, 1);
}"
};
}
}

View File

@@ -0,0 +1,34 @@
using System.Xml.Serialization;
using Intromat.Nodes.Code;
using Intromat.ViewModels;
namespace Intromat.Nodes.Textures
{
[XmlRoot("Shape2DGrayscale", Namespace = _namespace)]
public class Shape2DGrayscaleModel : DxTextureModelBase
{
public enum EPattern
{
Square
}
public IntLiteralModel Tiling { get; set; } = default!;
public EPattern Pattern { get; set; }
public FloatLiteralModel Scale { get; set; } = default!;
public FloatLiteralModel SizeX { get; set; } = default!;
public FloatLiteralModel SizeY { get; set; } = default!;
public FloatLiteralModel Angle { get; set; } = default!;
public BooleanLiteralModel Rotate45 { get; set; } = default!;
public override CodeGenNodeViewModel CreateViewModel()
{
return new Shape2DGrayscaleNode();
}
}
}

View File

@@ -0,0 +1,213 @@
using System;
using System.Diagnostics.CodeAnalysis;
using DynamicData;
using Intromat.Model.Compiler;
using Intromat.PersistentModel;
using Intromat.ViewModels;
using Intromat.ViewModels.Editors;
using Intromat.Views;
using NodeNetwork.Toolkit.ValueNode;
using NodeNetwork.ViewModels;
using ReactiveUI;
using SharpDX.D3DCompiler;
using SharpDX.Direct3D11;
using Splat;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using Buffer = SharpDX.Direct3D11.Buffer;
namespace Intromat.Nodes.Textures
{
[CategoryOrder("Shape 2D", 1)]
public class Shape2DGrayscaleNode : DxTextureNodeBase
{
private Buffer? _constantBuffer;
private ComputeShader? _cs;
private const string _computeShader = @"cbuffer cb
{
int t;
int p;
float s;
float sx;
float sy;
float a;
int r;
}
float Square(float2 uv)
{
return step(uv.x, sx) * step(-sx, uv.x) * step(uv.y, sy) * step(-sy, uv.y);
}
RWTexture2D<float> o;
[numthreads(16,16,1)]
void main(in uint3 i : SV_DispatchThreadID)
{
float sa, ca;
sincos(a, sa, ca);
float2 dim;
o.GetDimensions(dim.x, dim.y);
float2 uv = i.xy / dim;
if (r > 0)
{
float f = sqrt(2)/2;
uv = uv * 2 - 1;
uv = mul(uv, float2x2(f, -f, f, f));
uv = uv * .5 + .5;
}
uv *= t;
uv = frac(uv);
uv = uv * 2 - 1;
uv /= s;
uv = mul(uv, float2x2(ca, -sa, sa, ca));
float z = 0;
switch (p)
{
case 0: z = Square(uv); break;
}
o[i.xy] = z;
}";
static Shape2DGrayscaleNode()
{
Locator.CurrentMutable.Register(() => new CodeGenNodeView(), typeof(IViewFor<Shape2DGrayscaleNode>));
}
public Shape2DGrayscaleNode()
{
Name = "Shape 2D";
var group = new EndpointGroup("Shape 2D");
Inputs.Add(Tiling = new CodeGenInputViewModel<ITypedExpression<int>>(EPortType.Integer)
{
Name = "Tiling",
Group = group,
Editor = TilingEditor
});
Inputs.Add(Pattern = new CodeGenInputViewModel<int>(EPortType.None)
{
Name = "Pattern",
Group = group,
Editor = PatternEditor
});
Inputs.Add(Scale = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = "Scale",
Group = group,
Editor = ScaleEditor
});
Inputs.Add(SizeX = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = "Size X",
Group = group,
Editor = SizeXEditor
});
Inputs.Add(SizeY = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = "Size Y",
Group = group,
Editor = SizeYEditor
});
Inputs.Add(Angle = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = "Angle",
Group = group,
Editor = AngleEditor
});
Inputs.Add(Rotate45 = new CodeGenInputViewModel<ITypedExpression<bool>>(EPortType.Float)
{
Name = "Rotate 45°",
Group = group,
Editor = Rotate45Editor
});
}
public override NodeModelBase CreateModel()
{
return new Shape2DGrayscaleModel();
}
public override void SaveModel(NodeModelBase model)
{
base.SaveModel(model);
var shape2D = (Shape2DGrayscaleModel)model;
shape2D.Tiling = TilingEditor.CreateModel();
shape2D.Pattern = (Shape2DGrayscaleModel.EPattern)PatternEditor.Value;
shape2D.Scale = ScaleEditor.CreateModel();
shape2D.SizeX = SizeXEditor.CreateModel();
shape2D.SizeY = SizeYEditor.CreateModel();
shape2D.Angle = AngleEditor.CreateModel();
shape2D.Rotate45 = Rotate45Editor.CreateModel();
}
public override void LoadModel(NodeModelBase model)
{
base.LoadModel(model);
var shape2D = (Shape2DGrayscaleModel)model;
TilingEditor.LoadModel(shape2D.Tiling);
PatternEditor.Value = (int)shape2D.Pattern;
ScaleEditor.LoadModel(shape2D.Scale);
SizeXEditor.LoadModel(shape2D.SizeX);
SizeYEditor.LoadModel(shape2D.SizeY);
AngleEditor.LoadModel(shape2D.Angle);
Rotate45Editor.LoadModel(shape2D.Rotate45);
}
protected override unsafe void UpdateFrame(Device device, DeviceContext context)
{
base.UpdateFrame(device, context);
context.MapSubresource(_constantBuffer, 0, MapMode.WriteDiscard, MapFlags.None, out var stream);
var floats = new Span<float>((void*)stream.DataPointer, 8);
var ints = new Span<int>((void*)stream.DataPointer, 8);
ints[0] = Tiling.Value.Evaluate();
ints[1] = Pattern.Value;
floats[2] = Scale.Value.Evaluate();
floats[3] = SizeX.Value.Evaluate();
floats[4] = SizeY.Value.Evaluate();
floats[5] = Angle.Value.Evaluate();
ints[6] = Rotate45.Value.Evaluate() ? 1 : 0;
context.UnmapSubresource(_constantBuffer, 0);
context.ComputeShader.SetConstantBuffer(0, _constantBuffer);
context.ComputeShader.SetShader(_cs, null, 0);
context.ComputeShader.SetUnorderedAccessView(0, _output!.UnorderedAccessView);
context.Dispatch(_texDesc.Width / 16, _texDesc.Height / 16, 1);
context.ComputeShader.SetUnorderedAccessView(0, null);
}
[MemberNotNull(nameof(_constantBuffer))]
[MemberNotNull(nameof(_cs))]
protected override void CreateDeviceResources(Device device)
{
base.CreateDeviceResources(device);
var bufferDesc = new BufferDescription(32, ResourceUsage.Dynamic, BindFlags.ConstantBuffer, CpuAccessFlags.Write, ResourceOptionFlags.None, 0);
_constantBuffer = new Buffer(device, bufferDesc);
var flags = ShaderFlags.None;
#if DEBUG
flags |= ShaderFlags.Debug;
#endif
_cs = new ComputeShader(device, ShaderBytecode.Compile(_computeShader, "main", "cs_5_0", flags).Bytecode.Data);
}
public IntegerExpressionEditorViewModel TilingEditor { get; } = new() { CustomValue = 1, MinValue = 1, MaxValue = 16 };
public ValueNodeInputViewModel<ITypedExpression<int>> Tiling { get; }
public EnumEditorViewModel PatternEditor { get; } = new(typeof(Shape2DGrayscaleModel.EPattern));
public ValueNodeInputViewModel<int> Pattern { get; }
public FloatExpressionEditorViewModel ScaleEditor { get; } = new() { CustomValue = 1, MaxValue = 1 };
public ValueNodeInputViewModel<ITypedExpression<float>> Scale { get; }
public FloatExpressionEditorViewModel SizeXEditor { get; } = new() { CustomValue = .5f, MaxValue = 1 };
public ValueNodeInputViewModel<ITypedExpression<float>> SizeX { get; }
public FloatExpressionEditorViewModel SizeYEditor { get; } = new() { CustomValue = .5f, MaxValue = 1 };
public ValueNodeInputViewModel<ITypedExpression<float>> SizeY { get; }
public FloatExpressionEditorViewModel AngleEditor { get; } = new() { MaxValue = (float)(2 * Math.PI) };
public ValueNodeInputViewModel<ITypedExpression<float>> Angle { get; }
public BooleanExpressionEditorViewModel Rotate45Editor { get; } = new();
public ValueNodeInputViewModel<ITypedExpression<bool>> Rotate45 { get; }
}
}

View File

@@ -0,0 +1,19 @@
using System.Xml.Serialization;
using Intromat.Nodes.Code;
using Intromat.ViewModels;
namespace Intromat.Nodes.Textures
{
[XmlRoot("SolidColor", Namespace = _namespace)]
public sealed class SolidColorModel : DxTextureModelBase
{
public IntLiteralModel Red { get; set; } = null!;
public IntLiteralModel Green { get; set; } = null!;
public IntLiteralModel Blue { get; set; } = null!;
public override CodeGenNodeViewModel CreateViewModel()
{
return new SolidColorNode();
}
}
}

View File

@@ -0,0 +1,90 @@
using DynamicData;
using Intromat.Model.Compiler;
using Intromat.PersistentModel;
using Intromat.ViewModels;
using Intromat.ViewModels.Editors;
using Intromat.Views;
using NodeNetwork.Toolkit.ValueNode;
using NodeNetwork.ViewModels;
using ReactiveUI;
using SharpDX.Direct3D11;
using SharpDX.Mathematics.Interop;
using Splat;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
namespace Intromat.Nodes.Textures
{
[CategoryOrder("Solid Color", 1)]
public class SolidColorNode : DxTextureNodeBase
{
static SolidColorNode()
{
Locator.CurrentMutable.Register(() => new CodeGenNodeView(), typeof(IViewFor<SolidColorNode>));
}
public SolidColorNode()
{
Name = "Solid Color";
var group = new EndpointGroup("SolidColor");
Inputs.Add(Red = new CodeGenInputViewModel<ITypedExpression<int>>(EPortType.Integer)
{
Name = "Input",
Group = group,
Editor = RedEditor
});
Inputs.Add(Green = new CodeGenInputViewModel<ITypedExpression<int>>(EPortType.Integer)
{
Name = "Green",
Group = group,
Editor = GreenEditor
});
Inputs.Add(Blue = new CodeGenInputViewModel<ITypedExpression<int>>(EPortType.Integer)
{
Name = "Blue",
Group = group,
Editor = BlueEditor
});
}
public override NodeModelBase CreateModel()
{
return new SolidColorModel();
}
public override void SaveModel(NodeModelBase model)
{
base.SaveModel(model);
var solidColor = (SolidColorModel)model;
solidColor.Red = RedEditor.CreateModel();
solidColor.Green = GreenEditor.CreateModel();
solidColor.Blue = BlueEditor.CreateModel();
}
public override void LoadModel(NodeModelBase model)
{
base.LoadModel(model);
var solidColor = (SolidColorModel)model;
RedEditor.LoadModel(solidColor.Red);
GreenEditor.LoadModel(solidColor.Green);
BlueEditor.LoadModel(solidColor.Blue);
}
protected override void UpdateFrame(Device device, DeviceContext context)
{
base.UpdateFrame(device, context);
float r = Red.Value.Evaluate() / 255.0f;
float g = Green.Value.Evaluate() / 255.0f;
float b = Blue.Value.Evaluate() / 255.0f;
context.ClearUnorderedAccessView(_output!.UnorderedAccessView, new RawVector4(r, g, b, 1.0f));
}
public IntegerExpressionEditorViewModel RedEditor { get; } = new() { MinValue = 0, MaxValue = 255};
public ValueNodeInputViewModel<ITypedExpression<int>> Red { get; }
public IntegerExpressionEditorViewModel GreenEditor { get; } = new() { MinValue = 0, MaxValue = 255};
public ValueNodeInputViewModel<ITypedExpression<int>> Green { get; }
public IntegerExpressionEditorViewModel BlueEditor { get; } = new() { MinValue = 0, MaxValue = 255};
public ValueNodeInputViewModel<ITypedExpression<int>> Blue { get; }
}
}

View File

@@ -0,0 +1,22 @@
using System.Xml.Serialization;
using Intromat.Nodes.Code;
using Intromat.ViewModels;
namespace Intromat.Nodes.Textures
{
[XmlRoot("Transform2DColor", Namespace = _namespace)]
public sealed class Transform2DColorModel : DxTextureModelBase
{
public BooleanLiteralModel Clamp { get; set; } = default!;
public FloatLiteralModel ScaleX { get; set; } = default!;
public FloatLiteralModel ScaleY { get; set; } = default!;
public FloatLiteralModel OffsetX { get; set; } = default!;
public FloatLiteralModel OffsetY { get; set; } = default!;
public FloatLiteralModel Rotate { get; set; } = default!;
public override CodeGenNodeViewModel CreateViewModel()
{
return new Transform2DColorNode();
}
}
}

View File

@@ -0,0 +1,190 @@
using System;
using System.Diagnostics.CodeAnalysis;
using DynamicData;
using Intromat.Model.Compiler;
using Intromat.PersistentModel;
using Intromat.ViewModels;
using Intromat.ViewModels.Editors;
using Intromat.Views;
using NodeNetwork.Toolkit.ValueNode;
using NodeNetwork.ViewModels;
using ReactiveUI;
using SharpDX.D3DCompiler;
using SharpDX.Direct3D11;
using Splat;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using Buffer = SharpDX.Direct3D11.Buffer;
namespace Intromat.Nodes.Textures
{
[CategoryOrder("Transform 2D", 1)]
public class Transform2DColorNode : DxTextureFilterNodeBase
{
private Buffer? _constantBuffer;
private ComputeShader? _cs;
private const string _computeShader = @"cbuffer cb
{
float2 s;
float2 p;
float r;
int c;
}
Texture2D t;
SamplerState ss;
RWTexture2D<float4> o;
[numthreads(16,16,1)]
void main(in uint3 i : SV_DispatchThreadID)
{
float sa, ca;
sincos(r, sa, ca);
float2 dim;
o.GetDimensions(dim.x, dim.y);
float2 uv = i.xy / dim;
uv -= p;
uv = uv * 2 - 1;
uv = mul(uv, float2x2(ca, -sa, sa, ca));
uv /= s;
uv = uv * .5 + .5;
if (c == 0) uv = frac(uv);
o[i.xy] = t.SampleLevel(ss, uv, 0);
}";
static Transform2DColorNode()
{
Locator.CurrentMutable.Register(() => new CodeGenNodeView(), typeof(IViewFor<Transform2DColorNode>));
}
public Transform2DColorNode()
{
Name = "Transform 2D";
var group = new EndpointGroup("Transform 2D");
Inputs.Add(Clamp = new CodeGenInputViewModel<ITypedExpression<bool>>(EPortType.Boolean)
{
Name = "Clamp",
Group = group,
Editor = ClampEditor
});
Inputs.Add(ScaleX = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = "Scale X",
Group = group,
Editor = ScaleXEditor
});
Inputs.Add(ScaleY = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = "Scale Y",
Group = group,
Editor = ScaleYEditor
});
Inputs.Add(OffsetX = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = "Offset X",
Group = group,
Editor = OffsetXEditor
});
Inputs.Add(OffsetY = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = "Offset Y",
Group = group,
Editor = OffsetYEditor
});
Inputs.Add(Rotate = new CodeGenInputViewModel<ITypedExpression<float>>(EPortType.Float)
{
Name = "Rotate",
Group = group,
Editor = RotateEditor
});
}
public override NodeModelBase CreateModel()
{
return new Transform2DColorModel();
}
public override void SaveModel(NodeModelBase model)
{
base.SaveModel(model);
var transform2DColor = (Transform2DColorModel)model;
transform2DColor.ScaleX = ScaleXEditor.CreateModel();
transform2DColor.ScaleY = ScaleYEditor.CreateModel();
transform2DColor.OffsetX = OffsetXEditor.CreateModel();
transform2DColor.OffsetY = OffsetYEditor.CreateModel();
transform2DColor.Rotate = RotateEditor.CreateModel();
transform2DColor.Clamp = ClampEditor.CreateModel();
}
public override void LoadModel(NodeModelBase model)
{
base.LoadModel(model);
var transform2DColor = (Transform2DColorModel)model;
ScaleXEditor.LoadModel(transform2DColor.ScaleX);
ScaleYEditor.LoadModel(transform2DColor.ScaleY);
OffsetXEditor.LoadModel(transform2DColor.OffsetX);
OffsetYEditor.LoadModel(transform2DColor.OffsetY);
RotateEditor.LoadModel(transform2DColor.Rotate);
ClampEditor.LoadModel(transform2DColor.Clamp);
}
[MemberNotNull(nameof(_constantBuffer))]
[MemberNotNull(nameof(_cs))]
protected override void CreateDeviceResources(Device device)
{
base.CreateDeviceResources(device);
var bufferDesc = new BufferDescription(32, ResourceUsage.Dynamic, BindFlags.ConstantBuffer, CpuAccessFlags.Write, ResourceOptionFlags.None, 0);
_constantBuffer = new Buffer(device, bufferDesc);
var flags = ShaderFlags.None;
#if DEBUG
flags |= ShaderFlags.Debug;
#endif
_cs = new ComputeShader(device, ShaderBytecode.Compile(_computeShader, "main", "cs_5_0", flags).Bytecode.Data);
}
protected override unsafe void UpdateFrame(Device device, DeviceContext context)
{
base.UpdateFrame(device, context);
var srv = Input.Value?.ShaderResourceView;
if (srv == null)
return;
context.MapSubresource(_constantBuffer, 0, MapMode.WriteDiscard, MapFlags.None, out var stream);
var floats = new Span<float>((void*)stream.DataPointer, 8);
var ints = new Span<int>((void*)stream.DataPointer, 8);
floats[0] = ScaleX.Value.Evaluate();
floats[1] = ScaleY.Value.Evaluate();
floats[2] = OffsetX.Value.Evaluate();
floats[3] = OffsetY.Value.Evaluate();
floats[4] = Rotate.Value.Evaluate();
ints[5] = Clamp.Value.Evaluate() ? 1 : 0;
context.UnmapSubresource(_constantBuffer, 0);
context.ComputeShader.SetConstantBuffer(0, _constantBuffer);
context.ComputeShader.SetShaderResource(0, srv);
context.ComputeShader.SetSampler(0, _samplerState);
context.ComputeShader.SetShader(_cs, null, 0);
context.ComputeShader.SetUnorderedAccessView(0, _output!.UnorderedAccessView);
context.Dispatch(_texDesc.Width / 16, _texDesc.Height / 16, 1);
context.ComputeShader.SetShaderResource(0, null);
context.ComputeShader.SetUnorderedAccessView(0, null);
}
public ValueNodeInputViewModel<ITypedExpression<bool>> Clamp { get; }
public BooleanExpressionEditorViewModel ClampEditor { get; } = new();
public ValueNodeInputViewModel<ITypedExpression<float>> ScaleX { get; }
public FloatExpressionEditorViewModel ScaleXEditor { get; } = new() { CustomValue = 1, MaxValue = 4 };
public ValueNodeInputViewModel<ITypedExpression<float>> ScaleY { get; }
public FloatExpressionEditorViewModel ScaleYEditor { get; } = new() { CustomValue = 1, MaxValue = 4 };
public ValueNodeInputViewModel<ITypedExpression<float>> OffsetX { get; }
public FloatExpressionEditorViewModel OffsetXEditor { get; } = new() { MinValue = -1, MaxValue = 1 };
public ValueNodeInputViewModel<ITypedExpression<float>> OffsetY { get; }
public FloatExpressionEditorViewModel OffsetYEditor { get; } = new() { MinValue = -1, MaxValue = 1 };
public ValueNodeInputViewModel<ITypedExpression<float>> Rotate { get; }
public FloatExpressionEditorViewModel RotateEditor { get; } = new() { MaxValue = (float)(2 * Math.PI) };
}
}