Build a Galactic Hero Section with Three.js: From Starfield to Shaders
This article walks you through creating a visually striking hero section using Three.js, covering prerequisite knowledge, scene composition, nebula background, particle‑based star rings, custom GLSL shaders for animation, planet rendering with displacement maps, and performance‑boosting techniques like anisotropic filtering.
Prerequisites
Familiarity with core Three.js objects is required: THREE.Scene, THREE.Camera, THREE.Renderer, THREE.Geometry, THREE.Material and THREE.Mesh. The following sections assume this knowledge.
Scene Setup
The scene background is set to a nebula texture loaded from the Solar System Scope library (CC‑BY‑4.0). The intensity is reduced to keep the foreground visible.
this.scene.background = this.resources.items.spaceTexture;
this.scene.backgroundIntensity = 0.25;Stars and Ring
Particles are created with THREE.Points. Positions are generated in setGalaxy() using a spiral distribution across multiple branches.
// radius within the ring
const minRadius = this.parameters.innerRadius * this.parameters.radius;
const maxRadius = this.parameters.radius;
const radius = minRadius + Math.random() * (maxRadius - minRadius);
// spiral and branch angles
const spinAngle = radius * this.parameters.spin;
const branchAngle = (i % this.parameters.branches) / this.parameters.branches * Math.PI * 2;
// base position
const x = Math.cos(branchAngle + spinAngle) * radius;
const z = Math.sin(branchAngle + spinAngle) * radius;Two key parameters shape the effect: spinAngle – larger radius yields a larger rotation angle. branchAngle – distributes particles across several arms to form a galaxy‑like spiral.
Ring Constraint Algorithm
Particles are constrained to a ring using a normalized distance and a hat‑shaped fall‑off function.
const ringCenter = (minRadius + maxRadius) * 0.5;
const ringWidth = maxRadius - minRadius;
const distanceToRingCenter = Math.abs(radius - ringCenter) / (ringWidth * 0.5);
const ringConstraint = (1.0 - Math.pow(distanceToRingCenter, this.parameters.ringFalloff)) * this.parameters.constraintStrength;
const effectiveRandomness = this.parameters.randomness * ringConstraint;Particles near the centre receive stronger random perturbation; those near the edge stay tighter.
Random Y‑Axis Perturbation
const randomY = effectiveRandomness * radius * (Math.random() < 0.5 ? 1 : -0.4) * Math.pow(Math.random(), this.parameters.randomnessPower) * 20;Vertex‑Shader Animation
The vertex shader rotates each particle on the GPU, avoiding costly JavaScript updates. Rotation speed decays with distance to the centre, creating an “inner fast, outer slow” vortex.
float angle = atan(modelPosition.x, modelPosition.z);
float distanceToCenter = length(modelPosition.y);
float offset = (1.0 / distanceToCenter) * uTime * 0.2;
angle += offset;
modelPosition.x = cos(angle);
modelPosition.z = sin(angle);
distanceToCenter = clamp(distanceToCenter, 0.0, 10.0);Planet Rendering
Each planet uses a highly subdivided icosahedron geometry and a custom ShaderMaterial with displacement and normal maps for terrain detail.
// geometry with 64 subdivisions per axis
this.geometry = new THREE.IcosahedronGeometry(this.radius, 64, 64);Vertex displacement reads height from a displacement map and offsets the vertex along its normal.
float displacement = texture2D(uDisplacementMap, uv).r;
vec3 displacedPosition = position + normal * displacement * uDisplacementScale;Fragment shader lighting implements a full PBR pipeline (ambient, diffuse, specular) with a TBN matrix for correct normal‑map handling.
vec3 perturbNormal(vec3 normal, vec3 position, vec2 uv, sampler2D normalMap, float normalScale) {
vec3 normalMapColor = texture2D(normalMap, uv).rgb;
vec3 normalMapNormal = normalize(normalMapColor * 2.0 - 1.0);
vec3 q1 = dFdx(position);
vec3 q2 = dFdy(position);
vec2 st1 = dFdx(uv);
vec2 st2 = dFdy(uv);
vec3 tangent = normalize(q1 * st2.t - q2 * st1.t);
vec3 bitangent = normalize(-q1 * st2.s + q2 * st1.s);
mat3 tbn = mat3(tangent, bitangent, normal);
return normalize(mix(normal, tbn * normalMapNormal, normalScale));
}
vec3 ambient = uAmbientLight * textureColor * uAmbientLightIntensity;
vec3 lightDirection = normalize(uPointLightPosition - vPosition);
float distance = length(uPointLightPosition - vPosition);
float attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance);
float lightIntensity = max(dot(normal, lightDirection), 0.0);
vec3 diffuse = uPointLightColor * textureColor * lightIntensity * uPointLightIntensity * attenuation;
vec3 viewDirection = normalize(cameraPosition - vPosition);
float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 2.0);
vec3 metallic = mix(textureColor, vec3(1.0), uMetalness);
float roughnessFactor = 1.0 - uRoughness;
vec3 reflectDirection = reflect(-lightDirection, normal);
float specular = pow(max(dot(viewDirection, reflectDirection), 0.0), 4.0 * roughnessFactor);
vec3 specularColor = uPointLightColor * specular * metallic * fresnel * attenuation;
vec3 finalColor = ambient + diffuse + specularColor * 0.3;Anisotropic Filtering
To keep textures sharp at steep viewing angles, anisotropic filtering is enabled on the planet textures.
const texture = this.resources.items[this.textureName];
texture.generateMipmaps = true;
texture.minFilter = THREE.LinearMipMapLinearFilter;
texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = 8; // increase for higher quality at a performance cost
// Query maximum supported anisotropy
console.log('Max AF:', renderer.capabilities.getMaxAnisotropy());Result
The final hero section displays a rotating galaxy‑like particle ring, three detailed planets with displacement‑mapped terrain, and a lighting system that combines ambient, diffuse, and specular contributions for realistic shading. The complete source code is available at the following repository:
https://github.com/hexianWeb/isgalaxias
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
