Background

In addition to the standard UV texture map, Geopipe models contain a secondary UV map that is unique to each building facade within a model. This UV map can be used to uniquely identify a building and its features. If you want a general overview of the concepts behind this shader, check out our Window Metadata page.

Set up a ShaderGraph

These instructions use Unity’s ShaderGraph. If you are using Unity’s standard renderer, install the ShaderGraph package before following along. If you are using a different game engine or 3D renderer, use the provided graphs and code as a base for your own shader and feel free to reach out to us if you have any questions.

First create a new ShaderGraph. In the graph inspector’s graph settings, set the material type for your shader to specular color. Then add the following inputs using the + button on the ShaderGraph blackboard.

  • Diffuse, Normal, and Specular as Texture2D inputs. These are the textures that you will see on the side of each building.
  • Intensity as a float. This property only makes a difference if you are using the HDRP rendering pipeline. This will determine how bright the windows should be.
  • WindowMaskFactor as a float. This value is the threshold to extract the location of a window from the specular texture.
  • WorldIlluminationLevel as a float. This is an input that tells the shader how much light is in the world. When WorldIlluminationLevel equals 0.0, all the windows will be glowing. When WorldIlluminationLevel equals 1.0, none of the windows will be glowing.
  • MaxIlluminatedFraction as a float. In the real world, not all of a building’s windows are glowing at night. When MaxIlluminatedFraction equals 1.0, all of a building’s windows will be glowing when WorldIlluminationLevel equals 0.0. When MaxIlluminatedFraction equals 0.8, only 80% of the windows will be glowing when WorldIlluminationLevel equals 0.0.
  • NightGlow as a color. This is the color that windows will be glowing when they are turned on. This tutorial uses solid yellow (Hex #FFFF00), but you can change it to any shade that fits your needs.
  • DayGlow as a color. This tutorial uses solid black, because windows do not glow during the day. If you wanted to create a stylized world, you could change this color so that windows appear different during the day.

The inputs to the shader shown on the ShaderGraph blackboard

The ShaderGraph settings. The Material Type has been changed to "Specular Color"

Then, set up the provided materials to the correct outputs of your shader. The fastest way to add nodes will be to right-click in the empty space on the graph, select “Create node”, and start typing the name of the node. Then click and drag from each node’s output into the other nodes’ inputs. To add node inputs defined in your blackboard, drag them into the graph. The initial shader graph. The Diffuse input node is connected to a "Sample Texture 2D" node, whose RGBA node is connected to the fragment "Base Color" node. The Normal input node is connected to a "Normal From Texture" node, whose Out node is connected to the fragment "Normal (Tangent Space)" node. The Specular input node is connected to a "Sample Texture 2D" node whose RGBA output is connected to the fragment "Specular Color" node. The specular input's sample texture "Alpha" node is connected to the fragment "Smoothness" node.

Saving ShaderGraphs will not happen automatically, and Ctrl/Cmd+S will not save your graph. Every time you wish to save your ShaderGraph, you must manually press “Save Asset”.

Import your model

Follow the instructions from https://docs.geopipe.ai/models.html. You can skip the instructions that describe how to set up the windowed materials because this shader will replace those materials.

Add the Shader to Your Building

In the folder where you extracted your materials, find one of the materials that corresponds to a windowed material. Make sure to note the name of the default texture. It should use the following naming scheme:

windowedFacade_{material}_{number}_diffuse_1024.png.

The inspector window should show that the material currently uses a “Standard”, “HDRP/Lit”, or “Universal Render Pipeline/Lit” shader. Select that dropdown menu, go to the Shader Graphs section, and select your shader. Then set the following properties on your material.

The Diffuse property should receive the material’s default texture that you found previously.

The Normal and Specular properties should receive the textures that have similar names to your diffuse texture, but end their names with normal and specularGlossiness respectively.

Certain file formats will not contain Normal textures. If you are using such a model, you can ignore the normal map for the purposes of this tutorial.

Finally, set the NightGlow property to a yellow color, Intensity to 5.0, and WindowMaskFactor to 0.8.

You will want to repeat this process for all the windowed materials.

A material configured with the shader and properties as described above.

The following image shows the shader applied to the windowed materials that make up this building. A building with the basic shader.

Identify the Location of Each Window

In order to light up windows, we first have to extract a window mask from a texture. A good candidate for this role is the alpha channel from the Specular texture. Multiplying the alpha channel of the Specular texture by NightGlow will only emit light from where there is specularity. Note: If you are not using HDRP, the emission node does not exist and you can directly connect the output from the multiplication node to the emission input.

A ShaderGraph. The Specular input is connected to a "Sample Texture 2D" node. The "Alpha" output and the "NightGlow" input are both inputted into a "Multiply" node. The output from the "Multiply" node is connected to the "Color" input of an "Emission Node". The Intensity input is also connected to the Intensity input of the "Emission Node". THe output from the "Emission Node" is connected to the fragment "Emission" node.

The result of the above ShaderGraph. All facades with windows are glowing, even on the parts of the facade that do not have a window.

Hm, that doesn’t appear correct. It appears that some of the non-window part of the specular texture contains a small amount of specularity. To remedy this, we can add a step node to only create a mask where the alpha of the specular material is above a certain threshold (WindowMaskFactor). This shader uses 0.8 as the mask factor, but feel free to change it to fit your needs. Lower numbers will make more of the area around the window glow, and larger numbers will make less of the window glow.

The ShaderGraph with the "Step" node attached. The "WindowMaskFactor" input is set as the "Edge" input of the "Step" node and the "Alpha" output from the Specular sampled texture is set as the "In" of the "Step" node.

The result of adding the step node. Now only the parts of the building with windows are glowing.

Much better.

Extract an ID from Each Window

Although glowing windows look nice, we want to extract the window IDs so that each window can look different. To accomplish this task, we will use a custom shader that takes in the main UV and secondary UV as input and outputs an ID for each window.

Within your project, create a file named IDShader.hlsl and paste the following code into it.

#define FLOAT_MANTISSA_BITS 23
#define FLOAT_MANTISSA_MASK ((1 << FLOAT_MANTISSA_BITS) - 1)
void IDFunction_float(float4 uv0, float4 uv2, out uint id) {
   // This extracts the number of rows and columns in the facade
   float rows = floor(uv2.y);
   float cols = floor(uv2.x);

   // This extracts the row and column position of the current window
   uint row = (uint)floor(uv0.y * (rows + !rows));
   uint col = (uint)floor(uv0.x * (cols + !cols));

   // This extracts the unique facade ID by concatenating the fractional
   // portion of the UV2 map
   id = (asuint(uv2.y) & FLOAT_MANTISSA_MASK) ^
           ((asuint(uv2.x) & FLOAT_MANTISSA_MASK) << (32 - FLOAT_MANTISSA_BITS))
           ^ 0x5e1a743c;
   // This incorporates the row and column of the window with the facade ID
   // to produce a sufficiently random window ID
   id = (id << (col + 1 + (uint)sqrt(row))) ^ (id >> (row + 1)) ^ id;
   id = (id << (row + 1 + (uint)sqrt(col))) ^ (id >> (col + 1)) ^ id;
}

void IDtoColor_float(uint id, out float4 color) {
    color.r = (float)(id >> 22) / (float)((1 << 10) - 1);
    color.g = (float)((0x0fff) & (id >> 10)) / (float)((1 << 12) - 1);
    color.b = (float)(id & 0x03ff) / (float)((1 << 10) - 1);
    color.a = 1.0;
}

Within the ShaderGraph, create a custom function node. Give it two float4 inputs named Main_UV and Secondary_UV and a float output named ID. Set the name to IDFunction and add the IDShader.hlsl file as the source.

The node settings for IDFunction. "Main_UV" and "Secondary_UV" are both configured as "Vector 4" inputs. "ID" is configured as a "Float" output. The name of this function is set to "IDFunction" and the source points to the "IDShader.hlsl" file.

Before we turn this ID into an on/off switch, let’s visually confirm that the windows have different IDs. This can be done with another custom function node that takes an ID float as input and produces a Color vector4 as output. Set the function name to IDtoColor and use the same IDShader.hlsl file as the source. This custom function extracts an RGB color from the bits that make up ID.

The node settings for IDtoColor. "ID" is configured as a "Float" input. "Color" is configured as a "Vector 4" output. The name of the function is set to "IDtoColor" and the source points to the "IDShader.hlsl" file.

Connect two UV Geometry inputs to our IDFunction node, which has its ID output in turn connected to the IDtoColor function. Confirm that the UV0 channel is connected to Main_UV and the UV1 channel is connected to Secondary_UV. For now, replace NightGlow with the output Color from the IDtoColor node.

The ShaderGraph with the ID extraction and IDtoColor conversion. The output "Color" from "IDtoColor" is now connected to the "Multiply" node where "NightGlow" was previously located.

The result of this shader. Each window is lit up with a different color.

Disco time!

Use the Window ID to Control the Light State

Although the disco windows look nice, we still do not have any control over whether or not a given window is lit.

The function WorldIlluminationLevel + (1 - MaxIlluminatedFraction) produces a threshold that determines if a window should be lit or not. As WorldIlluminationLevel decreases, the threshold also decreases, and more windows are lit. As MaxIlluminatedFraction decreases, the threshold increases and fewer windows will be lit.

To compare the window ID to that threshold, we want a function that maps the ID to a value between one and zero. If the mapped value is greater than the threshold, then the window should glow. This shader uses the fractional part of the natural logarithm of an ID as that function. Although not a perfect choice for all IDs, it is sufficiently useful for this tutorial.

Once we’ve compared the state of the mapped ID to the threshold, we need to tell the shader to light up the window in a certain way. A branch node is used so that if the comparison was true, then the window emits its NightGlow.

The final ShaderGraph. The IDtoColor function is removed. The output of the IDFunction node is passed into a "Log" node (Base E), whose output is passed into a "Fraction" node. The output of the "Fraction" node is set as the "A" input of a "Comparison" node which is set to a "Greater" configuration. Separately, the "MaxIlluminatedFraction" input is passed into a "One Minus" node, whose output is passed into an "Add" node. The "WorldIlluminationLevel" input is connected to the other input of the "Add" node, and the output of the "Add" node is connected to the "B" input of the "Comparison" node. The output of the "Comparison" node is connected to a "Branch" node. The True input of the "Branch" node is connected to the "NightGlow" input and the False input is set to the "DayGlow" input. The output of the "Branch" node is connected to the "Multiply" node where the "IDtoColor" node was previously connected.

This shows the facade with WorldIlluminationLevel = 0.0 and MaxIlluminatedFraction = 0.5 This shows buildings with half of the windows glowing.

Next Steps

And that’s it! Now you can play around with the different settings to customize the window shader to your liking, or you can take the facade ID and apply it to different tasks.

Use the following bullet points as inspiration for your future projects

  • The lights currently just turn on and off. Adjust the shader so the windows slowly turn on and off
  • Create a script that converts the position of the sun into an illumination level so that the windows automatically turn on at night time and turn off at sunrise.
  • Instead of turning on/off all the windows at roughly the same time, some rooms get re-used during the night as people use them for different meetings. Rework the shader so that windows can turn on and off throughout the night.