(2023-04-28) Giving ASCII art the third dimension ------------------------------------------------- I usually try not to remember my school years, but some cool things definitely could be seen in that time. And one of them were stereograms, or, to put it more strictly, autostereograms. They were on the backs of some notebooks and day books, and to those schoolchildren who really could see them, they looked like magic or, to more tech-savvy ones like me, like holograms I had only read about in some old encyclopedias or seen in the hologram museum (yes, we did have one in our country). I had always been wondering, how does one draw such images? Little did I know back then that these graphical autostereograms (called SIRDS, single-image random dot stereograms, in case of randomized dots, or just SIS in case of hand-crafted image patterns) were preceded by SIRTS, single image random TEXT stereograms. I guess if I knew that it was possible to generate 3D images out of pure text abracadabra when I was a schoolboy, I... wouldn't have made a lot of silly choices in my life that I did. Anyway, now I understand the principle behind stereograms is very simple and based on how our sight and brain perceive objects at different distance, and there also is a ton of software generating both text and pixel 3D images out of depth maps and some other parameters, but it's baffling how few resources actually explain what's going on and how to implement the same effect from scratch. The best one of the few, called "SIRDS FAQ" ([1]), contains probably the fullest explanation of the entire phenomenon and even has some code and pseudocode examples of the algorithm. Still, the best way to understand the algorithm is to code it up yourself, so I've created my own SIRTS implementation in AWK called Textereo and published it on the main hoi.st page as usual. It doesn't use bitwise operations or any other non-standard extensions so should work on any AWK variant, but I only tested it on GAWK and Busybox. As an input, this script accepts a map file in the following format: * line 1: space-separated (desired) image width and pattern length > 8 chars * line 2: entire alphabet of characters to build the image from * depth map lines: either empty or a digit sequence from 0 to 7 Textereo follows the standard depth map convention that 0 is background and 7 is the highest level of embossment and visually appears the closest to the viewer. Normally though, SIRTS images don't contain depth levels higher than 3. The width is an important parameter that defines how your map will be positioned. All lines that have fewer depth digits than the width value are centered to fit the width from the first line. If the line is empty or contains only zeroes (fewer than the width), it's fully filled with zeroes. This allows to adjust not only width but the height of the background canvas. The second number of the first line defines the base pattern length, and I'll get to this parameter shortly. Here's an example of the 78x27 3D map in Textereo format using two depth levels and 10-character base patterns: 78 10 abcdefghijklmnopqrstuvwxyz@/0123456789$%!#ABCDEFGHIJKLMNOPQRSTUVWXY 0000000000000000000000000000000000000000000000000 0000000000000000000011111111111111111111111111100 0000000000000000000011111111111111111111111111100 0000011111111000000011111222222222221111111111100 0000111111111110000011111222222222222211111111100 0001111110111111000011111222222222222222111111100 0011111000011111100011111112222212222222221111100 0011111000000111100011111112222211111222221111100 0000000000001111100011111112222211111122221111100 0000001111111111000011111112222211111122222111100 0000001111111111000011111112222211111122222111100 0000001111111111000011111112222211111122222111100 0000000000001111100011111112222211111222221111100 0011111000000111100011111112222211111222221111100 0011111000011111100011111112222211122222211111100 0001111111111111000011111222222212222222111111100 0000111111111100000011111222222222222211111111100 0000011111111000000011111222222222222111111111100 0000000000000000000011111111111111111111111111100 0000000000000000000011111111111111111111111111100 0000000000000000000000000000000000000000000000000 Note the blank lines before and after the digits. They allow to tweak the height parameter to make sure the image is easier to visually perceive. And here is an example of what Textereo generates out of this map (sorry mobile users, you need to have 78-char width to see this properly): Uc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQtbQUc5rWnQt 3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov0I3vB@n@ov IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwBu9IF$fwYwB EPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhFWlEPI7KrhF 2@UK3A7OaB2@UK3A7OaB2@UK3A7OaB2@UK37OaB2@UK37OaB2@UK37OaB2@UK317OaB2@UK317OaB2 #uEyQB%R7V#uEyQB%R7V#uEyQB%R7V#uEyQ%R7V#uEyQ%R7V#uEyQ%R7V#uEyQ6%R7V#uEyQ6%R7V# pcdoCJuBxgpcdoCJuBxgcdoCJuBxjgcdoCJBxjgcoCJBxjgcoCJGBxjgcoCJGBXxjgcoCJGBXxjgco DzgMn#irjgDzgMn#irjDzgMn#irjDz@gMn#rjDz@Mn#rjDz@Mn#rjyDz@Mn#rjtyDz@Mn#rjtyDz@M D!%3isSRtHD!%3isSRHD!%3ieSRHD!%53ieRHD!%3ieRHD!%3ieRHD!6%3ieRHyD!6%3ieRHyD!6%3 !MxGt/zF2N!MxGt/z2N!MxYGt/2N!MxYSGt2N!MxYSt2N!MXYSt2N!MXYbSt2NH!MXYbSt2NH!MXYb %@ks3rLM$3%@ks3rL$3%@k8s3rL$%@k8ds3L$%@k8d3L$%@Rk8d3$%@RkO8d3$v%@RkO8d3$v%@RkO Omq/ksansaOmq/ksansaOmq/ksasaOmqN/kasaOmqNkasaOJmqNkaaOJmhqNka7aOJmhqNka7aOJmh xTbt9btvwoxTbt9btvwoxbt9btvwoxbpt9bvwoxbptbvwox%bptbvox%bputbv@ox%bputbv@ox%bp kUCI4e18!skUCI4e18!skCI4e18!skCMI4e8!skCMIe8!skfCMIe8skfCMqIe8oskfCMqIe8oskfCM rg6nETnT3Trg6nETnT3Tr6nETnT3Tr6%nETT3Tr6%nTT3Trt6%nTTTrt6%HnTTyTrt6%HnTTyTrt6% c!bLbTPzqic!bLbTPzqic!bLbTPqic!bfLbPqic!bfbPqicY!bfbqicY!hbfbq3icY!hbfbq3icY!h jXsOphd$RyjXsOphdRyjXsCOphdRjXsCJOpdRjXsCJpdRjXVsCJpRjXVsHCJpR6jXVsHCJpR6jXVsH 2Ft!Radwvo2Ft!Radvo2Ftm!Ravo2Ftmf!Rvo2FtmfRvo2F7tmRvo2F7ptmRvoA2F7ptmRvoA2F7pt 7HEsMg4E7R7HEsMg4ER7HEsMg4ER7HE%sMgER7HEsMgER7H9sMgER7H69sMgERu7H69sMgERu7H69s 7XcgSiygKP7XcgSiygK7XcgSiygK7mXcgSigK7mXgSigK7mXgSigKq7mXgSigK!q7mXgSigK!q7mXg v2I8b%!eLbv2I8b%!eLb2I8b%!eLHb2I8b%eLHb28b%eLHb28b%eFLHb28b%eFnLHb28b%eFnLHb28 yB20POSRWCyB20POSRWCyB20POSRWCyB20PSRWCyB20PSRWCyB20PSRWCyB20P8SRWCyB20P8SRWCy taBAzK7BGvtaBAzK7BGvtaBAzK7BGvtaBAz7BGvtaBAz7BGvtaBAz7BGvtaBAz67BGvtaBAz67BGvt #hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9Eko#hBD6s9E FjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w%6sFjcBa6w% Iu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO4VIu#fzcAO F$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtBvaF$Kf!GtB If you look at this correctly, you should see a large 3 and a box on one plane and then the D on top of that box on the second plane. The more distant-focused and relaxed your sight is, the better the effect. Don't strain your eyes too much though. Now that I have a working SIRTS generator written by myself, I can fully explain how it works. Besides all the preparation of the alphabet and equally-wide map strings, the general algorithm is as follows (assuming all positions and indexes are starting with 0): 1. Determine the desired image width W and pattern length PL (in our case, they are read from the first line of the input file). Select the alphabet A (in our case, it's read from the second line of the input file). 2. For each line M of width W in the depth map, repeat steps 3 to 17. 3. Generate a character pattern string P of basic length PL. The characters in P must be taken from the alphabet A. They can be chosen randomly or consecutively, they also can appear multiple times, but no _adjacent_ characters in the generated pattern P must be the same. 4. Shape a set of characters F consisting of all characters in the alphabet A except the ones present in P (so that, in set notation, {F} + {P} = {A}). 5. Duplicate the initial pattern length PL as the current length L. 6. Set the pattern tracking pointer PP to 0. 7. For each depth value V in the current map line M, repeat steps 8 to 16. 8. Calculate the new pattern length NL: NL = PL - V. 9. Calculate the delta value D: D = NL - L. 10. If D < 0, delete one character at the position PP from the pattern P (-D) times and go to step 14. 11. If D >= 0, perform steps 12 to 13 D times and go to step 14. 12. Retrieve a random character C from the set F and remove it from the set F. 13. Insert the character C into the pattern P at the position PP. 14. Copy the value of NL into L. 15. Emit a character from pattern P at the position PP mod NL. 16. Increment PP modulo NL: PP = (PP + 1) mod NL. End of iteration. 17. Emit a newline character. End of iteration. You can find a bit different description of these steps at [1], but I think my approach is more understandable. In short, we delete a character from our pattern at the current pattern position (before emitting anything at this position) if the depth value increases, and insert a new character from the set of unused characters if the depth value decreases. Since depth can change by more than 1 at a time, we must make sure the amount of deletions/insertions matches this delta. What all this does visually is creating a sharp boundary between layers where shorter patterns correspond to closer layers and vice versa. This is why we can't make a base pattern length too short: we must make sure it allows us to distinguish between all depth levels. A nice thing about this algorithm is that it also is fully line-oriented: each line is processed independently and can have its own character pattern to draw with. That's why it works so nicely with the AWK runtime. The only quirk I had to deal with was the fact that all string indexes in AWK start with 1 instead of 0. This is why several places in the Textereo code (which, by the way, is under 60 SLOC in total) have these 1's explicitly added. But the algo itself is quite flexible and can be adapted to virtually any programming language, including... Yes, you guessed it. But it's definitely not for today. --- Luxferre --- [1]: https://the.sunnyspot.org/asciiart/docs/sirdsfaq.html