From bf9ca14deb6ee5180f074c5cbeb3aa44f05af62f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 14 Aug 2025 22:14:03 +0200 Subject: [PATCH] Fix sampling textures in decals to use custom mip-level #3599 --- Content/Editor/MaterialTemplates/Decal.shader | 61 +++++++++++++++++-- .../Materials/DecalMaterialShader.cpp | 4 +- .../MaterialGenerator.Textures.cpp | 19 ++++-- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/Content/Editor/MaterialTemplates/Decal.shader b/Content/Editor/MaterialTemplates/Decal.shader index 06b44d498..b933fcbb3 100644 --- a/Content/Editor/MaterialTemplates/Decal.shader +++ b/Content/Editor/MaterialTemplates/Decal.shader @@ -13,7 +13,7 @@ META_CB_BEGIN(0, Data) float4x4 WorldMatrix; float4x4 InvWorld; -float4x4 SVPositionToWorld; +float4x4 SvPositionToWorld; @1META_CB_END // Use depth buffer for per-pixel decal layering @@ -27,12 +27,63 @@ struct MaterialInput float3 WorldPosition; float TwoSidedSign; float2 TexCoord; + float4 TexCoord_DDX_DDY; float3x3 TBN; float4 SvPosition; float3 PreSkinnedPosition; float3 PreSkinnedNormal; }; +// Calculates decal texcoords for a given pixel position (sampels depth buffer and projects value to decal space). +float2 SvPositionToDecalUV(float4 svPosition) +{ + float2 screenUV = svPosition.xy * ScreenSize.zw; + svPosition.z = SAMPLE_RT(DepthBuffer, screenUV).r; + float4 positionHS = mul(float4(svPosition.xyz, 1), SvPositionToWorld); + float3 positionWS = positionHS.xyz / positionHS.w; + float3 positionOS = mul(float4(positionWS, 1), InvWorld).xyz; + return positionOS.xz + 0.5f; +} + +// Manually compute ddx/ddy for decal texture cooordinates to avoid the 2x2 pixels artifacts on the edges of geometry under decal +// [Reference: https://www.humus.name/index.php?page=3D&ID=84] +float4 CalculateTextureDerivatives(float4 svPosition, float2 texCoord) +{ + float4 svDiffX = float4(1, 0, 0, 0); + float2 uvDiffX0 = texCoord - SvPositionToDecalUV(svPosition - svDiffX); + float2 uvDiffX1 = SvPositionToDecalUV(svPosition + svDiffX) - texCoord; + float2 dx = dot(uvDiffX0, uvDiffX0) < dot(uvDiffX1, uvDiffX1) ? uvDiffX0 : uvDiffX1; + + float4 svDiffY = float4(0, 1, 0, 0); + float2 uvDiffY0 = texCoord - SvPositionToDecalUV(svPosition - svDiffY); + float2 uvDiffY1 = SvPositionToDecalUV(svPosition + svDiffY) - texCoord; + float2 dy = dot(uvDiffY0, uvDiffY0) < dot(uvDiffY1, uvDiffY1) ? uvDiffY0 : uvDiffY1; + + return float4(dx, dy); +} + +// Computes the mipmap level for a specific texture dimensions to be sampled at decal texture cooordinates. +// [Reference: https://hugi.scene.org/online/coding/hugi%2014%20-%20comipmap.htm] +float CalculateTextureMipmap(MaterialInput input, float2 textureSize) +{ + float2 dx = input.TexCoord_DDX_DDY.xy * textureSize; + float2 dy = input.TexCoord_DDX_DDY.zw * textureSize; + float d = max(dot(dx, dx), dot(dy, dy)); + return (0.5 * 0.5) * log2(d); // Hardcoded half-mip rate reduction to avoid artifacts when decal is moved over dither texture +} +float CalculateTextureMipmap(MaterialInput input, Texture2D t) +{ + float2 textureSize; + t.GetDimensions(textureSize.x, textureSize.y); + return CalculateTextureMipmap(input, textureSize); +} +float CalculateTextureMipmap(MaterialInput input, TextureCube t) +{ + float2 textureSize; + t.GetDimensions(textureSize.x, textureSize.y); + return CalculateTextureMipmap(input, textureSize); +} + // Transforms a vector from tangent space to world space float3 TransformTangentVectorToWorld(MaterialInput input, float3 tangentVector) { @@ -116,7 +167,6 @@ Material GetMaterialPS(MaterialInput input) } // Input macro specified by the material: DECAL_BLEND_MODE - #define DECAL_BLEND_MODE_TRANSLUCENT 0 #define DECAL_BLEND_MODE_STAIN 1 #define DECAL_BLEND_MODE_NORMAL 2 @@ -153,7 +203,7 @@ void PS_Decal( float2 screenUV = SvPosition.xy * ScreenSize.zw; SvPosition.z = SAMPLE_RT(DepthBuffer, screenUV).r; - float4 positionHS = mul(float4(SvPosition.xyz, 1), SVPositionToWorld); + float4 positionHS = mul(float4(SvPosition.xyz, 1), SvPositionToWorld); float3 positionWS = positionHS.xyz / positionHS.w; float3 positionOS = mul(float4(positionWS, 1), InvWorld).xyz; @@ -166,8 +216,9 @@ void PS_Decal( materialInput.TexCoord = decalUVs; materialInput.TwoSidedSign = 1; materialInput.SvPosition = SvPosition; - - // Build tangent to world transformation matrix + materialInput.TexCoord_DDX_DDY = CalculateTextureDerivatives(materialInput.SvPosition, materialInput.TexCoord); + + // Calculate tangent-space float3 ddxWp = ddx(positionWS); float3 ddyWp = ddy(positionWS); materialInput.TBN[0] = normalize(ddyWp); diff --git a/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp b/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp index dc510f331..ffc8fa241 100644 --- a/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp @@ -16,7 +16,7 @@ PACK_STRUCT(struct DecalMaterialShaderData { Matrix WorldMatrix; Matrix InvWorld; - Matrix SVPositionToWorld; + Matrix SvPositionToWorld; }); DrawPass DecalMaterialShader::GetDrawModes() const @@ -67,7 +67,7 @@ void DecalMaterialShader::Bind(BindParameters& params) 0, 0, 1, 0, -1.0f, 1.0f, 0, 1); const Matrix svPositionToWorld = offsetMatrix * view.IVP; - Matrix::Transpose(svPositionToWorld, materialData->SVPositionToWorld); + Matrix::Transpose(svPositionToWorld, materialData->SvPositionToWorld); } // Bind constants diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp index 608f57ec3..6bd88f2ae 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp @@ -34,7 +34,6 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B const bool isArray = texture->Type == MaterialParameterType::GPUTextureArray; const bool isVolume = texture->Type == MaterialParameterType::GPUTextureVolume; const bool isNormalMap = texture->Type == MaterialParameterType::NormalMap; - const bool canUseSample = CanUseSample(_treeType); MaterialGraphBox* valueBox = parent->GetBox(1); // Check if has variable assigned @@ -63,6 +62,16 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B // Check if hasn't been sampled during that tree eating if (valueBox->Cache.IsInvalid()) { + bool canUseSample = CanUseSample(_treeType); + String mipLevel = TEXT("0"); + const auto layer = GetRootLayer(); + if (layer && layer->Domain == MaterialDomain::Decal && _treeType == MaterialTreeType::PixelShader) + { + // Decals use computed mip level due to ddx/ddy being unreliable + canUseSample = false; + mipLevel = String::Format(TEXT("CalculateTextureMipmap(input, {})"), texture->ShaderName); + } + // Check if use custom UVs String uv; MaterialGraphBox* uvBox = parent->GetBox(0); @@ -94,10 +103,10 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B // Sample texture if (isNormalMap) { - const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, 0).xyz"); + const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, {3}).xyz"); // Sample encoded normal map - const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv); + const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, mipLevel); const auto normalVector = writeLocal(VariantType::Float3, sampledValue, parent); // Decode normal vector @@ -123,12 +132,12 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B } else*/ { - format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, 0)"); + format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, {3})"); } } // Sample texture - String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, _ddx.Value, _ddy.Value); + String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, mipLevel); valueBox->Cache = writeLocal(VariantType::Float4, sampledValue, parent); } }