Custom Shader Deformer
Updated: 29 Jun 2026
Add a custom .fx shader script as a Deformer.
Updated: 29 Jun 2026
Add a custom .fx shader script as a Deformer.
See Working With Custom Shaders for an overview on managing .fx resources and HLSL scripts in Notch.
This node allows you to add a custom .fx shader script as a Deformer, to create your own deformers using shader code. Notch shaders are written in HLSL.
Custom Shader Deformers allow for the manipulation of each individual vertex’s position, motion, normal direction, colour and motion. For an overview of deformers, see This Page.
This node receives information on a mesh once per frame. As determined by your HLSL code, you can perform operations on that data and use it to deform the vertices. You can also define your own variables that can be controlled from the Property Editor or via modifiers, just like other parameters in Notch.
As with all deformers, the order of nodes in the node graph will impact the order of operation and therefore the behaviour of each node. In short, deformers that are placed at the top left of the node graph will be processed first, then those that are lower down and to the right. For more information on this, see this section on Node Hierarchy and Data Flow
Custom Shader Deformers require the implementation of your own code, and requires some knowledge of these systems to implement. Use of custom shaders is at your own risk. 10Bit does not provide support for custom shader code. If you experience an issue with a project/block you will need to demonstrate your issue without your custom shader in the nodegraph, to be able to receive support from the 10bit team. Support is only available for issues specifically related to the functionality of the Custom Shader Deformer Node, not for any custom code. External tools are available that can provide good assistance in writing these nodes when provided with the example script.
Once you have gotten set up with the example script as shown in Set Up you are ready to start reading and customising its HLSL code.
This script uses mathematical functions to animate the position and colour of the vertices it acts on, as determined by its falloff properties (the falloff applies a weighting to each vertex that can be used to set how much the deformer acts on any given vertex).
There will be more detail in later sections, but in brief, in order to achieve this within this file:
CS_ApplyDeformer. This function is run once per frame, per vertex. N.B. This code will run on multiple threads simultaneously.The example code has very useful information on how to edit this shader or write these shaders. But before we get into that let’s look at the most essential functions here.
The Custom Shader Deformer lets you input floats, textures and colours, with sliders, colour controls, check boxes or menu options.
These parameters are declared in the example script like so:
float Freq_X < string propertyName = "Freq X"; > = 1.0f;
float Freq_Y < string propertyName = "Freq Y"; > = 0.0f;
float Freq_Z < string propertyName = "Freq Z"; > = 1.0f;
float Rotate_All = 0.0f;
float Scale_All = 0.0f;
This is just a code snippet. You can download the full example script Here.
All global parameters without semantic ( : PARAM) binding are user params.
The property name can be set with < ... > annotations to describe its attributes. Supported annotations for properties are as follows:
| Annotation | Description |
|---|---|
string propertyName = "My Name" |
Name shown in the Notch property editor |
float propertyMin = 0.0f |
Controls the default min range of the slider property |
float propertyMax = 1.0f |
Controls the default max range of the slider property |
bool propertyHide = true |
Hides the shader parameter from the property editor |
string propertyType = "colour" / "checkbox" |
When not given, float slider is used as default |
string propertyEnums[] = { "Small", "Big" } |
A list of property enums, gives values 0, 1, .. to the shader code |
Custom RW buffers also support the following annotations:
| Annotation | Description |
|---|---|
int byteSize = 4 |
How many bytes are reserved for the custom RW buffer |
int numItems = 1000 |
Override the number of items in the custom buffer (default or 0 reserves number of vertices) |
For example:
// Would create a slider named Freq Y, which has the default range of -10.0 to 10,0, and a default value of 0.
float Freq_Y <
string propertyName = "Freq Y";
float propertyMin = -10.0f;
float propertyMax = 10.0f;
> = 0.0f;
// Would create a menu with options displayed as small and big, and would return 0 and 1 respectively.
float Size_Object <
string propertyName = "Size";
string propertyEnums[] = { "Small", "Big" };
> = 0.0f;
This is just a code snippet. You can download the full example script Here.
If no annotations are applied, the property will be displayed with the variable name as the property name, with underscores converted to spaces. It will be assigned a value slider with a default range of 0 to 1.
A key part of using this node is the use of buffers. These buffers are read once per frame. They are accessed to get information on each vertex. This data can then be manipulated and saved back into the same buffers. The buffers will then be used to update the properties of each vertex index once per frame.
The buffers that determine the vertex states that we have available to work with are shown within the example code:
| Buffer Name | Type | Description |
|---|---|---|
VertexCountBuffer |
StructuredBuffer<uint> |
Stores the total number of vertices in the mesh at index 0. Read only. |
GeneratedWeightmapBuffer |
StructuredBuffer<float> |
A per-vertex weighting from a generated weightmap, used to mask or blend the deformer effect. Read only. |
ChunkPositionBuffer |
StructuredBuffer<float4> |
Stores the centre position of each chunk when the mesh is using chunk-based deformation. |
VertexChunkIndexBuffer |
StructuredBuffer<uint> |
Maps each vertex to its corresponding chunk index. Used when UseChunks is set. |
RWVertexBuffer |
RWStructuredBuffer<float4> |
The current world or local position of each vertex as a float4. Read and write. |
RWVertexMotionBuffer |
RWStructuredBuffer<float4> |
Per-vertex motion vector, stored as the delta from the previous frame position. Used for motion blur. Read and write. |
NormalBuffer |
StructuredBuffer<float4> |
Per-vertex normal direction as a float4. Used to displace vertices along their normals. Read only. |
RWVertexColourBuffer |
RWStructuredBuffer<float4> |
Per-vertex RGBA colour stored as a float4. Value range for RGBA is 0.0 to 1.0. Read and write. |
The following system values are also available:
| Name | Type | Description |
|---|---|---|
WorldSpaceDeformer |
uint |
When set, vertex positions should be transformed into world space before deformation using the World matrix. |
World |
float4x4 |
The object-to-world transform matrix. |
WorldInverse |
float4x4 |
The inverse of the world transform matrix. |
AnimationTime |
float |
The current animation time in seconds. |
TimeDelta |
float |
The time elapsed since the last frame in seconds. |
As these buffers are read and written once per frame, any changes you write to these buffers are overwritten each frame by the deformer system, so you cannot use them to carry values between frames.
You can create your own custom buffers that allow you to hold information across frames.
By default, each vertex will have a dedicated entry in the buffer.
The buffers will be cleared once to 0 on load, and can be reset using the node’s reset pin.
The size of a buffer item must be defined with the byteSize annotation as shown in the example script:
RWStructuredBuffer<float> RWCustomCloneBuffer <int byteSize=4;>;
This is just a code snippet. You can download the full example script Here.
This is really where the fun starts. What you write within the function CS_ApplyDeformer is what is going to let you start messing with your geometry. This node is completely customisable, and within reason you can do what you want to it. Below is a description of how the example code works.
The example deformer displaces vertices along their normals using a combination of sine and cosine waves driven by AnimationTime, producing a rippling surface animation. An optional texture can also contribute to the displacement via TextureDisplace. The effect is blended back against the original vertex position using BlendAmount, and the falloff and weightmap are both applied so the deformation respects any masking you have set up on the node.
Motion vectors are also calculated correctly for each vertex by computing the displacement at both the current and previous frame times and writing the delta between them into RWVertexMotionBuffer, ensuring motion blur works as expected.
Finally, the per-vertex colour is blended towards the Colour parameter based on the falloff weight, so the deformer can also drive colour changes on the mesh.
#if 1
#include "FalloffShaderDef.h"
// Falloff API
// GetFalloff(float3 pos)
#endif
StructuredBuffer <uint> VertexCountBuffer : VERTEXCOUNTBUFFER;
StructuredBuffer <float> GeneratedWeightmapBuffer : GENERATEDWEIGHTMAPBUFFER;
uint UseGeneratedWeightmapBuffer : USEGENERATEDWEIGHTMAPBUFFER;
StructuredBuffer <float4> ChunkPositionBuffer : CHUNKPOSITIONBUFFER;
StructuredBuffer <uint> VertexChunkIndexBuffer : VERTEXCHUNKINDEXBUFFER;
uint UseChunks : USECHUNKS;
RWStructuredBuffer <float4> RWVertexBuffer : RWVERTEXBUFFER;
RWStructuredBuffer <float4> RWVertexMotionBuffer : RWVERTEXMOTIONBUFFER;
StructuredBuffer <float4> NormalBuffer : NORMALBUFFER;
RWStructuredBuffer<float4> RWVertexColourBuffer : RWVERTEXCOLOURFLOAT4BUFFER;
uint WorldSpaceDeformer : WORLDSPACEDEFORMER;
float4x4 World : WORLD;
float4x4 WorldInverse : WORLDINVERSE;
float AnimationTime : ANIMATIONTIME;
float TimeDelta : TIMEDELTA;
// -- Custom Shader Parameters --
// Set your custom shader parameters here. All global parameters without semantic ( : PARAM) binding are user params.
// The property name can be set with < ... > annotations or underscores like below (the underscores are converted as spaces in the name).
// The default values given here (= 1.0f; etc) propagate into the Notch property editor for new parameters.
// Supported annotations for properties:
// * string propertyName = "My Name"; - name shown in the Notch property editor
// * float propertyMin = 0.0f; - controls the default min range of the slider property
// * float propertyMax = 1.0f; - controls the default max range of the slider property
// * bool propertyHide = true; - hide the shader parameter from the property editor
// * string propertyType = "colour"; - colour editor (when not given, float slider is used as default)
// * string propertyType = "checkbox"; - checkbox editor (when not given, float slider is used as default)
// * string propertyEnums[] = { "Small", "Big" }; - a list of property enums, gives values 0,1,.. to the shader code
// Annotations for custom RW buffers:
// * int byteSize = 4; - how many bytes are reserved for custom RW buffers
// * int numItems = 1000; - override number of items in the custom buffer (default or 0 reserves number of vertices)
float BlendAmount < string propertyName = "Blend Amount"; > = 1.0f;
float Displacement = 1.0f;
float Freq_X = 16.0f;
float Freq_Z = 16.0f;
float Bias < float propertyMin = -1.0f; float propertyMax = 1.0f; > = 0.2f;
float4 Colour < string propertyType = "colour"; > = float4(1.0f, 0.5f, 0.0f, 1.0f);
float Size < string propertyEnums[] = { "Small", "Big" }; > = 0.0f;
Texture2D <float4> DisplacementTexture < string propertyName = "Displacement Texture"; > ;
float TextureDisplace < string propertyName = "Texture Displace"; > = 0.0f;
// Custom buffers can be used for state updates across frames.
// By default, each vertex has one dedicated entry in the buffer (index with vertexIndex).
// The buffer content is cleared once to 0, and on node Reset input signal.
// The size of a buffer item has to be defined with the byteSize annotation like below.
RWStructuredBuffer<float> RWCustomVertexBuffer <int byteSize=4;>;
sampler LinearWrapSampler
{
Filter = Min_Mag_Mip_Linear;
AddressU = Wrap; AddressV = Wrap;
};
float3 Displace(float3 pos, float weightmapWeight, uint vertexIndex, float time)
{
float3 origPos = pos;
[flatten] if (WorldSpaceDeformer)
pos = mul(float4(pos.xyz, 1.0f), World);
float falloffWeight = GetFalloff(origPos);
float defAmt = sin(pos.x * Freq_X + time) + cos(pos.z * Freq_Z + time);
defAmt = Displacement * defAmt + Bias;
defAmt += TextureDisplace * DisplacementTexture.SampleLevel(LinearWrapSampler, float2(pos.xy), 0).r;
float3 disp = NormalBuffer[vertexIndex].xyz * defAmt;
[flatten] if (UseChunks)
{
disp = pos * defAmt;
}
return disp * falloffWeight * weightmapWeight;
}
[numthreads(64, 1, 1)] void CS_ApplyDeformer(uint3 GroupId : SV_GroupID,
uint3 DispatchThreadId : SV_DispatchThreadID,
uint3 GroupThreadId : SV_GroupThreadID,
uint GroupIndex : SV_GroupIndex)
{
uint vertexIndex = DispatchThreadId.x;
if (vertexIndex < VertexCountBuffer[0])
{
float4 vertexPos = RWVertexBuffer[vertexIndex];
float4 origVertexPos = vertexPos;
float4 motion = RWVertexMotionBuffer[vertexIndex];
float3 prevVertexPos = vertexPos.xyz - motion.xyz;
float3 origPrevVertexPos = prevVertexPos;
float weightmapAmount = 1.0f;
[flatten] if (UseGeneratedWeightmapBuffer)
{
weightmapAmount = GeneratedWeightmapBuffer[vertexIndex];
}
float3 pos = vertexPos.xyz;
float3 prevPos = prevVertexPos.xyz;
prevPos = pos;
uint chunkIndex = vertexIndex;
[flatten] if (UseChunks)
{
chunkIndex = VertexChunkIndexBuffer[vertexIndex];
pos = ChunkPositionBuffer[chunkIndex].xyz;
}
float3 disp = Displace(pos, weightmapAmount, chunkIndex, AnimationTime);
float3 dispPrev = Displace(prevPos, weightmapAmount, chunkIndex, AnimationTime - TimeDelta);
vertexPos.xyz = lerp(origVertexPos.xyz, vertexPos.xyz + disp, BlendAmount);
prevVertexPos.xyz = lerp(origPrevVertexPos.xyz, prevVertexPos.xyz + dispPrev, BlendAmount);
float falloffWeight = GetFalloff(vertexPos.xyz);
float4 vertexColour = RWVertexColourBuffer[vertexIndex];
vertexColour = lerp(vertexColour, Colour, falloffWeight);
RWVertexColourBuffer[vertexIndex] = vertexColour;
RWVertexBuffer[vertexIndex] = vertexPos;
RWVertexMotionBuffer[vertexIndex] = float4(vertexPos.xyz - prevVertexPos.xyz, 0);
}
}
// A custom deformer must implement the ApplyDeformer technique
technique11 ApplyDeformer
{
pass p0
{
SetComputeShader(CompileShader(cs_5_0, CS_ApplyDeformer()));
}
};
The node can be reset by sending a momentary value of 1 into its “Reset” input pin. In practice this clears all its buffers and resets its effect.
When you are working with this node, you may make errors in your code that cause the node not to function. For this reason, useful information is shown inside of Notch’s log window to point out where you have gone wrong. It is recommended to work with your log window open when editing your code. This can be done from View -> Log Window.
This compute shader runs 64 threads simultaneously per group, each processing one vertex in parallel. This is part of what makes deformer systems efficient, but it introduces some rules you need to follow to avoid unpredictable results.
vertexIndex. As long as you only read and write to your own index, you are safe. However, if you start using the current thread to overwrite information about other vertices, that information could well be overwritten by another thread that runs just after it.The deformer can operate in either local or world space, controlled by the WorldSpaceDeformer flag. When world space is active, vertex positions are transformed using the World matrix before deformation is applied. This is useful when you want the deformation pattern to remain consistent regardless of how the object is moved or rotated in the scene.
Unlike the cloner effector, the deformer is responsible for correctly writing motion vectors into RWVertexMotionBuffer. This is important for motion blur to work correctly. The example script shows the correct pattern to compute the displacement for both the current frame (using AnimationTime) and the previous frame (using AnimationTime - TimeDelta), then write the delta between the two as the motion vector.
When a mesh is using chunk-based deformation (UseChunks is set), vertex positions are driven by the centre position of their chunk rather than their individual position. In this mode you should index into ChunkPositionBuffer using VertexChunkIndexBuffer[vertexIndex] to get the position to use as input to your deformation function, as shown in the example.
Remember that the presence of other deformers above this node in the hierarchy will impact the data that arrives in the vertex buffers. This is important to keep in mind as you are going to be working with the data as it arrives at your deformer, after it has already been changed by any other deformers.
These properties control the 3D transforms of the node. Transforms will generally be inherited by child nodes, although they can be ignored through the Inherit Transform Channels attributes.
| Parameter | Details |
|---|---|
| Position X | The objects position along the local x-axis. |
| Position Y | The objects position along the local y-axis. |
| Position Z | The objects position along the local z-axis. |
| Rotation Heading | The objects rotation around the local y-axis. |
| Rotation Pitch | The objects rotation around the local x-axis. |
| Rotation Bank | The objects rotation around the local z-axis. |
| Scale X | The objects scale along the local x-axis. |
| Scale Y | The objects scale along the local y-axis. |
| Scale Z | The objects scale along the local z-axis. |
Control the inheritance of the transforms from the parent.
| Parameter | Details |
|---|---|
| Position | Toggle inheritance of the Position from the parent. |
| Rotation | Toggle inheritance of the Rotation from the parent. |
| Scale | Toggle inheritance of the Scale from the parent. |
| World Position Only | Inherit the world position from the parent only, rotation and scale will be ignored. Overrides above properties. |
| Inherit Time | Toggle inheritance of time from the parent. |
These properties control the core behaviours of the node.
| Parameter | Details |
|---|---|
| Apply Mode |
Choose whether the deformer applies to the individual vertices or the mesh chunks.
|
| Transform Space |
Choose which transform space the deformer uses for deformation. Does not affect falloffs.
|
| Create Shader File.. | Create a template shader file to edit. |
| Shader | Choose which .fx shader file to use. |
The properties control the time at which the node is active. See Timeline for editing time segments.
| Parameter | Details |
|---|---|
| Duration |
Control the duration of the node’s time segment.
|
| Node Time | The custom start and end time for the node. |
| Duration (Timecode) | The length of the node’s time segment (in time). |
| Duration (Frames) | The length of the node’s time segment (in frames). |
| Time Segment Enabled | Set whether the node’s time segment is enabled or not in the Timeline. |
| Name | Description | Typical Input |
|---|---|---|
| Transform Node | Apply the transforms of other nodes to this node. | Null |
| Bounding Box | Limits the area that the deformer will affect. | Bounding Box |
| Falloff Node | Use an input falloff node to override the falloff. | Falloff |
| Generated Weightmap | Add a weightmap to vary the strength of the deformer across the surface. | Generate Weightmap |
| Reset | Resets the deformer and its buffers when the input value changes from 0 to 1. | Modifier |
| Transform Modifiers | Apply the transforms of another node to this node. | Null |
| Target Node | Modifiy the rotations of the node to always direct the z axis towards the input. | Null |
| Local Transform Override | Apply the transforms of another node to this node, relative to its parent. | Null |