# Using Custom Shader Operators Custom Shader Operators combine TiXL's preset system with its shader pipeline into a powerful and fun way to build your own effects. You write only the **body** of a shader function — the operator wraps it in a complete shader, compiles it on the fly, and shows errors directly in the editor. As of v4.1 TiXL has: - [CustomPixelShader] — compute a color for every pixel of a texture - [CustomPointShader] — move, rotate, scale and color points in a point buffer - [CustomForce] — apply custom forces to particles - [CustomVertexShader] — displace and color the vertices of a mesh - [CustomFaceShader] — manipulate mesh triangles, including breaking them apart - [CustomSDF] — define a distance field that plugs into the field / raymarching pipeline You don't need to write shader code to use them: open the **Variations** window, pick one of the presets and experiment with the parameters. Each op comes with a shared set of parameters — **Offset**, **A**, **B**, **C**, **D**, **GainAndBias** and a **Gradient** — that the shader fragment can read. Good presets include a comment explaining what each parameter does. ## How it works Each operator owns a **template** shader file (the `TemplateFile` parameter — open it to see exactly what wraps your code). The template declares the constant buffers, textures and helper functions, then pastes your `ShaderCode` into the body of its main function: ```hlsl // ...template sets up uv, c, p, v etc. ... { //- METHOD ------------------------------------- /*{method}*/ // ← your ShaderCode lands here //---------------------------------------------- } // ...template writes the expected variables back... ``` Your fragment works with **prepared local variables** and is expected to **set or modify specific output variables**. Everything else — thread dispatch, buffer reads and writes — is handled by the template. The `AdditionalCode` / `AdditionalDefines` parameter is inserted at **global scope** above the main function. Use it for your own helper functions, constants, or declarations of additional resources. ## Required output variables This is the contract per operator — what you get, and what you are expected to set: | Operator | Runs per... | Prepared variables | You set / modify | | -------------------- | ---------------- | ------------------------------------------------------------------------ | ------------------------------------------------- | | [CustomPixelShader] | pixel | `float2 uv`, `int2 PixelCoord`, `int2 TargetSize` | `float4 c` — the output color (defaults to white) | | [CustomPointShader] | point | `Point p`, `uint idx` (alias `i`), `float f` = idx normalized to 0…1 | `p` — written to the result buffer | | [CustomForce] | particle | `Particle p` (read), `float3 vel`, `float3 pos`, `float4 col`, `float age`, `float2 uv` | `vel`, `col`, `pos` — blended back with **Amount** | | [CustomVertexShader] | vertex | `PbrVertex v`, `uint vertexIndex` | `v` — written to the result mesh | | [CustomFaceShader] | triangle | `PbrVertex v1, v2, v3`, `int3 faceIndices`, `uint faceIndex`, `float3 pos1, pos2, pos3` | `v1`, `v2`, `v3` — written as an unshared triangle | | [CustomSDF] | field sample | `float3 p`, `float3 Offset`, `float A, B, C` | `return` the signed distance at `p` | The structs used above: ```hlsl struct Point struct Particle struct PbrVertex { { { float3 Position; float3 Position; float3 Position; float FX1; float Radius; float3 Normal; float4 Rotation; // quat float4 Rotation; // quat float3 Tangent; float4 Color; float4 Color; float3 Bitangent; float3 Scale; float3 Velocity; float2 TexCoord; float FX2; float BirthTime; float2 TexCoord2; }; }; float Selected; float3 ColorRGB; }; ``` ## CustomPixelShader Runs a pixel shader over the target texture. The inputs **ImageA** and **ImageB** are bound as `Texture2D` (with the aliases `Image` and `Image2`), the **Gradient** as a 1-pixel-high lookup texture. Three samplers are available: `Sampler` (wrapped), `ClampedSampler`, and `CustomSampler` (from the **CustomSampler** input). The default code is a simple vignette: ```hlsl float d = 1 - length(uv - 0.5 - Offset * float2(1,-1)); d = ApplyGainAndBias(d, GainAndBias); c = ImageA.Sample(Sampler, uv); c.rgb *= SampleGradient(d).rgb; ``` Displacing one image with another: ```hlsl // Connect two images and use the Offset parameter float4 cb = ImageB.Sample(Sampler, uv); float d = Biased(cb.r); c = ImageA.Sample(Sampler, uv - d * Offset * float2(1,-1)); ``` Additional inputs: use [FloatsToBuffer] / [IntsToBuffer] for extra constant buffers, [SrvFromTexture2d] for more textures, and [SamplerState] for custom samplers. Declare each of them in `AdditionalCode`, e.g. `Texture2D Image3 : register(t3);`. ## CustomPointShader A compute shader running once per point of the connected buffer. `p` is a copy of the source point; whatever you leave in it is written to the output. `f` is the point's normalized position in the buffer (0…1) — ideal for distributing points along curves. `Time` holds the playback time scaled by **TimeScale**, and `TotalCount` the buffer size. The default code moves points along their rotation and colors them with the gradient: ```hlsl float t = Biased( frac(f * (B+1) + A) ); float3 pForward = qRotateVec3(float3(0,0,1), p.Rotation); p.Position += pForward * (t - 0.5); p.Position.y += sin(t * 2 * PI); p.Color = SampleGradient(t); p.Scale += t; ``` A grid of points forming a wave (from the **Wave** preset): ```hlsl int gs = sqrt(TotalCount); float2 gpos = float2(idx % gs, idx / gs) / gs - 0.5; p.Position = float3(gpos.x, 0, gpos.y); float h = length(sin(gpos * 8 + Offset.xz) * 0.1); p.Position.y = h; p.Color = SampleGradient(h * 8); ``` Generating points procedurally also works — set **Count** and leave the **Points** input empty; `p` then starts as a default point at the origin. ## CustomForce Runs once per particle of a particle system. You modify the local copies `vel` (velocity), `col` (color) and `pos` (position); after your code they are blended into the particle with the **Amount** parameter. Avoid writing `pos` directly when you can express the change as a velocity — positions bypass the simulation's speed handling. `age` holds the particle's lifetime in seconds, and `uv` its current screen-space position (useful for sampling the **Image** input as a screen-aligned map). Note the sampler names here: `ClampedSampler` and `WrappedSampler`. Pushing particles out of a connected SDF field: ```hlsl float d = GetDistance(pos); float3 n = GetNormal(pos); if (d < 0) vel += n / (d + 1); ``` The full camera and object transform matrices (`WorldToClipSpace`, `ObjectToWorld`, …) are available for advanced effects. ## CustomVertexShader Runs once per vertex of the connected mesh. Modify the `PbrVertex v` — typically `v.Position`, `v.Normal` or `v.ColorRGB`. Random displacement along the normals (the **SimpleNoise** preset): ```hlsl float4 noise = hash41u(vertexIndex); v.Position += v.Normal * (noise.xyz - 0.5) * Offset * A; ``` Coloring by a connected distance field: ```hlsl // A: Falloff B: Size float d = GetField(float4(v.Position, 0)).w; float t = Biased(smoothstep(1, 0, (d - A) / B)); v.ColorRGB = SampleGradient(t).rgb; ``` ## CustomFaceShader Runs once per triangle and writes the three vertices back **unshared** — every face gets its own vertices. That allows faceting effects that would be impossible with shared vertices: shrinking, rotating or shifting whole triangles. `pos1`–`pos3` keep the original positions while you modify `v1`–`v3`. Coloring faces by their average height: ```hlsl float3 avgPos = (v1.Position + v2.Position + v3.Position) / 3; float4 color = SampleGradient((avgPos.y + Offset.y) * A + B); v1.ColorRGB = color.rgb; v2.ColorRGB = color.rgb; v3.ColorRGB = color.rgb; ``` Exploding faces with per-face noise: ```hlsl v1.Position += (hash41u(faceIndices.x).xyz - 0.5) * C; v2.Position += (hash41u(faceIndices.y).xyz - 0.5) * C; v3.Position += (hash41u(faceIndices.z).xyz - 0.5) * C; ``` ## CustomSDF Unlike the operators above, [CustomSDF] does not run as its own shader — it defines a node in TiXL's **shader graph**. You write the body of a distance function and the field pipeline assembles it into whatever shader samples it (e.g. [RaymarchField], or the Field input of the other Custom ops): ```hlsl float dCustom(float3 p, float3 Offset, float A, float B, float C) { // ← your DistanceFunction code; must return the signed distance at p } ``` The default is a simple sphere: ```hlsl return length(p - Offset) - A; ``` Organic blobs (from the **SinBlobs** preset): ```hlsl float scale = A; float thickness = B; float bias = C; p *= scale; return (abs(dot(sin(p*.5 + Offset), cos(p.zxy * 1.23)) - bias) / scale - thickness) * 0.55; ``` A GLSL-style `mod(x, y)` macro is predefined, which makes porting fractal one-liners from Shadertoy or [jbaker.graphics](https://jbaker.graphics/writings/DEC.html) straightforward — many of the shipped presets (BoxFold, SpiderCave, PipeMaze, …) come from there. Put helper functions into **AdditionalDefines**. There is no Gradient or Image here — a distance function only returns geometry; color it with the field operators that consume it. Visualize the result with [VisualizeFieldDistance], render it with [RaymarchField], and modify it with [BendField], [TransformField], [PolarRepeat] and friends. ## Using Fields [CustomPointShader], [CustomForce], [CustomVertexShader] and [CustomFaceShader] have a **Field** input that accepts field operators — anything from the `field` namespace, including your own [CustomSDF]. The template exposes them through: | Function | Description | | --------------------------------- | ---------------------------------------------------------------------- | | `GetField(float4 p)` | Samples the connected field at `p.xyz`; returns rgb = color, w = distance | | `GetDistance(float3 p)` | Shorthand for the distance value | | `GetFieldNormal(float3 p)` | Normalized field gradient (in [CustomForce] this is called `GetNormal`) | This makes it easy to color, displace or collide against any SDF you can build in the graph. ## Built-in helper functions Available in all template-based operators (everything except [CustomSDF], which only sees what its shader-graph context and **AdditionalDefines** provide): | Name | Description | | ------------------------------- | ------------------------------------------------------ | | `Biased(float f)` | Applies the **GainAndBias** parameter remapping to f | | `SampleGradient(float f)` | Samples the **Gradient** parameter at f (0…1) | | `ApplyGainAndBias(f, gainBias)` | Like `Biased` but with an explicit gain/bias `float2` | | `hash11(f)` … `hash44(v)` | Fast hash functions: digits = output/input component count, e.g. `hash41u(uint)` returns a `float4` from a `uint` seed | Noise functions (`cnoise`, `snoise`, `curlNoise`, …) are included in [CustomVertexShader] and [CustomFaceShader]. For the other ops you can include them via `AdditionalCode`. ### Using Quaternions Point rotations are stored as quaternions. These helpers are available in all ops except [CustomPixelShader]: | Name | Description | Parameters | | --- | --- | --- | | `qMul` | Performs standard Hamiltonian product of two quaternions. | `float4 q1`, `float4 q2` | | `qRotateVec3` | Rotates a 3D vector by a quaternion using an optimized formula. | `float3 v`, `float4 q` | | `qConjugate` | Returns the conjugate of a quaternion (negates the imaginary part). | `float4 q` | | `qInverse` | Calculates the inverse of a quaternion. | `float4 q` | | `qFromAngleAxis` | Creates a quaternion representing a rotation of `angle` around `axis`. | `float angle`, `float3 axis` | | `qFromVectors` | Creates a quaternion representing the rotation from `v1` to `v2`. | `float3 v1`, `float3 v2` | | `qLookAt` | Creates a rotation quaternion that points toward `forward` with specialized `up`. | `float3 forward`, `float3 up` | | `qSlerp` | Performs Spherical Linear Interpolation between two quaternions. | `float4 a`, `float4 b`, `float t` | | `qFromEuler` | Converts Euler angles (yaw, pitch, roll) to a quaternion. | `float yaw`, `float pitch`, `float roll` | | `qToMatrix` | Converts a quaternion into a $4\times4$ rotation matrix. | `float4 quat` | | `qFromMatrix3` | Simplified conversion from a $3\times3$ matrix to a quaternion. | `float3x3 m` | | `qFromMatrix3Precise` | Robust conversion from a $3\times3$ matrix to a quaternion (handles all cases). | `float3x3 m` | Example — orienting points along a generated curve (from the **Knot** preset, with the `Generator` function defined in `AdditionalDefines`): ```hlsl p.Position = Generator(f); float3 up = float3(0,-1,0); float3 fwd = normalize(Generator(f - .01) - Generator(f + .01)); p.Rotation = qLookAt(fwd, up); ``` ## Tips - **Open the template.** The `TemplateFile` parameter points to the full shader source — it is the authoritative reference for what surrounds your code and which registers are bound. - **Use the presets as starting points.** They are regular parameter snapshots; saving your own preset captures the shader code along with all parameters. - **Comment your parameters.** A line like `// A: Falloff` at the top of the fragment makes presets reusable for others. - **Compile errors** appear on the operator — click it to read the HLSL compiler message; line numbers refer to the assembled shader, so check the template for context. ## See also - [Writing code operators](WritingCodeOps.md) — when a shader fragment isn't enough - [Shader development example](ShaderDevelopmentExample.md) — working on full shader files with hot reload