diff --git a/Content/Editor/MaterialTemplates/Decal.shader b/Content/Editor/MaterialTemplates/Decal.shader index 58ef9492c..06b44d498 100644 --- a/Content/Editor/MaterialTemplates/Decal.shader +++ b/Content/Editor/MaterialTemplates/Decal.shader @@ -83,6 +83,12 @@ float3 GetObjectSize(MaterialInput input) return float3(1, 1, 1); } +// Gets the current object scale (supports instancing) +float3 GetObjectScale(MaterialInput input) +{ + return float3(1, 1, 1); +} + // Get the current object random value supports instancing) float GetPerInstanceRandom(MaterialInput input) { diff --git a/Content/Editor/MaterialTemplates/Deformable.shader b/Content/Editor/MaterialTemplates/Deformable.shader index 9ff0161eb..91f96607d 100644 --- a/Content/Editor/MaterialTemplates/Deformable.shader +++ b/Content/Editor/MaterialTemplates/Deformable.shader @@ -207,6 +207,20 @@ float3 GetObjectSize(MaterialInput input) return GeometrySize * float3(world._m00, world._m11, world._m22); } +// Gets the current object scale (supports instancing) +float3 GetObjectScale(MaterialInput input) +{ + float4x4 world = input.Object.WorldMatrix; + + // Extract scale from the world matrix + float3 scale; + scale.x = length(float3(world._11, world._12, world._13)); + scale.y = length(float3(world._21, world._22, world._23)); + scale.z = length(float3(world._31, world._32, world._33)); + + return scale; +} + // Get the current object random value float GetPerInstanceRandom(MaterialInput input) { diff --git a/Content/Editor/MaterialTemplates/GUI.shader b/Content/Editor/MaterialTemplates/GUI.shader index 170d95f1a..fad7afa32 100644 --- a/Content/Editor/MaterialTemplates/GUI.shader +++ b/Content/Editor/MaterialTemplates/GUI.shader @@ -163,6 +163,12 @@ float3 GetObjectSize(MaterialInput input) return float3(1, 1, 1); } +// Gets the current object scale (supports instancing) +float3 GetObjectScale(MaterialInput input) +{ + return float3(1, 1, 1); +} + // Get the current object random value supports instancing) float GetPerInstanceRandom(MaterialInput input) { diff --git a/Content/Editor/MaterialTemplates/Surface.shader b/Content/Editor/MaterialTemplates/Surface.shader index b0465d2ff..ba3c36440 100644 --- a/Content/Editor/MaterialTemplates/Surface.shader +++ b/Content/Editor/MaterialTemplates/Surface.shader @@ -42,6 +42,12 @@ struct GeometryData nointerpolation uint ObjectIndex : TEXCOORD8; }; +float3 DecodeNormal(float4 normalMap) +{ + float2 xy = normalMap.rg * 2.0 - 1.0; + return float3(xy, sqrt(1.0 - saturate(dot(xy, xy)))); +} + // Interpolants passed from the vertex shader struct VertexOutput { @@ -232,6 +238,24 @@ float3 GetObjectSize(MaterialInput input) return input.Object.GeometrySize * float3(world._m00, world._m11, world._m22); } +// Gets the current object scale (supports instancing) +float3 GetObjectScale(MaterialInput input) +{ + float4x4 world = input.Object.WorldMatrix; + + // Get the squares of the scale factors + float scaleXSquared = dot(world[0].xyz, world[0].xyz); + float scaleYSquared = dot(world[1].xyz, world[1].xyz); + float scaleZSquared = dot(world[2].xyz, world[2].xyz); + + // Take square root to get actual scales + return float3( + sqrt(scaleXSquared), + sqrt(scaleYSquared), + sqrt(scaleZSquared) + ); +} + // Get the current object random value (supports instancing) float GetPerInstanceRandom(MaterialInput input) { diff --git a/Content/Editor/MaterialTemplates/Terrain.shader b/Content/Editor/MaterialTemplates/Terrain.shader index 9b654e8f0..abc444316 100644 --- a/Content/Editor/MaterialTemplates/Terrain.shader +++ b/Content/Editor/MaterialTemplates/Terrain.shader @@ -236,6 +236,12 @@ float3 GetObjectSize(MaterialInput input) return float3(1, 1, 1); } +// Gets the current object scale (supports instancing) +float3 GetObjectScale(MaterialInput input) +{ + return float3(1, 1, 1); +} + // Get the current object random value float GetPerInstanceRandom(MaterialInput input) { diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 9da0ec067..05f67c0e1 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -15,6 +15,29 @@ namespace FlaxEditor.Surface.Archetypes [HideInEditor] public static class Material { + /// + /// Blend modes (each enum item value maps to box ID). + /// + internal enum BlendMode + { + Normal, + Add, + Subtract, + Multiply, + Screen, + Overlay, + LinearBurn, + LinearLight, + Darken, + Lighten, + Difference, + Exclusion, + Divide, + HardLight, + PinLight, + HardMix + }; + /// /// Customized for main material node. /// @@ -1073,6 +1096,49 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Output(0, string.Empty, typeof(Float3), 4), ] }, + new NodeArchetype + { + TypeID = 50, + Title = "Shift HSV", + Description = "Modifies the HSV of a color, values are from -1:1, preserves alpha", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(175, 80), + DefaultValues = + [ + 0.0f, // For Hue (index 0) + 0.0f, // For Sat (index 1) + 0.0f, // For Val (index 2) + ], + Elements = + [ + NodeElementArchetype.Factory.Input(0, "RGBA", true, typeof(Float4), 0), // No default + NodeElementArchetype.Factory.Input(1, "Hue", true, typeof(float), 1, 0), // Uses DefaultValues[0] + NodeElementArchetype.Factory.Input(2, "Sat", true, typeof(float), 2, 1), // Uses DefaultValues[1] + NodeElementArchetype.Factory.Input(3, "Val", true, typeof(float), 3, 2), // Uses DefaultValues[2] + NodeElementArchetype.Factory.Output(0, "RGBA", typeof(Float4), 4), + ] + }, + new NodeArchetype + { + TypeID = 51, + Title = "Color Blend", + Description = "Blends two colors using various blend modes. Passes base alpha through.", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(180, 80), + DefaultValues = new object[] + { + BlendMode.Normal, // Default blend mode + 1.0f, // Default blend amount + }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(1, "Base Color", true, typeof(Float4), 0), + NodeElementArchetype.Factory.Input(2, "Blend Color", true, typeof(Float4), 1), + NodeElementArchetype.Factory.Input(3, "Intensity", true, typeof(float), 2, 1), + NodeElementArchetype.Factory.Enum(0, 0, 120, 0, typeof(BlendMode)), // Blend mode selector + NodeElementArchetype.Factory.Output(0, "Result", typeof(Float4), 3), + } + }, }; } } diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index d941da939..6fe682197 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -402,21 +402,29 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 16, - Title = "World Triplanar Texture", - Description = "Projects a texture using world-space coordinates instead of UVs.", + Title = "Triplanar Texture", + Description = "Projects a texture using world-space coordinates with triplanar mapping.", Flags = NodeFlags.MaterialGraph, - Size = new Float2(240, 60), + Size = new Float2(280, 100), DefaultValues = new object[] { - 1.0f, - 1.0f + 1.0f, // Scale + 1.0f, // Blend + Float2.Zero, // Offset + 2, // Sampler + false, // Local }, Elements = new[] { NodeElementArchetype.Factory.Input(0, "Texture", true, typeof(FlaxEngine.Object), 0), - NodeElementArchetype.Factory.Input(1, "Scale", true, typeof(Float4), 1, 0), + NodeElementArchetype.Factory.Input(1, "Scale", true, typeof(float), 1, 0), NodeElementArchetype.Factory.Input(2, "Blend", true, typeof(float), 2, 1), - NodeElementArchetype.Factory.Output(0, "Color", typeof(Float4), 3) + NodeElementArchetype.Factory.Input(3, "Offset", true, typeof(Float2), 3, 2), + NodeElementArchetype.Factory.Output(0, "Color", typeof(Float4), 5), + NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 4, "Sampler"), + NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 4 - 1, 100, 3, typeof(CommonSamplerType)), + NodeElementArchetype.Factory.Text(155, Surface.Constants.LayoutOffsetY * 4, "Local"), + NodeElementArchetype.Factory.Bool(190, Surface.Constants.LayoutOffsetY * 4, 4), } }, new NodeArchetype @@ -456,7 +464,35 @@ namespace FlaxEditor.Surface.Archetypes { NodeElementArchetype.Factory.Output(0, "UVs", typeof(Float2), 0) } - } + }, + new NodeArchetype + { + TypeID = 23, + Title = "Triplanar Normal Map", + Description = "Projects a normal map texture using world-space coordinates with triplanar mapping.", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(280, 100), + DefaultValues = new object[] + { + 1.0f, // Scale + 1.0f, // Blend + Float2.Zero, // Offset + 2, // Sampler + false, // Local + }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "Texture", true, typeof(FlaxEngine.Object), 0), + NodeElementArchetype.Factory.Input(1, "Scale", true, typeof(float), 1, 0), + NodeElementArchetype.Factory.Input(2, "Blend", true, typeof(float), 2, 1), + NodeElementArchetype.Factory.Input(3, "Offset", true, typeof(Float2), 3, 2), + NodeElementArchetype.Factory.Output(0, "Vector", typeof(Float3), 5), + NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 4, "Sampler"), + NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 4 - 1, 100, 3, typeof(CommonSamplerType)), + NodeElementArchetype.Factory.Text(155, Surface.Constants.LayoutOffsetY * 4, "Local"), + NodeElementArchetype.Factory.Bool(190, Surface.Constants.LayoutOffsetY * 4, 4), + } + }, }; } } diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index e08171ca2..4258b65cd 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1499,12 +1499,12 @@ namespace FlaxEditor.Surface.Archetypes 2, // Stop 0 - 0.1f, - Color.CornflowerBlue, + 0.05f, + Color.Black, // Stop 1 - 0.9f, - Color.GreenYellow, + 0.95f, + Color.White, // Empty stops 2-7 0.0f, Color.Black, diff --git a/Source/Editor/Surface/Elements/FloatValue.cs b/Source/Editor/Surface/Elements/FloatValue.cs index 71ee26df9..d01abcf06 100644 --- a/Source/Editor/Surface/Elements/FloatValue.cs +++ b/Source/Editor/Surface/Elements/FloatValue.cs @@ -147,6 +147,11 @@ namespace FlaxEditor.Surface.Elements { value = (double)toSet; } + else if (parentNode.GroupArchetype.GroupID != 2) + { + // Per-component editing is used only by nodes from Constant group, otherwise use float + value = toSet; + } else if (value is Vector2 asVector2) { if (arch.BoxID == 0) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index f56f7843c..f220b49ce 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -4,6 +4,7 @@ #include "MaterialGenerator.h" #include "Engine/Content/Assets/MaterialFunction.h" +#include "Engine/Visject/ShaderStringBuilder.h" void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) { @@ -643,6 +644,136 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) value = writeLocal(ValueType::Float3, String::Format(TEXT("float3({0}, {1}, {2})"), innerMask.Value, outerMask.Value, mask.Value), node); break; } + // Shift HSV + case 50: + { + const auto color = tryGetValue(node->GetBox(0), Value::One).AsFloat4(); + if (!color.IsValid()) + { + value = Value::Zero; + break; + } + const auto hue = tryGetValue(node->GetBox(1), node->Values[0]).AsFloat(); + const auto saturation = tryGetValue(node->GetBox(2), node->Values[1]).AsFloat(); + const auto val = tryGetValue(node->GetBox(3), node->Values[2]).AsFloat(); + auto result = writeLocal(Value::InitForZero(ValueType::Float4), node); + + const String hsvAdjust = ShaderStringBuilder() + .Code(TEXT(R"( + { + float3 rgb = %COLOR%.rgb; + float minc = min(min(rgb.r, rgb.g), rgb.b); + float maxc = max(max(rgb.r, rgb.g), rgb.b); + float delta = maxc - minc; + + float3 grb = float3(rgb.g - rgb.b, rgb.r - rgb.b, rgb.b - rgb.g); + float3 cmps = float3(maxc == rgb.r, maxc == rgb.g, maxc == rgb.b); + float h = dot(grb * rcp(delta), cmps); + h += 6.0 * (h < 0); + h = frac(h * (1.0/6.0) * step(0, delta) + %HUE% * 0.5); + + float s = saturate(delta * rcp(maxc + step(maxc, 0)) * (1.0 + %SATURATION%)); + float v = maxc * (1.0 + %VALUE%); + + float3 k = float3(1.0, 2.0 / 3.0, 1.0 / 3.0); + %RESULT% = float4(v * lerp(1.0, saturate(abs(frac(h + k) * 6.0 - 3.0) - 1.0), s), %COLOR%.a); + } + )")) + .Replace(TEXT("%COLOR%"), color.Value) + .Replace(TEXT("%HUE%"), hue.Value) + .Replace(TEXT("%SATURATION%"), saturation.Value) + .Replace(TEXT("%VALUE%"), val.Value) + .Replace(TEXT("%RESULT%"), result.Value) + .Build(); + _writer.Write(*hsvAdjust); + value = result; + break; + } + // Color Blend + case 51: + { + const auto baseColor = tryGetValue(node->GetBox(0), Value::One).AsFloat4(); + const auto blendColor = tryGetValue(node->GetBox(1), Value::One).AsFloat4(); + const auto blendAmount = tryGetValue(node->GetBox(2), node->Values[1]).AsFloat(); + const auto blendMode = node->Values[0].AsInt; + auto result = writeLocal(Value::InitForZero(ValueType::Float4), node); + + String blendFormula; + switch (blendMode) + { + case 0: // Normal + blendFormula = TEXT("blend"); + break; + case 1: // Add + blendFormula = TEXT("base + blend"); + break; + case 2: // Subtract + blendFormula = TEXT("base - blend"); + break; + case 3: // Multiply + blendFormula = TEXT("base * blend"); + break; + case 4: // Screen + blendFormula = TEXT("1.0 - (1.0 - base) * (1.0 - blend)"); + break; + case 5: // Overlay + blendFormula = TEXT("base <= 0.5 ? 2.0 * base * blend : 1.0 - 2.0 * (1.0 - base) * (1.0 - blend)"); + break; + case 6: // Linear Burn + blendFormula = TEXT("base + blend - 1.0"); + break; + case 7: // Linear Light + blendFormula = TEXT("blend < 0.5 ? max(base + (2.0 * blend) - 1.0, 0.0) : min(base + 2.0 * (blend - 0.5), 1.0)"); + break; + case 8: // Darken + blendFormula = TEXT("min(base, blend)"); + break; + case 9: // Lighten + blendFormula = TEXT("max(base, blend)"); + break; + case 10: // Difference + blendFormula = TEXT("abs(base - blend)"); + break; + case 11: // Exclusion + blendFormula = TEXT("base + blend - (2.0 * base * blend)"); + break; + case 12: // Divide + blendFormula = TEXT("base / (blend + 0.000001)"); + break; + case 13: // Hard Light + blendFormula = TEXT("blend <= 0.5 ? 2.0 * base * blend : 1.0 - 2.0 * (1.0 - base) * (1.0 - blend)"); + break; + case 14: // Pin Light + blendFormula = TEXT("blend <= 0.5 ? min(base, 2.0 * blend) : max(base, 2.0 * (blend - 0.5))"); + break; + case 15: // Hard Mix + blendFormula = TEXT("step(1.0 - base, blend)"); + break; + default: + blendFormula = TEXT("blend"); + break; + } + + const String blendImpl = ShaderStringBuilder() + .Code(TEXT(R"( + { + float3 base = %BASE%.rgb; + float3 blend = %BLEND%.rgb; + float alpha = %BASE%.a; + float3 final = %BLEND_FORMULA%; + %RESULT% = float4(lerp(base, final, %AMOUNT%), alpha); + } + )")) + .Replace(TEXT("%BASE%"), baseColor.Value) + .Replace(TEXT("%BLEND%"), blendColor.Value) + .Replace(TEXT("%AMOUNT%"), blendAmount.Value) + .Replace(TEXT("%BLEND_FORMULA%"), *blendFormula) + .Replace(TEXT("%RESULT%"), result.Value) + .Build(); + _writer.Write(*blendImpl); + value = result; + break; + } default: break; } diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp index 1ec9acb94..e155a576a 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp @@ -3,6 +3,26 @@ #if COMPILE_WITH_MATERIAL_GRAPH #include "MaterialGenerator.h" +#include "Engine/Visject/ShaderStringBuilder.h" + +namespace +{ + enum CommonSamplerType + { + LinearClamp = 0, + PointClamp = 1, + LinearWrap = 2, + PointWrap = 3, + TextureGroup = 4, + }; + const Char* SamplerNames[] + { + TEXT("SamplerLinearClamp"), + TEXT("SamplerPointClamp"), + TEXT("SamplerLinearWrap"), + TEXT("SamplerPointWrap"), + }; +}; MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture) { @@ -491,22 +511,6 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) // Procedural Texture Sample case 17: { - enum CommonSamplerType - { - LinearClamp = 0, - PointClamp = 1, - LinearWrap = 2, - PointWrap = 3, - TextureGroup = 4, - }; - const Char* SamplerNames[] - { - TEXT("SamplerLinearClamp"), - TEXT("SamplerPointClamp"), - TEXT("SamplerLinearWrap"), - TEXT("SamplerPointWrap"), - }; - // Get input boxes auto textureBox = node->GetBox(0); auto uvsBox = node->GetBox(1); @@ -645,9 +649,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) // Decode normal map vector if (isNormalMap) { - // TODO: maybe we could use helper function for UnpackNormalTexture() and unify unpacking? - _writer.Write(TEXT("\t{0}.xy = {0}.xy * 2.0 - 1.0;\n"), textureBox->Cache.Value); - _writer.Write(TEXT("\t{0}.z = sqrt(saturate(1.0 - dot({0}.xy, {0}.xy)));\n"), textureBox->Cache.Value); + _writer.Write(TEXT("\t{0}.xyz = UnpackNormalMap({0}.xy);\n"), textureBox->Cache.Value); } value = textureBox->Cache; @@ -690,12 +692,10 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) value = box == gradientBox ? gradient : distance; break; } - // World Triplanar Texture + // Triplanar Texture case 16: { auto textureBox = node->GetBox(0); - auto scaleBox = node->GetBox(1); - auto blendBox = node->GetBox(2); if (!textureBox->HasConnection()) { // No texture to sample @@ -704,28 +704,62 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) } const bool canUseSample = CanUseSample(_treeType); const auto texture = eatBox(textureBox->GetParent(), textureBox->FirstConnection()); - const auto scale = tryGetValue(scaleBox, node->Values[0]).AsFloat4(); - const auto blend = tryGetValue(blendBox, node->Values[1]).AsFloat(); + const auto scale = tryGetValue(node->GetBox(1), node->Values[0]).AsFloat(); + const auto blend = tryGetValue(node->GetBox(2), node->Values[1]).AsFloat(); + const auto offset = tryGetValue(node->GetBox(3), node->Values[2]).AsFloat2(); + const bool local = node->Values.Count() >= 5 ? node->Values[4].AsBool : false; + + const Char* samplerName; + const int32 samplerIndex = node->Values[3].AsInt; + if (samplerIndex == TextureGroup) + { + auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[3].AsInt); + samplerName = *textureGroupSampler.ShaderName; + } + else if (samplerIndex >= 0 && samplerIndex < ARRAY_COUNT(SamplerNames)) + { + samplerName = SamplerNames[samplerIndex]; + } + else + { + OnError(node, box, TEXT("Invalid texture sampler.")); + return; + } + auto result = writeLocal(Value::InitForZero(ValueType::Float4), node); - const String triplanarTexture = String::Format(TEXT( - " {{\n" - " float tiling = {1} * 0.001f;\n" - " float3 worldPos = (input.WorldPosition.xyz + GetLargeWorldsTileOffset(1.0f / tiling)) * tiling;\n" - " float3 normal = abs(input.TBN[2]);\n" - " normal = pow(normal, {2});\n" - " normal = normalize(normal);\n" - " {3} += {0}.{4}(SamplerLinearWrap, worldPos.yz{5}) * normal.x;\n" - " {3} += {0}.{4}(SamplerLinearWrap, worldPos.xz{5}) * normal.y;\n" - " {3} += {0}.{4}(SamplerLinearWrap, worldPos.xy{5}) * normal.z;\n" - " }}\n" - ), - texture.Value, // {0} - scale.Value, // {1} - blend.Value, // {2} - result.Value, // {3} - canUseSample ? TEXT("Sample") : TEXT("SampleLevel"), // {4} - canUseSample ? TEXT("") : TEXT(", 0") // {5} - ); + + const String triplanarTexture = ShaderStringBuilder() + .Code(TEXT(R"( + { + // Get world position and normal + float tiling = %SCALE% * 0.001f; + float3 position = ((%POSITION%) + GetLargeWorldsTileOffset(1.0f / tiling)) * tiling; + float3 normal = normalize(%NORMAL%); + + // Compute triplanar blend weights using power distribution + float3 blendWeights = pow(abs(normal), %BLEND%); + blendWeights /= dot(blendWeights, float3(1, 1, 1)); + + // Sample projections with proper scaling and offset + float4 xProjection = %TEXTURE%.%SAMPLE%(%SAMPLER%, position.yz + %OFFSET%%SAMPLE_ARGS%); + float4 yProjection = %TEXTURE%.%SAMPLE%(%SAMPLER%, position.xz + %OFFSET%%SAMPLE_ARGS%); + float4 zProjection = %TEXTURE%.%SAMPLE%(%SAMPLER%, position.xy + %OFFSET%%SAMPLE_ARGS%); + + // Blend projections using computed weights + %RESULT% = xProjection * blendWeights.x + yProjection * blendWeights.y + zProjection * blendWeights.z; + } +)")) + .Replace(TEXT("%TEXTURE%"), texture.Value) + .Replace(TEXT("%SCALE%"), scale.Value) + .Replace(TEXT("%BLEND%"), blend.Value) + .Replace(TEXT("%OFFSET%"), offset.Value) + .Replace(TEXT("%RESULT%"), result.Value) + .Replace(TEXT("%POSITION%"), local ? TEXT("TransformWorldVectorToLocal(input, input.WorldPosition - GetObjectPosition(input)) / GetObjectScale(input)") : TEXT("input.WorldPosition")) + .Replace(TEXT("%NORMAL%"), local ? TEXT("TransformWorldVectorToLocal(input, input.TBN[2])") : TEXT("input.TBN[2]")) + .Replace(TEXT("%SAMPLER%"), samplerName) + .Replace(TEXT("%SAMPLE%"), canUseSample ? TEXT("Sample") : TEXT("SampleLevel")) + .Replace(TEXT("%SAMPLE_ARGS%"), canUseSample ? TEXT("") : TEXT(", 0")) // Sample mip0 when cannot get auto ddx/ddy in Vertex Shader + .Build(); _writer.Write(*triplanarTexture); value = result; break; @@ -747,6 +781,96 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) value = output; break; } + // Triplanar Normal Map + case 23: + { + auto textureBox = node->GetBox(0); + if (!textureBox->HasConnection()) + { + // No texture to sample + value = Value::Zero; + break; + } + const bool canUseSample = CanUseSample(_treeType); + const auto texture = eatBox(textureBox->GetParent(), textureBox->FirstConnection()); + const auto scale = tryGetValue(node->GetBox(1), node->Values[0]).AsFloat(); + const auto blend = tryGetValue(node->GetBox(2), node->Values[1]).AsFloat(); + const auto offset = tryGetValue(node->GetBox(3), node->Values[2]).AsFloat2(); + const bool local = node->Values.Count() >= 5 ? node->Values[4].AsBool : false; + + const Char* samplerName; + const int32 samplerIndex = node->Values[3].AsInt; + if (samplerIndex == TextureGroup) + { + auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[3].AsInt); + samplerName = *textureGroupSampler.ShaderName; + } + else if (samplerIndex >= 0 && samplerIndex < ARRAY_COUNT(SamplerNames)) + { + samplerName = SamplerNames[samplerIndex]; + } + else + { + OnError(node, box, TEXT("Invalid texture sampler.")); + return; + } + + auto result = writeLocal(Value::InitForZero(ValueType::Float3), node); + + // Reference: https://bgolus.medium.com/normal-mapping-for-a-triplanar-shader-10bf39dca05a + const String triplanarNormalMap = ShaderStringBuilder() + .Code(TEXT(R"( + { + // Get world position and normal + float tiling = %SCALE% * 0.001f; + float3 position = ((%POSITION%) + GetLargeWorldsTileOffset(1.0f / tiling)) * tiling; + float3 normal = normalize(%NORMAL%); + + // Compute triplanar blend weights using power distribution + float3 blendWeights = pow(abs(normal), %BLEND%); + blendWeights /= dot(blendWeights, float3(1, 1, 1)); + + // Unpack normal maps + float3 tnormalX = UnpackNormalMap(%TEXTURE%.%SAMPLE%(%SAMPLER%, position.yz + %OFFSET%%SAMPLE_ARGS%).rg); + float3 tnormalY = UnpackNormalMap(%TEXTURE%.%SAMPLE%(%SAMPLER%, position.xz + %OFFSET%%SAMPLE_ARGS%).rg); + float3 tnormalZ = UnpackNormalMap(%TEXTURE%.%SAMPLE%(%SAMPLER%, position.xy + %OFFSET%%SAMPLE_ARGS%).rg); + + // Apply proper whiteout blend + normal = normalize(input.TBN[2]); + float3 axisSign = sign(normal); + float2 sumX = tnormalX.xy + normal.zy; + float2 sumY = tnormalY.xy + normal.xz; + float2 sumZ = tnormalZ.xy + normal.xy; + tnormalX = float3(sumX, sqrt(1.0 - saturate(dot(sumX, sumX))) * axisSign.x); + tnormalY = float3(sumY, sqrt(1.0 - saturate(dot(sumY, sumY))) * axisSign.y); + tnormalZ = float3(sumZ, sqrt(1.0 - saturate(dot(sumZ, sumZ))) * axisSign.z); + + // Blend the normal maps using the blend weights + float3 blendedNormal = normalize( + tnormalX.zyx * blendWeights.x + + tnormalY.xzy * blendWeights.y + + tnormalZ.xyz * blendWeights.z + ); + + // Transform to tangent space + %RESULT% = normalize(TransformWorldVectorToTangent(input, blendedNormal)); + } +)")) + .Replace(TEXT("%TEXTURE%"), texture.Value) + .Replace(TEXT("%SCALE%"), scale.Value) + .Replace(TEXT("%BLEND%"), blend.Value) + .Replace(TEXT("%OFFSET%"), offset.Value) + .Replace(TEXT("%RESULT%"), result.Value) + .Replace(TEXT("%POSITION%"), local ? TEXT("TransformWorldVectorToLocal(input, input.WorldPosition - GetObjectPosition(input)) / GetObjectScale(input)") : TEXT("input.WorldPosition")) + .Replace(TEXT("%NORMAL%"), local ? TEXT("TransformWorldVectorToLocal(input, input.TBN[2])") : TEXT("input.TBN[2]")) + .Replace(TEXT("%SAMPLER%"), samplerName) + .Replace(TEXT("%SAMPLE%"), canUseSample ? TEXT("Sample") : TEXT("SampleLevel")) + .Replace(TEXT("%SAMPLE_ARGS%"), canUseSample ? TEXT("") : TEXT(", 0")) // Sample mip0 when cannot get auto ddx/ddy in Vertex Shader + .Build(); + _writer.Write(*triplanarNormalMap); + value = result; + break; + } default: break; } diff --git a/Source/Engine/Visject/ShaderStringBuilder.cpp b/Source/Engine/Visject/ShaderStringBuilder.cpp new file mode 100644 index 000000000..84657a292 --- /dev/null +++ b/Source/Engine/Visject/ShaderStringBuilder.cpp @@ -0,0 +1,34 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +#include "ShaderStringBuilder.h" + +ShaderStringBuilder& ShaderStringBuilder::Code(const Char* shaderCode) +{ + _code = shaderCode; + return *this; +} + +ShaderStringBuilder& ShaderStringBuilder::Replace(const String& key, const String& value) +{ + _replacements.Add(Pair(key, value)); + return *this; +} + +String ShaderStringBuilder::Build() const +{ + String result = _code; + for (const auto& replacement : _replacements) + { + const auto& key = replacement.First; + const auto& value = replacement.Second; + int32 position = 0; + while ((position = result.Find(key)) != -1) + { + result = String::Format(TEXT("{0}{1}{2}"), + StringView(result.Get(), position), + value, + StringView(result.Get() + position + key.Length())); + } + } + return result; +} diff --git a/Source/Engine/Visject/ShaderStringBuilder.h b/Source/Engine/Visject/ShaderStringBuilder.h new file mode 100644 index 000000000..2492c91e9 --- /dev/null +++ b/Source/Engine/Visject/ShaderStringBuilder.h @@ -0,0 +1,21 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Types/Pair.h" +#include "Engine/Core/Types/String.h" +#include "Engine/Core/Types/StringView.h" +#include "Engine/Core/Collections/Array.h" + +// Helper utility for shader source code formatting. +class ShaderStringBuilder +{ +private: + String _code; + Array> _replacements; + +public: + ShaderStringBuilder& Code(const Char* shaderCode); + ShaderStringBuilder& Replace(const String& key, const String& value); + String Build() const; +}; diff --git a/Source/Shaders/MaterialCommon.hlsl b/Source/Shaders/MaterialCommon.hlsl index 1c77d85ae..b36c0a05a 100644 --- a/Source/Shaders/MaterialCommon.hlsl +++ b/Source/Shaders/MaterialCommon.hlsl @@ -261,6 +261,14 @@ struct GBufferOutput float4 RT3 : SV_Target4; }; +float3 UnpackNormalMap(float2 value) +{ + float3 normal; + normal.xy = value * 2.0 - 1.0; + normal.z = sqrt(saturate(1.0 - dot(normal.xy, normal.xy))); + return normal; +} + float3x3 CalcTangentBasis(float3 normal, float3 pos, float2 uv) { // References: