<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Exploring Creative Development and WebGL]]></title><description><![CDATA[Exploring Creative Development and WebGL]]></description><link>https://fariu.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 17 Sep 2026 10:16:36 GMT</lastBuildDate><atom:link href="https://fariu.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Creating a "Shockwave-Reveal Effect" with Three.js, GLSL and GSAP]]></title><description><![CDATA["This tutorial assumes you have basic Three.js and GLSL knowledge, a basic scene set up with lights, camera and a renderer, alongside some primitive geometries lying around and a plane for your base f]]></description><link>https://fariu.hashnode.dev/creating-a-shockwave-reveal-effect-with-three-js-glsl-and-gsap</link><guid isPermaLink="true">https://fariu.hashnode.dev/creating-a-shockwave-reveal-effect-with-three-js-glsl-and-gsap</guid><category><![CDATA[ThreeJS]]></category><category><![CDATA[GLSL]]></category><category><![CDATA[shader]]></category><category><![CDATA[creative coding]]></category><dc:creator><![CDATA[Fariu Oyetola]]></dc:creator><pubDate>Tue, 01 Sep 2026 10:49:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a391bef238f78d1a13c4507/3864d7e9-d22b-43d1-8055-5100ed82605d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>"This tutorial assumes you have basic Three.js and GLSL knowledge, a basic scene set up with lights, camera and a renderer, alongside some primitive geometries lying around and a plane for your base floor"</em></p>
<p>If you are a fan of Sci-Fi, Virtual Realities and Augmented Realities like I am, then you have probably seen the "shockwave reveal effect" maybe once or twice. Imagine your avatar or a protagonist in a VR or AR world, all around him is pitch darkness, and then some initialization takes place and the world comes alive inch by inch, distance by distance or in our case pixel by pixel, this is exactly what happens with this effect regarding our 3D scene.</p>
<p>To get this working, we need to:</p>
<ul>
<li><p>Create shared uniforms by setting up a main JavaScript object which holds the radius, width, origin point and vertical lift for the wave itself.</p>
</li>
<li><p>Create a vertex shader chunk which calculates each vertex position in world space (our actual 3D scene coordinates).</p>
</li>
<li><p>Create a mesh fragment shader chunk that compares each pixel's distance from the wave origin to the instantaneous wave radius, delays the reveal by pixel height, and discard unrevealed pixels. It also adds a glow on the edge of the wave.</p>
</li>
<li><p>Inject our GLSL code into Three.js <code>MeshStandardMaterial</code> using the <code>onBeforeCompile</code>, a built-in callback function, this allows us to keep realistic lighting from Three.js without writing a raw shader from scratch.</p>
</li>
<li><p>Instruct Three.js to compile our custom shader for each material instead of reusing cached programs.</p>
</li>
<li><p>Animate our wave radius using GSAP.</p>
</li>
</ul>
<h3>Creating the Shared Uniforms</h3>
<pre><code class="language-js">import * as THREE from 'three';

// 1. Shared Uniforms
const shockUniforms = {
    uWaveRadius:    { value: -2.0 },
    uWaveWidth:     { value: 2.5 },
    uColor:         { value: new THREE.Color(0x60d5ff) },
    uOrigin:        { value: new THREE.Vector3(0, 0, 0) },
    uVerticalLift:  { value: 3.0 }
};
</code></pre>
<p><code>uWaveRadius</code> refers to the current radius of the animated, expanding wave. We start at -2.0 (a negative number) so all meshes are initially outside the wave and hidden.</p>
<p><code>uWaveWidth</code> this controls how thick the wave crest (the rising part) is.</p>
<p><code>uColor</code> holds the color for the glowing outer edge.</p>
<p><code>uOrigin</code> refers to the base floor center coordinates (x, y, z) where the shockwave begins from. Because all shared materials will point to this exact same <code>shockUniforms</code> object in memory, updating shockUniforms.uWaveRadius.value once in JavaScript instantly updates every single material across the entire GPU.</p>
<h3>Injecting The Vertex Shader and Capturing World Positions</h3>
<pre><code class="language-js">// to be injected into the top of the vertex shader
export const vertCommon = /* glsl */`
varying vec3 vWorldPos;
uniform vec3 uOrigin;
`;

