| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771 |
- #include "ShaderUnit.h"
- #include "WallpaperEngine/Logging/Log.h"
- #include <exception>
- #include <regex>
- #include <stack>
- #include <string>
- #include <utility>
- #include "GLSLContext.h"
- #include "WallpaperEngine/Assets/AssetLoadException.h"
- #include "WallpaperEngine/Render/Shaders/Variables/ShaderVariable.h"
- #include "WallpaperEngine/Render/Shaders/Variables/ShaderVariableFloat.h"
- #include "WallpaperEngine/Render/Shaders/Variables/ShaderVariableInteger.h"
- #include "WallpaperEngine/Render/Shaders/Variables/ShaderVariableVector2.h"
- #include "WallpaperEngine/Render/Shaders/Variables/ShaderVariableVector3.h"
- #include "WallpaperEngine/Render/Shaders/Variables/ShaderVariableVector4.h"
- #include "WallpaperEngine/Data/Builders/VectorBuilder.h"
- #include "WallpaperEngine/FileSystem/Container.h"
- #define SHADER_HEADER(filename) \
- "#version 330\n" \
- "// ======================================================\n" \
- "// Processed shader " \
- + filename \
- + "\n" \
- "// ======================================================\n" \
- "precision highp float;\n" \
- "#define mul(x, y) ((y) * (x))\n" \
- "#define max(x, y) max (y, x)\n" \
- "#define lerp mix\n" \
- "#define frac fract\n" \
- "#define CAST2(x) (vec2(x))\n" \
- "#define CAST3(x) (vec3(x))\n" \
- "#define CAST4(x) (vec4(x))\n" \
- "#define CAST3X3(x) (mat3(x))\n" \
- "#define float2 vec2\n" \
- "#define float3 vec3\n" \
- "#define float4 vec4\n" \
- "#define int2 ivec2\n" \
- "#define int3 ivec3\n" \
- "#define int4 ivec4\n" \
- "#define saturate(x) (clamp(x, 0.0, 1.0))\n" \
- "#define texSample2D texture\n" \
- "#define texSample2DLod textureLod\n" \
- "#define atan2 atan\n" \
- "#define fmod(x, y) ((x)-(y)*trunc((x)/(y)))\n" \
- "#define ddx dFdx\n" \
- "#define ddy(x) dFdy(-(x))\n" \
- "#define GLSL 1\n\n";
- #define FRAGMENT_SHADER_DEFINES \
- "out vec4 out_FragColor;\n" \
- "#define varying in\n"
- #define VERTEX_SHADER_DEFINES \
- "#define attribute in\n" \
- "#define varying out\n"
- #define DEFINE_COMBO(name, value) "#define " + name + " " + std::to_string (value) + "\n";
- using namespace WallpaperEngine::Render;
- using namespace WallpaperEngine::Data::Builders;
- using namespace WallpaperEngine::Render::Shaders;
- ShaderUnit::ShaderUnit (
- const GLSLContext::UnitType type, std::string file, std::string content, const AssetLocator& assetLocator,
- const ShaderConstantMap& constants, const TextureMap& passTextures, const TextureMap& overrideTextures,
- const ComboMap& combos, const ComboMap& overrideCombos
- ) :
- m_type (type), m_file (std::move (file)), m_content (std::move (content)), m_combos (combos),
- m_overrideCombos (overrideCombos), m_constants (constants), m_passTextures (passTextures),
- m_overrideTextures (overrideTextures), m_link (nullptr), m_assetLocator (assetLocator) {
- this->preprocess ();
- }
- void ShaderUnit::preprocess () {
- this->m_preprocessed = this->m_content;
- this->m_includes = "";
- this->preprocessIncludes ();
- this->preprocessRequires ();
- this->preprocessVariables ();
- this->preprocessBalanceConditionals ();
- const std::string from = "gl_FragColor";
- const std::string to = "out_FragColor";
- size_t start_pos = 0;
- while ((start_pos = this->m_preprocessed.find (from, start_pos)) != std::string::npos) {
- this->m_preprocessed.replace (start_pos, from.length (), to);
- start_pos += to.length (); // avoids re-matching if 'to' is a substring of 'from'
- }
- }
- void ShaderUnit::preprocessVariables () {
- size_t start = 0, end = 0;
- while ((end = this->m_preprocessed.find ('\n', start)) != std::string::npos) {
- std::string line = this->m_preprocessed.substr (start, end - start);
- const size_t combo = line.find ("// [COMBO] ");
- const size_t uniform = line.find ("uniform ");
- const size_t comment = line.find ("// ");
- const size_t semicolon = line.find (';');
- if (combo != std::string::npos) {
- this->parseComboConfiguration (line.substr (combo + strlen ("// [COMBO] ")), 0);
- } else if (
- uniform != std::string::npos && comment != std::string::npos && semicolon != std::string::npos &&
- // semicolon before comment means it's a trailing comment, not a commented-out line
- // (doesn't account for block comments)
- semicolon < comment
- ) {
- // uniforms with trailing comments never have a value assigned, which is what lets this find type/name
- const size_t last_space = line.find_last_of (' ', semicolon);
- if (last_space != std::string::npos) {
- const size_t previous_space = line.find_last_of (' ', last_space - 1);
- if (previous_space != std::string::npos) {
- std::string type = line.substr (previous_space + 1, last_space - previous_space - 1);
- std::string name = line.substr (last_space + 1, semicolon - last_space - 1);
- std::string json = line.substr (comment + 2);
- this->parseParameterConfiguration (type, name, json);
- }
- }
- }
- start = end + 1;
- }
- }
- void ShaderUnit::preprocessIncludes () {
- size_t start = 0, end = 0;
- while ((start = this->m_preprocessed.find ("#include", end)) != std::string::npos) {
- // TODO: CHECK FOR ERRORS HERE, MALFORMED INCLUDES WILL NOT BE PROPERLY HANDLED
- const size_t quoteStart = this->m_preprocessed.find_first_of ('"', start) + 1;
- const size_t quoteEnd = this->m_preprocessed.find_first_of ('"', quoteStart);
- const std::string filename = this->m_preprocessed.substr (quoteStart, quoteEnd - quoteStart);
- // a missing include isn't necessarily an error - it may come from commented-out content
- std::string content;
- try {
- content += "// begin of include from file ";
- content += filename;
- content += "\n";
- content += this->m_assetLocator.includeShader (filename);
- content += "\n// end of included from file ";
- content += filename;
- content += "\n";
- } catch (AssetLoadException&) {
- content += "// tried including file ";
- content += filename;
- content += " but was not found\n";
- }
- // comment out just the "#i" so the string length/offsets are unaffected
- this->m_preprocessed = this->m_preprocessed.replace (start, 2, "//");
- this->m_includes += content;
- end = start;
- }
- // resolve #include directives found inside already-included content too
- end = 0;
- while ((start = this->m_includes.find ("#include", end)) != std::string::npos) {
- const size_t lineEnd = this->m_includes.find_first_of ('\n', start);
- // TODO: CHECK FOR ERRORS HERE, MALFORMED INCLUDES WILL NOT BE PROPERLY HANDLED
- const size_t quoteStart = this->m_includes.find_first_of ('"', start) + 1;
- const size_t quoteEnd = this->m_includes.find_first_of ('"', quoteStart);
- const std::string filename = this->m_includes.substr (quoteStart, quoteEnd - quoteStart);
- // a missing include isn't necessarily an error - it may come from commented-out content
- std::string content;
- try {
- content = "// begin of include from file ";
- content += filename;
- content += "\n";
- content += this->m_assetLocator.includeShader (filename);
- content += "\n// end of included from file ";
- content += filename;
- content += "\n";
- } catch (AssetLoadException&) {
- content = "// tried including file ";
- content += filename;
- content += " but was not found\n";
- }
- this->m_includes = this->m_includes.replace (start, lineEnd - start, content);
- end = start;
- }
- // place the accumulated include contents right before the main function
- end = 0;
- bool includesAdded = false;
- while ((start = this->m_preprocessed.find (" main", end)) != std::string::npos) {
- char value = this->m_preprocessed.at (start + 5);
- end = start + 5;
- if (value != ' ' && value != '(') {
- continue;
- }
- size_t lastAttribute = this->m_preprocessed.rfind ("attribute", start);
- size_t lastVarying = this->m_preprocessed.rfind ("varying", start);
- size_t lastUniform = this->m_preprocessed.rfind ("uniform", start);
- size_t latest = lastAttribute;
- if (latest == std::string::npos) {
- latest = lastVarying;
- } else if (latest < lastVarying && lastVarying != std::string::npos) {
- latest = lastVarying;
- }
- if (latest == std::string::npos) {
- latest = lastUniform;
- } else if (latest < lastUniform && lastUniform != std::string::npos) {
- latest = lastUniform;
- }
- if (latest < start) {
- // find the end of the current line
- latest = this->m_preprocessed.find ('\n', latest);
- } else {
- // find the end of the previous line
- latest = this->m_preprocessed.rfind ('\n', start);
- }
- // start points at the end of the previous line, used below to place the includes
- start = this->m_preprocessed.rfind ('\n', start);
- // tracks nested #if/#endif so the includes can be moved before the start of the enclosing chain
- std::stack<size_t> ifdefStack;
- const std::regex ifdef (R"((#if|#endif))");
- std::smatch match;
- size_t current = 0;
- while (
- std::regex_search (this->m_preprocessed.cbegin () + current, this->m_preprocessed.cend (), match, ifdef)) {
- current += match.position ();
- if (this->m_preprocessed.substr (current, 3) == "#if") {
- ifdefStack.push (current++); // advance past this match so regex_search doesn't rematch it
- continue;
- }
- current++; // same reason: advance past this match
- // an unmatched #endif is most likely a syntax error; ignored for now
- if (ifdefStack.empty ()) {
- continue;
- }
- size_t stackStart = ifdefStack.top ();
- ifdefStack.pop ();
- if (latest > stackStart && latest <= current) {
- // insertion point is inside a conditional block - move before the #if so includes are
- // available to all branches (e.g. genericropeparticle.vert has #if GS_ENABLED wrapping two main()s)
- size_t beforeIfdef = this->m_preprocessed.rfind ('\n', stackStart);
- latest = (beforeIfdef != std::string::npos) ? beforeIfdef : 0;
- }
- }
- // TODO: IS THIS GOOD ENOUGH? MAYBE WE SHOULD BE GETTING THE FIRST #IF BLOCK INSTEAD?
- latest = std::min (latest, start);
- this->m_preprocessed.insert (latest + 1, this->m_includes + '\n');
- includesAdded = true;
- break;
- }
- if (!includesAdded) {
- sLog.exception ("Could not find where to place includes for shader unit ", this->m_file);
- }
- }
- void ShaderUnit::preprocessRequires () {
- size_t start = 0, end = 0;
- while ((start = this->m_preprocessed.find ("#require", end)) != std::string::npos) {
- const size_t lineEnd = this->m_preprocessed.find_first_of ('\n', start);
- const size_t nameStart = start + std::string ("#require ").length ();
- if (nameStart >= lineEnd) {
- sLog.error ("Malformed #require directive (no module name) in shader ", this->m_file);
- end = lineEnd;
- continue;
- }
- std::string moduleName = this->m_preprocessed.substr (nameStart, lineEnd - nameStart);
- while (!moduleName.empty () && (moduleName.back () == ' ' || moduleName.back () == '\r')) {
- moduleName.pop_back ();
- }
- if (moduleName.empty ()) {
- sLog.error ("Malformed #require directive (empty module name) in shader ", this->m_file);
- end = lineEnd;
- continue;
- }
- sLog.out ("Resolving require module: ", moduleName, " in shader ", this->m_file);
- std::string moduleCode = this->resolveRequireModule (moduleName);
- this->m_preprocessed = this->m_preprocessed.replace (start, 2, "//");
- if (!moduleCode.empty ()) {
- // inserted directly here, not appended to m_includes - that was already consumed by preprocessIncludes
- this->m_preprocessed.insert (start, moduleCode);
- end = start + moduleCode.length ();
- } else {
- end = lineEnd;
- }
- }
- }
- std::string ShaderUnit::resolveRequireModule (const std::string& moduleName) const {
- if (moduleName == "LightingV1") {
- return this->generateLightingV1 ();
- }
- sLog.error ("Unknown #require module: ", moduleName, " in shader ", this->m_file);
- return "";
- }
- std::string ShaderUnit::generateLightingV1 () const {
- // Wallpaper Engine generates this from the scene's light sources; since light objects
- // aren't supported yet, stub it out with no dynamic light contribution.
- return "// begin of generated module LightingV1\n"
- "vec3 PerformLighting_V1(vec3 worldPos, vec3 albedo, vec3 normal, vec3 viewDir,\n"
- " vec3 specularTint, vec3 baseReflectance, float roughness, float metallic)\n"
- "{\n"
- " return vec3(0.0);\n"
- "}\n"
- "// end of generated module LightingV1\n";
- }
- void ShaderUnit::preprocessBalanceConditionals () {
- static const std::regex directive (R"((?:^|\n)[ \t]*#(ifndef|ifdef|if|endif)\b)");
- int depth = 0;
- std::vector<size_t> extraEndifs;
- auto begin = std::sregex_iterator (this->m_preprocessed.cbegin (), this->m_preprocessed.cend (), directive);
- auto end = std::sregex_iterator ();
- for (auto it = begin; it != end; ++it) {
- const std::string& keyword = (*it)[1].str ();
- if (keyword == "endif") {
- if (depth == 0) {
- // position of the '#' character for this directive
- extraEndifs.push_back (it->position (1) - 1);
- } else {
- depth--;
- }
- } else {
- depth++;
- }
- }
- // comment out the extra #endif directives, from the end so earlier offsets stay valid
- for (auto it = extraEndifs.rbegin (); it != extraEndifs.rend (); ++it) {
- sLog.out ("Found #endif with no matching #if in shader ", this->m_file, ", ignoring it");
- this->m_preprocessed.replace (*it, 2, "//");
- }
- }
- std::string ShaderUnit::applyLinkedVaryingCompatibility (std::string source) const {
- if (this->m_type != GLSLContext::UnitType_Vertex || this->m_link == nullptr) {
- return source;
- }
- std::regex fragmentVec4Varying (R"(\bvarying\s+vec4\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
- std::smatch varyingMatch;
- std::string linked = this->m_link->m_preprocessed;
- size_t linkedOffset = 0;
- while (std::regex_search (linked.cbegin () + linkedOffset, linked.cend (), varyingMatch, fragmentVec4Varying)) {
- const std::string name = varyingMatch[1].str ();
- linkedOffset += varyingMatch.position () + varyingMatch.length ();
- const std::regex vertexVec2Decl ("\\bvarying\\s+vec2\\s+" + name + "\\s*;");
- if (!std::regex_search (source, vertexVec2Decl)) {
- continue;
- }
- source = std::regex_replace (source, vertexVec2Decl, "varying vec4 " + name + ";");
- const std::regex assignment ("(^|\\n)([ \\t]*)" + name + "\\s*=\\s*([^;\\n]+);");
- std::smatch assignmentMatch;
- size_t offset = 0;
- while (std::regex_search (source.cbegin () + offset, source.cend (), assignmentMatch, assignment)) {
- const std::string prefix = assignmentMatch[1].str ();
- const std::string indent = assignmentMatch[2].str ();
- const std::string expression = assignmentMatch[3].str ();
- const std::string replacement = prefix + indent + name + " = vec4(" + expression + ", 0.0, 1.0);";
- const size_t position = offset + assignmentMatch.position ();
- source.replace (position, assignmentMatch.length (), replacement);
- offset = position + replacement.length ();
- }
- }
- return source;
- }
- std::string ShaderUnit::applyFragmentTexCoordCompatibility (std::string source) const {
- if (this->m_type != GLSLContext::UnitType_Fragment) {
- return source;
- }
- const std::regex texCoordBeforeCast2 (R"(\bv_TexCoord\b(\s*[-+*/]\s*CAST2\s*\())");
- const std::regex cast2BeforeTexCoord (R"((CAST2\s*\([^)]+\)\s*[-+*/]\s*)\bv_TexCoord\b)");
- const std::regex wideTexCoordDecl (R"(\bvarying\s+vec[34]\s+v_TexCoord\s*;)");
- if (!std::regex_search (source, wideTexCoordDecl)
- || (!std::regex_search (source, texCoordBeforeCast2) && !std::regex_search (source, cast2BeforeTexCoord))) {
- return source;
- }
- const std::string original = source;
- source = std::regex_replace (source, texCoordBeforeCast2, "v_TexCoord.xy$1");
- source = std::regex_replace (source, cast2BeforeTexCoord, "$1v_TexCoord.xy");
- if (source != original) {
- sLog.out ("Applied fragment TexCoord vec2 compatibility in ", this->m_file);
- }
- return source;
- }
- std::string ShaderUnit::applyFragmentVaryingShadowCompatibility (std::string source) const {
- if (this->m_type != GLSLContext::UnitType_Fragment) {
- return source;
- }
- static const std::regex varyingDecl (R"(\bvarying\s+(vec[234]|float)\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
- std::vector<std::pair<std::string, std::string>> shadowed;
- for (auto it = std::sregex_iterator (source.cbegin (), source.cend (), varyingDecl); it != std::sregex_iterator ();
- ++it) {
- const std::string type = (*it)[1].str ();
- const std::string name = (*it)[2].str ();
- // only shadow varyings the shader actually reassigns - a plain read-only "in" is fine as-is,
- // and touching the declaration unnecessarily risks breaking a shader that works today
- const std::regex assignmentUse (
- "\\b" + name + "\\b(?:\\.[xyzwrgba]+)?\\s*(?:=(?!=)|\\+=|-=|\\*=|/=)"
- );
- if (!std::regex_search (source, assignmentUse)) {
- continue;
- }
- shadowed.emplace_back (type, name);
- }
- if (shadowed.empty ()) {
- return source;
- }
- static const std::regex mainOpen (R"(\bvoid\s+main\s*\([^)]*\)\s*\{)");
- std::smatch mainMatch;
- if (!std::regex_search (source, mainMatch, mainOpen)) {
- return source;
- }
- // shadows each with a same-named local, initialized from the real (read-only) input, so the
- // rest of main() can keep mutating it exactly like the original compatibility-profile shader did
- std::string shadowCode;
- for (const auto& [type, name] : shadowed) {
- shadowCode += " " + type + " wpeShadowIn_" + name + " = " + name + "; " + type + " " + name + " = wpeShadowIn_" + name + ";";
- }
- const size_t insertAt = mainMatch.position (0) + mainMatch.length (0);
- source.insert (insertAt, shadowCode);
- std::string names;
- for (const auto& [type, name] : shadowed) {
- names += (names.empty () ? "" : ", ") + name;
- }
- sLog.out ("Applied fragment varying shadow compatibility in ", this->m_file, " for ", names);
- return source;
- }
- void ShaderUnit::parseComboConfiguration (const std::string& content, const int defaultValue) {
- // TODO: SUPPORT REQUIRES SO WE PROPERLY FOLLOW THE REQUIRED CHAIN
- JSON data;
- try {
- data = JSON::parse (content);
- } catch (const std::exception& e) {
- sLog.error ("Cannot parse combo metadata in shader ", this->m_file, ": ", e.what ());
- return;
- }
- const auto combo = data.require<std::string> ("combo", "cannot parse combo information");
- // "type" is ignored - appears to be editor-only metadata
- const auto defvalue = data.find ("default");
- const auto entry = this->m_combos.find (combo);
- const auto entryOverride = this->m_overrideCombos.find (combo);
- this->m_usedCombos.emplace (combo, true);
- // not predefined anywhere -> fall back to the JSON's own default value
- if (entry == this->m_combos.end () && entryOverride == this->m_overrideCombos.end ()) {
- if (defvalue == data.end ()) {
- // TODO: PROPERLY SUPPORT EMPTY COMBOS
- this->m_discoveredCombos.emplace (combo, defaultValue);
- } else if (defvalue->is_number_float ()) {
- sLog.exception ("float combos are not supported in shader ", this->m_file, ". ", combo);
- } else if (defvalue->is_number_integer ()) {
- this->m_discoveredCombos.emplace (combo, defvalue->get<int> ());
- } else if (defvalue->is_string ()) {
- sLog.exception ("string combos are not supported in shader ", this->m_file, ". ", combo);
- } else {
- sLog.exception ("cannot parse combo information ", combo, ". unknown type for ", defvalue->dump ());
- }
- }
- }
- void ShaderUnit::parseParameterConfiguration (
- const std::string& type, const std::string& name, const std::string& content
- ) {
- JSON data;
- try {
- data = JSON::parse (content);
- } catch (const std::exception& e) {
- sLog.error ("Cannot parse parameter metadata for ", name, " in shader ", this->m_file, ": ", e.what ());
- return;
- }
- const auto material = data.optional ("material");
- const auto defvalue = data.optional ("default");
- const auto combo = data.find ("combo");
- auto constant = this->m_constants.end ();
- if (material.has_value ()) {
- constant = this->m_constants.find (*material);
- }
- if (constant == this->m_constants.end () && !defvalue.has_value ()) {
- if (type != "sampler2D") {
- sLog.exception ("Cannot parse parameter data for ", name, " in shader ", this->m_file);
- }
- }
- Variables::ShaderVariable* parameter = nullptr;
- if (type == "vec4") {
- parameter
- = new Variables::ShaderVariableVector4 (VectorBuilder::parse<glm::vec4> (defvalue->get<std::string> ()));
- } else if (type == "vec3") {
- parameter = new Variables::ShaderVariableVector3 (VectorBuilder::parse<glm::vec3> (*defvalue));
- } else if (type == "vec2") {
- parameter = new Variables::ShaderVariableVector2 (VectorBuilder::parse<glm::vec2> (*defvalue));
- } else if (type == "float") {
- if (defvalue->is_string ()) {
- parameter = new Variables::ShaderVariableFloat (std::stoi (defvalue->get<std::string> ()));
- } else {
- parameter = new Variables::ShaderVariableFloat (defvalue->get<float> ());
- }
- } else if (type == "int") {
- if (defvalue->is_string ()) {
- parameter = new Variables::ShaderVariableInteger (std::stoi (defvalue->get<std::string> ()));
- } else {
- parameter = new Variables::ShaderVariableInteger (defvalue->get<int> ());
- }
- } else if (type == "sampler2D" || type == "sampler2DComparison") {
- const auto textureName = data.find ("default");
- // TODO: CREATE TEXTURE WITH THE GIVEN COLOR
- const char value = name.at (std::string ("g_Texture").length ());
- const auto requireany = data.find ("requireany");
- const auto require = data.find ("require");
- // TODO: BETTER CONVERSION HERE
- size_t index = value - '0';
- // TODO: SUPPORT USER TEXTURES!!
- if (combo != data.end ()) {
- // TODO: CLEANUP HOW THIS IS DETERMINED FIRST
- const auto textureSlotUsed
- = this->m_passTextures.contains (index) || this->m_overrideTextures.contains (index);
- bool isRequired = false;
- int comboValue = 1;
- if (textureSlotUsed) {
- // texture already exists, so the combo must be set; these tend to have no default value
- isRequired = true;
- } else if (require != data.end ()) {
- if (requireany != data.end () && requireany->get<bool> ()) {
- // requireany: any one mismatching value makes this required (OR semantics)
- for (const auto& item : require->items ()) {
- const std::string& macro = item.key ();
- const auto it = this->m_combos.find (macro);
- if (it == this->m_combos.end () || this->m_overrideCombos.contains (macro)
- || it->second != item.value ()) {
- isRequired = true;
- break;
- }
- }
- } else {
- isRequired = true;
- // require without requireany: every listed value must match (AND semantics)
- for (const auto& item : require->items ()) {
- const std::string& macro = item.key ();
- const auto it = this->m_combos.find (macro);
- // a missing macro is fine here, only the value comparison matters
- if ((it != this->m_combos.end () || this->m_overrideCombos.contains (macro))
- && it->second == item.value ()) {
- isRequired = false;
- break;
- }
- }
- }
- }
- if (isRequired && !textureSlotUsed) {
- if (!defvalue.has_value ()) {
- isRequired = false;
- } else {
- if (this->m_combos.contains (*combo) || this->m_overrideCombos.contains (*combo)) {
- isRequired = false;
- } else if (defvalue->is_string ()) {
- comboValue = std::stoi (defvalue->get<std::string> ().c_str ());
- } else if (defvalue->is_number ()) {
- comboValue = *defvalue;
- } else {
- sLog.exception (
- "Cannot determine default value for combo ", combo->get<std::string> (),
- " because it's not specified by the shader and is not given a default value: ", this->m_file
- );
- }
- }
- }
- if (isRequired) {
- this->m_discoveredCombos.emplace (*combo, comboValue);
- this->m_usedCombos.emplace (*combo, true);
- }
- }
- // Some shaders (e.g. effects/refract.frag's normal map) declare `"default":""` on purpose -
- // an explicitly empty default means "no texture unless the object's own effect config
- // supplies one", not "a texture literally named the empty string". Registering it anyway
- // sent every such object into a doomed asset lookup for a blank path on every use of that
- // shader, whether or not the combo gating it was even active.
- if (textureName != data.end () && !textureName->get<std::string> ().empty ()) {
- this->m_defaultTextures.emplace (index, *textureName);
- }
- return;
- } else {
- sLog.error ("Unknown parameter type: ", type, " for ", name, " in shader ", this->m_file);
- return;
- }
- if (material.has_value () && parameter != nullptr) {
- parameter->setIdentifierName (*material);
- parameter->setName (name);
- this->m_parameters.push_back (parameter);
- }
- }
- const ComboMap& ShaderUnit::getCombos () const { return this->m_combos; }
- const ComboMap& ShaderUnit::getDiscoveredCombos () const { return this->m_discoveredCombos; }
- void ShaderUnit::linkToUnit (const ShaderUnit* unit) { this->m_link = unit; }
- const ShaderUnit* ShaderUnit::getLinkedUnit () const { return this->m_link; }
- const std::string& ShaderUnit::compile () {
- if (!this->m_final.empty ()) {
- return this->m_final;
- }
- this->m_final = SHADER_HEADER (this->m_file);
- // GLSL has no builtin log10 (unlike HLSL), so some shaders provide their own under "#if GLSL"
- // (which is always true here). Adding a blanket compatibility macro would get expanded right
- // over such a shader's own function definition and mangle it, so only add it when the shader
- // doesn't already define log10 itself.
- static const std::regex log10Definition (
- R"(\b(?:void|float|int|uint|bool|vec[234]|ivec[234]|uvec[234]|bvec[234]|mat[234](?:x[234])?)\s+log10\s*\()"
- );
- if (!std::regex_search (this->m_content, log10Definition)) {
- this->m_final += "#define log10(x) (log2(x) * 0.301029995663981)\n";
- }
- if (this->m_type == GLSLContext::UnitType_Fragment) {
- this->m_final += FRAGMENT_SHADER_DEFINES;
- } else {
- this->m_final += VERTEX_SHADER_DEFINES;
- }
- std::map<std::string, bool> addedCombos;
- for (const auto& [name, value] : this->m_overrideCombos) {
- std::string uppercase;
- std::ranges::transform (name, std::back_inserter (uppercase), ::toupper);
- if (!addedCombos.contains (uppercase)) {
- this->m_final += DEFINE_COMBO (uppercase, value);
- addedCombos.emplace (uppercase, true);
- }
- }
- for (const auto& [name, value] : this->m_combos) {
- std::string uppercase;
- std::ranges::transform (name, std::back_inserter (uppercase), ::toupper);
- if (!addedCombos.contains (uppercase)) {
- this->m_final += DEFINE_COMBO (uppercase, value);
- addedCombos.emplace (uppercase, true);
- }
- }
- for (const auto& [name, value] : this->m_discoveredCombos) {
- std::string uppercase;
- std::ranges::transform (name, std::back_inserter (uppercase), ::toupper);
- if (!addedCombos.contains (uppercase)) {
- this->m_final += DEFINE_COMBO (uppercase, value);
- addedCombos.emplace (uppercase, true);
- }
- }
- if (this->m_link != nullptr) {
- for (const auto& [name, value] : this->m_link->getCombos ()) {
- std::string uppercase;
- std::ranges::transform (name, std::back_inserter (uppercase), ::toupper);
- if (!addedCombos.contains (uppercase)) {
- this->m_final += DEFINE_COMBO (uppercase, value);
- addedCombos.emplace (uppercase, true);
- }
- }
- for (const auto& [name, value] : this->m_link->getDiscoveredCombos ()) {
- std::string uppercase;
- std::ranges::transform (name, std::back_inserter (uppercase), ::toupper);
- if (!addedCombos.contains (uppercase)) {
- this->m_final += DEFINE_COMBO (uppercase, value);
- addedCombos.emplace (uppercase, true);
- }
- }
- }
- this->m_final += this->applyFragmentVaryingShadowCompatibility (
- this->applyFragmentTexCoordCompatibility (this->applyLinkedVaryingCompatibility (this->m_preprocessed))
- );
- // actual GLSL compilation happens in the pass, which has the context this unit doesn't
- return this->m_final;
- }
- const std::vector<Variables::ShaderVariable*>& ShaderUnit::getParameters () const { return this->m_parameters; }
- const TextureMap& ShaderUnit::getTextures () const { return this->m_defaultTextures; }
|