5 C
New York
Thursday, December 4, 2025

Crafting Nature Past Know-how: A Undertaking from Roots to Leaves



After the summer season, I knew that I might have some free time, so I took it as a chance to create an internet site that will matter to me and likewise 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 growth.


Free GSAP 3 Express Course


Be taught fashionable internet animation utilizing GSAP 3 with 34 hands-on video classes and sensible tasks — excellent for all ability ranges.


Test it out

Index

  1. Constructing a powerful idea
  2. Type and matter
  3. Stack
  4. Key options
  5. Bonus methods
  6. Takeaways

Idea

I knew from the beginning that I needed to create a visually gorgeous piece, properly animated, and that will finally embody some WebGL. Good, however I wanted a powerful idea on which I may construct this visible expertise.

I’ve a deep admiration for nature and its infinite complexity. Nature creates programs which can be extremely environment friendly and self-sustaining. So I picked one of the crucial iconic components of nature as the bottom of my venture: bushes. Everyone is aware of them, however they’re additionally fairly mysterious.

I had my topic, but it surely was nonetheless not sufficient; it was not but an idea. I needed to speak about nature in a different way, so I wanted one other perspective, distant from the nice and cozy inexperienced vibe we are able to think about.

Then I got here throughout some cool tasks, 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. All of a sudden, all of the items got here collectively: I had my idea — bushes as high-end technological creations.

Design

Now I want to remodel it onto the display. I prefer to take a while to hunt inspiration in numerous 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 massively 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 needed to push my limits with WebGL, but it surely 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 correct ambiance.

Thankfully, the mannequin had vertex teams for bark and leaves, so I may simply separate them into two objects to govern 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 fashion 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 property just like the icosahedron wireframe background, the low-poly flooring, and the compass to trace rotations, to bolster the thought of a simulation.

UI

As I needed 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 data playing cards are actually impressed by FPS UIs like Name of Obligation.

I needed to stability it and never make it too technological, so I selected a pleasant sans-serif font that works effectively for each headings and physique textual content so as to add a contact of unpolluted fashion to the piece. I additionally added some small options to maintain monitor of the scrolling and the present part.

Stack

I used my standard stack for this venture to begin rapidly, 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 ultimately plan to translate the location, so it will likely be potential with ease.

I exploit a customized Docker container to deal with the native server and database. I didn’t use it for deployment although, as the online server was not suitable.

I exploit 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 excellent previous CSS styling.

Regarding the scripts, I exploit TypeScript and the primary 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 parts

Okay, for the stack, let’s see some components of the venture.

Key options

GUI

In the course of the WebGL growth course of, I like so as to add lots of GUI sliders and buttons to manage as many parameters as I can. It makes issues a lot sooner to tweak; even in the event you lose a while implementing it, you acquire quite a bit by discovering the correct values visually. I prefer to create a GUI Supervisor singleton that I can entry in every single place in my app.

import glMainStore from '@scripts/shops/gl/glMainStore';
import { createGUI } from '@scripts/utils/loadGui';

export kind GUIS = {
  mum or dad: Awaited<ReturnType<typeof createGUI>>;
  mainGui: Awaited<ReturnType<typeof createGUI>>;
  animGui: Awaited<ReturnType<typeof createGUI>>;
  effectGui: Awaited<ReturnType<typeof createGUI>>;
};

kind 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({
      mum or dad: parentGui,
      title: 'principal',
    });
    glMainStore.gui = mainGui;

    const animGui = await createGUI({
      mum or dad: parentGui,
      title: 'animations',
    });

    const effectGui = await createGUI({
      mum or dad: parentGui,
      title: 'results',
    });

    this.GUIS = {
      mum or dad: parentGui,
      mainGui,
      animGui,
      effectGui,
    };
  }

Then I can do that anyplace 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').identify('identify');

Which ultimately makes this sort of organized mess:

Digicam animations

An essential a part of the expertise is the digital camera itself, as it’s the perspective of the consumer. I created a rig across the fundamental Three.js perspective digital camera. I first created a null object whose place and rotation I tweak relying on mouse interplay. I then added my digital camera to it so it copies the placement and rotation. It retains issues clear, and it decouples the main focus of the digital camera from the rotation/place of the interplay. I additionally used one other rig on high of that to deal with place and rotation. The method is identical: I parented the primary rig to a brand new 3D object. I can now modify the place, the radius by transferring the rig1 Z place, and the rotation of my digital camera.

I then add tweaks to my GUI to manage the whole lot.

Particles

For the venture, I depend on two sorts of particle programs: GPGPU vector discipline particles and curve-guided particles.

For the particles current all through your complete scene and those within the local weather management part, I exploit a vector discipline computed by a GPUComputationRenderer from Three.js. To be transient, it lets me calculate the XYZ place of a particle because the RGB parts of a texture on the GPU, so it’s quick even when I’ve lots of particles. Then I can replace the place based mostly on this texture immediately contained in the vertex shader.

However, as an example 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 knowledge objects, replace the place relying on the chosen pace, then calculate the coordinates of the purpose comparable to the brand new progress on the curve, and replace my place BufferAttribute.

Wet shader

I needed as an example 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 digital camera refracting the scene. Each of those are made utilizing post-processing and a customized cross shader.

For the streaks, it’s fairly simple. I take the UV of the display, rotate it, and scale it by a big quantity on the x-axis and a smaller one on the y-axis, as I would like 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 exploit a UV grid as effectively, 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 life like. 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 imagined to be a scale-up mixed with a fade-in. However as soon as the animation went reside, it felt a bit unappealing, so I made a decision to modify 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 boost the ambiance even just a little extra, I added a couple of post-processing passes. Firstly, a refined noise cross helps create a numerical/technological contact to the 3D property in addition to unify the colour gradients a bit.

I then added a bloom cross to provide focus and power to the brighter components. I set the brink 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 identical (white), but it surely 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 immediately on the digital camera display.

Touches of interactivity

The venture is kind of narrative and linear, and the consumer can really feel a bit passive all through the expertise, so I needed 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 would like my animations to concentrate on exact components 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 digital camera, which I replace in my render loop with the cursor XY. I get the intersection of this ray with a aircraft in entrance of the tree, which all the time seems to be on the digital camera. 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

Every part between [0 – 1] or [-1, 1]

I exploit some mapping/clamping features extensively all through the venture to transform ranges from [x, y] to [0, 1]. It helps quite a bit when making use of values in shaders. For instance, I remap my MouseEvent clientX and clientY to a spread of [0, 1] to match fragment shader UVs.

I additionally remap lots 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] protecting 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 digital camera 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
      );

      // Digicam and leaf
      anims?.firstTraveling.digital camera.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 typically 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 colour management

I exploit this utility typically 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 principal takeaway, I might say that a very powerful a part of the venture is actually the idea. Upon getting a daring core, the remaining comes easily and with consistency.

Secondly, I might say that attempting new issues and pushing them till they shine is one other key. Should you don’t but know the way to do one thing, you’ll determine it out — it’s one of the simplest ways to be taught and progress.

And final however not least, if, like me, you begin this sort of venture alone, concentrate on the purpose. You’ll in all probability need to make some compromises right here and there. For me, it was utilizing my web site stack, but it surely allowed me to begin quick and gave me time to concentrate on a clear fashion and polished animations.

To complete, I can say that this venture has been a journey by all components of web site creation, from idea and design to code by content material. I actually discovered quite a bit and sharpened my information. I’m additionally fairly proud to ship a chunk that hyperlinks kind and matter.



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles