The GLSL function mod() creates a saw tooth shape — think pointy edges repeating over and over.

But what if you want a little more control over the sharpness of the tip of each saw ridge? Enter smoothMod(). When @charstiles came on the stream, she showed us her riff on the idea with the below function.

If you've worked with CSS, it might also be helpful to think of this like you would the border-radius property.

In the CodePen example below, we've commented out an implementation of the regular mod() function so that you can compare the two.

/* 
* SMOOTH MOD
* - authored by @charstiles -
* based on https://math.stackexchange.com/questions/2491494/does-there-exist-a-smooth-approximation-of-x-bmod-y
* (axis) input axis to modify
* (amp) amplitude of each edge/tip
* (rad) radius of each edge/tip
* returns => smooth edges
*/

float smoothMod(float axis, float amp, float rad){
    float top = cos(PI * (axis / amp)) * sin(PI * (axis / amp));
    float bottom = pow(sin(PI * (axis / amp)), 2.0) + pow(rad, 2.0);
    float at = atan(top / bottom);
    return amp * (1.0 / 2.0) - (1.0 / PI) * at;
}
<!--
 * #curiouslyminded
 * 
 * Follow:
 * https://www.twitch.tv/curiouslyminded
 * https://www.youtube.com/curiouslyminded
-->

<div id="example"></div> <script id="vertex" type="x-shader/x-vertex"> void main() { gl_Position = vec4(position, 1.0); } </script> <script id="fragment" type="x-shader/x-vertex"> precision mediump float; #define PI 3.14159265359 uniform vec2 u_resolution; uniform float u_time;

// SMOOTH MOD // authored by @charstiles // based on https://math.stackexchange.com/questions/2491494/does-there-exist-a-smooth-approximation-of-x-bmod-y // (axis) input axis to modify // (amp) amplitude of each edge/tip // (rad) radius of each edge/tip float smoothMod(float axis, float amp, float rad){ float top = cos(PI * (axis / amp)) * sin(PI * (axis / amp)); float bottom = pow(sin(PI * (axis / amp)), 2.0) + pow(rad, 2.0); float at = atan(top / bottom); return amp * (1.0 / 2.0) - (1.0 / PI) * at; }

void main() { vec2 uv = (gl_FragCoord.xy - u_resolution * .5) / u_resolution.yy + 0.5; vec3 color = vec3(0.); // loop time float t = sin(u_time); // regular mod float xMod = mod(uv.x, 0.5); // smoothMod float xSmoothMod = smoothMod(uv.x, 0.5, 0.25);
// uv.x = mix(uv.x, xMod, t); uv.x = mix(uv.x, xSmoothMod, t); color += step(uv.y, uv.x); gl_FragColor = vec4(color, 1.0); } </script>

* {
user-select: none;
}

body {
height: 100vh;
background-color: black;
margin: 0;
padding: 0;
overflow: hidden;
position: relative;
}
/* 
 * #curiouslyminded
 *
 * This JS is an adapted template from the https://thebookofshaders.com/
 */

let camera, scene, renderer, clock;
let uniforms;

function init() {
	const container = document.getElementById("example");

	clock = new THREE.Clock();
	camera = new THREE.Camera();
	camera.position.z = 1;

	scene = new THREE.Scene();

	const geometry = new THREE.PlaneBufferGeometry(2, 2);

	uniforms = {
		u_time: { type: "f", value: 1.0 },
		u_resolution: { type: "v2", value: new THREE.Vector2() },
	};

	const material = new THREE.ShaderMaterial({
		uniforms,
		vertexShader: document.getElementById("vertex").textContent,
		fragmentShader: document.getElementById("fragment").textContent
	});

	const mesh = new THREE.Mesh(geometry, material);
	scene.add(mesh);

	renderer = new THREE.WebGLRenderer();
	renderer.setPixelRatio(window.devicePixelRatio);

	container.appendChild(renderer.domElement);

	onWindowResize();
	window.addEventListener("resize", onWindowResize);
}

function onWindowResize() {
	renderer.setSize(window.innerWidth, window.innerHeight);
	uniforms.u_resolution.value.x = renderer.domElement.width;
	uniforms.u_resolution.value.y = renderer.domElement.height;
}

function render() {
	uniforms.u_time.value = clock.getElapsedTime();
	renderer.render(scene, camera);
}

function animate() {
	render();
	requestAnimationFrame(animate);
}

init();
animate();