DIRECTIONAL WARP (JOV_GL)
Warp an image along a flow field — the shader trick that makes textures feel alive
- image
- distortion
- direction
- RGBA
- RGB
- MASK
The effect that makes procedural textures look expensive is almost always domain warping - taking a texture and bending its coordinates along some flow rather than just translating it. This node does exactly that to your images: it displaces the UV coordinates using two input maps, one for the direction of the warp and one for the amount of distortion, then re-samples the image along those shifted coordinates. It's the kind of node that goes from "what does this even do" to "I can't stop warping things" in about five minutes.
How it works
The shader calls a warp() helper on the UV before sampling. The direction image is read as a luma mask - bright areas push the sample one way, dark areas the other - and the distortion image controls how far those pushed samples travel. The strength scalar (default 64, no upper bound) sets the overall distance of the displacement, and the edge_x / edge_y enums (CLAMP, WRAP, or MIRROR) decide what happens at the borders when the warped coordinates run off the canvas. WRAP is your friend for seamless-feeling textures; CLAMP is the safe default.
This is a genuinely GPU-native effect - per-pixel, per-frame, zero CPU cost, which is the point of the whole pack. Feed it frames of a video and animate the distortion map and you've got an undulating, liquid look without a single Python loop.
The inputs
- image - the thing you're warping. RGB, RGBA, or MASK.
- distortion - the luma map controlling warp strength per pixel.
- direction - the luma map controlling warp direction.
- strength - displacement distance, default 64.
- edge_x / edge_y - border behavior, both default to CLAMP.
Outputs are the pack standard: RGBA, RGB, and MASK. The MASK output is a genuine bonus here - a warped mask is a cheap way to get "liquid mask" compositing.
Install
The pack's one-time install. Via ComfyUI Manager, search Jovi_GLSL; or:
cd ComfyUI/custom_nodes
git clone https://github.com/Amorano/Jovi_GLSL.git
cd Jovi_GLSL
pip install -r requirements.txt
Restart ComfyUI. Dependencies: PyOpenGL, glfw, opencv-contrib-python, plus cozy_comfyui from git. Needs an OpenGL context at runtime. No models, no keys. Look under JOVI_GLSL 🌈 → MODIFY.
Where people get tripped up
If you feed in two plain images and see nothing, check that your direction and distortion maps actually contain luma variation - flat gray maps produce zero warp, and that's not a bug. Start with a noise texture as the distortion input and you'll see the effect immediately. Also, the strength field's tooltip says "Pixel data range allowed," which is another copy-paste from the posterize node - trust the number, not the label.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| imageopt | IMAGE | RGB(A) image | |
| distortionopt | IMAGE | RGB(A) image used as a LUMA mask for distortion | |
| directionopt | IMAGE | RGB(A) image used as a LUMA mask for direction | |
| strengthopt | FLOAT | 640–9223372036854776000 | Pixel data range allowed |
| edge_xopt | COMBO | CLAMP | Clamp, Wrap or Mirror the Image Edge |
| edge_yopt | COMBO | CLAMP | Clamp, Wrap or Mirror the Image Edge |
| FRAGMENTopt | STRING | // name: DIRECTIONAL WARP // desc: Domain warp an image with a direction and distortion map // category: MODIFY // control: edge #ifndef LIB_CONST #define LIB_CONST //------------------------------------------------------------------------------ // CONSTANT //------------------------------------------------------------------------------ #define M_EPSILON 1.0e-10 // zero value for float comparisons #define M_DEG2RAD 0.017453292519943 // Degree to radian conversion factor #define M_RAD2DEG 57.29577951308232 // Radian to degree conversion factor #define M_TAU 6.283185307179586 // TAU (2 * π) #define M_TAU_INV 0.159154943091895 // TAU Inverse (1 / TAU) #define M_PI 3.141592653589793 // π #define M_PI_INV 0.318309886183790 // π Inverse (1 / π) #define M_PI_2 1.570796326794896 // π divided by 2 (π / 2) #define M_PI_4 0.785398163397448 // π divided by 4 (π / 4) #define M_3PI_4 2.356194490192345 // 3 * π divided by 4 (3π / 4) #define M_PHI 1.618033988749895 // Golden ratio (φ) #define M_PHI_INV 0.618033988749895 // Inverse of golden ratio (1 / φ) #define M_PHI_SQ 2.618033988749895 // Square of the golden ratio (φ^2) #define M_PHI_SQRT5 0.723606797749979 // φ / √5 (useful for fibonacci spherical distribution) #define M_GOLD_ANG 2.399963229728653 // Golden angle in radians #define M_E 2.718281828459045 // Euler's number (base of natural logarithm) #define M_LOG2E 1.442695040888963 // Log base 2 of e #define M_LOG10E 0.434294481903252 // Log base 10 of e #define M_LN2 0.693147180559945 // Natural log of 2 #define M_LN10 2.302585092994046 // Natural log of 10 #define M_SQRT2 1.414213562373095 // Square root of 2 #define M_SQRT3 1.732050807568877 // Square root of 3 #define M_SQRT2_INV 0.707106781186547 // 1 divided by square root of 2 (1 / sqrt(2)) #define M_SQRT3_INV 0.577350269189626 // 1 divided by square root of 3 (1 / sqrt(3)) #define M_SQRT5 2.236067977499790 // Square root of 5 //------------------------------------------------------------------------------ // GENERAL //------------------------------------------------------------------------------ // useful for triangle interpolation vec3 barycentricCoords(vec2 p, vec2 a, vec2 b, vec2 c) { vec2 v0 = b - a; vec2 v1 = c - a; vec2 v2 = p - a; float d00 = dot(v0, v0); float d01 = dot(v0, v1); float d11 = dot(v1, v1); float d20 = dot(v2, v0); float d21 = dot(v2, v1); float denom = d00 * d11 - d01 * d01; vec3 result; result.y = (d11 * d20 - d01 * d21) / denom; result.z = (d00 * d21 - d01 * d20) / denom; result.x = 1.0 - result.y - result.z; return result; } #endif uniform sampler2D image; // | RGB(A) image uniform sampler2D distortion; // | RGB(A) image used as a LUMA mask for distortion uniform sampler2D direction; // | RGB(A) image used as a LUMA mask for direction uniform float strength; // 64;0;;1 | Pixel data range allowed vec2 warp(vec2 uv) { vec4 uv_distortion = texture(distortion, uv); float distortion_val = dot(uv_distortion.rgb, vec3(0.299, 0.587, 0.114)); vec4 uv_direction = texture(direction, uv); float angle = dot(uv_direction.rgb, vec3(0.299, 0.587, 0.114)) * M_TAU; vec2 direction_val = vec2(cos(angle), sin(angle)); uv += direction_val * distortion_val * strength / iResolution.xy; return uv; } void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = warp(fragCoord / iResolution.xy); fragColor = texture(image, uv); } | — |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| RGBA | IMAGE | Full channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output. |
| RGB | IMAGE | Three channel [RGB] image. There will be no alpha. |
| MASK | MASK | Single channel mask output. |