📜 ⬆️ ⬇️

xfcRS - original laconic, nimble rendering of smoothed tiles, "expansion fast cell - Rounded Squares"

image

xfcRS is a multifunctional fast algorithm, for a tile render with smooth transitions / to build an isosurface / to select an edge in a raster / for postprocessing as a pixel shader - for pixel art 8x8 scaling (for fast rasterization of fonts, other material for upscale'ing without modifications is not recommended) . The acronym 'eXpansion Fast Cell - Rounded Squares'

In this article we will consider it mainly in the context of the rendering of smoothed tiles:
')
If you have any questions about the terms, take a look here.
Thesaurus:
- Rendering - the process of building a frame on the screen. Render, respectively, the algorithm that does it.
- Raster - an array of points on the screen or image file texture. Rasterization is the process of building this array of points from some input data.
- Upscale - This is the name of the process and the algorithms implementing it, which are designed to scale the graphics specifically towards increasing size. The main focus in them is on combating the appearance of negative effects of image distortion, such as excessive graininess, blurring, etc.
- Pixelart - raster graphics, either by virtue of its long-term year of release, or specially imitating retro computers, not fighting for high raster resolution.
- Postprocessing - In general, an algorithm that works after some other algorithm that precedes its operation (the latter may in this case be called preprocessing).
- Shader - In general, a post-processing algorithm implemented for hardware performance on a video card.

- Tile - an element of the partitioning of space, in particular a piece of texture from the tileset.
- Taylset - all the tiles of a certain set, necessary for your algorithm, collected in one image.
- “Transitional tiles” - here, those 16 tiles of the quarters, of which the “Diffuse Circle” consists, on which two textures smoothly transfer one into the other. By combining which, choosing a quarter to each of the four corners, you can create all the options you need to seamlessly add the resulting image.
- Subcell - here it means one of the quarters of the cell, of which the algorithm is complete. Sabtile, respectively, a piece of the texture of this cell.

- Marshing Squares - the algorithm for generating isolines on a two-dimensional scalar field. see ru.wikipedia.org/wiki/Marching_squares
- Isolina - A conditional line that serves to approximate the exact position of the position.
- Approximation - The approximate value of something accurate.


Looking ahead, I will say straight away: this is not improved Marshing Squares,
although technology ...
The splitting into 16 variants of tiles is similar, but this is only a superficial resemblance, the algorithm was developed from scratch. If you look deeper, 16 quarters are used, and only for transitions. For integrally filled or completely empty tiles, a separate full-sized tile is used.

While the isosurface in Marshing Squares tends to maximally approximate the position of the line and visually change the volume of the original cells less, this algorithm tends to raise the minimum curvature of the lines, hence the name Rounded Squares.
With equal number of checks with Marshing Squares, it requires less input tileset (and therefore less work for artists), although in the worst case it can work up to 4 times slower due to output by quarters (in practice depends on the complexity of the cell topology and on the implementation of the drawing function tayl, there can be almost no difference).

Topologically, the algorithm uses the same lines, rotated 45 degrees from each other, but starts to postpone them not from 0 like Marsing Squares, but from 22.5 degrees (and this makes them symmetrical to each other with reflections, both horizontal and diagonal, which allows get all 16 quarters of transitions from just 2x, again less work for an artist if you have top-down or simply not oriented sprites.) In case you don’t even need smooth transitions (diffusion of textures), but only cell rounding is enough artist can be reduced up to the task "to draw a quarter-tile - a piece of the parabola" (which will automatically rebuild your avtokompilyatsiya).
In the presented example, the circle of transition texture tiles is turned inside out only by their permutation due to complete symmetry on 8 sides.


A very simple scheme for smoothing tiles is used. This is achieved due to the “Diffuse Circle”, parts of which are superimposed at the junction of different cells, or at the junction of any cell with an empty one. The “diffuse circle” should be drawn for all cell types and reflect in their texture a smooth transition into an empty cell.

That is, all the textures pass one into the other through the texture of an empty cell. This speeds up the work of the artist!
I want to note, for tile engines this is the norm!
Do not rush to judge if you do not like this restriction on the same overall texture for all. The fact is that if you want to support direct transitions of “all to all”, you create a quadratic dependence, and your tileset will grow as a multiplication table (each subsequent addition will require more and more work to reconcile the relationships - “every tile to all the others”). If even with human resources you have solved the problem, then at least at the program level, this will in any case load the occupied memory (tileset size) and the size of the data transfer (if the browser), or load the speed (if you dynamically consider diffusion);
- in another way, the diversity will be adapted by this algorithm in case of many common textures. In the general case, each type of surface can consist of any other type in a group (through a common texture) only once (i.e., a pair of types must uniquely identify a common texture). A special case is the texture loop. And it goes into a common AB, into which B also goes, which goes into a common BS, into which also C ... and so on (if necessary, the latter loops on the first one);
- It is also a good option to make a change in the general for all transitional texture only when moving to new levels. So it is cached at boot once. (In my opinion, this is the most effective way to apply this algorithm for creating simple 2D games).


The algorithm breaks the cells into quarters 2x2, each of them fills with a smaller oriented tile from the prepared set. But this is the result. In fact, xfcRS is a post-processing algorithm. And the viewer is an external addition. The xfcRS itself only collapses the cell map into an index table, which can be used in a wide variety of ways. What is this table, how is it built and how is it used later?

Here the article will give an example of a simple post-render. By “post” render, I mean that it is some kind of another passage in a view and does not display the entire map from scratch, but draws only additional sub-tiles where they are needed.

Let's not hurry, I will start an explanation in the order in which it was thought up:

1.Texture texture
We look at the "Topology", we see 16 pieces of tiles, from which a cell can be composed depending on its neighbors.
The simplest situation: all neighbors are equal (the cell is made up of edge pieces) can be remembered by analogy, - that the pieces stick to the neighbors
(Let it be the initial state from which we change).
If a neighbor is not equal, he pushes away a piece from himself to two positions.

When all the neighbors are checked and all pieces of the tile are repulsed accordingly, you can remove the cage with these four pieces.
(For efficiency, you can check for bulk conditions, for example, if the cell is completely intact or completely empty).

The first thing to think about is how to choose the formulation of the task of smoothing tiles so that it is implemented more easily, but without significant qualitative differences.
It is clear that “smoothing” regular cells visually, some will creep onto others. But not all "creeping" is equally minimal in terms of labor costs of the renderer.
You can see that you can round a cell in two ways - by crawling a common texture onto it, or vice versa - by pulling the cell out of its original boundaries. Two options give us little visually. We choose one - only the narrowing of the cells when meeting with strangers or with zero texture. It also means that we exclude interaction with diagonal cells, which is also useful for further optimization.

In this case, the transition contour is best taken close to the ideal circle (although this is not a limitation, but the closer to it, the smoother the rounding will seem). The ideal circle is accurately described around the cell — it passes through its corners (this is already a restriction for smooth transitions). Symmetry is also important, it should be bi-radial. This allows smoothing bends on thin sections. If your transition texture is not uniform - draw a torus (see the figure on the original texture, you can take it as a template. This application can be considered a creative commons license).


On the picture:
The black grid shows the original cells. The marginal area where smoothing cannot be calculated due to the lack of neighbors is shaded. Those. the width and height of the resulting grid is reduced by the cell. The remaining white grid is positioned indented in the half-cell - the resulting sub-cell indices will be written into it.

In the lower left corner the original transition texture is added, from which all the curves in the figure are gathered.

If you want to experiment with a curve, look at the picture on the left “topology of diffusion zones of textures”. The white line shows a logical contour. The black lines on either side of it are the permissible displacement ranges (with this, the transition points of the tile connections are pairwise left - right, upper - lower, must be equidistant from the center). For example, you can draw an ellipse, and get an isometric effect (tilting the camera view).

2.Two Checks - Four cells
A naive approach would be to bypass all sub-cells with a test for equality with the current cell of its 2 neighbors. The result of the check is the shift of the coordinates of the vibrated piece of the texture to the center (see fig.). Initially, all quarters are accepted as angular pieces of texture and form a blue circle when connecting (the neighbors are blue on all sides). Inequality with a neighbor vertically shifts the selected piece of texture to the center vertically (by two positions, so it follows the opposite angle in the vertical direction). Horizontal uses the same test and shift. The case when after checks nothing moved (there was a whole blue circle) it is desirable to process as an exception and to display the whole cell at once and not in quarters.
Here the naive method ends.
(further in the code, the initial position of the tiles will have the constant 0xfc30. And the shifts along the horizontals / verticals will work with the corresponding HEX bits of the desired cells of the array)
It is important to note that any verification of the equality of 2 cells in fact always affects 4 sub-cells, and 2 already 8 respectively! So let's get this eightfold acceleration (from the method to the forehead) and process them in bulk. see figure


Here is the core of the algorithm (JS example):
function XFCR(map, w, h){ // "eXpansion Fast Cell - Rounded Squares" for(var xfc = [], n = 0, C = map[n - 1], D; n < w * h; xfc[n++ - w - 1] ^= 0xfc30, C = D){ if(C ^ (D = map[n])) xfc[n - w - 1] |= 0x8800, xfc[n - 1] |= 0x0088; if(D ^ map[n — w]) xfc[n - w] |= 0x202, xfc[n - w - 1] |= 0x2020; } return xfc; } 


So we will bypass the desired cells instead of each sub-cell, just in the resulting array we will add the changes in place. We also note that the combination of presence / absence of neighbors can be perceived as 2 independent factors (the presence of a neighbor on top shifts the texture in x, the neighbor on the left in y). And since the factors are independent, there is no need to rest on synchronicity, you can handle them asynchronously. By the way, xfcRS generally perfectly parallelized!
When finding factors, we simply put down the appropriate X / igriki in the resulting array (all this is cunning, but, if you look, it is very conveniently stored in the index bits).
Comment, why did you need to pack values ​​in general
- when “memory is not so important now”, I will immediately answer that in this case memory compression leads to acceleration. Cell instructions are processed and queried several cells at a time. Also, when rendering, to quickly determine the need to process a cell in quarters, it is enough to compare the value of the indices at 0xfc30 - this constant indicates that the environment of this cell is uniform and can be withdrawn in bulk, without breaking it up (without packaging, all 4 sub cells would have to be checked separately );
- in fact, the binary result format taken contains a double excess of information (on the other hand, each HEX bit stores a ready offset index for a tile. In this case, it still turns out to store the coordinates of 4 cell shifts in 2 bytes). If you suddenly need to run it on very large volumes, you can store only 2 bits per sub-cell. We'll have to make the "magic" constant 0xfc30 in the rendering logic and take into account the positioning of the sub-cells, but all 4 will occupy the entire byte. And if you go for extraordinary tricks, you can save only the values ​​of the conditions and spend exactly half of the bits on each sub-cell =), although the essence of the xfcRS algorithm will already be unwound to zero, because its essence can be quickly compared with its neighbors, giving a convenient index card. (By the way, in C, for example, it would be possible to pack an array of bytes (for each sub-cell) by hardware allowing it in Int32. That would be stronger to lose from memory, but to win in speed and convenience).


-! IMPORTANT! For the same reasons, the zone for which xfcRS gives the resulting array is MOVED by half a cell (one sub-cell). And the fact that it is impossible to calculate the edge cells is not the main thing. The main thing is that by moving (and rearranging) the indices accordingly, we get blocks that are convenient for checking and quickly determining completely filled 2x2 sub-cells. - example: if a 2x2 block of the desired cells is present on the incoming map, as a result it will be smoothed along the contour by half of the cell, and there will be one whole cell in the middle (half-shifted of the cell), so you want to easily determine it from the index. The definition of empty cells is not required, because (according to the definition of a pass-through algorithm) they should already be drawn by a standard tile algorithm. The render code from the example just skips them.
Sample post render:
 function draw(map, w, h){// View: //(multipass - empty cell background pattern needed) var sub = XFCR(map, w, h); // Compute sub-tiles by XFCR algorithm for (var n = 0, y = 4, j = h; --j; y += 8, n++){ for (var x = 4, S, i = w; --i; x += 8, n++){ if((S = sub[n]) ^ 0xfc30){ //go sub-tiles 4x4 x4: if(map[n]) ctx.drawImage(fuse, S << 2 & 12, S & 12, 4, 4, x, y, 4, 4); if(map[n+1]) ctx.drawImage(fuse, S >> 2 & 12, S >> 4 & 12, 4, 4, x + 4, y, 4, 4); if(map[n+w]) ctx.drawImage(fuse, S >> 6 & 12, S >> 8 & 12, 4, 4, x, y + 4, 4, 4); if(map[n+w+1]) ctx.drawImage(fuse, S >> 10 & 12, S >> 12 & 12, 4, 4, x + 4, y + 4, 4, 4); }else if(map[n]) ctx.drawImage(full, x, y); //full sub-tile block 4x4 }; }; } 


With reference to JS, it is enough to set the blank cell pattern on the background, and move the output to the canvas to the sub-cell. Additionally, I recommend to predict adaptive css scaling. I do this for the 320x192 canvas by stretching it through CSS by 100% in this example:
 var ctx = cnv.getContext('2d'); ctx.drawImage(habr, 0, 0); //draw text var w = cnv.width >> 3, h = cnv.height >> 3, z = cnv.width / window.innerWidth, //calc dynamic scale - ratio map = ctx.getImageData(0, 0, w, h).data.filter( (x, i) => !(i & 3) ).map(x => x >> 7); //convert pixel data to map cnv.style.width = '100%'; cnv.style.backgroundSize = (8/z)+'px '+(8/z)+'px'; //dynamic scale - background pattern 

I will not give HTML here, I will say that fuse is an IMG 16x16 with a diffuse tileset; full - 8x8 full (blue texture) cell; habr - pixel art inscription, which is converted pixel by pixel into a map; body margin reset to 0, background canvas - empty (beige) cell with a tiling.


3. Implementation Comments
For the complete source code, see github.com/impfromliga/xfcRS
When implementing it is important to take into account that the returned date goes for sub-cells (which are grouped 2x2 between the available ones), since there is no possibility to calculate the value of the edge (there are not enough neighbors) - the size of the returned field will be less per cell (half cells from each edge).
As you handle it, xfcRS does not concern. The edge can be drawn without sub-cells by itself, but if the map involves scrolling, on the edge of the screen the terrain will visually “tremble” (when there are influencing neighbors). In this case, it is recommended to cut the output to half cells from the source data or to give xfcRS per cell more input data in advance.

A group of subbolic 2x2 is packed into one value in the order of big-endian, i.e. Values ​​of cells
AB
CD
- are in bits numbers starting from the lowest to the highest. And the index value = ((D * 16 + C) * 16 + B) * 16 + A
so on there is one hexadecimal digit per cell (it contains the index of the desired blur tile).

4. Expansions / Additions
- Since In general, xfcRS does not say anything about rendering, but only gives an indexed table of boundaries, it is possible to add additional data to this table - this can be useful for laying “paths” inside large homogeneous areas. These paths, obtained from an external data source, will not be blocked when the surrounding cells change. It is also interesting that being “laid” initially on empty space, the paths will not be visible. And to find them, you will need to sprinkle them with flour (ashes, choose a game designer to taste).
- xfcRS can be modified to handle the local area (in this case, again, you have to imagine how the trimmed and smoothed sub-cells are related to the required ones).
This will allow highly efficient redrawing of local map changes.
As part of the article, I did not do it, because I fought for simplicity. In addition, the algorithm is so easy that it works quite well, redrawing the entire screen.
- The expansion of the algorithm to fractal without modifications does not bring growth results. Expansion is possible if the diagonal cell is processed if two equalities of the neighbors coincide at the same time (set it to ¾ blurred). Although the textures are not designed for such a scheme, let them work on the old (just approximating the values ​​when checking cells), but it is necessary to expand the stored values ​​of empty / non-empty cells to some ranges. The last pass can always use the original algorithm, since on the last pass, in any case, it is necessary to reduce the fractional coefficients to the whole form, understood by the renderer.

- Being used as a scaling pixel filter, the algorithm increases the original pixel map * 8x8, the initial tileset size makes IT only 16x24px (textures are oriented! Without orientation / diffusion, and even less)

- The algorithm can be used to identify the edges of raster objects (by adding the function of comparing pixels for a range)
- (applied to JS) computed edge will help to group the operations of drawing a set of patterns on the canvas.


5. Combinations:
- When implementing scrolling, xfcRS is easily placed on the 2D buffer that I disassembled in the previous article. => habrahabr.ru/post/280830
- In addition, as you have already seen from the image for the post - it draws wonderful original fonts (which can be styled raster).

6.For history
I was born (not without difficulty, but only because it was more pleasant), I liked the algorithm so much that I decided to name it loudly.
I began to look for an abbreviation, I wanted something similar to the algorithms of either FXAA –analytical, or xBR or EPX - scaling pixel art graphics (by the way, after a recent inspection of the latter, it was found out that it has some quite tangible similarity with mine).
So came to the two options "eXpansion Fast Cell Rounder" or simply "Rounded Squares", as a result, began to lean more towards the latter.
But you know what else struck me? As a result, in the code, after the name, I came to the constant 0xfc30.
(I'm sure everyone will understand and support me as I am an IT-Nerd, even if it’s a single name, good for SEO, without hyphens, but in this case the two cannot be avoided!)

Another gift of fate was the earlier discovery of an interesting pattern on the transition texture:
- It displays a beautiful ball of texture (just like in the 3D editors materials)
- The fact is that pixel by pixel this ball can be turned inside out using only a permutation of 4x4 tiles. Those. no matter what the texture basis, they can be flipped.

In fact, xfcRS did not turn out so slim right away - in the old tests, for example, I didn’t see the remarkable layout of the tileset, in which the smoothness of the addition is simply visually checked.

By the way, there is another aesthetic task for the layout, although it is not a task, rather a question that bothers me. In view of such an unusually remarkable opportunity to turn the circle inside out, is there any more concise way of storing textures ... any ideas?

Well, for dessert:
the simplest code for dynamic editing (with mobile chrome support)
 onload = window.ontouchstart = onmousedown = function(e){ //Controll: e = e || window.event; e.preventDefault ? e.preventDefault() : e.returnValue = false; var E = e.touches ? e.touches[0] : e, n = E.pageX * z / 8 + w * (E.pageY * z / 8 | 0) | 0; map[n] = !map[n]; ctx.clearRect(0, 0, cnv.width, cnv.height); draw(map, w, h); } 


Screenshots:







and live demo codepen.io/impfromliga/pen/qNOazj

That's all. I wanted to write articles more often, but firstly, the conscience does not allow to lay out something not worked through, and secondly, a clear description takes more time than the implementation itself, but not always it is enough ...

Comments, criticism, suggestions, thanks in advance for any constructive!

And by the way, time tends to be the faster, the more people are kicking you, and you are interested in your findings.

If you have read to here, Thank you very much for your attention!
And see you soon.

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


All Articles