Recreating the Windows 3D Pipes Screensaver With React / THREE.js

Uploaded Image
Posted on December 20, 2024

Overview

If you were a kid in the early 2000s, there’s a good chance you remember sitting in front of your family’s Windows XP computer, mesmerized by the seemingly endless combinations generated by the Windows 3D Pipes screensaver. If you're anything like me, it might have left a lasting curiosity as to how exactly those random pipes were being created, what technologies were required to make such a thing, and how we can make our own version of it. In this blog post we'll try to answer some of those questions and attempt to recreate a modern version of that beloved screensaver using Three.js, a powerful JavaScript library for rendering 3D graphics in the browser.

Prerequisites

Before diving into the code, ensure you have the following prerequisites:

  • Node.js environment
  • NEXT.js (we're using 15 + App Router)
  • Typescript
  • THREE.js

The first thing we need to do is create our project. We're using NEXT.js so this can be done pretty easily using npx create-next-app

You'll want to run that command in your terminal and follow the prompts on the screen. You should configure it so that the project is using the App Router, Typescript, and Tailwind CSS. The other settings won't matter for the sake of this tutorial.

Next, we need to install the THREE.js package and its Typescript types with the following commands:

Now that we have all of that installed, we are ready to jump into the project! If you ran npx create-next-app and ensured you chose the App Router for your project, you should be able to navigate to your project folder and find src/app/page.tsx. This file will be served at the index route of your project when you run npm run dev. By default, there is some example project code located in that file that we can get rid of. Once you've cleared that file, lets begin building out our project by defining a functional component and declaring this page to be client side only for the app router:

This is the skeleton of the page that our 3D scene will be rendered on. Now that its established lets move on to creating our 3D scene by importing THREE.js and adding a camera, lighting, renderer, and orbit controls that will render our scene and each animation to a canvas on the page:

At this point, we have utilized THREE.js to initialize a basic scene with a camera, lighting, renderer, and orbit controls which will allow us to see any 3D objects we add to the scene later on as well as rotate the camera and zoom around them. We've also added an animation loop which we'll expand later to control animating our pipes to grow and change direction. All of this is defined in a useEffect hook with an empty dependency array to ensure it runs only once on page load.

At this stage you should have no errors and be able to run your project with npm run dev and check it out at localhost:3000/ . You should simply see an empty black screen though because we have no objects in the scene! Next up, we'll begin building a Pipe class that encapsulates logic for handling pipe creation and behavior such as creating random starting points, handling random pipe growth, and ensuring pipes don't collide with themselves or others. We'll also implement a 3D Cube grid which the pipes will be placed within the bounds of.

The Pipe class is designed to encapsulate all the functionality necessary for generating and growing a 3D pipe structure within a scene. Its implementation focuses on managing the pipe's geometry, material, position tracking, and collision avoidance logic. At its core, the class relies on Three.js to create 3D objects such as joints (spheres) and segments (cylinders), which are dynamically added to the scene as the pipe grows. Each pipe starts at a random position within the defined grid bounds, making use of helper functions like randomInteger to generate coordinates within valid ranges.

The growth logic of the Pipe class is central to its functionality. The update method randomly selects a direction from the six possible axes while avoiding backtracking into its previous position. It validates potential paths using the isPathValid method, which checks each intermediate point along the proposed growth path to ensure it is within bounds and not already occupied. Once a valid path is found, the reservePath method marks all intermediate points as occupied, and the pipe's geometry is extended by creating a cylindrical segment and a spherical joint at the endpoint. This ensures that the pipes grow in a structured yet randomized way while staying within the 3D Cube's bounds and avoiding collision with other pipe segments.

With the pipe class and the 3D Cube grid in place, we now want to create instances of our pipes that have random starting positions on the grid, attempt to make the pipes grow, and animate their progress along the way! We also want to check if pipes have stopped growing at any point. The grid bounds and collision prevention makes it so that pipes have a natural limit to their ability to grow and when all pipes stop growing we should reset our scene and start over again, just like the original 3D Pipes screensaver!

As you can see in the previous code snippet, we create an array of pipes and instantiate numOfPipes and push them into the array. In our animation loop, we iterate over the pipes in the array and call pipes.update to attempt to grow the pipes. If its successful we set the pipesUpdated flag to true. If it is not successful we set it to false. If pipesUpdated is false, later on in the animation loop we check every iteration if that flag has changed at all. If it remains false, we begin counting how long the program is idle for without any pipe growth. If enough time passes without any pipe growth, we call the resetScene() function which will clear the current existing pipes, instantiate new ones, reset our idle time to zero and start the process over again!

That's it! At this point our program is complete and you should be able to run it and relive those memories of the coolest thing Microsoft has ever produced whenever you want!

Final Thoughts

The idea for this project came around after a coworker was tasked with adding 3D models and animations to our company website. I joked with him how cool it would be to build something like this using THREE.js on the browser. Admittedly, I have no clue how close this is to the actual 3D Pipes screensaver implementation but I am super happy with the results! It is definitely not a faithful recreation and there are some things missing that I'd really like to add in the future like the Utah teapot or the candy cane material easter eggs but perhaps I'll come back to those at a later date! For now, I'll be enjoying my recreation over at https://lucasmiller.dev/pipes and you can check out the source code over at https://github.com/Lucas-Miller/3d-pipes

Comments

Comments