// to be injected where vertex positions are calculated
export const vertBegin = /* glsl */`
vec4 _wPos = modelMatrix * vec4(position, 1.0);
vWorldPos  = _wPos.xyz;
`;
</code></pre>
<p>By default, vertices only know their very own local positions, to know how far a vertex is from the center of the entire scene, we need to know its WORLD POSITION</p>
<p>we create a varying <code>vec3 vWorldPos</code>, a variable passed down from vertex shader to fragment shader, in here you will be storing the obtained world position.</p>
<p><code>modelMatrix * vec4(position, 1.0)</code>, this multiplies the mesh's local position by its <code>modelMatrix</code> converting local coordinates into world coordinates.</p>
<p><code>vWorldPos = _wPos.xyz</code> saves the full (X,Y,Z) world coordinate into our varying so the fragment shader knows exactly where each pixel is in 3D space.</p>
<h3>The Mesh Fragment Shader</h3>
<pre><code class="language-js">// injected into the top of the fragment shader
export const fragCommon = /* glsl */`
varying vec3  vWorldPos;
uniform float uWaveRadius;
uniform float uWaveWidth;
uniform float uVerticalLift;
uniform vec3  uColor;
uniform vec3  uOrigin;
`;

// injected after lighting calculation
export const fragMesh = /* glsl */`
// calculates ground distance
float xzDist = distance(vWorldPos.xz, uOrigin.xz);

// adds vertical lift
float fDist  = xzDist + max(0.0, vWorldPos.y) * uVerticalLift;

// checks, is the wave past this point?
float fReveal = 1.0 - smoothstep(uWaveRadius - 0.5, uWaveRadius + 0.5, fDist);
if (fReveal &lt; 0.001) discard;

// glowing wave front
float fFlash = pow(1.0 - smoothstep(0.0, uWaveWidth, abs(fDist - uWaveRadius)), 3.0);
gl_FragColor.rgb += uColor * fFlash * 0.20;
`;
</code></pre>
<p>this is where we write the logic that runs on every pixel of our 3D geometries.</p>
<p><code>distance(vWorldPos.xz, uOrigin.xz)</code> calculates the horizontal distance along the ground from the wave origin to the current pixel</p>
<p><code>fDist = xzDist + max(0.0, vWorldPos.y) * uVerticalLift</code> this handles sweeps across geometries while accounting for the height too.</p>
<p><code>smoothstep(min, max, value)</code> creates a smooth transition between 0.0 and 1.0</p>
<p>if <code>fReveal &lt; 0.001</code>, the wave has not reached this pixel yet, calling <code>discard</code> tells the GPU to completely throw this specified pixel away, making it appear completely invisible.</p>
<p><code>fFlash</code> this is responsible for the edge glow.</p>
<p><code>abs(fDist - uWaveRadius)</code> finds out how close this pixel is to the wavefront</p>
<p><code>pow(..., 3.0)</code> sharpens the glow into a clean, bright band of light</p>
<p><code>gl_FragColor.rgb += ...</code> adds the cyan light on top of the object's regular PBR lighting from MeshStandardMaterial</p>
<h3>Shader Injection with onBeforeCompile</h3>
<p>This is where we connect our GLSL snippets into Three.js native <code>MeshStandardMaterial</code>.</p>
<pre><code class="language-js">function applyShockwave(mat) {
    mat.onBeforeCompile = (shader) =&gt; {
        // pass uniforms into the shader
        shader.uniforms.uWaveRadius   = shockUniforms.uWaveRadius;
        shader.uniforms.uWaveWidth    = shockUniforms.uWaveWidth;
        shader.uniforms.uColor        = shockUniforms.uColor;
        shader.uniforms.uOrigin       = shockUniforms.uOrigin;
        shader.uniforms.uVerticalLift = shockUniforms.uVerticalLift;

        // inject vertex shader chunks
        shader.vertexShader = shader.vertexShader.replace(
            '#include &lt;common&gt;',
            `#include &lt;common&gt;\n${vertCommon}`
        );
        shader.vertexShader = shader.vertexShader.replace(
            '#include &lt;begin_vertex&gt;',
            `#include &lt;begin_vertex&gt;\n${vertBegin}`
        );

        // inject fragment shader chunks
        shader.fragmentShader = shader.fragmentShader.replace(
            '#include &lt;common&gt;',
            `#include &lt;common&gt;\n${fragCommon}`
        );

                // compatible across all Three.js versions (r154+ uses opaque_fragment, older uses dithering_fragment)
        const targetChunk = shader.fragmentShader.includes('#include &lt;opaque_fragment&gt;')
            ? '#include &lt;opaque_fragment&gt;'
            : '#include &lt;dithering_fragment&gt;';

        shader.fragmentShader = shader.fragmentShader.replace(
            targetChunk,
            `${targetChunk}\n${fragMesh}`
        );
    };

    // force unique shader compilation
    mat.customProgramCacheKey = () =&gt; mat.uuid;
}
</code></pre>
<p><code>mat.onBeforeCompile</code> is a native method provided by Three.js, right before Three.js compiles the material into a WebGL program, it passes the internal GLSL code to this function so we can modify it.</p>
<p><code>replace('#include &lt;...&gt;', ...)</code> Three.js shaders are assembled from reusable blocks called chunks such as <code>#include &lt;common&gt;</code> or <code>#include &lt;opaque_fragment&gt;</code>. we perform a string replacement to insert our code where it belongs.</p>
<p><code>mat.customProgramCacheKey = () =&gt; mat.uuid</code> a really important line, by default, Three.js reuses compiled shaders between materials with the same setttings, attaching <code>customProgramCacheKey</code> guarantees that Three.js compiles our custom code for every material rather than skipping some.</p>
<h3>Applying the Effect to your Meshes</h3>
<p>Now, whenever we create a material, we can simply pass it to <code>applyShockwave()</code></p>
<pre><code class="language-js">// Floor
const floorMat = new THREE.MeshStandardMaterial({ color: 0x050a14, roughness: 1.0 });
applyShockwave(floorMat); 
const floor = new THREE.Mesh(new THREE.PlaneGeometry(60, 60), floorMat);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);

