Hey there 👋! I’m Matias, a inventive net dev based mostly in Buenos Aires, Argentina. I at present run joyco.studio, and we just lately launched the primary model of our web site. It contains a comic-like dot grid shader background, and on this submit, I’ll present you the way it’s coded, breaking it down into 4 easy steps.
However earlier than we start, I’ll ask you to show ON the compositional considering change. Attending to a fancy render is the results of attaining smaller and fewer advanced outputs first, after which mixing all of them into one thing fascinating. Take a look at what we will likely be creating:
That being stated, let’s begin with a base dot grid.
Step 1: The Dot Grid
Our background’s base aesthetic will likely be a dotted grid sample, for which we solely want a display screen quad (full-screen airplane geometry) and a customized shader materials.
Making a display screen quad is tremendous easy since we don’t want any 3D projection calculation, so it’s even less complicated than the typical vertex shader:
void important() {
gl_Position = vec4(place.xy, 0.0, 1.0);
}
And right here’s the fragment shader:
void important() {
vec2 screenUv = gl_FragCoord.xy / decision; // get uvs from pixel coord
vec2 uv = coverUv(screenUv); // facet appropriate
vec2 gridUv = fract(uv * gridSize);
gl_FragColor = vec4(gridUv.x, gridUv.y, 0.0, 1.0);
}
That’s it; now you’ve the bottom grid. Sure, I do know—why is it squared? what are these colours? the place are my dots?! We’re getting there. They’re squared as a result of our shader subdivides the display screen coordinates that go from 0 (left)
to 1 (proper)
into smaller chunks managed by our gridSize
uniform. These colours are UVs for every grid field, principally native coordinates. The colour grading is because of uv.x
and uv.y
getting used because the pink and inexperienced channels, respectively.
Our last item to do right here is to show our pointy squares into spherical dots. We will try this by measuring the space from the middle of every native field; we will obtain it utilizing the sdfCircle(level, radius)
operate (SDF stands for “Signed Distance Operate“). The dimensions of the circle will likely be decided by the radius—let’s say 0.3
(tweak it your self and see the end result!). Let’s replace the fragment code:
//...
float baseDot = sdfCircle(gridUv, radius); // sdfCircle code obtainable on demo beneath
gl_FragColor = vec4(vec3(baseDot), 1.0);
//...
If the results of the sdfCircle
operate is lower than or equal to zero, then that pixel fragment is taken into account a part of our circle. That’s why our circles are black within the heart and as you go additional away, they flip white 0 -> ∞
. There are a LOT of well-known SDF capabilities, and Inigo Quilez caught ’em all.
Step 2: The Mouse Path
@react-three/drei
bought us coated right here; it has a hook for this. It handles the creation of a 2D canvas and drawing to it based mostly on the onPointerMove
occasion. The occasion.uv
tells the place the mouse intersection was. Verify the supply code right here.
If it doesn’t begin straight away, click on and transfer the mouse.
That is simply excellent! Later, we’ll pattern this mouse path texture to focus on the dots which can be being hovered. However we gained’t do it the “simple” means, which might be utilizing the dots as a masks over the path texture. It’s not dangerous, however that will render the underlying path texture gradient contained in the circles. As a substitute, I need every circle to be coloration uniform, and we will obtain that by sampling the path texture from the middle of every grid field (which is the middle of our dots too). See? It’s basically a pixelation impact.
Step 3: The Masks
The primary is a radial gradient at y: 110%
and x: 0.7
from the bottom-left.
A linear gradient from the display screen high to the display screen backside.
And a time-based animated radial gradient with the middle on the identical level as the primary one.
We’ll solely mix the primary two, and the animated one will likely be used later. Right here’s the code:
float circleMaskCenter = size(uv - vec2(0.70, 1.0));
float circleMask = smoothstep(0.4, 1.0, circleMaskCenter);
float circleAnimatedMask = sin(time * 2.0 + circleMaskCenter * 10.0);
float screenMask = smoothstep(0.0, 1.0, 1.0 - uv.y);
// Mix
float combinedMask = screenMask * circleMask;
Step 4: The Composition
Sure! Began from the underside, now we’re right here! We made it to our final step: mixing all of it collectively.
Let’s decide particular colours for this particular submit (my first one in codrops 🥳) I’ll use #FF5001 and #FFF for the background and dots respectively. The enjoyable half is that we’re free to tweak how every composition step interacts with one another. Eg, how a lot opacity provides the mouse path to the dots? Ought to it scale them too? Can it additionally change their colours?! My reply right here is “Comply with your coronary heart“. I made my decisions for this demo however be happy to tweak them.
The dot scale and opacity are affected by the mouseTrail, combinedMask, and circleAnimatedMask.
// The mouse path is a B&W picture, we solely want the pink channel.
float mouseInfluence = texture2D(mouseTrail, gridUvCenterInScreenCoords).r;
float scaleInfluence = max(mouseInfluence * 0.5, circleAnimatedMask * 0.3);
float opacityInfluence = max(mouseInfluence * 15.0, circleAnimatedMask * 0.5);
float sdfDot = sdfCircle(gridUv, dotSize * (1.0 + scaleInfluence * 0.5));
float smoothDot = smoothstep(0.05, 0.0, sdfDot); // Easy the perimeters
vec3 composition = combine(bgColor, dotColor, smoothDot * combinedMask * dotOpacity * (1.0 + opacityInfluence));
gl_FragColor = vec4(composition, 1.0);
As our final act, we must always apply tone mapping and modify the output to the renderer’s coloration house; in any other case, our colours gained’t show precisely. Earlier than our shader will get compiled, threejs
imports its inside lib chunks of code if it finds an #embody <{chunk_name}>
snippet within the shader. We will borrow the tonemapping_fragment
and colorspace_fragment
from there. Right here’s the code:
#embody <tonemapping_fragment>
#embody <colorspace_fragment>
FYI: There’s an entire library of shader chunks obtainable in threejs
verify ’em out. It would prevent a while sooner or later!
That’s it for this submit! I hope you loved diving into the inventive course of behind constructing a customized shader-powered dot grid and studying some new methods alongside the best way.
Thanks for following alongside, and don’t overlook to eat your greens! See you round on X!