// Copyright (c) Wojciech Figat. All rights reserved. #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) { ASSERT(texture && box); // Cache data const auto parent = box->GetParent>(); const bool isCubemap = texture->Type == MaterialParameterType::CubeTexture; 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 if (texture->Type != MaterialParameterType::Texture && texture->Type != MaterialParameterType::NormalMap && texture->Type != MaterialParameterType::SceneTexture && texture->Type != MaterialParameterType::GPUTexture && texture->Type != MaterialParameterType::GPUTextureVolume && texture->Type != MaterialParameterType::GPUTextureCube && texture->Type != MaterialParameterType::GPUTextureArray && texture->Type != MaterialParameterType::CubeTexture) { OnError(caller, box, TEXT("No parameter for texture sample node.")); return nullptr; } // Check if it's 'Object' box that is using only texture object without sampling if (box->ID == 6) { // Return texture object value.Value = texture->ShaderName; value.Type = VariantType::Object; return nullptr; } // Check if hasn't been sampled during that tree eating if (valueBox->Cache.IsInvalid()) { // Check if use custom UVs String uv; MaterialGraphBox* uvBox = parent->GetBox(0); bool useCustomUVs = uvBox->HasConnection(); bool use3dUVs = isCubemap || isArray || isVolume; if (useCustomUVs) { // Get custom UVs auto textureParamId = texture->ID; ASSERT(textureParamId.IsValid()); MaterialValue v = tryGetValue(uvBox, getUVs); uv = MaterialValue::Cast(v, use3dUVs ? VariantType::Float3 : VariantType::Float2).Value; // Restore texture (during tryGetValue pointer could go invalid) texture = findParam(textureParamId); ASSERT(texture); } else { // Use default UVs uv = use3dUVs ? TEXT("float3(input.TexCoord.xy, 0)") : TEXT("input.TexCoord.xy"); } // Select sampler // TODO: add option for texture groups and per texture options like wrap mode etc. // TODO: changing texture sampler option const Char* sampler = TEXT("SamplerLinearWrap"); // Sample texture if (isNormalMap) { const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, 0).xyz"); // Sample encoded normal map const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv); const auto normalVector = writeLocal(VariantType::Float3, sampledValue, parent); // Decode normal vector _writer.Write(TEXT("\t{0}.xy = {0}.xy * 2.0 - 1.0;\n"), normalVector.Value); _writer.Write(TEXT("\t{0}.z = sqrt(saturate(1.0 - dot({0}.xy, {0}.xy)));\n"), normalVector.Value); valueBox->Cache = normalVector; } else { // Select format string based on texture type const Char* format; /*if (isCubemap) { MISSING_CODE("sampling cube maps and 3d texture in material generator"); //format = TEXT("SAMPLE_CUBEMAP({0}, {1})"); } else*/ { /*if (useCustomUVs) { createGradients(writer, parent); format = TEXT("SAMPLE_TEXTURE_GRAD({0}, {1}, {2}, {3})"); } else*/ { format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, 0)"); } } // Sample texture String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, _ddx.Value, _ddy.Value); valueBox->Cache = writeLocal(VariantType::Float4, sampledValue, parent); } } return &valueBox->Cache; } void MaterialGenerator::sampleTexture(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture) { const auto sample = sampleTextureRaw(caller, value, box, texture); if (sample == nullptr) return; // Set result values based on box ID switch (box->ID) { case 1: value = *sample; break; case 2: value.Value = sample->Value + _subs[0]; break; case 3: value.Value = sample->Value + _subs[1]; break; case 4: value.Value = sample->Value + _subs[2]; break; case 5: value.Value = sample->Value + _subs[3]; break; default: CRASH; break; } value.Type = box->Type.Type; } void MaterialGenerator::sampleSceneDepth(Node* caller, Value& value, Box* box) { // Sample depth buffer auto param = findOrAddSceneTexture(MaterialSceneTextures::SceneDepth); const auto depthSample = sampleTextureRaw(caller, value, box, ¶m); if (depthSample == nullptr) return; // Linearize raw device depth linearizeSceneDepth(caller, *depthSample, value); } void MaterialGenerator::linearizeSceneDepth(Node* caller, const Value& depth, Value& value) { value = writeLocal(VariantType::Float, String::Format(TEXT("ViewInfo.w / ({0}.x - ViewInfo.z)"), depth.Value), caller); } void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) { switch (node->TypeID) { // Texture case 1: { // Check if texture has been selected Guid textureId = (Guid)node->Values[0]; if (textureId.IsValid()) { // Get or create parameter for that texture auto param = findOrAddTexture(textureId); // Sample texture sampleTexture(node, value, box, ¶m); } else { // Use default value value = Value::Zero; } break; } // TexCoord case 2: { const auto layer = GetRootLayer(); if (layer && layer->Domain == MaterialDomain::Surface) { const uint32 channel = node->Values.HasItems() ? Math::Min((uint32)node->Values[0], 3u) : 0u; value = Value(VariantType::Float2, String::Format(TEXT("input.TexCoords[{}]"), channel)); } else { // TODO: migrate all material domain templates to TexCoords array (of size MATERIAL_TEXCOORDS=1) value = getUVs; } break; } // Cube Texture case 3: { // Check if texture has been selected Guid textureId = (Guid)node->Values[0]; if (textureId.IsValid()) { // Get or create parameter for that cube texture auto param = findOrAddCubeTexture(textureId); // Sample texture sampleTexture(node, value, box, ¶m); } else { // Use default value value = Value::Zero; } break; } // Normal Map case 4: { // Check if texture has been selected Guid textureId = (Guid)node->Values[0]; if (textureId.IsValid()) { // Get or create parameter for that texture auto param = findOrAddNormalMap(textureId); // Sample texture sampleTexture(node, value, box, ¶m); } else { // Use default value value = Value::Zero; } break; } // Parallax Occlusion Mapping case 5: { auto heightTextureBox = node->GetBox(4); if (!heightTextureBox->HasConnection()) { value = Value::Zero; // TODO: handle missing texture error //OnError("No Variable Entry for height texture.", node); break; } auto heightTexture = eatBox(heightTextureBox->GetParent(), heightTextureBox->FirstConnection()); if (heightTexture.Type != VariantType::Object) { value = Value::Zero; // TODO: handle invalid connection data error //OnError("No Variable Entry for height texture.", node); break; } Value uvs = tryGetValue(node->GetBox(0), getUVs).AsFloat2(); if (_treeType != MaterialTreeType::PixelShader) { // Required ddx/ddy instructions are only supported in Pixel Shader value = uvs; break; } Value scale = tryGetValue(node->GetBox(1), node->Values[0]); Value minSteps = tryGetValue(node->GetBox(2), node->Values[1]); Value maxSteps = tryGetValue(node->GetBox(3), node->Values[2]); Value result = writeLocal(VariantType::Float2, uvs.Value, node); createGradients(node); ASSERT(node->Values[3].Type == VariantType::Int && Math::IsInRange(node->Values[3].AsInt, 0, 3)); auto channel = _subs[node->Values[3].AsInt]; Value cameraVectorWS = getCameraVector(node); Value cameraVectorTS = writeLocal(VariantType::Float3, String::Format(TEXT("TransformWorldVectorToTangent(input, {0})"), cameraVectorWS.Value), node); auto code = String::Format(TEXT( " {{\n" " float vLength = length({8}.rg);\n" " float coeff0 = vLength / {8}.b;\n" " float coeff1 = coeff0 * (-({4}));\n" " float2 vNorm = {8}.rg / vLength;\n" " float2 maxOffset = (vNorm * coeff1);\n" " float numSamples = lerp({0}, {3}, saturate(dot({9}, input.TBN[2])));\n" " float stepSize = 1.0 / numSamples;\n" " float2 currOffset = 0;\n" " float2 lastOffset = 0;\n" " float currRayHeight = 1.0;\n" " float lastSampledHeight = 1;\n" " int currSample = 0;\n" " while (currSample < (int)numSamples)\n" " {{\n" " float currSampledHeight = {1}.SampleGrad(SamplerLinearWrap, {10} + currOffset, {5}, {6}){7};\n" " if (currSampledHeight > currRayHeight)\n" " {{\n" " float delta1 = currSampledHeight - currRayHeight;\n" " float delta2 = (currRayHeight + stepSize) - lastSampledHeight;\n" " float ratio = delta1 / max(delta1 + delta2, 0.00001f);\n" " currOffset = ratio * lastOffset + (1.0 - ratio) * currOffset;\n" " break;\n" " }}\n" " currRayHeight -= stepSize;\n" " lastOffset = currOffset;\n" " currOffset += stepSize * maxOffset;\n" " lastSampledHeight = currSampledHeight;\n" " currSample++;\n" " }}\n" " {2} = {10} + currOffset;\n" " }}\n" ), minSteps.Value, // {0} heightTexture.Value, // {1} result.Value, // {2} maxSteps.Value, // {3} scale.Value, // {4} _ddx.Value, // {5} _ddy.Value, // {6} channel, // {7} cameraVectorTS.Value, // {8} cameraVectorWS.Value, // {9} uvs.Value // {10} ); _writer.Write(*code); value = result; break; } // Scene Texture case 6: { // Get texture type auto type = (MaterialSceneTextures)(int32)node->Values[0]; // Some types need more logic switch (type) { case MaterialSceneTextures::SceneDepth: { sampleSceneDepth(node, value, box); break; } case MaterialSceneTextures::DiffuseColor: { auto gBuffer0Param = findOrAddSceneTexture(MaterialSceneTextures::BaseColor); auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::Metalness); auto gBuffer0Sample = sampleTextureRaw(node, value, box, &gBuffer0Param); if (gBuffer0Sample == nullptr) break; auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param); if (gBuffer2Sample == nullptr) break; value = writeLocal(VariantType::Float3, String::Format(TEXT("GetDiffuseColor({0}.rgb, {1}.g)"), gBuffer0Sample->Value, gBuffer2Sample->Value), node); break; } case MaterialSceneTextures::SpecularColor: { auto gBuffer0Param = findOrAddSceneTexture(MaterialSceneTextures::BaseColor); auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::Metalness); auto gBuffer0Sample = sampleTextureRaw(node, value, box, &gBuffer0Param); if (gBuffer0Sample == nullptr) break; auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param); if (gBuffer2Sample == nullptr) break; value = writeLocal(VariantType::Float3, String::Format(TEXT("GetSpecularColor({0}.rgb, {1}.b, {1}.g)"), gBuffer0Sample->Value, gBuffer2Sample->Value), node); break; } case MaterialSceneTextures::WorldNormal: { auto gBuffer1Param = findOrAddSceneTexture(MaterialSceneTextures::WorldNormal); auto gBuffer1Sample = sampleTextureRaw(node, value, box, &gBuffer1Param); if (gBuffer1Sample == nullptr) break; value = writeLocal(VariantType::Float3, String::Format(TEXT("DecodeNormal({0}.rgb)"), gBuffer1Sample->Value), node); break; } case MaterialSceneTextures::AmbientOcclusion: { auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::AmbientOcclusion); auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param); if (gBuffer2Sample == nullptr) break; value = writeLocal(VariantType::Float, String::Format(TEXT("{0}.a"), gBuffer2Sample->Value), node); break; } case MaterialSceneTextures::Metalness: { auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::Metalness); auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param); if (gBuffer2Sample == nullptr) break; value = writeLocal(VariantType::Float, String::Format(TEXT("{0}.g"), gBuffer2Sample->Value), node); break; } case MaterialSceneTextures::Roughness: { auto gBuffer0Param = findOrAddSceneTexture(MaterialSceneTextures::Roughness); auto gBuffer0Sample = sampleTextureRaw(node, value, box, &gBuffer0Param); if (gBuffer0Sample == nullptr) break; value = writeLocal(VariantType::Float, String::Format(TEXT("{0}.r"), gBuffer0Sample->Value), node); break; } case MaterialSceneTextures::Specular: { auto gBuffer2Param = findOrAddSceneTexture(MaterialSceneTextures::Specular); auto gBuffer2Sample = sampleTextureRaw(node, value, box, &gBuffer2Param); if (gBuffer2Sample == nullptr) break; value = writeLocal(VariantType::Float, String::Format(TEXT("{0}.b"), gBuffer2Sample->Value), node); break; } case MaterialSceneTextures::ShadingModel: { auto gBuffer1Param = findOrAddSceneTexture(MaterialSceneTextures::WorldNormal); auto gBuffer1Sample = sampleTextureRaw(node, value, box, &gBuffer1Param); if (gBuffer1Sample == nullptr) break; value = writeLocal(VariantType::Int, String::Format(TEXT("(int)({0}.a * 3.999)"), gBuffer1Sample->Value), node); break; } case MaterialSceneTextures::WorldPosition: { auto depthParam = findOrAddSceneTexture(MaterialSceneTextures::SceneDepth); auto depthSample = sampleTextureRaw(node, value, box, &depthParam); if (depthSample == nullptr) break; const auto parent = box->GetParent>(); MaterialGraphBox* uvBox = parent->GetBox(0); bool useCustomUVs = uvBox->HasConnection(); String uv; if (useCustomUVs) uv = MaterialValue::Cast(tryGetValue(uvBox, getUVs), VariantType::Float2).Value; else uv = TEXT("input.TexCoord.xy"); value = writeLocal(VariantType::Float3, String::Format(TEXT("GetWorldPos({1}, {0}.rgb)"), depthSample->Value, uv), node); break; } default: { // Sample single texture auto param = findOrAddSceneTexture(type); sampleTexture(node, value, box, ¶m); break; } } // Channel masking switch (box->ID) { case 2: value = value.GetX(); break; case 3: value = value.GetY(); break; case 4: value = value.GetZ(); break; case 5: value = value.GetW(); break; } break; } // Scene Color case 7: { // Sample scene color texture auto param = findOrAddSceneTexture(MaterialSceneTextures::SceneColor); sampleTexture(node, value, box, ¶m); break; } // Scene Depth case 8: { sampleSceneDepth(node, value, box); break; } // Sample Texture case 9: // Procedural Texture Sample case 17: { // Get input boxes auto textureBox = node->GetBox(0); auto uvsBox = node->GetBox(1); auto levelBox = node->TryGetBox(2); auto offsetBox = node->GetBox(3); if (!textureBox->HasConnection()) { // No texture to sample value = Value::Zero; break; } const bool canUseSample = CanUseSample(_treeType); const auto texture = eatBox(textureBox->GetParent(), textureBox->FirstConnection()); // Get UVs Value uvs; const bool useCustomUVs = uvsBox->HasConnection(); if (useCustomUVs) { // Get custom UVs uvs = eatBox(uvsBox->GetParent(), uvsBox->FirstConnection()); } else { // Use default UVs uvs = getUVs; } const auto textureParam = findParam(texture.Value); if (!textureParam) { // Missing texture value = Value::Zero; break; } const bool isCubemap = textureParam->Type == MaterialParameterType::CubeTexture || textureParam->Type == MaterialParameterType::GPUTextureCube; const bool isArray = textureParam->Type == MaterialParameterType::GPUTextureArray; const bool isVolume = textureParam->Type == MaterialParameterType::GPUTextureVolume; const bool isNormalMap = textureParam->Type == MaterialParameterType::NormalMap; const bool use3dUVs = isCubemap || isArray || isVolume; uvs = Value::Cast(uvs, use3dUVs ? VariantType::Float3 : VariantType::Float2); // Get other inputs const auto level = tryGetValue(levelBox, node->Values[1]); const bool useLevel = (levelBox && levelBox->HasConnection()) || (int32)node->Values[1] != -1; const bool useOffset = offsetBox->HasConnection(); const auto offset = useOffset ? eatBox(offsetBox->GetParent(), offsetBox->FirstConnection()) : Value::Zero; const Char* samplerName; const int32 samplerIndex = node->Values[0].AsInt; if (samplerIndex == TextureGroup) { auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[2].AsInt); samplerName = *textureGroupSampler.ShaderName; } else if (samplerIndex >= 0 && samplerIndex < ARRAY_COUNT(SamplerNames)) { samplerName = SamplerNames[samplerIndex]; } else { OnError(node, box, TEXT("Invalid texture sampler.")); return; } // Create texture sampling code if (node->TypeID == 9) { // Sample Texture const Char* format; if (useLevel || !canUseSample) { if (useOffset) format = TEXT("{0}.SampleLevel({1}, {2}, {3}, {4})"); else format = TEXT("{0}.SampleLevel({1}, {2}, {3})"); } else { if (useOffset) format = TEXT("{0}.Sample({1}, {2}, {4})"); else format = TEXT("{0}.Sample({1}, {2})"); } const String sampledValue = String::Format(format, texture.Value, samplerName, uvs.Value, level.Value, offset.Value); textureBox->Cache = writeLocal(VariantType::Float4, sampledValue, node); } else { // Procedural Texture Sample textureBox->Cache = writeLocal(Value::InitForZero(ValueType::Float4), node); auto proceduralSample = String::Format(TEXT( " {{\n" " float3 weights;\n" " float2 vertex1, vertex2, vertex3;\n" " float2 uv = {0} * 3.464; // 2 * sqrt (3);\n" " float2 uv1, uv2, uv3;\n" " const float2x2 gridToSkewedGrid = float2x2(1.0, 0.0, -0.57735027, 1.15470054);\n" " float2 skewedCoord = mul(gridToSkewedGrid, uv);\n" " int2 baseId = int2(floor(skewedCoord));\n" " float3 temp = float3(frac(skewedCoord), 0);\n" " temp.z = 1.0 - temp.x - temp.y;\n" " if (temp.z > 0.0)\n" " {{\n" " weights = float3(temp.z, temp.y, temp.x);\n" " vertex1 = baseId;\n" " vertex2 = baseId + int2(0, 1);\n" " vertex3 = baseId + int2(1, 0);\n" " }}\n" " else\n" " {{\n" " weights = float3(-temp.z, 1.0 - temp.y, 1.0 - temp.x);\n" " vertex1 = baseId + int2(1, 1);\n" " vertex2 = baseId + int2(1, 0);\n" " vertex3 = baseId + int2(0, 1);\n" " }}\n" " uv1 = {0} + frac(sin(mul(float2x2(127.1, 311.7, 269.5, 183.3), vertex1)) * 43758.5453);\n" " uv2 = {0} + frac(sin(mul(float2x2(127.1, 311.7, 269.5, 183.3), vertex2)) * 43758.5453);\n" " uv3 = {0} + frac(sin(mul(float2x2(127.1, 311.7, 269.5, 183.3), vertex3)) * 43758.5453);\n" " float2 fdx = ddx({0});\n" " float2 fdy = ddy({0});\n" " float4 tex1 = {1}.SampleGrad({2}, uv1, fdx, fdy, {4}) * weights.x;\n" " float4 tex2 = {1}.SampleGrad({2}, uv2, fdx, fdy, {4}) * weights.y;\n" " float4 tex3 = {1}.SampleGrad({2}, uv3, fdx, fdy, {4}) * weights.z;\n" " {3} = tex1 + tex2 + tex3;\n" " }}\n" ), uvs.Value, // {0} texture.Value, // {1} samplerName, // {2} textureBox->Cache.Value, // {3} offset.Value // {4} ); _writer.Write(*proceduralSample); } // Decode normal map vector if (isNormalMap) { _writer.Write(TEXT("\t{0}.xyz = UnpackNormalMap({0}.xy);\n"), textureBox->Cache.Value); } value = textureBox->Cache; break; } // Flipbook case 10: { auto uv = Value::Cast(tryGetValue(node->GetBox(0), getUVs), VariantType::Float2); auto frame = Value::Cast(tryGetValue(node->GetBox(1), node->Values[0]), VariantType::Float); auto framesXY = Value::Cast(tryGetValue(node->GetBox(2), node->Values[1]), VariantType::Float2); auto invertX = Value::Cast(tryGetValue(node->GetBox(3), node->Values[2]), VariantType::Float); auto invertY = Value::Cast(tryGetValue(node->GetBox(4), node->Values[3]), VariantType::Float); value = writeLocal(VariantType::Float2, String::Format(TEXT("Flipbook({0}, {1}, {2}, float2({3}, {4}))"), uv.Value, frame.Value, framesXY.Value, invertX.Value, invertY.Value), node); break; } // Sample Global SDF case 14: { auto param = findOrAddGlobalSDF(); Value worldPosition = tryGetValue(node->GetBox(1), Value(VariantType::Float3, TEXT("input.WorldPosition.xyz"))).Cast(VariantType::Float3); Value startCascade = tryGetValue(node->TryGetBox(2), 0, Value::Zero).Cast(VariantType::Uint); value = writeLocal(VariantType::Float, String::Format(TEXT("SampleGlobalSDF({0}, {0}_Tex, {0}_Mip, {1}, {2})"), param.ShaderName, worldPosition.Value, startCascade.Value), node); _includes.Add(TEXT("./Flax/GlobalSignDistanceField.hlsl")); break; } // Sample Global SDF Gradient case 15: { auto gradientBox = node->GetBox(0); auto distanceBox = node->GetBox(2); auto param = findOrAddGlobalSDF(); Value worldPosition = tryGetValue(node->GetBox(1), Value(VariantType::Float3, TEXT("input.WorldPosition.xyz"))).Cast(VariantType::Float3); Value startCascade = tryGetValue(node->TryGetBox(3), 0, Value::Zero).Cast(VariantType::Uint); auto distance = writeLocal(VariantType::Float, node); auto gradient = writeLocal(VariantType::Float3, String::Format(TEXT("SampleGlobalSDFGradient({0}, {0}_Tex, {0}_Mip, {1}, {2}, {3})"), param.ShaderName, worldPosition.Value, distance.Value, startCascade.Value), node); _includes.Add(TEXT("./Flax/GlobalSignDistanceField.hlsl")); gradientBox->Cache = gradient; distanceBox->Cache = distance; value = box == gradientBox ? gradient : distance; break; } // Triplanar Texture case 16: { 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]).AsFloat3(); const auto blend = tryGetValue(node->GetBox(2), node->Values[1]).AsFloat(); const auto offset = tryGetValue(node->TryGetBox(6), node->Values.Count() >= 3 ? node->Values[2] : Float2::Zero).AsFloat2(); const bool local = node->Values.Count() >= 5 ? node->Values[4].AsBool : false; const Char* samplerName; const int32 samplerIndex = node->Values.Count() >= 4 ? node->Values[3].AsInt : LinearWrap; 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 = ShaderStringBuilder() .Code(TEXT(R"( { // Get world position and normal float3 tiling = %SCALE% * 0.001f; float3 position = ((%POSITION%) + GetLargeWorldsTileOffset(1.0f / length(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; } // Get Lightmap UV case 18: { auto output = writeLocal(Value::InitForZero(ValueType::Float2), node); auto lightmapUV = String::Format(TEXT( "{{\n" "#if USE_LIGHTMAP\n" "\t {0} = input.LightmapUV;\n" "#else\n" "\t {0} = float2(0,0);\n" "#endif\n" "}}\n" ), output.Value); _writer.Write(*lightmapUV); 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]).AsFloat3(); const auto blend = tryGetValue(node->GetBox(2), node->Values[1]).AsFloat(); const auto offset = tryGetValue(node->GetBox(6), 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 float3 tiling = %SCALE% * 0.001f; float3 position = ((%POSITION%) + GetLargeWorldsTileOffset(1.0f / length(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; } } #endif