In the beginning of May, we had a functioning Flask app that was able to take a Twitter username and generate a custom 3D room as a response. However, we wanted to take the app further by giving more thought to some details. I’ll focus on three things: adding generative sculptures, improving the performance on the Flask side and designing a start and loading page.

Initially, I had three different 3D models that I was using as sculptures in the room. These were by doubletwisted, Aiekick and justindesigner. However, Nikolai and I thought that sculptures could be a natural way to introduce more generative elements in the room. We studied some shape grammar experiments by Maya Gao and became interested in them. Nikolai also suggested testing L-systems to create organic shapes and provided me with some code to build on.

I ended up switching between three different sculpture types in the room: one with randomly placed boxes (in picture), one blob-like made with TorusGeometries and a L-system and one that is deforming one big TorusGeometry.

Box sculpture

Code for the BoxSculpture class as an example:

export class BoxSculpture {
  constructor({
    dim = 20,
    width = 4,
    height = 4,
    depth = 4,
    numOf = 600,
  } = {}) {

    function makeCube(dim) {
      let cube = [];
      for (let x = 0; x < dim; x++) {
        cube[x] = [];
        for (let y = 0; y < dim; y++) {
          cube[x][y] = [];
          for (let z = 0; z < dim; z++) {
            cube[x][y][z] = 0;
          }
        }
      }
      return cube;
    }

    function randomPoint(dim) {
      return Math.floor(Math.random() * dim);
    }

    function randomPosition(dim) {
      return [randomPoint(dim), randomPoint(dim), randomPoint(dim)];
    }

    function hasSpace(cube, [x, y, z]) {
      for (let i = x - 0.5 * width; i <= x + 0.5 * width; i++) {
        if (i < 0 || i > cube.length - 1) {
          return false;
        }
        for (let j = y - 0.5 * height; j <= y + 0.5 * height; j++) {
          if (j < 0 || j > cube.length - 1) {
            return false;
          }
          for (let k = z - 0.5 * depth; k <= z + 0.5 * depth; k++) {
            if (k < 0 || k > cube.length - 1) {
              return false;
            }
            if (cube[i][j][k] === 1) {
              return false;
            }
          }
        }
      }
      return true;
    }

    let bound = makeCube(dim);
    const sculpt = new THREE.Group();

    const standHeight = 20;
    const standGeometry = new THREE.BoxGeometry(18, standHeight, 18, 30, 30);
    const stand = new THREE.Mesh(standGeometry, standDarkMaterial);
    stand.receiveShadow = true;
    stand.castShadow = true;
    stand.position.y = standHeight / 2;

    for (let i = 0; i < numOf; i++) {
      let geometry = new THREE.BoxGeometry(
        (width / dim) * (Math.floor(Math.random() * 3) + 1),
        (height / dim) * (Math.floor(Math.random() * 3) + 1),
        (depth / dim) * (Math.floor(Math.random() * 3) + 1),
        10,
        10
      );
      // these are blocks that we add to the group and finally draw
      const block = new THREE.Mesh(geometry, sharpMaterial);
      // individual random position with each round of the loop
      let position = randomPosition(dim);
      if (bound[position[0]][position[1]][position[2]] === 0) {
        block.position.x = position[0] * (width / dim) - width / 2;
        block.position.y =
          position[1] * (height / dim) + standHeight + 0.5 * (height / dim);
        block.position.z = position[2] * (depth / dim) - depth / 2;
        block.castShadow = true;
        block.receiveShadow = true;
        // add that small cube to the group
        sculpt.add(block);
        bound[position[0]][position[1]][position[2]] = 1;
      }
    }

    sculpt.add(stand);
    // this is the result that class returns
    return sculpt;
  }
}

In the main code:

const sculpture = new BoxSculpture({
  // value coming from data
  dim: resizeBoxes,
  width: 26,
  height: 36,
  depth: 26,
  numOf: 30
});

sculpture.position.x = 20;
sculpture.position.y = 0;
sculpture.position.z = -20;
scene.add(sculpture);

Virtually every user gets a unique sculpture because of randomness and differencies in data, so it was definitely worth it to spend some time studying generative rules.

Minimalist Design

On the Python side, we wanted to improve performance. One important change was to get rid of a text file called textMass.txt. I had previously appended all the user’s tweets to that one file in to make it easier to inspect during prototyping. However, this had meant opening the text file, appending to it and closing it before moving on to calculations. A much more straightforward solution is just using a string instead:

textMass = ""

Then cleaning the tweet and adding it to a long string in for loop:

cleanedText = cleanTxt(tweet.full_text)
textMass += cleanedText

And outside the for loop:

// ready for calculating NLP values
whole_text_sample = TextBlob(textMass)

I also learned how great sets can be for NLP calculations. Since they ignore duplicate values, they can easily be used to store unique words of the text corpus:

for sentence in whole_text_sample.sentences:
    for word in sentence.tags:
        uniqueWords.add(word[0])
        # checking tags that indicate verbs
        if word[1] == 'JJ' or word[1]=='JJR' or word[1]=='JJS':
            uniqueAdjectives.add(word[0])

Finally, it was time to design a minimal start page and add a loading page. I created a simple design with a gradient background that resonated with the sky shader of the 3D scene. Nicholas suggested using a blue background instead, as a reference to Twitter, and made great suggestions for the font and the spacing of the elements. The loading page is simply a div element that is only displayed while the scene is loading.

#loadingImg {
  background-color: #54a1e9;
  background: linear-gradient(#54a1e9, #7ba7f8); /*optional*/
  margin: 0;
  height: 100%;
  width: 100%;
  background-attachment: fixed;
  font-weight: 400;
  font-size: 16px;
  opacity: 0.9;
  position: fixed;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 1000ms ease-in-out;
}

#loadingImg.animate-out {
  opacity: 0;
}

#loadingImg p {
  animation-name: pulse;
  animation-duration: 1000ms;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}

These are the data values that are affecting the room in its current form:

  • Average tone: room type, the plant type vs. a pool, sky colors, plant shape, artwork type, artwork and mirror frame, sculpture type and shape
  • Number of positive / negative / neutral tweets: tints and shades, sky colors, pillows vs. chair
  • Average subjectivity: height of the mirror
  • Number of "I", "me", "myself", "my" and "mine": width of the mirror
  • Average sentence length: sky colors
  • Number of unique words: level of detail in the artwork, sculpture type, sculpture detail in torus sculpture
  • Interactions with other users: window size, plant growth
  • Number of adjectives: sculpture dimensions, sculpture shape in torus sculpture
  • Number of "nature", "plants" and "animals": a possible extra plant
  • "Love" mentioned twice or more: a pink table

I already have plenty of ideas how the project could be taken further. I would love to implement Pascal's idea of creating different rooms for different time periods; for example, comparing someone's Twitter room before and after the pandemic by limiting Tweepy’s search accordingly. One could also switch from Twitter to other text sources. One option would be to create a room that reflects one’s browsing history, or to compare the rooms for classic novels by using Project Gutenberg.

All in all, one single project managed to teach me new skills in NLP, introduce me to Three.js and take my web development skills to a new level. I’m thankful for the great teammates at MAD and I hope you readers have enjoyed the blog series as well!

Tweet Room app