📜 ⬆️ ⬇️

How was the frame rendered in the 1998 Thief game

image

In 1998, Looking Glass Studios released the stealth game Thief: The Dark Project . While 3D hardware acceleration was in its infancy, it was not used during the development process, the game was rendered only in software.

I was the main author of the basic rendering technology Thief (although I did not write renderers of objects and characters), as well as related elements. The same rendering engine, modified by other people for using 3D hardware acceleration, was also used to render System Shock 2 and Thief 2 .
')
The engine was written about the same time as Quake (although the game came out much later), and the overall appearance is very similar to Quake . Many technologies were copied from or inspired by Quake, but often their work was slightly or significantly different.

Quake's software rendering is documented in detail by Michael Abrash in a series of articles that were reprinted in his book, The Graphics Programming Black Book . The techniques used in Thief have never been described before, and I will be glad to finally tell you about them, even if they are now completely outdated. If possible, I will try to describe them in connection with the more well-known Quake techniques.

Important games of the time with similar rendering technology:


Content



Visibility


In Looking Glass games prior to Thief , mesh-based worlds were used. In System Shock 1 and Ultima Underworlds, visibility was calculated by traversing the grid. However, Thief could have a completely arbitrary shape, so I had to develop its engine from scratch (I started working long before Thief and thought about the engine even before we released Terra Nova: Strike Force Centauri ).

