The LAND auction has started, participate and buy LAND before it ends! Learn More
Hello! Please choose your
desired language:
Dismiss

In Part 1 of this two-part tutorial, we saw how to obtain real-time weather data from the Weather Unlocked API so that our scene can mirror the weather of a real-world location.

However, in Part 1 we could only place white or dark clouds in our scene. Today, we’ll take our weather simulation a step further by adding rain, snowflakes, and thunder. As we add these new weather patterns, we’ll discuss storing data in objects and using a particle system to simulate snowflakes and raindrops.

My name is Nico Earnshaw, and I take care of the Decentraland documentation. That means everything you can find in docs.decentraland.org. If you have any feedback about our docs, please make a pull request or create an issue in the GitHub repo!

If you were following along with Part 1, then you can pick up right where we left off last week.

If you’re just starting out, you can download this version of the sample project that includes just the code we wrote last week.

You can also download all of the assets we’ll use in the scene, including 3D models and textures for raindrops and snowflakes, from this link.

Finally, you can also find the complete scene code, including both what we did last week and what we’ll do today, in the scene’s GitHub repository.

Design raindrops

The clouds that we added last week look nice, but we can take this to the next level by adding some rain. To create rain we made a simple 2d texture that looks like a raindrop. By using 2D models, we save on triangles, which helps with the scene’s performance. We could have created a 3D model shaped like a drop, but the realism probably would not be worth the performance cost.

Note that the image has a transparent background, and the raindrop itself is also partially transparent itself to help add a little more realism.

Storing data for each raindrop

Let’s think about how we want to use these drops in the scene. We want drops to appear in a random position up in the clouds, fall to the ground, and then appear back up in a new random position in the clouds. We want to keep doing this with a fixed number of drops at regular intervals as long as the weather is rainy.

A different way we could do this is to regularly create new drop entities, make them fall, and then delete them from the scene. That would be a more accurate representation of what happens in nature, but it would also be very demanding on your computer and network.

As a very wise person in our team keeps saying, game development is all about faking things in clever ways.

It’s standard practice to recycle a fixed number of entities to represent complex particle systems, and it does the trick quite well.

We need to pay attention to one important detail here: if we want to reposition our drops up in the clouds after they’ve fallen, we don’t want to see water miraculously rising back into the sky. I know that in school they teach that the rain cycle works like this, but you don’t really see it happen in such a literal way. So while the drops are rising back, we’ll make them temporarily invisible.

We also want our drops to fall slowly so we can see them, but then we want them to rise instantly back into the clouds. That way we can cycle through our collection of drops as fast as possible so we don’t have to render as many. The dilemma is that in Decentraland, scene transition settings aren’t configured in reference to a specific movement. Instead, they’re configured in reference to an entity and affect all movements of that entity. However, we can switch the transition component of the entities on or off at will! So for each drop, we can store a boolean variable that tells us if the drop is currently falling or rising, and switch the raindrop entity’s visibility and transition settings based on that.

Each drop that is rendered must have its own unique key. Otherwise, the rendering engine has to guess how the collection of drops we are rendering relates to the last collection we rendered before. If all the engine receives is a list of identical plane entities, each with a position, how does it know that a given plane must move downwards to its true new position and not sideways or even back up to another position being rendered?

To summarize, the information that we want to keep track of for each raindrop is its key, its position, and if it’s currently falling or rising. We will create a custom type to represent this information.

export type Drops = {
  [key: string]: [Vector3Component, boolean]
}

This type definition makes use of the Vector3Component, which isn’t available by default, so you add the following line to the scene.tsx file.

import { Vector3Component } from 'decentraland-api'

Instead of treating our collection of drops as an array, as we did in a previous tutorial, we’ll be treating it as an object, where each drop in the collection is an attribute of a single object, with a key and a value. The value in this case is an array that holds both the drop’s position and the boolean that tells us if the drop is falling or rising.

Treating our data as a collection rather than as an array requires a different syntax that might look slightly more intimidating, but it becomes very useful when we want to store several items of information about each drop. This practice gets even more useful as you decide to store more information about each entity.

We must now include a drops object in our scene state, and set its type to match the custom type we just defined.

