Building ux3d.io with three.js and react-three-fiber
The hero section on ux3d.io is built with three.js, react-three-fiber, and react-three-drei. This post delves into the process of developing this part.
ux3d.io
The ux3d.io website is primarily built using Astro, a static site generator that seamlessly integrates static and dynamic content. The landing page is a static site that uses three.js to render a 3D scene. Astro was specifically chosen for its speed when delivering website content which it achieves by prerendering your pages on the server. However, this does not work well for the interactive three.js-based hero section, since it relies on dynamic interactions with the user. To overcome this limitation, the hero section is implemented as a React application embedded within an Astro island.
The tech stack
The entire page is built using Astro. The hero section is then embeded into this page as an Astro island. It is implemented using React and react-three-fiber (https://github.com/pmndrs/react-three-fiber). react-three-fiber provides a React renderer for three.js. In addition, react-three-drei (https://github.com/pmndrs/drei) provides several helpers and components that simplify the development process.
The scene
The scene comprises several elements:
- The Ariane 5 rocket in the foreground
- The Earth in the background
- A starry sky backdrop
The rocket and Earth’s base color layer are imported as 3D models, while the stars, nebula, and Earth’s atmosphere are procedurally generated using shaders.
Building the Earth
The Earth is created from a simple sphere with a texture applied to it. The image was sourced from a NASA repository and downsampled to optimize performance (source: https://earthobservatory.nasa.gov/features/NightLights). Additionally, the atmospheric effect is achieved through a second sphere with an applied shader that simulates atmospheric diffraction and scattering. This shader calculates the color based on the fragment’s distance and angle from the camera, thereby creating a realistic atmospheric illusion.
vec3 color = vec3(0.3, 0.3, 0.8);
vec3 cameraDir = normalize(cameraPosition - v_position);
float cameraAngle = dot(cameraDir, normalize(v_normal));
float atmosphereStrength = 1. - cameraAngle;
color = color * atmosphereStrength;
gl_FragColor = vec4(color, atmosphereStrength / 2.0);
Animating the rocket
The rocket model was purchased from an online source and then refined and animated in Blender (https://www.blender.org/). During refinement, all non-visible components of the model, such as the second stage engine and parts on the backside invisible to the camera, were removed, to reduce file size and enhance performance on low-end devices. The animation was created by keyframing the rocket’s movement from off-screen to its final position.
Gestaltor (https://gestaltor.com/) was then employed to optimize the texture file size and apply Draco mesh compression. Gestaltor was chosen over gltf-transform (https://gltf-transform.dev/) due to the latter’s tendency to disrupt animations in this specific glTF. Again, these optimization steps were crucial for minimizing file size and maximizing performance.
Finally, gltfjsx (https://github.com/pmndrs/gltfjsx) was used to convert the glTF file into a JSX file compatible with react-three-fiber, significantly simplifying the model’s integration into React. It is worth noting that any modifications to the model necessitated repeating this entire workflow.
Animation setup
The entire scene contains two main animations. The first is the transitional animation where the rocket enters the scene from off-screen, created using keyframe animation in Blender. The second is the orbit animation. For this, both the rocket and the camera are parented to a node located at the Earth’s center, which is then rotated using a react-three-fiber animation. This ensures the keyframed rocket animation is rotated in sync with the rest of the scene.
rotationGroup.current?.rotateOnAxis(
new THREE.Vector3(0, 1, 0),
-delta / 150 // delta -> delta time from useFrame; devide by 150 to slow down the rotation
);
Additionally, the scene features a camera tilt animation controlled by cursor movement. This dynamic animation enhances the scene’s interactivity, distinctly setting it apart from a static video.
const strength = 0.005;
const angle = 40; // inverse of max rotation
camera.rotation.y = THREE.MathUtils.lerp(
camera.rotation.y,
(cursorPosition.x * Math.PI) / -angle,
strength
);
camera.rotation.x = THREE.MathUtils.lerp(
camera.rotation.x,
(cursorPosition.y * Math.PI) / angle,
strength
);
Procedural stars
The stellar background consists of two primary components: stars and nebulae. These elements are generated by a shader which creates the background texture, subsequently applied to a spherical surface enveloping the scene.
General structure
The background shader operates through a sequence of fundamental steps. Initially, each fragment is initialized with a black color. Subsequently, the base star layer color is blended into this initial black canvas. Following this, the nebulae color is applied in layers through straightforward addition. Finally, the shader integrates large stars onto the nebulae backdrop. The resulting color value is then outputted by the fragment shader.
Stars
The stars consist of two separate layers, a base layer of simple procedural noise-based stars and a second layer of larger Voronoi noise-based stars. Both layers exhibit twinkling animation achieved through a sinusoidal base animation. The distance field generated by the Voronoi noise enables stars of different sizes whereas the simple noise used for the background stars only allows stars with a size of one pixel.
Voronoi code source: https://www.shadertoy.com/view/MslGD8
Background stars source: https://www.shadertoy.com/view/cdj3DW
Nebulae
The nebulae are based on a fractal simplex noise. It comprises two distinct noise layers, each responsible for defining different color layers of the nebulae. The source code for the noise can be found here: https://github.com/ashima/webgl-noise. The final nebulae are achieved through a straightforward addition of the two noise layers.
Stars development takeaways
During the development phase, we initially explored using a simple starfield texture as the background image. However, this approach proved unsatisfactory due to inherent challenges. Stars, characterized by their small, bright points against a dark backdrop, require a high-resolution texture to maintain clarity; lower resolutions result in blurred, indistinct representations. Moreover, because the camera orbits around the Earth and thus around the background stars, the texture must cover the entire sphere surrounding the Earth, despite only a fraction being visible at any given time. This necessitates a larger texture size to accommodate the required detail.
Ultimately, achieving a visually appealing texture meant creating files several tens of megabytes in size. Recognizing the impracticality of such large assets for a performant website, we opted instead to implement a procedural texture generation approach.
Lighting
The lighting setup uses a directional light to simulate sunlight, ensuring realistic illumination. To enhance this effect, the same shader generating the background space environment was also used to generate environmental lighting.
<Environment backgroundIntensity="{0}">
<mesh position="{[0, 0, 0]}" scale="{400}" rotation="{[0, 0, 0]}">
<sphereGeometry args="{[1, 64, 64]}" />
<shaderMaterial
fragmentShader="{fragmentShader}"
vertexShader="{vertexShader}"
side="{THREE.BackSide}"
/>
</mesh>
</Environment>
Conclusion
react-three-fiber provides a powerful renderer for websites. Utilizing it in conjunction with a handcrafted animation and an interactive camera tilt, we were able to craft a stunning hero section for ux3d.io. The procedural generation of the stars and nebulae, along with the realistic lighting setup, further enhances the scene’s visual appeal without compromising load performance.
For more information on how we can help you with your next project, please contact us here.