Frontend Development 14 min read

Building a Three.js Hero Section with Environment Maps, PMREMGenerator, and Post‑Processing Effects

This article walks through building a Three.js hero section by setting up a basic scene, loading a GLTF model, applying environment maps with PMREMGenerator, adding bloom post‑processing, and animating the envMap rotation to create a dynamic, visually striking 3D header.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Building a Three.js Hero Section with Environment Maps, PMREMGenerator, and Post‑Processing Effects

The tutorial introduces a "Hero Section" as a prominent banner area in web design and explains its importance for user engagement. It then guides readers through creating a Three.js project that displays a 3D hero header.

0. Prerequisites – Readers should be familiar with Three.js but not with shaders. The article provides a live preview link.

1. Environment Setup – The basic Three.js elements (Scene, Camera, Renderer) are instantiated, and a responsive canvas is configured.

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const device = {
  width: window.innerWidth,
  height: window.innerHeight,
  pixelRatio: window.devicePixelRatio
};

export default class Three {
  constructor(canvas) {
    this.canvas = canvas;
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(
      75,
      device.width / device.height,
      0.1,
      100
    );
    this.camera.position.set(0, 0, 2);
    this.scene.add(this.camera);

    this.renderer = new THREE.WebGLRenderer({
      canvas: this.canvas,
      alpha: true,
      antialias: true,
      preserveDrawingBuffer: true
    });
    this.renderer.setSize(device.width, device.height);
    this.renderer.setPixelRatio(Math.min(device.pixelRatio, 2));

    this.controls = new OrbitControls(this.camera, this.canvas);
    this.clock = new THREE.Clock();
    this.setLights();
    this.setGeometry();
    this.render();
    this.setResize();
  }

  setLights() {
    this.ambientLight = new THREE.AmbientLight(new THREE.Color(1, 1, 1, 1));
    this.scene.add(this.ambientLight);
  }

  setGeometry() {
    this.planeGeometry = new THREE.PlaneGeometry(1, 1, 128, 128);
    this.planeMaterial = new THREE.MeshBasicMaterial({
      color: 0x00_ff_00
    });
    this.planeMesh = new THREE.Mesh(this.planeGeometry, this.planeMaterial);
    this.scene.add(this.planeMesh);
  }

  render() {
    const elapsedTime = this.clock.getElapsedTime();
    this.planeMesh.rotation.x = 0.2 * elapsedTime;
    this.planeMesh.rotation.y = 0.1 * elapsedTime;
    this.renderer.render(this.scene, this.camera);
    requestAnimationFrame(this.render.bind(this));
  }

  setResize() {
    window.addEventListener('resize', this.onResize.bind(this));
  }

  onResize() {
    device.width = window.innerWidth;
    device.height = window.innerHeight;
    this.camera.aspect = device.width / device.height;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(device.width, device.height);
    this.renderer.setPixelRatio(Math.min(device.pixelRatio, 2));
  }
}

2. Loading a GLTF Model – A GLTFLoader is added, the plane geometry is replaced with a dog head model, and the model is added to the scene.

// In the constructor add a model loader
this.gltfLoader = new GLTFLoader();

setObject() {
  this.gltfLoader.load('goutou.glb', (gltf) => {
    this.object = gltf.scene;
    this.scene.add(this.object);
  });
}

// Call the method in the constructor
this.setLights();
this.setObject();
// this.setGeometry(); // removed
this.render();
this.setResize();

3. Using PMREMGenerator for Environment Maps – The article explains how to create a PMREMGenerator, load an equirectangular texture, and apply the generated envMap to a PBR material.

const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();

const textureLoader = new THREE.TextureLoader();
textureLoader.load('path/to/envmap.jpg', (texture) => {
  const envMap = pmremGenerator.fromEquirectangular(texture).texture;
});

const material = new THREE.MeshStandardMaterial({
  color: 0xffffff,
  metalness: 1,
  roughness: 0.2,
  envMap: envMap
});

pmremGenerator.dispose();

In the class, the envMap is loaded and assigned to the dog material:

this.pmremGenerator = new THREE.PMREMGenerator(this.renderer);
this.pmremGenerator.compileEquirectangularShader();
this.envMap = new THREE.TextureLoader().load('./img/white.png', (texture) => {
  this.envMap = this.pmremGenerator.fromEquirectangular(texture).texture;
  this.envMap.encoding = THREE.SRGBColorSpace;
  this.dogMaterial = new THREE.MeshStandardMaterial({
    color: '#a7c6b1',
    metalness: 1,
    roughness: 0.28
  });
  this.dogMaterial.envMap = this.envMap;
  this.gltfLoader.load('./goutou.glb', (gltf) => {
    const { scene } = gltf;
    this.dog = scene.children[0];
    this.dog.geometry.center();
    this.dog.material = this.dogMaterial;
    this.scene.add(this.dog);
  });
  this.pmremGenerator.dispose();
});

4. Post‑Processing with UnrealBloomPass – The tutorial adds a bloom effect using EffectComposer, RenderPass, and UnrealBloomPass.

setPost() {
  this.renderScene = new RenderPass(this.scene, this.camera);
  this.bloomPass = new UnrealBloomPass(
    new THREE.Vector2(device.width, device.height),
    0.06,
    0.4,
    0.85
  );
  this.outputPass = new OutputPass();
  this.composer = new EffectComposer(this.renderer);
  this.composer.addPass(this.renderScene);
  this.composer.addPass(this.bloomPass);
  this.composer.addPass(this.outputPass);
}

render() {
  const elapsedTime = this.clock.getElapsedTime();
  this.composer.render(this.scene, this.camera);
  requestAnimationFrame(this.render.bind(this));
}

onResize() {
  // ... same as before ...
  this.composer.setSize(device.width, device.height);
  this.composer.setPixelRatio(Math.min(device.pixelRatio, 2));
}

5. Rotating the Environment Map – By updating envMapRotation each frame, the lighting appears to move around the model.

render() {
  const elapsedTime = this.clock.getElapsedTime();
  if (this.dog) {
    this.dog.material.envMapRotation = new THREE.Euler(
      0 + elapsedTime * 3,
      0 + elapsedTime * 2,
      0
    );
    this.dog.material.needsUpdate = true;
  }
  this.renderer.render(this.scene, this.camera);
  this.composer.render(this.scene, this.camera);
  requestAnimationFrame(this.render.bind(this));
}

6. Final Touches – Adding a starfield background and mouse‑move interaction for subtle parallax.

setBackground() {
  const textureLoader = new THREE.TextureLoader();
  const starSprite = textureLoader.load('./img/circle.png');
  const stars = getStarfield({ numStars: 4500, sprite: starSprite });
  this.scene.add(stars);
}

setMouseMoveEvent() {
  document.addEventListener('mousemove', function (event) {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  });
}

The article concludes with a link to the full source repository ( https://github.com/hexianWeb/hero-profile.git ) and encourages readers to follow the upcoming "Hero Section" series for more 3D front‑end examples.

Frontend DevelopmentThree.jsWebGL3DpostprocessingHero SectionPMREMGenerator
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.