Cellular Triangulation

This is an advanced, procedural Triangular Tiling Shader adapted for Godot’s CanvasItem. It generates a continuous, mosaic-like pattern where the color of each triangle is determined by its center point and a time-varying gradient.

The technique involves:

  1. Triangular Grid: Transforming screen coordinates into a triangular grid space (cart2tri).

  2. Point Jitter: Using Cubic Spline Interpolation (randCircleSpline) driven by time and noise (hash) to smoothly move the vertices of the grid triangles, creating organic, flowing motion.

  3. Color Blending: Applying a dynamic linear gradient based on the triangle’s position and time, and blending between the custom “Ice” and “Fire” colors (pal).

  4. Anti-aliasing: Using signed distance functions (dseg) and smoothstep to create smooth anti-aliased borders between the triangles.

 

Adjustable Uniforms (Shader Parameters):

 

Parameter Type Description
color_hielo vec3 The base color for one end of the spectrum (the “Ice” color).
color_fuego `vec3** The base color for the opposite end of the spectrum (the “Fire” color).
Shader code
shader_type canvas_item;


uniform vec3 color_hielo : source_color = vec3(0, 1, 1);   
uniform vec3 color_fuego : source_color = vec3(0.41, 0.0, 0.0);   
const float s3 = 1.7320508075688772;
const float i3 = 0.5773502691896258;
const mat2 tri2cart = mat2(vec2(1.0, 0.0), vec2(-0.5, 0.5 * s3));
const mat2 cart2tri = mat2(vec2(1.0, 0.0), vec2(i3, 2.0 * i3));
const float HASHSCALE1 = 0.1031;
const vec3 HASHSCALE3 = vec3(443.897, 441.423, 437.195);


vec3 pal(in float t) {
	return mix(color_hielo, color_fuego, t);
}


float hash12(vec2 p) {
	vec3 p3 = fract(vec3(p.xyx) * HASHSCALE1);
	p3 += dot(p3, p3.yzx + 19.19);
	return fract((p3.x + p3.y) * p3.z);
}
vec2 hash23(vec3 p3) {
	p3 = fract(p3 * HASHSCALE3);
	p3 += dot(p3, p3.yzx + 19.19);
	return fract((p3.xx + p3.yz) * p3.zy);
}
vec3 bary(vec2 v0, vec2 v1, vec2 v2) {
	float inv_denom = 1.0 / (v0.x * v1.y - v1.x * v0.y);
	float v = (v2.x * v1.y - v1.x * v2.y) * inv_denom;
	float w = (v0.x * v2.y - v2.x * v0.y) * inv_denom;
	float u = 1.0 - v - w;
	return vec3(u, v, w);
}
float dseg(vec2 xa, vec2 ba) {
	return length(xa - ba * clamp(dot(xa, ba) / dot(ba, ba), 0.0, 1.0));
}
vec2 randCircle(vec3 p) {
	vec2 rt = hash23(p);
	float r = sqrt(rt.x);
	float theta = 6.283185307179586 * rt.y;
	return r * vec2(cos(theta), sin(theta));
}
vec2 randCircleSpline(vec2 p, float t) {
	float t1 = floor(t);
	t -= t1;
	vec2 pa = randCircle(vec3(p, t1 - 1.0));
	vec2 p0 = randCircle(vec3(p, t1));
	vec2 p1 = randCircle(vec3(p, t1 + 1.0));
	vec2 pb = randCircle(vec3(p, t1 + 2.0));
	vec2 m0 = 0.5 * (p1 - pa);
	vec2 m1 = 0.5 * (pb - p0);
	vec2 c3 = 2.0 * p0 - 2.0 * p1 + m0 + m1;
	vec2 c2 = -3.0 * p0 + 3.0 * p1 - 2.0 * m0 - m1;
	vec2 c1 = m0;
	vec2 c0 = p0;
	return (((c3 * t + c2) * t + c1) * t + c0) * 0.8;
}
vec2 triPoint(vec2 p) {
	float t0 = hash12(p);
	return tri2cart * p + 0.45 * randCircleSpline(p, 0.15 * TIME + t0);
}
void tri_color(in vec2 p, in vec4 t0, in vec4 t1, in vec4 t2, in float scl, inout vec4 cw) {
	vec2 p0 = p - t0.xy;
	vec2 p10 = t1.xy - t0.xy;
	vec2 p20 = t2.xy - t0.xy;
	vec3 b = bary(p10, p20, p0);
	float d10 = dseg(p0, p10);
	float d20 = dseg(p0, p20);
	float d21 = dseg(p - t1.xy, t2.xy - t1.xy);
	float d = min(min(d10, d20), d21);
	d *= -sign(min(b.x, min(b.y, b.z)));
	if (d < 0.5 * scl) {
		vec2 tsum = t0.zw + t1.zw + t2.zw;
		vec3 h_tri = vec3(hash12(tsum + t0.zw), hash12(tsum + t1.zw), hash12(tsum + t2.zw));
		vec2 pctr = (t0.xy + t1.xy + t2.xy) / 3.0;
		float theta = 1.0 + 0.01 * TIME;
		vec2 dir = vec2(cos(theta), sin(theta));
		float grad_input = dot(pctr, dir) - sin(0.05 * TIME);
		float h0 = sin(0.7 * grad_input) * 0.5 + 0.5;
		h_tri = mix(vec3(h0), h_tri, 0.4);
		float h = dot(h_tri, b);
		vec3 c = pal(h);
		float w = smoothstep(0.5 * scl, -0.5 * scl, d);
		cw += vec4(w * c, w);
	}
}

void fragment() {
	vec2 p = (SCREEN_UV - 0.5) * 8.0;
	float scl = 0.01;
	vec2 tfloor = floor(cart2tri * p + 0.5);
	vec2 pts[9];
	for (int i = 0; i < 3; ++i) {
		for (int j = 0; j < 3; ++j) {
			pts[3 * i + j] = triPoint(tfloor + vec2(float(i - 1), float(j - 1)));
		}
	}
	vec4 cw = vec4(0.0);
	for (int i = 0; i < 2; ++i) {
		for (int j = 0; j < 2; ++j) {
			vec4 t00 = vec4(pts[3 * i + j],     tfloor + vec2(float(i - 1), float(j - 1)));
			vec4 t10 = vec4(pts[3 * i + j + 3], tfloor + vec2(float(i),     float(j - 1)));
			vec4 t01 = vec4(pts[3 * i + j + 1], tfloor + vec2(float(i - 1), float(j)));
			vec4 t11 = vec4(pts[3 * i + j + 4], tfloor + vec2(float(i),     float(j)));
			tri_color(p, t00, t10, t11, scl, cw);
			tri_color(p, t00, t11, t01, scl, cw);
		}
	}
	if (cw.w > 0.0) {
		COLOR = cw / cw.w;
	} else {
		COLOR = vec4(0.0, 0.0, 0.0, 1.0);
	}
}
Tags
Abstract, animated, Cellular, FireAndIce, godotshader, Procedural, Spline, tiling, Triangular, Truchet
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Gerardo LCDF

Animated Cellular Grid

Bouncing Reflective Logo

Super Mario World Transition

Related shaders

Animated Cellular Grid

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments