Write a shader-only node¶
A shader-only node runs a GPU pass and has no C++ behind it at all — no source file, no compiled
binary, no exported node functions. You write a node definition, point it at a shader, and
nos.sys.vulkan does the rest.
This is how nodes like Color Correct are built, and it is the right choice for any node that is purely a fragment or compute pass over its inputs.
1. Declare the dependency¶
The plugin needs nos.sys.vulkan, because that subsystem owns shader compilation:
{
"info": {
"id": { "name": "mycorp.myplugin", "version": "0.1.0" },
"display_name": "My Plugin",
"dependencies": [
{ "name": "nos.sys.vulkan", "version": "8.0" }
]
},
"sdk_version": "41.0",
"schema_version": "1.4-v1"
}
Note there is no binary_path. A plugin containing only shader nodes ships no library.
2. Write the shader¶
Put it anywhere in the plugin; Shaders/ is conventional.
Texture2D Input : register(t0);
SamplerState Sampler : register(s0);
cbuffer Params : register(b0)
{
float Gain;
};
float4 main(float2 uv : TEXCOORD0) : SV_TARGET
{
return Input.Sample(Sampler, uv) * Gain;
}
HLSL and GLSL both work. nos.sys.vulkan tries glslc and dxc and uses whichever compiles.
If the file extension is .spv it is taken as a precompiled SPIR-V blob and used as-is.
3. Define the node¶
The contents field is what makes this a GPU node rather than a job the engine expects C++ for.
{
"nodes": [
{
"class_name": "ColorCorrect",
"menu_info": {
"category": "Filters",
"display_name": "Color Correct"
},
"node": {
"class_name": "ColorCorrect",
"contents_type": "Job",
"contents": {
"type": "nos.sys.vulkan.GPUNode",
"options": {
"shader": "../Shaders/ColorCorrect.hlsl",
"stage": "FRAGMENT"
}
},
"pins": [
{
"name": "Input",
"type_name": "nos.sys.vulkan.Texture",
"show_as": "INPUT_PIN",
"can_show_as": "INPUT_PIN_ONLY"
},
{
"name": "Gain",
"type_name": "float",
"show_as": "PROPERTY",
"can_show_as": "INPUT_PIN_OR_PROPERTY",
"data": 1.0
},
{
"name": "Output",
"type_name": "nos.sys.vulkan.Texture",
"show_as": "OUTPUT_PIN",
"can_show_as": "OUTPUT_PIN_ONLY"
}
]
}
}
],
"schema_version": "1.4-v1"
}
Two details that matter:
The shader path is relative to the manifest, not to the node definition file. In the layout
above, Nodes/ColorCorrect.nosnode and the manifest sit in the plugin root, so ../Shaders/...
resolves from there.
Pins bind to shader parameters by name. The Gain pin above feeds the Gain constant buffer
member. This is the whole point of shader-only nodes — you do not write binding code, you match
names.
Set "stage": "COMPUTE" for a compute shader instead. A render pass needs a fragment shader; the
vertex shader is optional, and a full-quad vertex shader is supplied when you omit it.
4. Build and load¶
There is no C++ to compile, but the plugin still has to be registered with the workspace:
Then load it from the editor's Modules pane. The node appears under Filters.
Iterating on the shader¶
Shaders can be recompiled without restarting the engine. The nos.sys.vulkan API exposes
ReloadShaders(nosName nodeName) for this; the editor surfaces it on GPU nodes.
When you need C++ after all¶
Shader-only nodes cannot hold state between frames, allocate their own resources, or decide at runtime which pass to run. When you need any of that, write a normal node and record GPU commands yourself — see Use the Vulkan subsystem.
The two approaches coexist in one plugin. Shader-only nodes just skip the parts they do not need.