📜 ⬆️ ⬇️

Procedural generator of Khrushchev

Once I sat at home, read an article about Khrushchev and admired the genius of the architect. Then he let me go, and I thought that the despondency and monotony of the Khrushchev can be described mathematically very easily. Right angles, equal intervals, minimum decorations - what could be simpler?

In fact, the Khrushchev there are several dozen modifications, but some kind of basis, the essence of the Khrushchev still can be traced.

In general, without thinking twice, I sat down and wrote a C # generator for C # under Unity3d . Under the cat description of the algorithm and thinking on the topic of uv-cards, submesh and shaders.
')

▣ First try. Total programming of all geometry.


For starters, I tried to do everything programmatically. There are several advantages to this approach: there is no need to open the editor, the size of the distribution kit is smaller and it is easier to manipulate the mathematical side of the models. There are no downsides, if you type quickly, you have a lot of paper and good imagination. Manually connecting vertices into triangles is not very easy.

For those who do not know:
You have probably heard that three-dimensional models consist of polygons. Polygons are usually understood as triangles. Triangles are described using vertices and links between them. In addition to this information, a list of normals is applied — vectors that tell the graphics engine how to light the model.

Take for example a window. If it is completely flat and is just a small square in a small square, then it is already eight peaks and ten triangles. If you drown the window a little into the wall, then these are four more vertices and eight more triangles. And if you add a window sill or a box with flowers? Personally, from such calculations, my ears begin to blow steam.

Code to create a primitive window
At the entrance there are four panel tops with a window. From the product of vectors we find the normal. Calculate four more vertices for the corners of the window. Putting everything into suitable arrays, making triangles from vertices. We distribute triangles on submesh. About submeshs read on.

Mesh EntrancePanel(Vector3 vertex0, Vector3 vertex1, Vector3 vertex2, Vector3 vertex3) { var normal = Vector3.Cross((vertex1 - vertex0), (vertex2 - vertex0)).normalized; var window0 = vertex0 + (vertex3 - vertex0) * 0.25f + (vertex1 - vertex0) * 0.25f; var window1 = vertex0 + (vertex3 - vertex0) * 0.25f + (vertex1 - vertex0) * 0.75f; var window2 = vertex0 + (vertex3 - vertex0) * 0.75f + (vertex1 - vertex0) * 0.75f; var window3 = vertex0 + (vertex3 - vertex0) * 0.75f + (vertex1 - vertex0) * 0.25f; var mesh = new Mesh { vertices = new[] {vertex0, vertex1, vertex2, vertex3, window0, window1, window2, window3, window0, window1, window2, window3}, normals = new[] { normal, normal, normal, normal, normal, normal, normal, normal, normal, normal, normal, normal }, uv = new[] {new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0), new Vector2(0.25f, 0.25f), new Vector2(0.25f, 0.75f), new Vector2(0.75f, 0.75f), new Vector2(0.75f, 0.25f), new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0)}, triangles = new[] { 0, 1, 4, 4, 1, 5, 1, 2, 5, 5, 2, 6, 2, 3, 6, 6, 3, 7, 3, 0, 7, 7, 0, 4, 8, 9, 10, 10, 11, 8}, subMeshCount = 2 }; mesh.SetTriangles(new[] { 0, 1, 4, 4, 1, 5, 1, 2, 5, 5, 2, 6, 2, 3, 6, 6, 3, 7, 3, 0, 7, 7, 0, 4}, 0); mesh.SetTriangles(new[] { 8, 9, 10, 10, 11, 8 }, 1); return mesh; } 

The option is interesting, but after I spent a lot of time creating a wall, window, front door and a glazed balcony, I suddenly felt sad, and I decided to go in from the other side.

▣ The second attempt. All through the editor, with submeshs and lots of materials


Generally speaking, the engine is completely indifferent to the origin of vertices, triangles and normals. Two models, one of which was “born” in the code, and the other in Blender, will make excellent friends and will work the same way. Moreover, they can be merged into one model with submeshes (submodels? Subgrids? Subguns? ), All of whose transformations will affect submeshes. Submeshes are just additional lists with vertex indices, nothing more.

On each of the submes, you can hang your own material, which will make the glass shiny and the walls rough. The idea is quite tolerable, only each material is an additional challenge to the shader, and spying on submesh indices turned out to be an incredibly dreary task. I played a little with submeshes and abandoned this venture.

▣ Third attempt. Camo shader and texture maps


Video cards love when they are fed polygons and textures in large chunks. For example, it is much easier for them to swallow one model with a million triangles than a million models with one triangle. The same principle applies to textures. When a bunch of pictures are glued together into one large, a texture atlas is obtained. Often, several texture atlases are made: one for color, the other for lighting, and the third for reflections.

I decided to write my own shader, the benefit of Unity is simple, and I used a single atlas for texturing the models. The shader accepts this atlas and an additional map of shiny surfaces, into which I have brought all the windows.

