Last server records
Pro Nub

Pixelwalk physics

Posted by Kpoluk 5 Aug 2023 in 16:04
We've learned the basic concepts concerning the structure of map, so now we can begin to analyze specific phenomena, and the first one is pixelwalk. From practice we know that this technique is somewhat reminiscent of a slide performed at the junction of a regular brush and a brush entity (or at the junction of two brush entities). However, firstly not every such junction is suitable for pixelwalking and secondly you need to fall onto such a junction from a certain height. The easiest way to do this is to perform a doubleduck followed by duck from the same height as the desired joint. In this case, we will first be thrown 18 units up, and then the crouched player model will gradually descend 36 units down, exactly on the height of the joint. For the article, I created a map project hull_pixelcheck.jmf (archive with files can be downloaded here), from which I compiled the map hull_pixelwalk.bsp. It is a default room created by the J.A.C.K. map editor, where I placed two brushes, turning the top one into a func_wall. From the bottom brush you can do a doubleduck followed by duck on the joint on the left and “slide” with D held along the joint:


At the same time, the right joint between the brushes does not allow pixelwalking. Let's try to figure out why this happens.

As we saw earlier, every frame CS engine calculates the new position of the player and calls the function PM_PlayerTrace , which actually checks if the line connecting the old and new positions intersects any of the planes of the surrounding brushes. To do this, it needs to iterate through all the models on the map (in the code the models are designated as physents):


You can clearly see how the resulting trace changes its end point if it intersects a closer plane. The search for this point for a specific model occurs in a more sophisticated way, with a recursive pass through clip nodes. Let's look at this process step by step, looking inside PM_RecursiveHullCheck (code is cut down cause for simplicity we examine planes with normals directed along the basic axes):


The points p1 and p2 (the beginning and end of the trace) can lie either on the same side of the plane of the corresponding clipnode or on different ones. We find out this using the signs of t1 and t2, the modules of which are actually equal to the distance from p1 and p2 to the plane. If both points are in a half-space with a positive normal direction, then we take the left child clipnode and go to the next step of the loop. If both points lie on the other side of the plane, then take the right child clipnode for the next step. If the signs of t1 and t2 are different, then we need to find the coordinates of the point [/code]mid[/code] of intersection with the plane:


Note that this point does not lie on the plane itself. If p1 is spaced from the plane by more than DIST_EPSILON = 0.03125 (hereinafter, to be short, we will call this value eps), then the corresponding coordinate of the point mid is selected exactly at a distance eps from the plane. If p1 is closer, then the coordinate of p1 itself is selected as the mid coordinate:


Why do we need this eps indentation? The answer lies in the next step:


Let’s say the point p1 was in a half-space with a positive normal direction (side = 0), then PM_RecursiveHullCheck is called for the left child clipnode, and the trace goes from p1 to mid. In other words, we are trying to find an intersection point in this half-space that is closer to p1 by treating everything below the child clipnode as a separate subtree. If we didn’t find anything interesting in the left subtree, we move on to the right child clipnode. However, before going into the right subtree, we call the PM_HullPointContents function for the point mid, which goes through this subtree in order to find out whether the point mid lies outside of the model for any of its planes. This is where we need an indentation of eps, cause without it we simply would not have been able to figure this out. If the point mid for at least one plane of the right subtree lies outside the model, then we go to the next step of the cycle, selecting the right child clipnode and replacing p1 with mid (if there are no intersections with planes in the right subtree, then the point mid will not become the end point of the trace, that is, a collision with the model did not occur). If mid lies inside, then there is no sence in going into the right subtree, and we finally move on to filling out the information on the trace by storing there the last intersected plane and the point mid as the end point (preliminarily using PM_HullPointContents for the entire clip-tree as a safe check to see if the point mid lies inside the model).

At first glance, such a recursive pass through the clip tree gives quite correct results, but the indentation on eps hides several insidious peculiarities, one of which makes pixelwalk possible. Let's return to our map hull_pixelwalk.bsp and schematically depict a cross-sectional view of the func_wall and the lower brush along with its clipnodes for a sitting player (the planes of the clipnodes in the picture limit the orange area, and the eps-area is shown in pink):


