Ashton C Montgomery

Web Developer

3D City Scene #2: Expanding the City to New Heights

After the success of 3D City Scene #1, I wanted to take things to the next level and create something much larger and more complex. Enter 3D City Scene #2—a massive, expansive city with 6 districts, each featuring 32 buildings. Each building is equipped with multiple windows on each side, making this project one of the most intricate and large-scale endeavors in my 3D CSS series.

Project Overview

The goal of 3D City Scene #2 was to build upon the concepts established in City Scene #1, but on a much larger scale. What was once a single district in the previous project has now expanded into a full city, with each district housing dozens of buildings. With the sheer scale of the project, it’s easy to imagine the complexity involved, especially when it comes to structuring and positioning all the elements.

Approach & Execution

This project took the lessons learned from City Scene #1 and made several adjustments to accommodate the larger scale. Here’s an overview of the approach and key features:

  1. Larger Scale and Adjustable Depth: While City Scene #1 used square bases for the buildings, City Scene #2 introduced a new approach. The blocks are positioned in such a way that the depth of each building is adjustable, allowing for more varied and dynamic city blocks.

    .side {
    position: absolute;
    background: var(--buildingColor1);
    backface-visibility: inherit;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 2.5% 2.5%;
    flex-wrap: wrap;
    box-shadow: 0 0 1.5em #000a inset;
    }

    .front,
    .back {
    --coefficient: 0.5;
    height: var(--Height);
    width: var(--Width);
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%) translate3d(0, 0, calc(var(--Depth) * var(--coefficient)));
    }

    .back {
    --coefficient: -0.5;
    }

    .left,
    .right {
    --rotation: -90deg;
    height: var(--Height);
    width: var(--Depth);
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%) rotateY(var(--rotation)) translate3d(0, 0, calc(var(--Width) * 0.5));
    }

    .right {
    --rotation: 90deg;
    }

    .top,
    .bottom {
    --rotation: 90deg;
    height: var(--Depth);
    width: var(--Width);
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%) rotateX(var(--rotation)) translate3d(0, 0, calc(var(--Height) * 0.5)) ;
    }

    .bottom {
    --rotation: -90deg;
    }

    Setting up the creation of the boxes this way allowed me to size them "dynamically" by adding classes that descibed their sizes.

    .tall {
    --Height: 4.5em;
    --Width: 2em;
    --Depth: 2em;
    }

    .wide {
    --Height: 2em;
    --Width: 4em;
    --Depth: 2em;
    }

    .penthouse {
    --Height: .5em;
    --Width: .75em;
    --Depth: .5em;
    }

  2. JavaScript Functions for Structure Setup: With the increased complexity, I created several JavaScript functions to streamline the setup process:

    • One function is used to set the sides of each structure, which was originally for buildings but was later adapted for billboards and posts.

      function setSides() {
      let sides = ["front", "back", "right", "left", "top", "bottom"];
      for (let i = 0; i < buildings.length; i++){
      buildings[i].innerHTML = "";
      for (let j = 0; j < sides.length; j++) {
      buildings[i].innerHTML += "div class=\"side "+ sides[j] + "\"/div";
      }
      console.log("Sides have been added to the building " + i);
      }
      }

      To streamline the process, I created a separate function called "setStructureSides". This function ensured that billboards could be added without needing to check within the setSides function to verify if the structure already had all of its sides. The goal was to prevent new sides from being added to buildings when placing billboards, keeping the building structure intact.

    • Another function ensures that the correct number of windows are added to each side of the buildings, with 16 windows on three sides and 14 on the side with the entrance.

      function setWindows() { let sides = ["front", "back", "right", "left"];
      const numOfWindows = 16;
      const numOfWindowsWithEntrance = 14;
      let frontOfBuildings = Array.from(document.getElementsByClassName("front"));
      for (let i = 0; i < sides.length; i++) {
      if (sides[i] == "front") {
      for (let j = 0; j < frontOfBuildings.length; j++) {
      for (let k = 0; k < numOfWindowsWithEntrance; k++) {
      let windowElement = document.createElement("div");
      windowElement.classList.add("window");
      frontOfBuildings[j].appendChild(windowElement);
      }
      }
      console.log("Windows have been set on the front side");
      } else {
      let sideElements = document.getElementsByClassName(sides[i]);
      let sideArray = Array.from(sideElements);
      for (let m = 0; m < sideArray.length; m++) {
      for (let n = 0; n < numOfWindows; n++) {
      let windowElement = document.createElement("div");
      windowElement.classList.add("window");
      sideArray[m].appendChild(windowElement);
      }
      console.log("Windows have been set on the " + sides[i] + " side");
      }
      }
      }
      }

  3. City-Wide Zoom Out: Unlike City Scene #1, which featured a zoom-in effect, this project introduces a zoom-out effect triggered by keypress. This zoom-out function allows users to see the entire cityscape, offering a bird's-eye view of the massive, sprawling city.

    This is essentially the same function from City Scene #1, just adapted to add a "zoomOut" class instead of a "zoomIn".

  4. Roof Access and Penthouse Sheds: I added functionality for roof access and penthouse maintenance sheds on top of all the buildings. These features add a layer of realism to the scene and make it feel more lived-in and functional.

  5. Billboards for Wide Buildings: A separate function was added to populate the wide buildings with billboards, enhancing the urban feel and giving the city a more commercial, lively atmosphere.

    function addBillboards() {
    let wideBuildings = Array.from(document.getElementsByClassName("wide"));
    for (let i = 0; i < wideBuildings.length; i++){
    let billboards = document.createElement("div");
    billboards.classList.add("structure");
    billboards.classList.add("billboard");
    wideBuildings[i].appendChild(billboards);
    console.log("A billboard has been added to wide building " + i);
    }

    let billboards = Array.from(document.getElementsByClassName("billboard"));
    for (let i = 0; i < billboards.length; i++){
    let billboardPost = document.createElement("div");
    billboardPost.classList.add("structure");
    billboardPost.classList.add("billboardPost");
    wideBuildings[i].appendChild(billboardPost);
    console.log("A billboard post has been added to wide building " + i);
    }
    setStructureSides();
    }

  6. Retaining the SetSky Function: One of the key features carried over from City Scene #1 is the setSky function. This function adjusts the sky’s appearance based on the time of day, transitioning between day and night to add a layer of realism to the scene.

Final Thoughts

While 3D City Scene #2 is an ambitious project with an enormous scale, it’s also a powerful demonstration of the potential of CSS and JavaScript in creating immersive 3D environments. Despite the project’s size, it’s clear that careful planning and the addition of custom functions made it all come together.

That being said, the sheer volume of buildings and structures does mean the project may not load optimally in all situations. Optimization is an area I’ll definitely focus on in future updates to ensure smoother performance. But overall, this project has been an exciting journey into creating a much larger, dynamic 3D world.

As I continue building on this, I’m excited to refine the functionality and eventually add full interactivity to allow users to navigate the city and explore each district in greater detail.

The next project in this 3D CSS series is the user controlled cube.

Need Help Shaping Your Corner of the Internet?

I'd love to work with you.

Off to the Contact Page!