Thief was based on the idea of ​​calculating visibility and clipping using portals and cells, published by Seth Teller in his Ph.D. thesis 1992
of the year. In fact, the Thief world renderer was called “Portal”, but since this name had a new popular meaning, I simply called it “Thief” or “Thief engine” (but the engine had much more than just a renderer, for example, object system, AI, physics, and I had nothing to do with it. I didn't write “Thief engine, only the renderer itself).

Initially, I explored this idea as the imagined Holy Grail of Looking Glass: the development of a CRPG with open and closed spaces, since the company's recent CRPG ( Ultima Underworld 1 and 2 , System Shock 1 ) were “closed dungeons”. But we also had an open-space engine for Terra Nova , and besides, many of us felt the urge to create a hypothetical Underworld 3 or something like that with dungeons, open spaces, buildings, and everything like that (before Daggerfall ) . Trying to imagine how we can cut a hole in Terra Nova's landscape to add dungeons, I realized that portals can be used to seamlessly integrate several separate renderers (for example, open and closed renderers) by placing them on the borders of the spaces. Therefore, over the long Christmas holidays, I wrote a long document fixing my thoughts. I think then these thoughts swam in my head, and I knew that we had no idea how to realize the world without grids. Therefore, it seemed vital to try to write the entire renderer of closed spaces using portals. And for research, I created one implementation.

The idea of ​​portals and cells was used in Quake for a preliminary calculation of a calculated set of potentially visible objects (PVS, potentially visible set). She is also described in the thesis of Seth Teller. But in Thief, it was applied in real time: Thief calculated the portals and cells in advance, but did not calculate any additional information about visibility. (I believe that in Descent , which was released before the start of work on the Thief engine, this is implemented, but I do not know the details.)

The Thief engine kept a model of the level with open spaces (which the player could see and / or walk on them), divided into convex polyhedra, called "cells". Each cell contained zero and more visible "polygons of the world" along the cell borders, which were the visible surfaces of the world. Cells connected to other cells had special boundary polygons called “portals” that marked the proximity between the cells. If a player can see inside a given cell, then he can see inside neighboring cells through the portals in them.

To determine which part of the level is visible at the moment and requires rendering, the engine performed a search in the width of the portals and cells, starting from the point of view. After passing through all the portals, the reached cells were added to the list of visible. Each considered portal was transformed into 3D, then it was checked for its reverse side, if it was not reversed, it was projected in 2D. Then a "boundary octagon" was generated, consisting of ordinary two-dimensional bounding rectangles and a bounding rectangle rotated 45 degrees.

If the portal led from cell A to cell B, then we compared the boundary octagon of the portal with the boundary octagon of cell A. If they did not intersect, the portal was invisible. If it was visible, then the intersection of the octagon of cell A and the octagon of the portal was part of cell B, which is visible through this portal, so it was added to the list. (It was possible that the player sees inside cell B along different paths, so the engine had to accumulate all visible areas, which he did, simply keeping the conservative boundary octagon of all octagons of the incoming paths along the paths. If the player had two small visibility paths in one cell paths at opposite ends of the cell, the boundary octagon became much larger, usually the size of the entire cell.) The cell containing the viewpoint was always considered visible.

Quake calculated similar information in advance: for each cell, Quake kept a list of all other cells visible from any point in that cell. This result could be less accurate: for example, if you have a long corridor with many side rooms, and this corridor is a single cell, Quake will try to render all the side rooms, and Thief tries to render only the input cell to each of the side rooms (because these cells are always adjacent to the corridor and are visible), but Thief can cut off the rooms themselves if at the moment they are invisible.

A complete analysis of portals and cells does not scale well to a larger number of polygons, so by the time the last game was released on the engine, it slowed down (in other words, with hardware acceleration, it was very limited).

Reduce redrawing


Quake reduced redrawing using spacing buffers (a “list of faces”), in which the surfaces of the world were (conceptually) drawn front to back and each pixel was drawn only once.

Thief rendered the polygons back to front, so they redraw each other. The redrawing by the Thief engine was already smaller compared to the “naive” redrawing of Quake (according to a predetermined list of faces) due to the more rigid boundaries of the passage of the portals described above.

Thief further reduced redrawing, cutting off each rendered polygon of the world along the boundary octagon of the cell containing this polygon. (Boundary octagons were also used for this reason: in some cases they significantly reduced the redrawing compared to the usual bounding rectangles. In the limiting case, if we cut the polygons to an exact portal leading to each cell, this would also lead to a single pixel drawing .) I don’t remember that this approach was used in our typical redrawing.

For this to work, Thief had to store the polygons of the world in cells. This means that polygons of the world belonging to several cells must be separated. In Quake , it was often possible to save them as single polygons, because it stored polygons in the BSP tree directly on the dividing planes. But I don’t know how much this made a difference, because both Thief and Quake still had to separate polygons for caching surfaces.

2D trimming to the boundary octagon meant that many Thief polygons were rendered as 8-sided polygons as a result. This created problems. In fact, the trimming was clear and effective, because it was only 2D along simple axes, and there were no S, T texture coordinates on the vertices of the polygons (for example, U, V), because Thief used the technique I described in PC Game Programmer's Encyclopedia. According to this technique, texture coordinates were defined as the basis of 3D vectors independent of vertices, after which the coordinates of the textures for the intervals and pixels were calculated directly from the vector bases.

The sky (skybox) was drawn by marking the polygons with a special texture. When drawing, a transformed 2D polygon was taken and clipped for each sky polygon using a skybox texture overlay on each one or something similar. Now I do not remember exactly.

Object clipping


When moving objects along the level, the engine should track in which cells the object was located. It does this incrementally: using the cells in which the object was previously, the engine makes the current decision. This means that this part of the engine could actually process arbitrary unrealistic topologies (in which portals can lead to spaces not inconsistently, and fold space on themselves), because "physics" could perform such local displacement operations, but we never was able to implement such tricks. Later we began to use the BSP tree to sort the cells, which required their sequential definition. The Thief engine itself did not support them anyway. (Actually, we had a way to implement such things: before writing this engine, I created a Doom-style portal engine that supports spaces above the spaces. It allowed us to create overlapping rooms that could have different heights (I think Dark Forces used similar technology) or the same height (because in fact the height does not affect the operation of this engine in the style of Doom). With the help of my colleagues, I even created an editor for it, so was able to use it later levels in the new, fully three-dimensional vizhke. But we never had the opportunity to make a fully three dimensional levels with the same properties.)

After determining all the cells that should have been rendered, Thief decided which objects to render. Render needed only objects located in visible cells. However, Thief also computed the bounding 2D rectangle (or octagon) of each potentially visible object and compared it with the boundary octagon of all cells containing the object. (In Quake , nothing like this happened because he did not process the portals in real time.)

Since the Thief engine coped better with the complete rejection of invisible cells,
it could cut off objects that were currently invisible, although they were in visible cells. Thief could generally handle worlds more densely filled with objects than the renderer with Quake technology would allow. Maybe we could not display more objects on the screen, but they could be placed on levels, because the engine limited only the number of visible objects. Thanks to this, in Thief it was possible to create kitchens crowded with objects, dining tables and wardrobes.

But in fact, we were just lucky, because we did not select algorithms specifically for this purpose. The development of the engine was carried out much earlier than the features of game design Thief began to appear.

image

Sort objects


In Quake, objects were rendered by generating a z-buffer from the polygons of the world (even though the polygons of the world did not go through z-testing), followed by testing and updating this z-buffer when rendering objects.

In Thief, the z-buffer is not used. Instead, Thief rendered the world backwards ("the algorithm of the artist"), and interleaved the rendering of polygons of the world and objects, so that they correctly cut off each other.

In many games of the software rendering era (where z-buffers were rarely used), sorting problems arose: objects became visible through the walls or invisible in front of the walls. This should not happen, and portals / cells do not guarantee the absence of such errors. (For example, they appeared in Descent .)

It really upset me, and I worked hard to solve the problem. Thief 's sorting algorithm is the most sophisticated sorter by the artist algorithm I've ever seen. I remember that at some point I even had to write a little mathematical proof of its part.

I don’t remember the details, but I will try to tell in general about some of the problems and his work.

Initially, cells were sorted based on the order of passage of the detected portals, which ensured the sort order from front to back. It could be turned back for forward rendering. However, problems arose, and at the time of the release of Thief, the cells were actually sorted by the BSP tree. This meant that the sort order was very far from wide search. If the player was near the root dividing plane, the order of drawing could draw very close to the viewer cells to render very distant from the viewer, in the case when the cells and the viewer were on the corresponding sides of some sort of BSP separation plane.

Thanks to the BSP tree, there was no danger that the polygons of the world would be rendered in the wrong order, but there was a possibility that the objects could be sorted incorrectly relative to each other or the polygons of the world. To avoid this, the Thief engine (here I am not sure again) assigned numbers to the cells in the drawing order. The object in cell N usually had to be rendered immediately after drawing (inward) the polygons of the world in cell N and before drawing the walls of cell N + 1. However, sometimes objects belonged to several cells. An object in cells M and N, where M <N, should be drawn after all the walls of cell M, but its parts in cell M can be obscured by walls in cell N or in any of the cells between M and N. (A frequent example comes to mind: a corridor (cell) with a niche (cell) with a torch. The torch is slightly emitted into the corridor. The niche is M, and the corridor is N. For example, if the viewpoint is in the corridor, then the corridor is “closer” to the viewpoint in relation to rendering back to front. For example, one of the walls of the corridor may overlap portions of a niche.)

To cope with such difficulties, Thief made a decision whether it was acceptable to draw the given object in the nearest cell N (or, in essence, at any point of the drawing order between the farthest and nearest). To do this, calculate from the boundary octagons for objects and polygons and check whether the polygons overlap with objects. If the polygon of the world should be “ahead” of the object and the boundary octagons intersected, then it was not safe to move the object in the rendering order later than the polygon of the world.

The Thief engine decided in which range each object could be drawn between the farthest and nearest cells containing this object. If the object was only in one cell, it was always considered to be in this cell. Having determined this, Thief then tried to solve the problem of sorting objects relative to each other. Since an object can be in several cells, and be behind or in front of an object that is in one cell, objects in only one cell might in any case need to be shifted in the sort order forward or backward. In other words, the final range of cells in which the object could be considered for rendering at the end could be more than just the range from the furthest and nearest cells.

Thief use these ranges to search for the sort order of objects, keeping the cell sort order unchanged so that the objects and polygons of the world are sorted correctly. (It was here that I needed mathematical proof.) However, sometimes this was not possible. For example, in the above case of a torch in a niche, one polygon of the world in cell N could overlap with a torch (a wall with a niche on it, outstanding for a niche), but another polygon of the world could block a niche and part of a torch (a wall with a niche on it, giving out to the side viewer). In this case, there is no sorting order of objects and cells that would work, because parts of the torch overlap the cell, and parts of the same cell overlap the torch. This can be corrected by interleaving the polygons of the world from a cell with a torch, instead of constantly drawing all the polygons of the world from one cell as a whole. But not all cases can be corrected in this way, therefore in Thief such an approach was never used (he always painted all the polygons of the world from one cell as a whole).

Thief was a completely different, extremely costly mechanism that the engine relied on to solve difficult sorting problems. Each object could be “divided” into several parts, one per cell. In each cell, only the part of the object that was contained in this cell was rendered. This was done not by the actual separation of the object, but by multiple rendering, one for each “part”. Also, for each part, the object was dynamically trimmed with the “user trimming plane” using a technology similar to the trimming along the visibility pyramid (which was also done). In difficult cases, several trimming planes were required. However, this was only necessary for trimming to the portals between the cells in which the object was located, and the trimming was not performed literally in all the planes defining each cell. If this technique was used, then part of the object could always be drawn in the cell or after it. However, although this did not increase the number of computations per pixel, it did require multiple transformation and cropping of the object, so it was quite expensive. (Therefore, I advised designers to place torches completely in niches.) This was especially bad when rendering characters, because they had skinning. I think because of this, I had to perform skin transformations several times.

But there was another problem: if you have three cells arranged in such a way that the portals between them form the letter T, then dynamic cropping can create a T-shaped intersection on the border. In the future, they can lead to discontinuities in rendering.
I do not remember how we solved this problem, and whether we decided at all.

When developing, I noticed that if you use all the cell walls for custom clipping planes (not just portals), then if you intersect an object with a wall, it is clipped, which looks exactly like rendering with a z-buffer. Usually such artifacts arose from the fact that the physics of the game allowed objects to intersect for a short time, so it was better not to cut these objects so that they did not look (from most angles) passing through the walls. But we could use this effect to allow characters to move through walls, even though we didn't have a z-buffer.

After my positive experience with custom clipping planes, I was frustrated for a long time that graphics processors couldn’t effectively / comfortably support them. (Even in D3D9, they were still executed in a pixel shader, although you could use a z-buffer and a pattern to more accurately cut off and reduce the calculation of pixel shaders.)

image

Lighting and shadows


The basic technique of rendering polygons with superimposed textures and high-quality pre-calculated shadows is very similar (similar to?) Used in Quake . Thief used lighting maps and cache surfaces to store illuminated surfaces. I advise you to study this topic in the notes of Mike Abrash.

As I recall, this technique was added to the engine after we (the Thief development team) saw the release of “QTest” Quake . However, I doubt that at that moment there was already a Thief development team. According to Wikipedia, QTest was released on February 24, 1996. Terra Nova was released on March 5, 1996, so I think we’ve put together a final version of Terra Nova by the time QTest is released. But I do not remember that we already had a whole team. In fact, I’m not sure exactly when I created the original research version of this engine.

Objects emit rays to all (or N upper) light sources to determine their visibility. Depending on the visibility of the object from the source, all the lighting was turned on or off for it. So we simulated the entry or exit of objects from the shadows. But I do not know whether we released the game with this technique, or used a check of the lighting map on the floor. As I recall, we at least used such a check to determine the player’s visibility to the guards. Thanks to this, the players could not be confused by the places that externally looked in the shadows.

CSG model


Thief levels were created using constructive solid geometry (Constructive Solid Geometry, CSG) methods based on Quake knowledge. Technologically, however, the implementation of Thief was very different from Quake .

The CSG model in Thief was “temporal.” It is better to explain it on the example of the analogy with Photoshop. Photoshop has layers. Objects located on the same layer overlap objects in the lower levels, unless more interesting mixing models are used. In this case, the objects in the layers change the visible objects below them, but do not affect the objects above them.

From the point of view of algorithms, it is possible to perceive the generation of the final image as a sequential processing of each image layer from bottom to top with a combination of the current image. During the processing of each image, the current image contains the accumulated effects of all previous (lower) layers. After processing the layer, the current image changes and this layer no longer affects the current image, except for the data remaining in the current image.

Usually, we perceive the Photoshop layer model as a stack of 2D layers, but instead we can consider the above algorithm as a model, and think of layers not as a “vertical” stack, but as an ordinal sequence of operations to be performed. This is what I mean by the “time” model that was used in Thief for the CSG. (If the vertical stack of Photoshop layers is the vertical third dimension of two-dimensional images, then the Thief layer model will be the fourth dimension of 3D shapes, and it is not very efficient to perceive it as four-dimensional space.)

CSG Thief receives as input data sets of “operations” arranged in order of execution. These operations were “brush arrangements”, where the brush was a three-dimensional convex solid, as well as a characteristic of how the area covered by this solid body would be changed by the operation. The whole space is hard at first, so one brush operation was to “cut a hole in this area”, in other words, “change the area covered by this brush to free it”. For example, such an operation is used to cut a room. Another operation had a solid body, it could be used to place the column. Another operation added water. Another one is lava. Since space could be one of 4 types (solid, air, water, or lava — hey, these are 4 classic elements!), Each operation can be considered according to the type of output it created. In addition, we made these operations selective. For example, the “brush fill” turned air into water, but did not affect other types. This simplified filling the area with water - it was possible to create it completely from the air, and then fill the lower part with water. Thanks to the time aspect, it was then possible, if desired, to change part of the water to “air”. ( , — , — , ), , , , « » « ».

Photoshop, ó : . , ( «» ). , .

Quake , , . CSG . , á : => => . «» , , «» , ó ( Quake , . Thief, designers sometimes went on rather strange sequences of operations to create complex shapes).

Since the Quake levels were initially empty, there were invisible "external" surfaces in the game, which required a separate detection and removal process. If the level was not hermetic, then external surfaces could be reached. At the same time, automated tools could not remove them. In Thief, the levels were initially “hard,” so there was never a need for this. (I think the CSG in Unreal is also initially “solid.”)

image

CSG implementation


I had no idea what I was doing when I implemented the CSG Thief system (despite the opportunity to throw John Carmack questions), so I made a terrible choice. The Thief system divided the world into convex polyhedra and tracked the type of each polyhedron (air, solid, water, lava, which I called “environments”). For this, I kept the world as an immutable BSP-tree. BSP-tree classifies space as convex polyhedrons (except for faces, in which the world can have unlimited forms, expanding to infinity).

Using the BSP tree, I got a performance advantage, but I didn’t use it for that reason: in fact, I just couldn’t think of any other way to calculate the output. In this way, I could consistently add each brush to the BSP tree and apply medium transformation operations to each BSP sheet that contained the brush.

But if you examine the sources of QuakeYou can understand that there is another way: directly cross each brush with each other brush without a spatial data structure. With a neat implementation, you can create an increasing list of non-overlapping convex polyhedra. You can then add a spatial data structure to speed up the definition of those polyhedra that can overlap without affecting the calculations themselves.

, CSG- BSP- , , , , BSP , . , -. CSG -, . Thief , CSG . , «».

Thief CSG float double -. , , . , BSP-.

Epsilon problems were aggravated by how crazy I was building landfills and portals directly from the BSP tree. There were no T-shaped intersections; a common grid was calculated along each BSP split plane to ensure that the vertices on one side of the separation plane always have the corresponding vertex on the other side. At the same time, a much more complex set of invariants was introduced, which needed to be maintained and which also had epsilon problems. This meant that I didn’t need to write a T-shaped post-processing interceptor, as implemented in Quake , but now I see that such an approach would be better.

Perspective texturing


In Looking Glass games prior to Thief , such as System Shock 1 and Terra Nova , a “Z constant-line” converter was used for texturing.
In such games, perspective texturing was usually used for large nearby polygons, and for distant polygons - affine transformation.

In Thief , a specially designed perspective converter for all polygons was used. It used the trick used in Quake.: floating-point division for perspective correction for each N, which was performed in parallel with the imposition of textures on the next N. The trick worked because the Pentium could perform floating-point division "in the background" if only integer instructions were used after it.

In Thief, a perspective correction was performed for every 8 pixels (i.e., N was equal to 8). The Thief engine adjusted the correction so that it was performed for pixels whose X coordinate was a multiple of 8. The beginning and end of the pixel range could be less than 8, then a common N-pixel converter was called, but for 8-pixel ranges a procedure was designed to calculate exactly 8 pixels.

, . 256x256, , , 256. , , PCGPE (PC Games Programmers Encyclopedia). , , . , , , 256, . , , , .

x86-:

, , 8 :

adc ebx,edi add eax,ebp sbb edi,edi ;    v add esi,edx ;  u mov byte ptr _pt_buffer+1,al mov al,[ebx] mov edi,_pt_step_table[edi*4+4] 

Here is the code for the second pixel of the texture converter 256x256 with a scan for 8 pixels (it was not used in the release of the game, maybe it was used only for the surface of the water):

 add esi,edx mov ecx,ebx adc bl,dl add eax,ebp adc bh,dh mov al,[edi+ecx] mov byte ptr _pt_buffer+1,al and ebx,0babebeach 

The intervals in both code fragments show that they are optimized for the Pentium. Each of them uses nominal 4 cycles per pixel. (In addition, both of them lead to a “partial register idle” in the Pentium Pro.) The Pentium had a two-cycle simple “AGI”, in which it was impossible to use the register for two cycles after calculating it, so you can see a texture selection (from ebx or edi + ecx), designed to calculate these registers for two cycles before sampling. Full 8-pixel routines use pairs of Pentium 32 and 33 instructions for 8 pixels (although it is also possible to write the correct values ​​in the correct registers).

0babebeach at the end is a constant in the style of 0xdeadbeef, which, when updating a constant, relies on the “self-modifying” code. This is a standard trick for 486 and Pentium. This processor has the instruction “and ebx, memory_location”, but it is not single-cycle, but “and ebx, # immediate” - single-cycle. This meant that in the code it was necessary to change 9 fragments (this is a cycle of scans for 8 pixels, plus a cycle without scans), but the texture changed only after that.

Unfortunately, I completely missed the ability to relock the texture to improve cache utilization, which could significantly improve performance (but I'm not sure about that). I heard that in Build and other engines this process was performed, and since the cache of Thief surfaces consisted of 8x8 blocks, it was probably not so difficult to support such a process.

image

Texturing effects


Looking glass games usually required flexibility. Therefore, to ensure such flexibility, engines had to experience a combinatorial explosion of situations that had to be processed in various cases. The texture converters I wrote for Thief sacrificed a bit of performance for the sake of simplicity, ease of support and maximum flexibility. The texturing procedures described above resulted in a side branch that could lead to a branch making a decision on how to handle the current 8 pixels written to the temporary buffer.

At this point, one of the two types of functions could be called. The most frequent was the type that was written to the “frame buffer” (which was in fact the area of ​​RAM transferred later to the screen) and returned from the original function that caused the texture converter chain. Another type of function read the pixels from the temporary buffer, modified them and wrote them back into the temporary buffer, and then moved on to another function. Theoretically, it was possible to relate arbitrary numbers of these processing functions, but in practice this was not useful.

There were many such functions in the code base:


and several other predefined combinations, plus functions for generating Gouraud shading in a temporary lighting buffer. (And two versions of each type described above - one with an 8-pixel scan, one for arbitrary N pixels to handle the beginning and end of each pixel range.)

I think most of them were not used. In particular, the palette lighting required special adjustment of the palette, and I used it in the original research engine for a demo level with Guro shading, which worked on the Pentium 90 at a resolution of 640x400 with a frequency of about 20 frames per second. The level was a re-textured version of E1M1 from Doom 1, using the texture created by Kurt Bickenbach. We experimented with it a bit, trying to figure out how to make eight-bit textures look quality and not too alternating with palette lighting. We had rather pleasant results, but in the end the palette lighting turned out to be too limited, so we abandoned it. (It was very limited from an artistic point of view and could not display the black color correctly. When we came up with the idea of ​​a game related to shadows, such lighting obviously turned out to be completely wrong, but I think we abandoned it long before the first problems occurred.) Refusal of it also meant refusal to comply with the received frame rate and resolution, but in the long run it was not very important, because LGS games were quite complicated and 95% of the frame processing time was still not held in the renderer. In addition, the game was still far from over.

In the engine it was possible to specify an arbitrary color lookup table (color look-up table, “clut”) for each portal. She allowed to paint everything visible through the portal using this table. This process was performed by not applying the coloring of the surface of the portal in post-processing. Such an approach would require additional fill rates. Therefore, instead, the search tables were saved (and concatenated into new tables), and then applied when rendering surfaces visible through the portal. This effect could be used for force fields and the like, although as a result it was rarely used. (However, it should be noted that if one cell was visible through several routes, and these routes had different sequences of lookup tables, then there was no correct way to render it: Thief chose tables randomly. Perhaps this problem did not have a good solution, but since it never manifested itself, it didn't matter.)

When developing, this technique was used to create a “nebula” of water: the more underwater portals went away, the more opaque the water became. However, it looked awful, because the borders of the portals were clearly visible. And although we all got used to this, because the engine was such a few years, I eventually turned off the underwater "fog" from the portals, replacing it with a fixed "fog".

This effect was also used to color the surface of the water. An absolutely transparent texture was actually applied to the surface of the water, and the underwater surfaces were painted as they were being rendered. This made it possible to avoid the use of the read-modify-write process of a translucent texture. It turns out that object renderers also had to support rendering using search tables. I do not remember the details, because I did not work on them. But, in all likelihood, we planned to maintain them by default, so this did not pose any problems for renderers.

I do not know exactly what happened to the object, which was issued from the water. I think for it the dynamic user trimming plane was forcibly used. Since Quake was using a z-buffer, there at the same time there could not be at the same time the far submarine wall and the surface of the water (the z-buffer can store only one depth). Therefore, the water surface in Quake was opaque, at least in the software renderer. I’m not sure if this feature was added in Quake- derived engines, for example, as in Half-Life . One of the cost solutions is the following: render the whole world except water, then render objects, and finally render the transparent water through the z-buffer (almost like hardware). The effect of clear water with a z-buffer should have been much more expensive than in the Thief approach, although Thief spends more resources on the triangle / vertex because it has to render the object several times.

In Thief, most surfaces used a non-collapsing sampler with an arbitrary offset and a simple function of writing from storage to memory.

I think the object renderers used the general LGS software rendering libraries, so I didn’t use these converters at all.

image

Other


I did not describe all the graphics technologies used in Thief . Below I will list all that I have not written about. I associate each of their technician with the employee LGS, who created it, but not sure that these people were the only ones. Therefore, in order not to risk and miss any of those who contributed, I simply will not give names, because several people could participate in the work.

The 3D camera system, vertex transformation, and trimming were all part of a common technology library used in all LGS products. (Perhaps Thief became the first "user" of this technology - in the previous games a fixed view was used, so they could work on x86 computers without speeding up floating-point calculations.) Thief combined all the vertices used in one cell and transformed them simultaneously, regardless of whether it was visible polygons or portals. This was made possible thanks to the 3D vertex API, which made it possible to separate such elements. (Approximately like compiled arrays of vertices.)

As I said above, standard object rendering was also performed through shared libraries. Object polygons were sorted using a BSP tree. James Fleming (James
Fleming) wrote a very smart system that significantly reduced the number of decisions made in the BSP-node. The system used the fact that one-sided polygons often can not overlap each other from any angle. (For example, I think that a polygonal torus, which is a self-overlapping object and requires sorting, as if the polygons are two-sided, can be statistically sorted if it were made of one-sided polygons.)

More importantly, in Thief , skinning with skeletal animation was used to render characters, but I have never dealt with the code of this system.

I left Look Glass for a while. In my absence, support for hardware acceleration has been added. (I went back and added color lighting support to the generator and maybe a surface cache or something like that.)

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


All Articles