Files
2026-07-13 13:13:17 +08:00

275 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<float4>` (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<float4> 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