Shader code for Khrushchev
The code below is the usual Specular shader that comes with Unity, in which windows are rendered into a separate map and a parameter is added to change their color.

 Shader "Custom/Khrushchyovka" { Properties { _Color ("Main Color", Color) = (1,1,1,1) _MainTex ("Base (RGBA)", 2D) = "white" {} _GlassColor ("Glass Color", Color) = (0.5, 0.5, 0.5, 1) _Shininess ("Shininess", Range (0.01, 1)) = 0.078125 _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1) _SpecTex ("Specular (RGB)", 2D) = "gray" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf BlinnPhong sampler2D _MainTex, _SpecTex; fixed4 _Color, _GlassColor; half _Shininess; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { half4 main = tex2D (_MainTex, IN.uv_MainTex); half4 spec = tex2D(_SpecTex, IN.uv_MainTex); o.Albedo = main.rgb * _Color.rgb + spec.rgb * _GlassColor.rgb; o.Gloss = spec.rgb; o.Specular = _Shininess; } ENDCG } FallBack "Diffuse" } 


▣ We build a wall


Now the fun part is building the building. The easiest way to start is from one wall. To make a wall of several panels, they must be placed side by side. In the previous article I mentioned CombineMeshes , used to combine models. Together with the models, he can be fed transformation matrices with which you can move, rotate and resize models. The logic is simple: in the cycle we type the required number of models, each shift a certain distance, we get a continuous wall. If you need a wall of a certain size, then simply divide it by the length of one panel and find out the required number of panels.

After a brief googling, it turned out that the panels in the Khrushchev houses come in different sizes. In order not to bother much, I made all the plates in two sizes: 2.5 m long and 3 m. In fact, the dimensions should be slightly different, but I just could not find sensible documentation.

With two different plates to fill the interval of a given length is much more difficult. This task has its own name - Subset sum problem . There are a lot of options for solving this problem; I chose a simple recursive algorithm.

Initially, there is an array with the available panel lengths and a segment that needs to be filled. As a result of the algorithm, another array is obtained, the numbers in which mean the number of required panels with the length of the matching index from the first array. That is, the first array looks like this: {3, 2.5f}. And the second for a segment of 11 meters looks like this: {2, 2}. I also note that the array with panels is sorted in descending order.

The segment for filling is divided integrally by the length of the largest panel, the result is added to the second array, the remainder is written to the variable. If the remainder is zero, then the task is completed, and the result is in the second array. If the remainder is greater than zero, this means that the largest panel overlaps it, throw out one large panel, add its length to the remainder and repeat the first operation with the remainder and the next panel in the list. We throw out large panels until the largest panel in the second array is the smallest in the first one, or the remainder becomes zero.

Algorithm Code
In addition to the above, a check for the size of the remainder is added; if it is smaller than the smallest panel, then one small panel is added.

 int[] ExteriorWallSizesDraft(float remainder, int[] draft = null, int startIndex = 0) { if (draft == null) { draft = new int[panels.Length]; for (int i = 0; i < draft.Length; i++) { draft[i] = 0; } } if (remainder < panels[panels.Length - 1]) { draft[draft.Length - 1] = 1; return draft; } for (var i = startIndex; i < panels.Length; i++) { draft[i] += (int)(remainder / panels[i]); remainder %= panels[i]; } if (remainder > 0) { for (var i = 0; i < draft.Length; i++) { if (draft[i] != 0) { if (i == draft.Length - 1) { return draft; } draft[i]--; remainder += panels[i]; startIndex = i+1; break; } } draft = ExteriorWallSizesDraft(remainder, draft, startIndex); } return draft; } 

No matter how hard I tried, I couldn’t understand the principle of distribution of panels of different lengths, so after converting an array with a quantity into an array with lengths, it is mixed using the Fisher – Yets algorithm. It turns out quite tolerable result.



▣ We design the facade


On different floors Khrushchev uses different panels. On the first floor there are windows and empty walls, on the second there can be balconies. Additionally, the building has a basement and sometimes an attic. To create a facade map, you can take an array with the lengths of the walls and the number of floors and make a two-dimensional array in which the panel type is stored in the array of each floor. Then on this template, you can make the appropriate panels.

For the front facade, the algorithm is as follows. The ground floor is filled with basement panels. The first floor is completely filled with windows. The second floor completely copies the first, except that part of the windows is replaced by balconies. The third floor and above simply copy the second floor pattern all the way to the roof. If there is an attic, then attic panels are added on top.

If the balconies are crammed completely randomly, then an ugly jumble turns out, so it is better to place the balconies on both sides of the facade in a separate cycle symmetrically about the center. Similarly, with windows on the front facades, there may be empty walls and windows on the first floor. To make it look good, the windows must be symmetrical. On the second floor of the butt, part of the windows, like the front facade, is replaced with balconies, but only on the central panels; on the extreme panels, balconies are extremely rare.

The final touch is the door. If you divide the number of panels by the number of doors plus one, then you can get the segments through which you need to place the doors so that they are located more or less evenly. In fact, in real Khrushchev houses the entrance to the building is located differently, judging from the photographs and plans, but so far I have not been able to think of anything better. On the ground floor there is the main entrance, on the next one there are windows that need to be slightly shifted during the model creation stage. On the last floor, instead of the window, there is an empty low panel. In some buildings, the main entrance is with a porch, then the scheme changes, but I was already too lazy to make models, so this option is not taken into account.