export interface IState {
  weather: Weather,
  drops: Drops
}

Also, we must initiate a value for this new variable in the scene state. It will start as an empty object.

state: IState = {
  weather: Weather.sun,
  drops: {}
}

Building a particle system

The first thing we’ll do is modify the getWeather() function that we created in Part 1 of this tutorial. After we’ve received a response from the API (or faked one) and mapped its weather description to the list of weather values we’re interested in, we will check if the type of weather implies rain or snow, and if so we’ll call a a new function called startPrecipitation(). We’ll use the startPrecipitation() function for both rain and snow.

getWeather() {
  let weather: Weather = Weather.sun

  if (fakeWeather) {
    weather = this.mapWeather(fakeWeather)
  } else {
    console.log('getting new weather')
    axios
      .get(callUrl)
      .then((response: any) => {
        console.log(response.data.wx_desc)
        weather = this.mapWeather(response.data.wx_desc)
      })
      .catch((error: any) => {
        console.log(error)
      })
  }

  if (weather == this.state.weather) {
    return
  }

  this.setState({ weather: weather, drops: {} })

  if (weather == (Weather.sun | Weather.clouds)) {
    return
  }

  this.startPrecipitation()
}

Note that when we’re setting the scene state to add the new weather value, we’re also also setting the drops variable back to a default empty object. This is to make sure that we don’t keep rendering raindrops if the weather stops being rainy.

The startPrecipitation() function checks the weather in the scene state and, if applicable, calls startRain(), passing it a different number of drops depending on the kind of rain.

startPrecipitation() {
  switch (this.state.weather) {
    case Weather.storm:
      this.startRain(40)
      break
    case Weather.heavyRain:
      this.startRain(100)
      break
    case Weather.rain:
      this.startRain(25)
      break
    }
}

startRain(), on the other hand, initiates a series of raindrops and then starts updating each.

async startRain(drops: number) {
  let dropsAdded: Drops = {}

  for (let drop = 0; drop < drops; drop++) {
    let newDrop: Vector3Component = {
      x: (Math.random() * 9) + 0.5,
      y: 9,
      z: (Math.random() * 9) + 0.5
    }

    const dropName = 'drop' + objectCounter++

    dropsAdded[dropName] = [newDrop, false]
  }

  this.setState({ drops: dropsAdded })

  for (let drop in this.state.drops) {
    this.updateDrop(drop)
    await sleep(dropSpeed / drops)
  }
}

Let’s go over each step of the startRain() function:

  1. The startRain() function creates a new variable called dropsAdded and fills it with drops that have a random location up in the clouds. Drop names are sequentially created based on a universal counter we initiate in the .tsx file. All drops start with their boolean value as false, making them invisible before they drop.

  2. We save that collection of drops as an object called drops in the scene state.

  3. We use a for loop to go over each drop in the collection and start running updateDrop() for every drop. We wait between each time we call the function to have an evenly spread stream of drops falling all the time. To make the most out of our drops, we set this delay to the time it takes for a drop to fall divided by the number of drops we’re handling.

If you pasted this function into your scene, you’ll notice that a couple of things are unrecognized by your editor. You’ll have to add these manually to the scene:

  • We use objectCounter as a simple way to ensure that all of our ids are unique as the counter is increased each time we use it. Initiate objectCounter as a variable by adding the following line near the start of the* scene.tsx* file.
let objectCounter: number = 0
  • We use dropSpeed as a way to easily configure the speed at which raindrops fall in our scene, this makes it easy to change in case you feel that the speed doesn’t look right to you. Initiate dropSpeed as a constant by adding the following line near the start of the scene.tsx file.
const dropSpeed: number = 3000
  • We use sleep() as a helper function to temporarily pause execution threads. We added this helper function manually to the scene ourselves. To use it in your scene, add the following to your scene.tsx file.
export function sleep(ms: number = 0) {
  return new Promise(r => setTimeout(r, ms));
}

Below is what updateDrop() executes for each individual drop, each running on a separate thread with phased timing.

