Tomb Raider II Playstation Waving Inventory Algorithm
Hello! Some of you know that I have been working for a long time on a project, related with the decompilation, bugfixing and software improvement of the first games of the Tomb Raider series, mainly TR2 and TR3. Several people even accused me of not sharing the technical details of my work with the community. Well, in my defense, I can say that I spend almost all my free time on the project itself, and most of the knowledge received and still rapidly arriving is in the form of separate drafts that have not been properly drawn up for use by anyone else. It's just pointless to give out incomprehensible material, 50% consisting of wild pseudocode, 25% of the assembler and 25% of the notes that are clear to me alone.
Nevertheless, I plan to write and submit to you articles with the most interesting in my opinion details in the jungle of the program code of the Tomb Raider games. Here is the first such article, and it describes the animated wallpaper of the PlayStation TR2 inventory. It takes time to complete the documentation, but time is not always there. The preparation of this article and the project itself for the release into open source took two weeks. All these two weeks I did not deal with the main programming tasks (except, of course, my fulltime work in the office, for which I get wages). But sooner or later, it was necessary to do this.
Update: Beta test is started. You can inspect the code and try the patch right now.
Before we begin, I want to say that all of the below that I discovered in the PlayStation game code I've implemented for PC too. Version of the game itself with the necessary improvements is in active development. Though it is in closed beta testing now, but will soon be available for everyone. Before this happens, I need to complete my TR2Draw dynamic library, which will allow to port the game to native DirectX 9 code (or any other API later, since the DLL is opensource). At last we could throw away DirectX 5 and the wrapper crutches (Yuk, I fiercely hate these workarounds!). The source code of a small part of this TR2Draw library, related to the animated wallpaper of the inventory, is already presented on GitHub.
There is a video on YouTube comparing the original implementation of wallpaper on the PlayStation, my implementation for the PC with the original textures, as well as one with the HD textures (also remastered by me).
If this article is TLDR for you, you can just click through links, videos, images and animations.
For everyone else, here we go!
Behind the scenes, the Playstation TR2 generates an 13x9 array of vertices located in 2D plane. These vertices form 12x8 grid of equal textured squares. The center vertex is anchored to the center of the screen. The resulting grid of squares is scaled so that the height of the screen is equal to 4.5 of the height of a square. You can see bright red frame on the image below - this is game screen limits.
Every such vertex has 3 parameters:
• X screen coordinate
• Y screen coordinate
Coordinates can be both within the boundaries of the screen, and outside. As you see there is no Z coordinate, this mesh is totally flat. Color is always one of the shades of gray. When mixed with the colors of the texture, the color of the vertex determines the brightness of the texture. So next we will call this parameter as brightness, or amount of light. If brightness is 0% the texture becomes black, and if it is 100% the texture has original color.
All vertices receive a base 50% brightness (128/255), except for the farther ones (all of them are outside visible screen by the way). The farthermost vertices receive a 25% brightness (64/255), and the neighboring farther vertices receive 37.5% (96/255). Any square gets a gradient brightness depending on the brightness of each of its vertices. I do not know why the developers implemented a separate lighting scheme for these farther vertices, because this shade effect is actually not noticeable within the game screen at all. In my implementation for the PC, I did not obscure the vertices far beyond the screen.
Then the game initializes three angular variables that will control all subsequent animation. There is one small, but important detail. For most trigonometric calculations, Tomb Raider uses short integer (2 bytes) representations of angles, as well as the results of various functions such as the sine and cosine. This is in some way convenient, and also such calculations work much faster than full-fledged trigonometric floating-point operations. For all angle values we will use hexadecimal integer representation in this article.
• 0x0000 -> 0 degrees
• 0x2000 -> 45 degrees
• 0x4000 -> 90 degrees
• 0x6000 -> 135 degrees
• 0x8000 -> 180 degrees
• 0xA000 -> 225 degrees
• 0xC000 -> 270 degrees
• 0xE000 -> 315 degrees
• 0x0000 -> 360 degrees
When this hexadecimal value overflows through 0xFFFF it becomes 0x0000 and vice versa (0 degrees = 360 degrees).
Well so, the game initializes three angle variables.
The game updates these variables every frame while you're in the inventory:
• deformWavePhase and shortWavePhase decreases by 0x0267 (3.92 degrees)
• longWavePhase decreases by 0x0200 (2.81 degrees)
We will need them in the next stages.
Each vertex of the grid mesh, which we prepared in the last stage, must be shifted from the base position to a distance of 10% of the grid square width before the rendering. The direction of the shift is given by the individual angular phase of each vertex, which depends on the common phase (common phase is stored in deformWavePhase variable). In fact, each vertex spins in a circle around its base position with a radius equal to 10% of the grid square width. Cosines of its phase is X offset, sines is Y offset.
The extreme upper left vertex gets the common angular phase. The right vertex next to it gets a phase increased by 0x3000 (67.5 degrees), the lower vertex next to it gets a phase increased by 0x33E7 (73 degrees). And so each of the points gets its individual phase, calculated from points to the left and above. With every new frame, the phase changes, and all points synchronously rotate counter-clockwise by the amount of the common phase value change.
Schematically the simplified deformation is shown in the figure below. To shift the phase down and to the left, a value of 60 degrees is taken. Thus, in this simplified scheme, vertices are arranged at the corners of synchronously rotating hexagons (as you know the angle between the corners of the hexagon is 60 degrees).
This animated wallpaper is completely flat but it looks three-dimensional. This illusion is created by a special lighting formula that, like a deformation, spreads from the vertex to the vertex beginning from the top left corner.
The lighting of the vertices depends on two waves. One is short but slower, the second is long but faster. Each uses its common angular phase: shortWavePhase and longWavePhase. We need to get sines of these phases to get brightness values. As you remember, the base brightness of each vertex was 50% (128/255). In the highest position, each wave increases the base vertex brightness by 12.5% of the maximum (+32/255). At the lowest position, the wave reduces the brightness by 12.5% (-32/255). Two waves are summed up and, in some cases, the brightness value is changed to 25% (64/255) in the higher or lower side. So the resultant vertex brightness varies from 25% (64/255) to 75% (192/255).
Like for deformation, there are phase shifts from top left corner of the grid.
Short wave horizontal and vertical shifts are exactly the same as deformation shifts: 0x3000 (67.5 degrees) and 0x33E7 (73 degrees).
Long wave horizontal and vertical shifts have individual values: 0x1822 (33.94 degrees) and 0x1422 (28.31 degrees).
These different shifts give us interesting two dimensional wave interference effects, because we have different wave lengths, different wave velocities, and even little different wave 2D directions. By the way you can read on Wikipedia about wave interference.
You can see the animated lighting scheme on the link below. For clarity, a pure red, undeformed, untextured sheet is used. For this demonstration, I wrote several chart functions, executed right in the game.
THERE IS EVEN MORE TO COME!
This is only one feature, which I tried to describe as clearly and briefly as possible. Maybe I missed something. In any case, I still have a whole warehouse for such improvements and even more bugfixes, and they are waiting for their time to be published. I propose to discuss these in this thread on tombraiderforums.com.
=> Return to Articles & Interviews <=