// Example Mesh
const meshMat = new THREE.MeshStandardMaterial({ color: 0xff4081, roughness: 0.3, metalness: 0.2 });
applyShockwave(meshMat); 
const box = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), meshMat);
box.position.set(0, 1, 0);
scene.add(box);
</code></pre>
<p>Any material passed to applyShockwave() gets the features to react to the wave.</p>
<h3>Animating the Shockwave with GSAP</h3>
<p>To trigger the shockwave, animate <code>uWaveRadius.value</code> using GSAP, consider:</p>
<pre><code class="language-js">import gsap from 'gsap';

let isPlaying = false;

function triggerShockwave() {
    if (isPlaying) return;
    isPlaying = true;

    // resets wave radius to behind everything
    shockUniforms.uWaveRadius.value = -2.0;

    // animates the radius outwards across the scene
    gsap.to(shockUniforms.uWaveRadius, {
        value: 60.0,
        duration: 4.5,
        ease: 'power2.out',
        onComplete: () =&gt; {
            isPlaying = false;
        }
    });
}

// trigger automatically on load or hook it up to a button
triggerShockwave();
</code></pre>
<p><code>shockUniforms.uWaveRadius.value = -2.0</code> resets the radius so everything turns dark again.</p>
<p><code>gsap.to(..., {value: 60.0, ease: 'power2.out'})</code> smoothly tweens the radius from -2.0 to 60.0 over 4.5 seconds</p>
<p>The <code>power2.out</code> easing causes the wave to expand rapidly then gently decelerate, like an actual blast wave.</p>
<p>There! We have our <strong>shockwave-reveal</strong> effect in motion! Have fun trying this out and tweaking things to your satisfaction, and if you have any questions or get stuck along the way, don't forget to reach out so we can figure this out together.</p>
<p>Thanks for sticking around this far!</p>
<p><strong><a href="https://github.com/thedevtobiii/shockwave">Code</a></strong>      <strong><a href="https://shockwave-iota.vercel.app/">Demo</a></strong></p>
]]></content:encoded></item></channel></rss>