Merge branch 'Muzz-Triplanar-Features' into 1.10

This commit is contained in:
Muzz
2025-02-28 14:54:44 +01:00
committed by Wojtek Figat
parent dad8c0cd6b
commit 7885590593
14 changed files with 536 additions and 55 deletions

View File

@@ -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;
}

View File

@@ -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<Node>(), 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<Node>(), 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;
}

View File

@@ -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<String, String>(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;
}

View File

@@ -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<Pair<String, String>> _replacements;
public:
ShaderStringBuilder& Code(const Char* shaderCode);
ShaderStringBuilder& Replace(const String& key, const String& value);
String Build() const;
};