When we stand on the bottom brush, thanks to PM_RecursiveHullCheck there is a non-zero gap of eps or less between the player model and the brush. By performing a doubleduck we fall down in duck to exactly the same height above the level of the top plane of the brush (and not exactly to the level of the junction of the brush and func_wall, as we thought at the beginning of this article). At the same time, by clinging to the side plane of func_wall during the fall, we also maintain a gap of eps or less between the player model and func_wall. When we pass the junction, our center will be at the intersection of the eps-areas, that is, in the red square:


Here we took point p1 exactly at a distance eps from both planes (this is the most common case), and point p2, where we want to be, lies lower and to the right on the other side of the planes, since we fall and at the same time hold down the D key. Based on the downloaded map data (see hull_pixelwalk.txt in the archive with the project files), we build a cliptree:


The trace from p1 to p2 intersects the planes of clipnodes #24 and #27, at that PM_RecursiveHullCheck when passing through the cliptree first encounter a clipnode #24, receive the point mid (it coincides with p1), and then reach the clipnode #27 (the point p1 will again be taken as mid). As a result, the horizontal plane of clipnode #27 will act as the collision plane only because it was encountered later when traversing the cliptree. The vertical velocity will be zeroed, and only the component along the X axis will remain (func_wall does not allow us to move along Y), so our movement will look like sliding along the joint. If we try to make a pixelwalk on the right side, then the trace there will affect clipnodes #27 and #29, and the function will get to #29 later, which means here the result of the trace is the vertical plane, hence we won’t be able to do any pixelwalk.

Another important condition for pixelwalk here was that the top brush was turned into a func_wall, not the bottom one. In PM_PlayerTrace, the first model in the cycle was the room itself along with the lower brush, and the total trace gave it a fraction equal to zero. The func_wall trace that followed it also gave zero fraction, but the resulting trace was not updated, and the collision plane remained horizontal. Thus, pixelwalk is only possible if the index of the lower model at the junction is less than that of the upper one. That is, there can be a regular brush below, and a brush entity on top, or both models can be brush entities, but the lower one should have a smaller index.

It would seem that the secret has been revealed, but that’s not all yet. Firstly the order of the corresponding clipnodes in the tree is not necessarily the same for standing and sitting player models, and secondly the order within each tree can change with each map compilation. For example, having compiled the same project hull_pixelcheck.jmf again, I got another map hull_nopixelwalk.bsp (see archive with files), where pixelwalk can be done neither on the right nor on the left sides.


Fall height


Let's determine the fall heights suiting for a pixelwalk (we are also talking about a fall after a doubleduck or some specific bhop). We have already made a similar calculation of heights for JumpBug, but this time it should be taken into account that at the moment of the trace the speed has already decreased by 4 units/s in the function PM_AddCorrectGravity:

h = 0.01 * 4 + 0.01 * 12 + 0.01 * 20 + ... + 0.01 * (4 + (N - 1) * 8) = 0.04 * N^2 = (N / 5)^ 2

To get into the eps-area, you need to get the integer height h, and to do this it is enough to count the number of frames N multiple of 5. For example, after 30 frames we will go down by h = 36 units, which is exactly what we get after the doubleduck followed by duck we did on our test map.

Finally let's highlight one more important nuance. You may have noticed that on some maps pixelwalk is very unstable. The fact is that when calculating the end point of the trace, the PM_FlyMove function uses the [ifloat[/i] variable type, and its accuracy is approximately 7 significant digits. This means that if, for example, during the fall we wanted to drop to a height of 123.03125 units, then due to an float error we may end up at a height of 123.031265 units. It would seem like a small difference, but we no longer get into the eps-area, hence we can’t do a pixelwalk. And after a minute of attempts, the calculation may turn out to be correct, and pixelwalk will work. Here is another randomness in an already non-obvious technique.

We've figured out pixelwalk, but in the next article the eps indentation will continue to bring surprises.