Files
2026-07-13 13:13:17 +08:00

256 lines
10 KiB
C#

using System.Diagnostics;
using T3.Core.DataTypes.ShaderGraph;
using T3.Core.Utils;
// ReSharper disable UnusedMember.Local
namespace Lib.field.combine;
[Guid("82270977-07b5-4d86-8544-5aebc638d46c")]
internal sealed class CombineSDF : Instance<CombineSDF>
,IGraphNodeOp
{
[Output(Guid = "db0bbde0-18b6-4c53-8cf7-a294177d2089")]
public readonly Slot<ShaderGraphNode> Result = new();
public CombineSDF()
{
ShaderNode = new ShaderGraphNode(this, InputFields);
Result.UpdateAction += Update;
Result.Value = ShaderNode;
}
private void Update(EvaluationContext context)
{
var combineMethod = CombineMethod.GetEnumValue<CombineMethods>(context);
if (combineMethod != _combineMethod)
{
_combineMethod = combineMethod;
ShaderNode.FlagCodeChanged();
}
ShaderNode.Update(context);
// Get all parameters to clear operator dirty flag
InputFields.DirtyFlag.Clear();
}
void IGraphNodeOp.AddDefinitions(CodeAssembleContext c)
{
c.Globals[nameof(ShaderGraphIncludes.Common)] = ShaderGraphIncludes.Common;
c.Globals[nameof(ShaderGraphIncludes.CommonHgSdf)] = ShaderGraphIncludes.CommonHgSdf;
c.Globals[nameof(ShaderGraphIncludes.GetColorBlendFactor)] = ShaderGraphIncludes.GetColorBlendFactor;
// Register global method
switch (_combineMethod)
{
case CombineMethods.UnionSmooth:
c.Globals["fOpSmoothUnion"]
= """
float fOpSmoothUnion(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return min(a, b) - (h * h) / (4.0 * k);
};
""";
break;
case CombineMethods.UnionChamfer:
c.Globals["fOpUnionChamfer"]
= """
// The "Chamfer" flavour makes a 45-degree chamfered edge (the diagonal of a square of size <r>):
float fOpUnionChamfer(float a, float b, float r) {
return min(min(a, b), (a - r + b)*sqrt(0.5));
}
""";
break;
case CombineMethods.IntersectChamfer:
case CombineMethods.CutOutChamfer:
c.Globals["fOpIntersectionChamfer"]
= """
// Intersection has to deal with what is normally the inside of the resulting object
// when using union, which we normally don't care about too much. Thus, intersection
// implementations sometimes differ from union implementations.
float fOpIntersectionChamfer(float a, float b, float r) {
return max(max(a, b), (a + r + b)*sqrt(0.5));
}
""";
c.Globals["fOpDifferenceChamfer"]
= """
// Difference can be built from Intersection or Union:
float fOpDifferenceChamfer (float a, float b, float r) {
return fOpIntersectionChamfer(a, -b, r);
}
""";
break;
case CombineMethods.UnionRound:
c.Globals["fOpUnionRound"]
= """
// The "Round" variant uses a quarter-circle to join the two objects smoothly:
float fOpUnionRound(float a, float b, float r) {
float2 u = max(float2(r - a,r - b), 0);
return max(r, min (a, b)) - length(u);
}
""";
break;
case CombineMethods.IntersectRound:
case CombineMethods.CutOutRound:
c.Globals["fOpIntersectionRound"]
= """
float fOpIntersectionRound(float a, float b, float r) {
float2 u = max(float2(r + a,r + b), 0);
return min(-r, max (a, b)) + length(u);
}
""";
c.Globals["fOpDifferenceRound"]
= """
float fOpDifferenceRound (float a, float b, float r) {
return fOpIntersectionRound(a, -b, r);
}
""";
break;
// Similar to fOpUnionRound, but more lipschitz-y at acute angles
// (and less so at 90 degrees). Useful when fudging around too much
// by MediaMolecule, from Alex Evans' siggraph slides
case CombineMethods.UnionSoft:
c.Globals["fOpUnionSoft"] = """
float fOpUnionSoft(float a, float b, float r) {
float e = max(r - abs(a - b), 0);
return min(a, b) - e*e*0.25/r;
}
""";
break;
// produces a cylindrical pipe that runs along the intersection.
// No objects remain, only the pipe. This is not a boolean operator.
case CombineMethods.Pipe:
c.Globals["fOpPipe"] = """
float fOpPipe(float a, float b, float r) {
return length(float2(a, b)) - r;
}
""";
break;
// first object gets a v-shaped engraving where it intersects the second
case CombineMethods.Engrave:
c.Globals["fOpEngrave"] = """
float fOpEngrave(float a, float b, float r) {
return max(a, (a + r - abs(b))*sqrt(0.5));
}
""";
break;
}
}
void IGraphNodeOp.GetPostShaderCode(CodeAssembleContext cac, int inputIndex)
{
// Just pass along subcontext if not enough connected fields...
if (ShaderNode.InputNodes.Count <= 1)
{
cac.AppendCall("// skipping combine with single or no input...");
return;
}
Debug.Assert(cac.ContextIdStack.Count >= 2);
var contextId = cac.ContextIdStack[^2];
var subContextId = cac.ContextIdStack[^1];
if (inputIndex == 0)
{
// Keep initial value
cac.AppendCall($"f{contextId} = f{subContextId};");
}
else
{
cac.AppendCall($"f{contextId}.rgb = lerp(f{contextId}.rgb, f{subContextId}.rgb, GetColorBlendFactor(f{contextId}.w, f{subContextId}.w, {ShaderNode}K ));");
// Combine initial value with new value...
switch (_combineMethod)
{
case CombineMethods.Union:
cac.AppendCall($"f{contextId}.w = min(f{contextId}.w, f{subContextId}.w);");
break;
case CombineMethods.Intersect:
cac.AppendCall($"f{contextId}.w = max(f{contextId}.w, f{subContextId}.w);");
break;
case CombineMethods.UnionSmooth:
cac.AppendCall($"f{contextId}.w = fOpSmoothUnion(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
case CombineMethods.CutOut:
cac.AppendCall($"f{contextId}.w = max(f{contextId}.w, -f{subContextId}.w);");
break;
case CombineMethods.UnionChamfer:
cac.AppendCall($"f{contextId}.w = fOpUnionChamfer(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
case CombineMethods.IntersectChamfer:
cac.AppendCall($"f{contextId}.w = fOpIntersectionChamfer(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
case CombineMethods.IntersectRound:
cac.AppendCall($"f{contextId}.w = fOpIntersectionRound(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
case CombineMethods.CutOutChamfer:
cac.AppendCall($"f{contextId}.w = fOpDifferenceChamfer(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
case CombineMethods.UnionRound:
cac.AppendCall($"f{contextId}.w = fOpUnionRound(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
case CombineMethods.CutOutRound:
cac.AppendCall($"f{contextId}.w = fOpDifferenceRound(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
case CombineMethods.UnionSoft:
cac.AppendCall($"f{contextId}.w = fOpUnionSoft(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
case CombineMethods.Pipe:
cac.AppendCall($"f{contextId}.w = fOpPipe(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
case CombineMethods.Engrave:
cac.AppendCall($"f{contextId}.w = fOpEngrave(f{contextId}.w, f{subContextId}.w, {ShaderNode}K);");
break;
}
}
}
public ShaderGraphNode ShaderNode { get; }
private CombineMethods _combineMethod;
private enum CombineMethods
{
Union,
UnionSoft,
UnionRound,
UnionChamfer,
UnionSmooth,
CutOut,
CutOutRound,
CutOutChamfer,
Intersect,
IntersectRound,
IntersectChamfer,
Pipe,
Engrave,
}
[Input(Guid = "7248C680-7279-4C1D-B968-3864CB849C77")]
public readonly MultiInputSlot<ShaderGraphNode> InputFields = new();
[GraphParam]
[Input(Guid = "9E4F5916-722D-4C4B-B1CA-814958A5B836")]
public readonly InputSlot<float> K = new();
[Input(Guid = "4648E514-B48C-4A98-A728-3EBF9BCFA0B7", MappedType = typeof(CombineMethods))]
public readonly InputSlot<int> CombineMethod = new();
}