async updateDrop(drop: string) {
  let dropsAdded: Drops = { ...this.state.drops }

  dropsAdded[drop][0].y = -1
  dropsAdded[drop][1] = true

  this.setState({ drops: dropsAdded })

  await sleep(dropSpeed)

  dropsAdded = { ...this.state.drops }

  let newDrop: Vector3Component = {
    x:  (Math.random() * 9) + 0.5,
    y: 9,
    z: (Math.random() * 9) + 0.5
  }

  dropsAdded[drop] = [newDrop, false]

  this.setState({ drops: dropsAdded })
  await sleep(10)

  if (this.state.weather == Weather.storm || Weather.rain || Weather.heavyRain) {
    this.updateDrop(drop)
  }
}

Let’s go over what updateDrop() does for each drop:

  1. First we obtain drop data from the scene state and update the position of the drop at a height of -1 and set the boolean variable to true, this makes it visible and enables it to move with a transition. Then we update the drops variable in the scene state with this data.

  2. We wait for the drop to fall. updateDrops() is an asynchronous function, so this pause only affects the thread that relates to a specific drop.

  3. After waiting, we obtain the state of all the drops as they are now. This is important because other loops for other drops have surely changed the scene state while we were waiting.

  4. We set a new random position for our drop up in the sky and set its boolean to false so that it rises instantly and invisibly. Then we set the scene state again.

  5. We check if the weather conditions are still rainy, if they are, we call updateDrop() recursively and start all over again.

We’ve gone over this in a previous tutorial, but remember that you shouldn’t change the variables in the scene state without calling setState(). When a variable is a collection, like an array or an object, you still need to provide a full value for the whole variable, you can’t set just one of its elements or properties at a time. In this case, that means passing a new value for the full collection of drops, including all the ones that haven’t changed.

Rendering raindrops

So far, all of the code we’ve added makes changes to the variables in the scene state that represent raindrops, but you won’t see any of that in the scene until we start rendering drops based on this information. Here’s the code to render them:

renderDrops() {
  let dropModels: any[] = []

  dropModels.push(
    <material
      id="drop"
      albedoTexture="materials/drop.png"
      hasAlpha
    />
  )

  for (var drop in this.state.drops) {
    dropModels.push(
      <plane
        key={drop}
        material="#drop"
        scale={0.1}
        billboard={2}
        position={this.state.drops[drop][0]}
        visible={this.state.drops[drop][1]}
        transition={
          this.state.drops[drop][1]
            ? {}
            : { position: { duration: dropSpeed, timing: 'linear' } }
        }
      />
    )
  }

  return dropModels
}

Here we iterate over the collection of drops spelling out the corresponding entity and storing this in an array that we can then return to the render function.

Each plane is configured to act as a billboard, this sets the rotation of the drop so that it always faces the user. Setting its value to 2 sets the mode so that it rotates only on the Y axis, so if you look at a drop from below you would see it flatten rather than as having its tail sideways.

Note that both the visibility of the drop and its transition settings depend on the value of this.state.drops[drop][1]. Remember that we defined the Drops type as [key:string]:[Vector3Component, boolean], so this.state.drops[drop1] refers to the value that corresponds to a specific key, which consists of an array holding a vector position and a boolean.

this.state.drops[drop][1] therefor refers to the second element in that array, which is the field that determines if the drop is falling or rising. When the value of that variable is false, the drop will be invisible and will have no transition for its position.

Now we just need to modify the render() function to call the renderDrops() function when the weather condition includes rain.

async render() {
  switch (this.state.weather) {
    case Weather.sun:
      return (
        <scene>
          {this.renderHouse()}
        </scene>
      )
    case Weather.clouds:
      return (
        <scene>
          {this.renderClouds('white')}
          {this.renderHouse()}
        </scene>
      )
    case Weather.rain:
      return (
        <scene>
          {this.renderClouds('white')}
          {this.renderDrops()}
          {this.renderHouse()}
        </scene>
      )
    case Weather.heavyRain:
      return (
        <scene>
          {this.renderClouds('dark')}
          {this.renderDrops()}
          {this.renderHouse()}
        </scene>
      )
    case Weather.snow:
      return (
        <scene>
          {this.renderClouds('dark')}
          {this.renderHouse()}
        </scene>
      )
    case Weather.storm:
      return (
        <scene>
          {this.renderClouds('dark')}
          {this.renderDrops()}
          {this.renderHouse()}
        </scene>
      )
  }
}

