Concept

Cyberpunk... kind of.
The original idea of going for a Cyberpunk theme came up because the genre tends to have a lot of fancy usages of light. But, the kind of assets you can find, especially free ones, are heavily limited, and Cyberpunk is too specific to find enough assets. So right before giving up that idea, we realized that this theme is much broader than what aesthetic one might first think of (I'm looking at you, Cyberpunk 2077).
Besides aforementioned hyper-futuristic Cyberpunk, there is also a more dirty, realistic kind. A good example for this is yet another game, Deus Ex: Mankind Divided. Thus, our scene went on to draw a lot more inspiration from there. That means, trying to retain a sense of futuristic elements, while working with a more old-fashioned interior design as a baseline. We also settled for an indoor scene, because again, assets seemed to be easier to find.
Scene

We started out by going through any category of assets that might fit into our scene and throwing them into a Blender scene haphazardly. For this, the BlenderKit addon was a huge help, since it greatly simplified adding objects to the scene. After getting a rough idea of what kind of objects we have at our disposal, we started selecting and properly placing objects.
Then we first placed the major elements, so the bed, the shelf next to it, the table in the corner and the sofa. From there, we kept filling in the scene piece by piece.
For objects that did not already have a texture assigned to them, we also just tried out different things, particularly we tested how they interact with our lights. In order to find good placements for the lights, we put point lights in Blender to figure out the coordinates. We also created a few of the simple assets ourselves, if we could not find what we were looking for and they were simple enough to make, like the windows that are out of frame.
Interesting Areas
Features
Line Lights
Line lights are essentially just a variant of point lights, as their intensity works the same way. The difference lies in the light position: On initialization, they receive two position coordinates instead of one. So when we want to know this light's intensity during raytracing, we interpolate between these two positions randomly, and take the result as our position. This will result in the position changing for every computation, so it is as if we had a lot of lights on a line, instead of just one fixed point light. This feature also really profits from supersampling.
The code for this feature can be found in its own class next to the other lights, simply called LineLight. It was used to evenly distribute light in the area around the neon light tubes, the monitors and the small lights hanging from the stairs. You can somewhat notice its existence if you look at the support cables of the neon lights.
Implemented by Franziska

Bloom
When you look at Cyberpunk images, you may notice that they tend to use bloom a lot, so this was a must-have. To achieve this effect, we look at the completed image at the end of rendering and create two masks: The first mask stores the RGB values of that pixel if at least one of its components is larger than 1, otherwise it will store the color black. The second mask is called weight mask, this one behaves similarly to the first one, except that it stores a 1 if the condition is fulfilled.
Then, we start computing the bloom, separately for each dimension for efficiency. Blurring is done by sliding a 1D kernel over the image, so for each pixel we calculate a gaussian filter for the pixels around it. The result of that is multiplied with our mask, so that only values over the threshold receive bloom. We do not immediately apply this to our image, however.
After computing the bloom for both dimensions, we multiply the result for each pixel with (1 - weight mask) first, then add our result to the current value of the pixel. The reason for using the weight mask is that it reduces the amount of extra light we add to areas with at least one RGB component larger than 1. Those areas are already bright, so adding the same amount of bloom to them as the areas around them would make them overly bright.
Bloom was implemented inside of the renderer, in the form of an alternative rendering method called render_with_bloom. The relevant parts come after the main render loop, which was simply taken from the normal rendering method. Its effects can be seen everywhere where it's bright enough in the image, for example around the sunlight falling on the sofa, noticeable by that fuzzy light glow.
Implemented by Franziska

Alternative Combine Material
We encountered limits of the standard Combine Material while setting up the chandelier you can see in the top left of the scene. The model is in one piece, but consists of lampshades and metallic arms. The texture is just one image that gets mapped onto this object, with an additional metallic map to determine which part behaves metallic in which does not. We needed to use this metallic map, so we implemented an alternative Combine Material that can work with it.
The main difference to the normal Combine Material is that we use the metallic map as our weight instead of a fixed value. In theory, one could set values between 0 and 1 for the different materials, but in our usage we only had black and white in the metallic map, so each pixel of the object either has one material or the other. Essentially, we look up the value of the corresponding pixel in the map, then assign a weight according to the value to our current spot. This does not only affect emission and reflectance, but also sample reflectance. Note that this version of combine material only supports consisting of two materials, not more. This limitation is simply due to us not needing more for our scene.
This feature was implemented into a new class of material, confusingly called CombineTwoMaterial. The implementation of the gaussian filter is inside of a file called "utils.cpp", in the core directory. As mentioned, it can be seen in the chandelier.
Implemented by Franziska

Ambient Light
Without ambient light, images tend to look much darker than they should be in reality, since light usually bounces around a lot more.
The way we implemented it is by always adding some amount of light to an intersected solid, even if no light reaches it. Note that this only needs to be done for Lambertian materials and combine materials that contain a Lambertian. So, we added a new method for materials next to getReflectance and getEmission: getAmbient. This method simply returns the color at a given pixel, multiplied by a parameter that we set on initialization of the material. The result of this computation is then multiplied with the color of the light, also multiplied with some kind of parameter. These parameters are just there to control how much ambient light a light sends out or the model is affected by.
The method getAmbient is inside of each material method, while getIntensityParam (the method that determines the color of the light) is inside of all lights, although only properly implemented for the lights used in our scene. Both methods are called inside of the RecursiveRayTracingIntegrator, at the beginning of each of the loops that iterate through the lights in the scene. It is particularly noticeable from the yellow tint covering most of the scene, as the other light colors are weaker and less noticeable. For example, the armchair in this image is made of a blue material, but has a bit of yellow on it due to the ambient light.
For even better comparison, here is a version of our scene without ambient light, although also with less samples for supersampling and smaller in size.
Implemented by Franziska

Cook Torrance Material
Cook Torrance BRDF is a physically-based model that takes microfacet distribution into consideration, it can be used as an alternative to Phong. Microfacet distribution means that light is scattered across the surface, as if reflecting off multiple "microfacets". It calculates the reflectance of the surface with respect to inputted Fresnel value and roughness. Fresnel value determines how the light is reflected at the surface, and it is dependent on the angle of incoming light. Roughness represents the number of microfacets on the surface, and it is used to compute how much the light is scattered.
It is used in the scene by the brass vase and tube ends of the neon lights, and the implementation can be found in materials/cooktorrance.cpp.
Implemented by Emirhan

Cook-Torrance

Cobined material: Phong and Lambertian
References
- Coffee table (we removed the glass plate)
- Office chair
- Chandelier
- Spiral stairs
- Bed
- Shelf
- Sofa
- Armchair
- Floor texture
- Wallpaper texture (we cut out a repeatable part)
- Wall panels
- Wall panel texture
- Office desk (we removed the monitors)
- Office desk texture
- Monitor texture (we cut this one into pieces)
- Monitor texture
- Jade vases
- First book, second book, third book, fourth book
- Aged Sideboard
- Persian carpet
- Noodle cup
- Brass vase on couch table
- Bottles
- Vases on the sideboard (table is removed)
- First vase on the shelf
- Cup on the shelf (handles are removed)
- Sunglasses
- The models for the windows that are out of frame, floor and walls, broken window shutters, cables, neon tubes, monitors, two of the monitor contents and small lights hanging from the stairs are self-made.