Nodes/Jovi_GLSL/TRANSFORM (JOV_GL)
ComfyUI Node

TRANSFORM (JOV_GL)

Move, rotate, scale, and tile — one shader node instead of a stack of geometry nodes

By Amorano·Created 2 years ago·Updated about a year ago· 20
TRANSFORM (JOV_GL)
  • image
  • offset
  • tile
  • RGBA
  • RGB
  • MASK
rotate0.000
edge_xCLAMP
edge_yCLAMP
FRAGMENT// name: TRANSFORM // desc: Move, Rotate, Scale and Tile an image // category: TRANSFORM // 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) input to repeat uniform vec2 offset; // 0.0,0.0;-0.5;0.5;0.001 | positional offset (-0.5..0.5) uniform float rotate; // 0;0;1;0.001 | rotation from 0..2pi uniform vec2 tile; // 1.0,1.0;1;2048;1 | repetitions on X and Y void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // normalize + offset vec2 uv = (fragCoord - offset * iResolution.xy) / iResolution.xy; // rotation matrix float cosAngle = cos(rotate * M_TAU); float sinAngle = sin(rotate * M_TAU); mat2 rotationMatrix = mat2(cosAngle, -sinAngle, sinAngle, cosAngle); // center rotate, scale uv = rotationMatrix * (uv - 0.5) + 0.5; vec2 repeat = vec2(min(iResolution.x / 4., tile.x), min(iResolution.y / 4., tile.y)); uv *= repeat; fragColor = texture(image, uv); }

Positioning an image inside ComfyUI is usually a small ceremony: a scale node, a crop node, a rotation node, maybe a tile node, all chained together with geometry you can't see. This node folds the whole thing into one shader pass - offset, rotate, and tile, plus edge behavior, all on the GPU and all live-updating as you drag. It's the node you reach for when you want to reposition a texture, build a tiling pattern, or composite something at an angle without turning your graph into a geometry jungle.

How it works

The shader takes the UV coordinates, applies a positional offset, rotates around the center by the rotate angle, and multiplies by a tile count before sampling the image. Rotation is in turns, not degrees: the rotate input is normalized 0–1, where 1 equals a full 360°, so 0.25 is a quarter turn. The tile VEC2 controls repetitions on X and Y - set both to 4 and you get a 4×4 grid of the input. The edge_x / edge_y enums (CLAMP, WRAP, or MIRROR) decide what happens at the borders, and MIRROR is the one you want if you're building seamless tiles out of a non-seamless source.

The inputs

  • image - the thing to transform, RGB, RGBA, or MASK.
  • offset - positional offset in normalized space, default (0, 0).
  • rotate - rotation in turns (0–1), default 0.
  • tile - repetitions on X and Y, default (1, 1).
  • edge_x / edge_y - border behavior, both default to CLAMP.

Outputs are the pack's RGBA, RGB, and MASK. Because it transforms the sampling rather than resampling the pixels, you can tile a mask through the MASK output and get a repeated selection for free.

Install

One-time pack 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. Standard pack deps (PyOpenGL, glfw, opencv-contrib-python, cozy_comfyui from git), needs an OpenGL context. No models, no keys. Under **JOVI_GLSL 🌈 → TRANSFORM`.

Gotchas and tips

The two things that bite people: rotation is in turns (0.5 = 180°, not 0.5°), and the tile caps at a quarter of the resolution per axis - the shader won't let you tile down to single pixels, which keeps sampling sane but means "infinite tiny tiles" isn't available. Also, because it's coordinate-space transformation, there's no interpolation-quality dial; for most tiling work that's fine, but if you're doing precision downscaling, a proper scale node is still the better tool. For building patterns and repositioning, this is the one.

CategoryJOV_GL 🌈/TRANSFORM

Inputs (7)

NameTypeDefaultDescription
imageoptIMAGERGB(A) input to repeat
offsetoptVEC20,0positional offset (-0.5..0.5)
rotateoptFLOAT0.0000–1rotation from 0..2pi
tileoptVEC21,1repetitions on X and Y
edge_xoptCOMBOCLAMPClamp, Wrap or Mirror the Image Edge
edge_yoptCOMBOCLAMPClamp, Wrap or Mirror the Image Edge
FRAGMENToptSTRING// name: TRANSFORM // desc: Move, Rotate, Scale and Tile an image // category: TRANSFORM // 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) input to repeat uniform vec2 offset; // 0.0,0.0;-0.5;0.5;0.001 | positional offset (-0.5..0.5) uniform float rotate; // 0;0;1;0.001 | rotation from 0..2pi uniform vec2 tile; // 1.0,1.0;1;2048;1 | repetitions on X and Y void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // normalize + offset vec2 uv = (fragCoord - offset * iResolution.xy) / iResolution.xy; // rotation matrix float cosAngle = cos(rotate * M_TAU); float sinAngle = sin(rotate * M_TAU); mat2 rotationMatrix = mat2(cosAngle, -sinAngle, sinAngle, cosAngle); // center rotate, scale uv = rotationMatrix * (uv - 0.5) + 0.5; vec2 repeat = vec2(min(iResolution.x / 4., tile.x), min(iResolution.y / 4., tile.y)); uv *= repeat; fragColor = texture(image, uv); }

Outputs (3)

NameTypeDescription
RGBAIMAGEFull channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output.
RGBIMAGEThree channel [RGB] image. There will be no alpha.
MASKMASKSingle channel mask output.