Conditionally show a different house model

To make the scene a little more responsive to different weather conditions, we’ll modify the renderHouse() function to switch the house model depending on the weather conditions.

renderHouse(water: string) {
  let houseModel: string = ''

  switch (water) {
    case 'dry':
      houseModel = 'models/house_dry.gltf'
      break
    case 'wet':
      houseModel = 'models/house_wet.gltf'
      break
    case 'snow':
      houseModel = 'models/house_snow.gltf'
      break
  }

  return (
    <gltf-model
      id="house"
      src={houseModel}
      scale={1}
      position={{ x: 5, y: 0, z: 5 }}
    />
  )
}

We included three different 3D models for the house, one dry, another with puddles of water, and another with snow.

This link points at a server where we deployed the scene so you can explore what it looks like. In this parallel universe, the weather is always faked as “heavy rain”.

Let it snow!

For snow, we want to do something similar, but we want each snowflake to slowly rotate as it falls. We also want our snowflakes to look more unique, we created 20 different materials that we want our flakes to use randomly.

Just as we did for drops, we will define a new type called flakes. This new type will be just like what we created for drops, but will include a second Vector3Component to represent the flake’s rotation and a string to represent the type of flake to use for the material.

export type Flakes = {
  [key: string]: [Vector3Component, Vector3Component, string, boolean]
}

I’ll let you figure out how to handle this yourself, it shouldn’t be very complicated if you start from what we built for our raindrops, you only need to add the flake’s rotation as well as the position. You also need to make sure you are rendering each different type of snowflake as a separate material, and apply these randomly to your flakes when you instance them.

The end result should look something like this:

This link points at a server where we deployed the scene so you can explore what it looks like. In this parallel universe, the weather is always faked as “snow”.

Create lightning

Yes, now things get even more fun!

In our previous tutorial, we downloaded this model by Poly by Google from Google Poly. I like the lightning bolts, but I don’t want them to stay static. Let’s make it a little more realistic.

We produced 6 different 3D models out of the original 3D model we got from Google Poly, each model with a different combination of branches from the lightning in this original model. We’ll switch randomly between these models to make it look more lively.

While we were working with this model on Blender, we also edited the material being used by it to make it emissive. Emissive materials emit their own light, so that would look quite cool for lightning.

All we had to do was set the Emit property of the material to a high value, in this case to 3 so that it’s super bright.

The code to animate our lightning is extremely simple. All we’re doing is changing the source file for a gltf-model by picking a random number. We named the gltf files for the lightning bolts we exported following a simple pattern of ln + a number to make this easier.

renderLightning() {
  let lightningNum: number = Math.floor(Math.random() * 25) + 1

  if (lightningNum > 6) {
    return
  }

  return (
    <gltf-model
      id="lightning"
      src={'models/ln' + lightningNum + '.gltf'}
      position={{ x: 5, y: 8, z: 5 }}
      scale={4}
    />
  )
}

Then we just need to call the renderLightning() function from render() when we have a storm, just as we called the renderRain() function.

We’re already calling the render() function every time we update a drop, if we randomly pick a different glTF model each time, that looks pretty good to me. If you wanted to change this interval, you could do this in a slightly more sophisticated way by storing a variable in the scene state to represent what lightning model to use and update that at the desired pace.

Final Thoughts

So there you have it! In this tutorial we went over several practices and scene mechanics that could be applied to all sorts of other projects. Remember that particle systems can get expensive in terms of resources, but there are plenty of clever ways to simplify their mechanics without sacrificing too much quality.

If you’re building your scene to be a multiplayer scene, as we’ll explore in future blog posts, keep in mind that it’s probably not important for all users to be seeing the exact same raindrop at the same location. The presence of rain alone is probably the only bit of information that needs to be shared for users of the scene to feel like they’re sharing the same experience.

Since keeping all of the information from our collection of raindrops in sync between users is tremendously costly in terms of network usage, all of the random movements of each drop could just as well be computed separately on each user’s local machine.

Thanks for following along, and happy building!

Start Building!
Our SDK provides everything you need to start developing games and applications.
Get Started