assar.dev
Create Your First Project
Start adding your projects to your portfolio. Click on "Manage Projects" to get started
Visual Shader Scripting
For my specialization, I decided to expand the visual scripting tools I had developed for our engine to facilitate interactive and intuitive creation of shaders.

Premise
Visual scripting tools are popular for creating all sorts of content in games. Among these are shader scripting tools, that offer artists an interface for making bespoke effects to elevate a game's quality.
​
Having enjoyed implementing a node scripting system into our engine already, in addition to my love for programming both tools and graphics, creating a shader scripting tool was a very compelling prospect.
Implementation
I decided to go the route of actually generating valid HLSL code from the node graph, as opposed to using existing framework solutions like DirectX11's Function Linking Graph, for a number of reasons. Primarily, I wanted the tool to be able to stand on its own as much as possible, avoiding cases where a graphics API change would entail a full rewrite.
​
Additionally, generating code from the nodes opens the door to supporting code of any type, and an idea I was initially toying around with was generating Lua code for things like scripted events and gameplay logic.
​
How it works
generation process is split into three steps: Language Definition, Script Parsing, and Shader Compilation.
​
Language Definition
Before any nodes can be parsed and any code generated, the system needs to know what it's working with. This is accomplished by a class very creatively called LanguageDefiner, which receives a JSON file that contains information for understanding the language. The most important of these are a dictionary of built-in data types, and a list of other files that contain annotated code to interpret.
​
Generating Nodes
A node editor is little without its nodes, so they will need to be generated based on the files provided to the language definer. Using the aforementioned annotations, snippets from code files are read and processed into structs that provide a convenient representation of the source. These structs are later provided to specialised nodes that are able to configure themselves based on their assigned data. Continuing the example of functions, the node will in this case add an input pin for each function parameter, as well as an output pin for the function's return value.
​
​
​

Two annotated functions; Forward and Right.

Struct representation of interpreted functions.
Script Parsing
To generate a code output, the system needs to be able to parse a node script, evaluating the order in which nodes should be turned into code, as well as what scopes within the code they belong to.
Order Evaluation
Determining the pasing order is, overall, rather straightforward. The algorithm is implemented as a recursive function, that for a given node repeats itself for each parent node — that is, a node from whom the given node receives an input —, and then for each child node. Due to the recursive nature of the function, this results in a valid parsing order that ensures all resources needed for generating a node's code has been created beforehand.
​
Code Generation
Finally, with parsing order and scopes calculated, the final step can be performed. Since the nodes are akin to wrappers for underlying language objects, they can simply be unwrapped at this stage. Unwrapping essentially consists of generating a string of valid code that declares a new variable and assigns to it a call to the wrapped function.
Given this, creating the final output simply consists of iterating through all scopes in the code and filling them with their nodes' unwrapped strings.
​
Shader Compilation
With all preparation done, it's time for the exciting part! Given that valid code has already been generated, this step is decently simple, with generating preview images for the node editor being the main source of complexity.
​
Final Shader
Given that I had already implemented runtime recompilation of shaders in our engine, getting a working shader from the generated code was extremely simple. The engine's shader factory simply receives the generated shader code, and uses DirectX11's D3DCompile function to turn that into a working shader, ready to use.
​
Shader Preview Steps
To really make the shader editor feel polished and nice to use, displaying all the steps a shader takes to reach its end result is crucial. To achieve this, the code generator keeps a list of nodes that output a colour. For each of these nodes, it creates a variant of the shader that ends at said node, treating its output as the final colour output of the shader. These variants are then used to render a preview model into an atlas, which is selectively displayed as part of the node rendering process.
​

Results, Final Thoughts
With all of the above implemented, the shader scripting tool is finished!
​
There are some improvements that I would like to make to improve the structure of the systems at play, such as decoupling them as much as possible from other parts of our engine. However, as these design changes would necessarily involve some adjustments of things around it, I decided they were outside the project's scope for now.
With that said, the tool is nevertheless capable of providing the intuitive workflow I had hoped to achieve.
​