Facade Generator Code
To indicate the type of panel I use an enum, it looks like this:

 public enum PanelType { Wall, Window, Balcony, Entrance, EntranceWall, EntranceWallLast, Socle, Attic, }; 


The branching code below is quite simple, the variable names speak for themselves, there is nothing special to explain.

 List<List<PanelType>> FacadePattern(int panelCount, int floorCount, bool haveAttic=false, bool longFacade=false, int entrancesCount=0) { var panelPattern = new List<List<PanelType>>(); var entranceIndex = panelCount / (entrances + 1); var entranceCount = 1; for (var i = 0; i < floorCount+1; i++) { panelPattern.Add(new List<PanelType>()); for (var j = 0; j < panelCount; j++) { if (i == 0) { if (entrancesCount > 0 && j == entranceIndex && entranceCount <= entrances) { panelPattern[0].Add(PanelType.Entrance); entranceCount++; entranceIndex = panelCount*entranceCount/(entrances + 1); } else { panelPattern[0].Add(PanelType.Socle); } } else if (i == 1) { if (panelPattern[0][j] == PanelType.Entrance) { panelPattern[1].Add(PanelType.EntranceWall); } else if (longFacade) { panelPattern[1].Add(PanelType.Window); } else { panelPattern[1].Add(PanelType.Wall); } } else { panelPattern[i].Add(panelPattern[i - 1][j]); } if (i == floorCount) { if (panelPattern[i - 1][j] == PanelType.Entrance || panelPattern[i - 1][j] == PanelType.EntranceWall) { panelPattern[i][j] = PanelType.EntranceWallLast; } } } if (i == 1 && !longFacade) { for (int j = 0; j <= panelPattern[1].Count / 2; j++) { if (j != 0 && j != panelCount - 1 && Random.value > 0.5f) { panelPattern[1][j] = PanelType.Window; panelPattern[1][panelPattern[1].Count - 1 - j] = PanelType.Window; } } } if (i == 2) { for (int j = 0; j <= panelPattern[2].Count/2; j++) { if (panelPattern[2][j] == PanelType.Window && panelPattern[2][panelPattern[2].Count - 1 - j] == PanelType.Window && Random.value > 0.5f) { panelPattern[2][j] = PanelType.Balcony; panelPattern[2][panelPattern[2].Count - 1 - j] = PanelType.Balcony; } } } } if (haveAttic) { panelPattern.Add(new List<PanelType>()); for (var j = 0; j < panelCount; j++) { panelPattern[panelPattern.Count-1].Add(PanelType.Attic); } } return panelPattern; } 




▣ Roof Khrushchev


Four of the facade is not the house, you need a roof. As it turned out, the roofs of Khrushchev are different. There are flat, there are gable, there are four-pitched. Unfortunately, the photos of Khrushchev roofs on the Internet are even smaller than the plans of their floors, so the roofs turned out naked to me. Generally speaking, there should be ventilation chambers and roof outlets that would be easy to place if the size of the building were constant, but for my case it was necessary to come up with an algorithm for placement it is not very clear what is not clear where. So let them be naked for now.

For the roof, I made models with a width and a length of one meter, and then just stretched them as needed.

Roof selection
 switch (roofType) { case RoofType.Flat: combine.Add(RandomItem(roofFlat)); matrices.Add(Matrix4x4.TRS(roofHeight, Quaternion.identity, new Vector3(length, 1, width))); break; case RoofType.FlatOverhang: combine.Add(RandomItem(roofFlat)); matrices.Add(Matrix4x4.TRS(roofHeight, Quaternion.identity, new Vector3(length + 1, 1, width + 1))); break; case RoofType.Gabled: combine.Add(RandomItem(roofGabled)); matrices.Add(Matrix4x4.TRS(roofHeight, Quaternion.identity, new Vector3(length, 1, width + 1))); break; case RoofType.Hipped: combine.Add(RandomItem(roofHipped)); matrices.Add(Matrix4x4.TRS(roofHeight, Quaternion.identity, new Vector3(length + 1, 1, width + 1))); break; } 

That's all. Khrushchev is ready.



▣ Conclusion


The described approach has one bottleneck. CombineMeshes is not only slow, but it also has a limit on the number of vertices and triangles to merge. If you suddenly as I decided to make a skyscraper Khrushchev, the number of peaks will quickly exceed 65 thousand, and you will get the ruins as in the picture below. To avoid this, you need to write your own function gluing models.



Project sources and binaries for different platforms can be downloaded from the links below.

Note: The link code below is outdated, see the Procedural Toolkit for the latest version

Unity Web Player | Windows | Linux | Mac | Sources on GitHub

Left mouse button - new building, Esc - Exit.

PS I would be glad if someone will help with the refinement of the models, from me the modeler is bad.

Source: https://habr.com/ru/post/196862/


All Articles