After the summer season, I knew that I’d have some free time, so I took it as a possibility to create a web site that will matter to me and in addition function a playground to check new stuff and enhance my information. It ended up as a WebGL expertise mixing nature and expertise. Let’s see how I managed to create it from design to improvement.
Index
Idea
I knew from the beginning that I wished to create a visually beautiful piece, properly animated, and that will ultimately embrace some WebGL. Good, however I wanted a robust idea on which I may construct this visible expertise.
I’ve a deep admiration for nature and its infinite complexity. Nature creates programs which are extremely environment friendly and self-sustaining. So I picked probably the most iconic elements of nature as the bottom of my venture: bushes. All people is aware of them, however they’re additionally fairly mysterious.
I had my topic, nevertheless it was nonetheless not sufficient; it was not but an idea. I wished to speak about nature in a different way, so I wanted one other viewpoint, distant from the nice and cozy inexperienced vibe we are able to think about.
Then I got here throughout some cool initiatives, Aether1, Ion, and 8Bit, that are nice items, by the way in which, and create a pleasant ambiance across the presentation of high-tech merchandise. Immediately, all of the items got here collectively: I had my idea — bushes as high-end technological creations.
Design
Now I would like to remodel it onto the display screen. I wish to take a while to hunt inspiration in several fields; it may be web sites but in addition artwork, design, movies, or animation.
I had behind my thoughts the work of Quayola and his Stays collection. I additionally found Joanie Lemercier and his Prairie and All of the Bushes items. These two references vastly influenced me within the ambiance of the venture, in addition to within the respect and significance given to bushes. Mixed with the three web sites beforehand cited, I had a transparent image of the venture in my thoughts.
3D
The selection of 3D got here naturally. I’ve a background in 3D movement design, and I wished to push my limits with WebGL, nevertheless it additionally made sense to navigate by the completely different options of the tree.
I didn’t have the time to create a customized 3D mannequin, so I grabbed one from Sketchfab and began to tweak issues early in Three.js to search out the appropriate ambiance.
Fortuitously, the mannequin had vertex teams for bark and leaves, so I may simply separate them into two objects to control with ease afterward. I needed to create the roots, and I selected to make them with some curves from which I may create geometry later in code.
The general type of the 3D rendering is achieved through the use of some transparency on the fabric mixed with a darkish fog, giving the phantasm that the scene disappears and including extra depth to the tree foliage.
I then added some tech-style belongings just like the icosahedron wireframe background, the low-poly flooring, and the compass to trace rotations, to bolster the concept of a simulation.
UI
As I wished a tech feeling for this piece, I regarded into some online game UIs (nice inspiration right here) and sci-fi movie screens to search out inspiration. The knowledge playing cards are actually impressed by FPS UIs like Name of Responsibility.
I wished to steadiness it and never make it too technological, so I selected a pleasant sans-serif font that works properly for each headings and physique textual content so as to add a contact of unpolluted type to the piece. I additionally added some small options to maintain observe of the scrolling and the present part.
Stack
I used my standard stack for this venture to start out shortly, so it’s a WordPress setup with Bedrock/Sage to deal with it as an MVC, which makes use of Vite as a bundler. I saved WordPress as a result of I finally plan to translate the location, so it will likely be doable with ease.
I take advantage of a customized Docker container to deal with the native server and database. I didn’t use it for deployment although, as the net server was not appropriate.
I take advantage of Tailwind with a customized configuration to deal with styling. I actually like how briskly you possibly can iterate with it, and with v4 it’s even simpler to mix it with some touches of fine outdated CSS styling.
In regards to the scripts, I take advantage of TypeScript and the principle libraries are:
- Three.js and PostProcessing for WebGL
- GSAP/ScrollTrigger/TextSplit to deal with timelines, scroll animations, and splitting
- Lenis for unimaginable scroll smoothing
- Piece.js for dealing with customized internet elements
Okay, for the stack, let’s see some elements of the venture.
Key options
GUI
Through the WebGL improvement course of, I like so as to add quite a lot of GUI sliders and buttons to regulate as many parameters as I can. It makes issues a lot quicker to tweak; even should you lose a while implementing it, you achieve lots by discovering the appropriate values visually. I wish to create a GUI Supervisor singleton that I can entry all over the place in my app.
import glMainStore from '@scripts/shops/gl/glMainStore';
import { createGUI } from '@scripts/utils/loadGui';
export sort GUIS = {
guardian: Awaited>;
mainGui: Awaited>;
animGui: Awaited>;
effectGui: Awaited>;
};
sort cbT = (guis: GUIS | undefined) => void;
export default class GUIMananger {
static #occasion: GUIMananger;
GUIS: GUIS;
guiCbs: cbT[];
hideGui: boolean;
constructor() {
this.hideGui = false;
if (import.meta.env.DEV) {
this.guiCbs = [];
this.initLilGui().then(() => {
this.set off();
});
}
}
public static get occasion(): GUIMananger {
if (!GUIMananger.#occasion) {
GUIMananger.#occasion = new GUIMananger();
}
return GUIMananger.#occasion;
}
async initLilGui() {
const parentGui = await createGUI();
glMainStore.parentGui = parentGui;
this.hideGui && parentGui?.cover();
// Init Debugger
const mainGui = await createGUI({
guardian: parentGui,
title: 'essential',
});
glMainStore.gui = mainGui;
const animGui = await createGUI({
guardian: parentGui,
title: 'animations',
});
const effectGui = await createGUI({
guardian: parentGui,
title: 'results',
});
this.GUIS = {
guardian: parentGui,
mainGui,
animGui,
effectGui,
};
}
Then I can do that wherever in my app:
import GUIMananger, { GUIS } from '@scripts/eventManagers/GUIManager';
// calling GUIMananger.occasion returns
// if already created the occasion of GUIMananger
// if not it creates it
const { animGui } = GUIMananger.occasion.GUIS;
animGui?.add(object, 'property').title('title');
Which in the long run makes this sort of organized mess:
Digital camera animations
An essential a part of the expertise is the digicam itself, as it’s the viewpoint of the consumer. I created a rig across the primary Three.js perspective digicam. I first created a null object whose place and rotation I tweak relying on mouse interplay. I then added my digicam to it so it copies the placement and rotation. It retains issues clear, and it decouples the main focus of the digicam from the rotation/place of the interplay. I additionally used one other rig on high of that to deal with place and rotation. The approach is similar: I parented the primary rig to a brand new 3D object. I can now regulate the place, the radius by transferring the rig1 Z place, and the rotation of my digicam.
I then add tweaks to my GUI to regulate every little thing.
Particles
For the venture, I depend on two sorts of particle programs: GPGPU vector area particles and curve-guided particles.
For the particles current all through your entire scene and those within the local weather management part, I take advantage of a vector area computed by a GPUComputationRenderer from Three.js. To be transient, it lets me calculate the XYZ place of a particle because the RGB elements of a texture on the GPU, so it’s quick even when I’ve quite a lot of particles. Then I can replace the place primarily based on this texture instantly contained in the vertex shader.
Alternatively, for instance some options, I wanted particles to observe curves. To take action, I exported Blender’s curves as JSON. Then in Three.js, I created a CatmullRomCurve3, and from there I may create a conventional BufferGeometry particle system.
import rainCurves from '@3D/scenes/tree-scene-1/curves/rain.json';
export default class RainParticles {
// remainder of the category ...
createCurves() {
const rotationMatrixX = new Matrix4().makeRotationX(degToRad(90));
const rotationMatrixY = new Matrix4().makeRotationX(degToRad(180));
const tempVec = new Vector3();
for (let i = 0; i < rainCurves.size; i++) {
const curve = rainCurves[i];
const factors = curve.factors.map(({ x, y, z }) =>
tempVec
.set(x, y, z)
.applyMatrix4(rotationMatrixX)
.applyMatrix4(rotationMatrixY)
.clone()
);
const threeCurve = new CatmullRomCurve3(factors);
this.curves.push(threeCurve);
}
}
}
In parallel, I created an array of objects to retailer and replace data for every particle. I saved the place on the curve (from 0 — begin to 1 — finish), the curve it depends on, the pace, and the dimensions.
createPoints() {
this.factors = [];
for (let i = 0; i < this.curves.size; i++) {
for (let j = 0; j < this.density; j++) {
this.factors.push({
curve: this.curves[i],
offset: Math.random(),
pace: minmax(0.5, 0.8, Math.random()) * 0.01,
currentPos: Math.random(),
opacity: minmax(0.5, 0.7, Math.random()),
});
}
}
}
So in my render loop, I can iterate by these particle information objects, replace the place relying on the chosen pace, then calculate the coordinates of the purpose similar to the brand new progress on the curve, and replace my place BufferAttribute.
Wet shader
I wished for instance that bushes can encourage rain but in addition assist regulate heavy rainfall. What may make it extra visible than droplets falling from the sky?
To create this impact, I wanted two issues: the rain streaks and droplets on the digital digicam refracting the scene. Each of those are made utilizing post-processing and a customized go shader.
For the streaks, it’s fairly simple. I take the UV of the display screen, rotate it, and scale it by a big quantity on the x-axis and a smaller one on the y-axis, as I need lengthy streaks. I create a grid by getting the fract a part of the brand new UV. Then I hint a streak utilizing smoothstep(), animate its place a bit, and make it blink to get the specified impact.
To create the droplets, I take advantage of a UV grid as properly, scaled by a big quantity. Then I get a UV grid by taking the fract a part of it. In every cell, I draw a drop utilizing the size to the middle and a smoothstep operate. I add a noise worth to imitate some distortion on the drops, making them look extra reasonable. I additionally calculate a time offset for every drop to make them seem with a random delay. I then multiply my UV subset by my masks form to get UV droplets. The final step is to subtract the UV of my render with my droplets’ UVs and apply this wet UV to my earlier render.
Leaf reveal
One other attention-grabbing impact I needed to create was the holographic reveal of the leaf. Initially, it was presupposed to be a scale-up mixed with a fade-in. However as soon as the animation went dwell, it felt a bit unappealing, so I made a decision to change it to a laser-style reveal.
The impact is a mix of a masks, a noise impact on the alpha channel, and a shiny line with a little bit of noise in a fraction shader.
Postprocessing
To reinforce the ambiance even a bit of extra, I added just a few post-processing passes. Firstly, a delicate noise go helps create a numerical/technological contact to the 3D belongings in addition to unify the colour gradients a bit.
I then added a bloom go to provide focus and power to the brighter components. I set the edge comparatively excessive to keep away from an excessive amount of blur on the tree. I deliberately boosted some colours to make them bloom — for instance, the laser is a vec3(2.) as a substitute of vec3(1.). The colour is similar (white), nevertheless it surpasses the bloom threshold, so it will get blurred.
I completed with my customized results — rain and chilly/icy results passes — on high of that, as they’re supposed to seem instantly on the digicam display screen.
Touches of interactivity
The venture is sort of narrative and linear, and the consumer can really feel a bit passive all through the expertise, so I wished so as to add some interactivity. The primary interplay is the management of the scene within the intro part. It’s merely a customized drag-and-drop occasion handler that provides some velocity to the rotation vector of the scene. I then lock and revert the rotation when the consumer scrolls to the sections, as I need my animations to concentrate on exact elements of the tree.
The second is the noisy motion of the water particles within the local weather management part. First, I create a Raycaster from the digicam, which I replace in my render loop with the cursor XY. I get the intersection of this ray with a airplane in entrance of the tree, which at all times seems to be on the digicam. It provides me a Vector3 coordinate of the digital cursor. I then add a noise worth to each particle place if they’re shut sufficient to it.
Bonus misc methods
All the things between [0 – 1] or [-1, 1]
I take advantage of some mapping/clamping features extensively all through the venture to transform ranges from [x, y] to [0, 1]. It helps lots when making use of values in shaders. For instance, I remap my MouseEvent clientX and clientY to a variety of [0, 1] to match fragment shader UVs.
I additionally remap quite a lot of different ranges, just like the ScrollTrigger.onUpdate callback progress. Relying on the necessity, I can pace up or decelerate values, or begin/cease earlier than the tip.
// convert a worth from [a, b] to [A, B] maintaining ratio
const map = (a: quantity, b: quantity, A: quantity, B: quantity, x: quantity): quantity =>
(x - a) * ((B - A) / (b - a)) + A;
// convert worth to a [0, 1] vary
const normalize = (min: quantity, max: quantity, worth: quantity): quantity =>
map(min, max, 0, 1, worth);
// clamp values
const clamp = (min: quantity, max: quantity, worth: quantity): quantity =>
worth < min ? min : worth > max ? max : worth;
// convert from [0 - 1] to [-1, 1]
worth * 2 - 1;
// convert from [-1, 1] to [0, 1]
(worth + 1) * 0.5
solarPanels: {
el: getSectionEl('solar-panels'),
scrollTriggerOptions: {
id: 'solar-panels',
...baseStartEnd,
markers: showMarkers,
onUpdate: (self) => {
const pCamera = mapProgress(0, 0.5, 0, 1, self.progress); // from [0-0.5] to [0-1] for digicam to complete animation when progress is 50%
const pLeaf = mapProgress(0.3, 0.6, 0, 1, self.progress); // from [0.3-0.6] to [0-1] for the leaf reveal
const pLeafBack = mapProgress(0.7, 1, 0, 1, self.progress); // from [0.7-1] to [0-1] for the leaf again animation
// Circle and title quantity
this.uiAnim.titleNumbers.circleProgress.progress(self.progress);
showHideTitle(
this.uiAnim.titleNumbersElements[0],
self,
0.01,
0.99
);
// Digital camera and leaf
anims?.firstTraveling.digicam.progress(pCamera);
anims?.firstTraveling.leaf.progress(pLeaf - pLeafBack);
// Anim the title
titleStore.solarPanel &&
showHideTitle(titleStore.solarPanel, self, 0.01, 0.9);
// Present cover Card
showHideCard(this.uiAnim.playing cards.solarPanel, self, 0.4, 0.9);
// Present cover step
showHideStep(progressStepsObj[0]!, self, 0.683);
},
},
}
Customized ease features
I usually use easing features in JavaScript, which is especially helpful together with map() or normalize() to get clean ratios.
const easeInQuad = (x: quantity): quantity => x * x;
const easeOutQuad = (x: quantity): quantity => 1 - (1 - x) * (1 - x);
const easeInOutQuad = (x: quantity): quantity =>
x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
// ... Cubic, Expo ...
I additionally set them in my Tailwind configuration to have extra potentialities than the default.
/* ex: class="ease-in-cubic" */
@theme {
--ease-in-sin: cubic-bezier(0.12, 0, 0.39, 0),
--ease-out-sin: cubic-bezier(0.61, 1, 0.88, 1);
--ease-in-out-sin: cubic-bezier(0.37, 0, 0.63, 1);
/* ... different timing features */
}
SVG coloration management
I take advantage of this utility usually to tweak SVG colours on the fly.
@utility svg-color-* {
--col : --value(--color-*);
--col : --value([color]);
[fill]:not([fill="none"]):not([fill="transparent"]) {
fill: var(--col);
}
[stroke]:not([stroke="none"]):not([stroke="transparent"]){
stroke: var(--col);
}
}
Conclusion
As a essential takeaway, I’d say that an important a part of the venture is really the idea. After you have a daring core, the remaining comes easily and with consistency.
Secondly, I’d say that attempting new issues and pushing them till they shine is one other key. When you don’t but know how one can do one thing, you’ll determine it out — it’s one of the best ways to study and progress.
And final however not least, if, like me, you begin this sort of venture alone, concentrate on the aim. You’ll in all probability should make some compromises right here and there. For me, it was utilizing my web site stack, nevertheless it allowed me to start out quick and gave me time to concentrate on a clear type and polished animations.
To complete, I can say that this venture has been a journey by all elements of web site creation, from idea and design to code by content material. I actually realized lots and sharpened my information. I’m additionally fairly proud to ship a bit that hyperlinks kind and matter.









