Ich habe ein DOMConverter geschrieben, welcher json Datein lesen und in HashListen flatten kann. Folgendes ist meine Render Pipeline Konfiguration und dient als Beispiel um das Konzept zu erklären.
Code: { "global_resources":{ "render_targets":[ {"name":"depth_buffer", "format":"D24F", "scale_width":1.0, "scale_height":1.0, "clear": true}, {"name":"albedo", "format":"RGBA8B", "scale_width":1.0, "scale_height":1.0, "clear": false}, {"name":"normal", "format":"RGBA8B", "scale_width":1.0, "scale_height":1.0, "clear": false}, {"name":"roughness", "format":"RGBA8B", "scale_width":1.0, "scale_height":1.0, "clear": false}, {"name":"hdr_accumulation", "format":"RGBA16F", "scale_width":1.0, "scale_height":1.0, "clear": true} ], "render_passes":[ {"name":"z_prepass", "depth_buffer":"depth_buffer", "process":"FRONT_BACK"}, {"name":"gbuffer", "render_targets":["albedo", "normal", "roughness"], "depth_buffer":"depth_buffer", "process":"PARALLEL"}, {"name":"deferred_shading", "render_targets":["hdr_accumulation"], "depth_buffer":"depth_buffer", "process":"PARALLEL"}, {"name":"transparent", "render_targets":["hdr_accumulation"], "depth_buffer":"depth_buffer", "process":"BACK_FRONT"} ] } }
Die Datein besteht aus mehreren Leveln, von Objekten und Arrays, wie es für ein DOM Tree üblich ist. Der DOM Konverter liest nun die Datei, und für jeden Endknoten wird ein Eintrag mit dem Weg zum Knoten und dem Wert angelegt. Ein Eintrag in der Liste wäre folgender.
Code: Key = .global_resources.render_targets[].name_0 Value = depth_buffer
Die Klammern[] makieren ein Array und _0 sagt das das erste Array Index 0 ist also eigentlich sage ich damit foglendes.
Code: .global_resources.render_targets[0].name
Also wie schreib ich das nicht einfach so hin ? Ich vermeide Strings im Code und in den Daten und mache aus allen Keys und Strings Hashes und speicher diese. Wenn ich nun den Eintrag zugreifen will, dann müsste ich Teile des Strings im Code hinterlegen, weil erst zur Laufzeit bekannt ist, welchen Eintrag ich denn im Array zugreifen will und man würde in etwa sowas verwenden.
Code: auto key = String::Format(".global_resources.render_targets[%d].name", index);
Wie gesagt möchte ich Strings im Code vermeiden und man kann das einfacher Lösen, wenn man die Dynamischen Informationen nachgelagert in den Hash einfliessen lässt.
Code: pass.Name = m_Config.ExpectString(RF_HASH(".global_resources.render_passes[].name"), i);
RF_HASH ist ein Template Konstruktion, welche der Compiler bei aktiver Optimierung zu einer Zahl(RF_TYPE::UInt64) runter vereinfacht und damit zur Runtime gar kein String existiert. Im 2. Schritt werden alle übergebenen Indices zu einem String zusammen gebaut, durch "_" voneinander getrennt und am Anfang der Hash gesetzt.
Code: Dieser String wird zur Runtime Konstruiert und ist im Stringpool ledeglich nur 1x als "%llu" und 1x als "_%llu"(llu == UInt64) zu finden, egal welche Kombinationen ich verwende. Nun weiter, der String wird auch durch die gleiche Hash Funktion zur laufzeit gejagt und der neue Hash entspricht einen existierenden Hash Eintrag in der Hashliste, die der DOM Konverter gebaut hatte.
Code: DomConvert output String = 8986800961242094592_3 String = 11476899374313023506_3 Parameter count = .global_resources.render_passes[].render_targets = 12797011335562384212 Array size = 12797011335562384212_3 String = 13853049662206441152_3_0 Parameter count = .global_resources.render_targets = 705631002783424115 Array size = .global_resources.render_targets Parameter count = .global_resources.render_targets[].clear = 4601265248431463828 Integer type = 4601265248431463828_0 ...
Folgendes ist dann ein Code Auszug aus meiner Renderer Klasse.
Code: m_Passes.Resize(m_Config.ExpectInteger(RF_HASH(".global_resources.render_passes"))); for (RF_Type::Size i = 0; i < m_Passes.Count(); ++i) { auto& pass = m_Passes(i); pass.Name = m_Config.ExpectString(RF_HASH(".global_resources.render_passes[].name"), i); pass.DepthBuffer = m_Config.ExpectString(RF_HASH(".global_resources.render_passes[].depth_buffer"), i); pass.Process = m_Config.ExpectString(RF_HASH(".global_resources.render_passes[].process"), i); pass.BindTargets.Resize(m_Config.ExpectInteger(RF_HASH(".global_resources.render_passes[].render_targets"), i)); for (RF_Type::Size j = 0; j < pass.BindTargets.Count(); ++j) { pass.BindTargets(j) = m_Config.ExpectString(RF_HASH(".global_resources.render_passes[].render_targets[]"), i, j); } }
Der Code ist leserlicher, weil man die Lesbaren Pfade sieht und der Code ist schneller, weil man zur Laufzeit mit UInt64 Werten rum hantiert und Hash Tables mögen die wesentlich mehr.
Das DomConvert Tool kann auch noch Resolve Points speicher, also Hash und String, damit man wieder an den Wert kommt und die Keys kann man auch mit Echtnamen exportieren lassen aber benutzen tu ich das nicht. Aktuell verwende ich folgende Varianten für die jeweiligen Use-Cases.
- globalconfig.json mit Value Resolve Entries
- localization/*.json mit Value Resolve Entries
- materials/*.json nur Hashes
- renderer/*.json nur Hashes
- textures/*.json nur Hashes
- shaders/*.json nur Hashes
Die globalconfig.json enthält Engine spezifische Pfade, die natürlich als Hash nich so viel bringen.
Code: { "RendererConfigPath":"renderer/config.json.table", "SoundConfigPath":"sound/config.json.table", "LocalizationPath":"localization/", "DefaultLanguage":"en", "resolve":[ {"from":"data", "to":"appdir:data/"}, {"from":"shader", "to":"data:shaders/"}, ...
Die Lokalisierung guckt nach [Sprache]_[unwichtigername].json und entscheidet ob die benötigt wird.
Code: { "groupname" : "errors", "NoRenderConfig" : "Couldn't load renderer configuration!" }
Im Programm hab ich dann folgendes.
Code: RF_IO::LogError(Localization::Instance().GetText(RF_HASH("errors"), RF_HASH(".NoRenderConfig")).c_str());
Der Renderer lädt und Konfiguriert sich übrigens zur Laufzeit neu, wenn ich Änderungen an der Konfiguration mache. Der RadonConvert Prozess läuft immer im Hintergrund und kümmert sich um das automatische Konvertieren und benachrichtigen von den Verbundenen Clients. Folgendes ist mein buildconfig.lua Script, welches ragiert, wenn das geänderte Asset aus dem renderer Ordner kommt und mit eine Json Datei ist. Der Client wird dann durch GenerateAsset Informiert, dass das outputFilename Asset neu geladen werden sollte.
Code: function splitfilename(strfilename) -- Returns the Path, Filename, and Extension as 3 values return string.match(strfilename, "(.-)([^\\/]-([^\\/%.]+))$") end function start(inputFilename) path, file, extension = splitfilename(inputFilename) outputFilename = path..file..".table" if (path == "renderer/" and extension == "json") then Execute("DOMConverter.exe", "-i "..GetAssetDir()..inputFilename.." -o "..GetExportDir()..outputFilename) GenerateAsset(outputFilename) end end
|