Advertisement
Guest User

Texel and image processing

a guest
Apr 30th, 2024
51
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 45.53 KB | None | 0 0
  1.  
  2. # Texel and image processing
  3.  
  4. here i will try to explain how to process drawing characters and turn 2D pixel maps into text format in great detail along with a technical deepdive into the methods used
  5.  
  6. If you read this whole article you should have very in depth and advanced level of understanding of how 2D rendering in computercraft works and you should for example be able to render png images with ease with incredible speeds
  7.  
  8. This article will cover a whole range of topics from basics to expert, someone who is even slightly competent in Lua and CC should be able to understand the stuff discussed here as i try to explain it from basics up
  9.  
  10. Each topic will be labeled with one of 3 difficulties,
  11. |marking| name |Short description |
  12. |--- |-------- |--------------------------------------------------------------------------------------------- |
  13. | B | Basic | Should be able to be understood by a decently competent programmer |
  14. | M | Medium | If you fully understood basics and know some basic graphics you should understand this fine |
  15. | E | Expert | Decently complex. Not as detailed explanations cause too many details |
  16.  
  17. ## List of contents:
  18. - **Rendering pixels**
  19. - [**Pixel rendering basics**](#rendering-pixels) **<sub>B</sub>**
  20. - [**Basics of binary**](#understanding-base2) **<sub>B</sub>**
  21. - Understanding drawing character (texel) structure
  22. - [**Binary encoding of texels**](#connection-of-binary-and-texels) [*ex. code*](#example-implementation) **<sub>B</sub>**
  23. - [**String bitstream reversing** (endian correcting)](#why-reverse-the-bitstream-this-is-because-if-you-look-at-the-actual-charset-we-start-at-values-in-the-top-left-which-have-the-lowest-value-index-in-the-actual-charset-however-if-we-just-map-it-into-a-binary-number-like-010110-the-top-left-pixel-will-be-the-left-most-bit-in-the-string-and-since-binary-values-are-represented-with-big-endian-meaning-leftmost-digit-has-the-highest-valuecontribution-to-the-number-this-woulnt-match-up-because-as-mentioned-the-top-left-pixel-actually-has-the-lowest-value-so-we-just-flip-the-bitstream-to-correct-for-this-this-wont-actually-matter-for-real-scenarios-and-purely-matters-for-this-explanation-as-when-we-actually-process-these-characters-we-wont-be-doing-it-like-this-this-is-purely-to-explain-the-theory-and-reasons-behind-it)
  24. - [**Handling sixth texel bit**](#the-6th-bit) [*ex. code*](#example-implementation-1) **<sub>B</sub>**
  25. - [**Table implemention**](#table-implementation) [*ex. code*](#example-implementation-2) **<sub>M-</sub>**
  26.  
  27. - **Color handling**
  28. - [**Issues with texels and colors**](#issues-with-color-handling-in-texels) **<sub>B</sub>**
  29. - [**Making a bitstream out of colors with mapping**](#making-a-bitstream-out-of-colors) [*ex. code*](#example-implementation-3) **<sub>B</sub>**
  30. - [**Simple color processing**](#simple-color-processing) **<sub>B</sub>**
  31. - [**Simple quantization**](#quantization-and-mapping) [*ex. code*](#example-implementation-4) **<sub>M</sub>**
  32. - [**Complex quantization**](#complex-quantization) [*ex. code*](#example-implementation-5) **<sub>M+</sub>**
  33.  
  34. - **Optimization**
  35. - Basic optimization ideas **<sub>M</sub>**
  36. - 16^6 problematic **<sub>M</sub>**
  37. - Using color "patterns" to make lookup possible **<sub>E</sub>**
  38. - Lookup ID generation **<sub>E</sub>**
  39. - Using lookups for character picking **<sub>E</sub>**
  40. - Using lookups for color quantization **<sub>E</sub>**
  41. - Bruteforcing IDs for best memory efficiency **<sub>E+</sub>**
  42.  
  43. - **Extra**
  44. - Screen edge artifacts **<sub>B</sub>**
  45. - Turning CC colors from and to blit codes **<sub>B</sub>**
  46. - Monitor character and pixel size calculation **<sub>B</sub>**
  47. - Simple RGB color quantization **<sub>B+</sub>**
  48. - Fast color quantization **<sub>M</sub>**
  49. - Png image loading with [**PngLua**](https://github.com/9551-Dev/pngLua) **<sub>B</sub>**
  50.  
  51. - **Credits**
  52. - [**People and project contributors**](#credit)
  53. - [**CCGR Project**](#part-of-computercraft-graphics-research)
  54.  
  55. ### rendering pixels:
  56. In computercraft we use specialized drawing characters to display "pixels", to these we will refer to texels from now on because well, its pixels made out of text, heres the computercraft charset, what we are interested in here are the characters 128 to 159
  57.  
  58. ![computercraft charset](resources/cc_charset.png)
  59.  
  60. For you to understand how to process these you will have to understand base2, also known as binary.
  61. #### understanding base2
  62. in base10 we have 10 digits (0,1,2,3,4,5,6,7,8,9) and each digit has 10x higher value than the one before it, for example to see this you can split a base10 number into its individual digits. Lets split 5189, that would be `5*1000 + 1*100 + 8*10 + 9*1` Notice the "coefficients" here, starting from left to right we have 1000,100,10,1, each one is 10x more significant than the one more to the right you could also easily note this as `5*10^3 + 1*10^2 + 8*10^1 + 9*10^0`, while this doesnt seem to be very useful you can use this to understand and convert any base to base10!, notice how we have `digit*base^(digit_index-1) + ...`, this means that if i told you to convert 101011 to base10 you would be able to do it just like this!
  63. ```lua
  64. 1*2^5 + 0*2^4 + 1*2^3 + 0*2^2 + 1*2^1 + 1*2^0
  65. ```
  66. Counting in base2 is bit more difficult but doesnt take too long to master, here i will do an example with 3 bits (binary digits)
  67. ```lua
  68. 0: 000
  69. 1: 001
  70. 3: 010
  71. 4: 011
  72. 5: 100
  73. 6: 101
  74. 7: 110
  75. 8: 111
  76. ```
  77. this also shows an interesting fact that you can calculate the maximum number you can archive in a given base with a given digit count with a simple formula, `base^(digit_count) - 1`. Here we use - 1 because `base^digit_count` calculates the amount of values we can represent **INCLUDING ZERO** with a given digit count, however we dont want that. We want the maximum value, so we subtract 1 to exclude 0 digit from the values.
  78. ex.
  79. ```lua
  80. -- base^(digit_count) - 1
  81. 2^3 - 1 = ?
  82. 2^3 = 2*2*2 = 8
  83. 8 - 1 = 7
  84. -- the maxmimum amount of digits we can represent with 3 bits in base2 is 7
  85. -- which checks out, tonumber("111",2) -> 7 ("111" being the maximum number representable with 3 bits)
  86. ```
  87. #### Connection of binary and texels
  88. Okay lets look at it like this, lets take our desired texel and turn it into a 2D color map, lets use 153 as an example
  89. our map will be 2x3 because all texels represent pixels in a 2 by 3 grid (note that colors are here taken from the chart above)
  90. ```yml
  91. blue white
  92. white blue
  93. blue white
  94. ```
  95. now lets turn it into a so called "stream" basically just put it into a 1 dimensional data structure instead of 2D
  96. ```yml
  97. blue white white balue blue white
  98. ```
  99. This was done by essentially just removing the newlines between the individual layers of the character.
  100.  
  101. Now lets just turn it into a "binary stream" by converting the "lit up" areas of the character to 1 and the "emtpy" (background color ones) to 0
  102. ```yml
  103. 100110
  104. ```
  105. this gets us the binary code for the character, now lets do this for all the characters in the charset, also first we can calculate how many entries we will even have, notice how no character has the sixth bit enabled, this means there is just 5 bits to calculate with which is 2^5, `2^5 = 2*2*2*2*2 = 32`, So there will be 32 entries in our table
  106. ```
  107. 000000
  108. 100000
  109. 010000
  110. 110000
  111. 001000
  112. 101000
  113. 011000
  114. 111000
  115. 000100 ...
  116. ```
  117. Im not gonna actually write out all of these, but if you look closely or flip the binary strings you will notice that its essentially binary counting, this means the order of the character in the list directly coresponds to what it is
  118. so if you wanna know how the sixth character looks like you can just encode 5 in binary (Why 5? because 5 is the 6th digit in base10, 0,1,2,3,4,5. Never forget about 0!), which is `101` then pad it to get all 6 bits you need to get your texel code, `101000`, lastly you can just split it into 2D map by adding a newline every two bits
  119. ```
  120. 10
  121. 10
  122. 00
  123. ```
  124.  
  125. Knowing this you can easily also lookup any character in the charset,
  126. first take your 2D character map
  127. ```
  128. 01
  129. 01
  130. 10
  131. ```
  132. Then turn it into a bitstream `010110`, reverse the bitstream, so that we have `011010` which is 26 in decimal (base10)
  133.  
  134. ###### *why reverse the bitstream?*, this is because if you look at the actual charset we start at values in the top left which have the lowest value (index in the actual charset), however if we just map it into a binary number like `010110` the top left pixel will be the left most bit in the string, and since binary values are represented with big endian (meaning leftmost digit has the highest value/contribution to the number) this woulnt match up, because as mentioned the top left pixel actually has the lowest value, so we just flip the bitstream to correct for this. This wont actually matter for real scenarios and purely matters for this explanation as when we actually process these characters we wont be doing it like this, this is purely to explain the theory and reasons behind it
  135.  
  136. Knowing this also tells you that the character with this specific pixel config is the 26th character in the computercraft texel charset,
  137. but of course the entire CC charset doesnt start off with the texel characters, to get to that range you need to add 128 to the values.
  138. This shifts the character into the telext character range
  139.  
  140. ###### example implementation
  141. ```lua
  142. -- defines character data
  143. local char = [[
  144. 01
  145. 01
  146. 10
  147. ]]
  148.  
  149. -- removes all newlines by replacing them with an empty string
  150. local binary_stream = char:gsub("\n","")
  151.  
  152. -- reverses to correct for order, reasoning explained above
  153. binary_stream = binary_stream:reverse()
  154.  
  155. -- turn the binary stream into base10 from base2 using tonumber
  156. local char_num = tonumber(binary_stream,2)
  157.  
  158. -- calculate the position in the actual charset using an offset
  159. local char_index = char_num + 128
  160.  
  161. -- get the character string from the number and display it
  162. print(string.char(char_index))
  163. ```
  164.  
  165. **TLDR**: Characters in the charset are essentially encoded as binary, if you put them into a 1D list of bits you notice they are essentially counting in binary
  166.  
  167. #### The 6th bit
  168. The sharper ones among you might have noticed that none of the the characters have the bottom right pixel enabled, this is the sixth bit and it has a bit of special handling to save up on charset size, ommiting this bit in the charset isnt actually big deal as you can still get your desired characters every time!
  169.  
  170. All you really need to do is flip flip the bits and your colors- lemme explain. Lets say you want to render
  171. ```
  172. red red
  173. red black
  174. red red
  175. ```
  176. And your algorithm encodes a red as 1 and black as a 0, which will give you
  177. ```
  178. 11
  179. 10
  180. 11
  181. ```
  182. but if you happen to try and encode this into a texel the normal method without any special handling the data will oveflow outside of the texel charset range.
  183.  
  184. We need a way to somehow convert this into a format where the sixth bit can never be 1 while muting all the data the same way to not actually alter the saved data
  185.  
  186. The solution is quite simple, simple invert the entire texels bitstream like this
  187. ```
  188. 00
  189. 01
  190. 00
  191. ```
  192. Okay now you have a viable character that you can process, the only issue is that your colors have been broken, oh no!
  193. ```
  194. black black
  195. black red
  196. black black
  197. ```
  198. Lucky for you the solution is super easy, as instead of red and black and 1 and 0 you can just think it of these as the text color (foreground/fg) and background color (bg)
  199. ```
  200. bg bg
  201. bg fg
  202. bg bh
  203. ```
  204. Which means that all you really need to fix your colors is to flip the background and foreground color of the rendered character and bam!
  205. ```
  206. red red
  207. red black
  208. red red
  209. ```
  210. you have corrected the colors back to your original character by flipping the background and using an inverted symbol, in this case you would specifically want a symbol like mentioned above (binary string being `000100`)
  211.  
  212. ###### example implementation
  213. ```lua
  214. -- applies the colors given in the explanation above
  215. term.setTextColor(colors.red)
  216.  
  217. local char = [[
  218. 11
  219. 10
  220. 11
  221. ]]
  222.  
  223. -- removes all newlines by replacing them with an empty string
  224. local binary_stream = char:gsub("\n","")
  225.  
  226. -- read the last bit in the stream and check if it happens to be enabled
  227. local bit6 = binary_stream:match(".$") == "1"
  228.  
  229. -- reverses to correct for order, reasoning explained above
  230. binary_stream = binary_stream:reverse()
  231.  
  232. -- if bit6 is enabled inverts all bits to get an alternate existing character
  233. -- this makes use of gsub to match all ones and zeroes and invert them
  234. -- accordingly using a lookup table, little known feature of gsub.
  235. -- also flips the colors of foreground and background to correct for
  236. -- the inversion of the character
  237. if bit6 then
  238. binary_stream = binary_stream:gsub("[10]",{["1"]=0,["0"]=1})
  239.  
  240. local fg = term.getTextColor()
  241. local bg = term.getBackgroundColor()
  242.  
  243. term.setTextColor(bg)
  244. term.setBackgroundColor(fg)
  245. end
  246.  
  247. -- turn the binary stream into base10 from base2 using tonumber
  248. local char_num = tonumber(binary_stream,2)
  249.  
  250. -- calculate the position in the actual charset using an offset
  251. local char_index = char_num + 128
  252.  
  253. -- moves cursor pos to not have character touch edges of the screen
  254. term.setCursorPos(2,2)
  255.  
  256. -- get the character string from the number and display it
  257. print(string.char(char_index))
  258. ```
  259. **TLDR**: Invert bits within binary stream and flip background and foreground color
  260.  
  261. #### Table implementation
  262. So far all of the examples shown have used strings to store bitstreams and string function like reverse and gsub to handle them, however this is now how we will work going forward as manipulating strings in any more complex ways than this is a major hassle, instead we will use tables for pretty much everything. Lets think about what we need to change.
  263. ###### character stream data structure
  264. While before we could just use a multline string, now we will use tables. There are 2 ways to do this, either we can do it using a **2D array** or the **stream method** we used with string
  265.  
  266. 2D array method
  267. ```lua
  268. local char = {
  269. {1,1},
  270. {1,0},
  271. {1,1}
  272. }
  273. ```
  274. While this may seem convenient (and at first it actually is) because we can look up value of any bit in the pixel with x,y coordinates like this: `char[y][x]` its actually more of a hassle going forward. Thats why there is the second bitstream method.
  275. There is essentially 2 ways to write this, one way more readable than the other but data wise essentially same.
  276. ```lua
  277. local char = {1,1,1,0,1,1}
  278. ```
  279. ```lua
  280. local char = {
  281. 1,1,
  282. 1,0,
  283. 1,1
  284. }
  285. ```
  286. Not only is this faster and more memory efficient, it also allows us to still index it as a 2D array with just a little index calculation equation!
  287. The general equation/rule for calculating indices in arrays like this is
  288. ```
  289. index = (y-1)*width + x
  290. ```
  291. so lets say we wanted the bit at position `x=1,y=3`, we know that the width of texel is 2 pixels
  292. ```lua
  293. index = (3-1)*2 + 1
  294. index = 2 *2 + 1
  295. index = 4 + 1
  296. index = 5
  297. ```
  298. the index 2 does indeed correspond to the value at `x=1,y=3` in the pixel
  299. ###### reading the last bit for 6th bit correction
  300. Well, this is super simple.
  301. Given by how we format the table we can decide that the 6th bit will always be on the sixth index in the table (duh), so we simply just do something like this
  302. ```lua
  303. -- comparing the last (sixth bit) to check if its enabled
  304. local bit6 = char[6] == 1
  305. ```
  306. ###### Reversing the bit stream
  307. Considering that we wont be able to use `tonumber` like we did with the string (`tonumber` uses big endian which requires as to invert our bitstream) we dont really have to invert the bitstream, this is because we will have full control over the function that turns the binary data into decimal and can make it work with small endian so we directly convert it backwards instead of inverting before conversion
  308. ###### Iverting the bit stream
  309. Reversing the bit stream can be handled the same way as reversing the bit stream, just do it in the `binary->decimal` conversion step
  310. ###### translating into base10
  311. There is 2 ways to do it, you can either use a for loop or you can use a bunch of if statements.
  312.  
  313. With a for loop we will essentially just do the same stuff we did in the [binary basics part](#understanding-base2) with this equation `digit*base^(digit_index-1) + ...`
  314. ```lua
  315. -- init number as 0 so we can add to it the different parts of the binary number
  316. local char_num = 0
  317.  
  318. -- defines the fact that we want to decode base2 numbers
  319. local base = 2
  320.  
  321. -- loop over all 6 bits of the binary number
  322. -- technically we can loop over just 5 as the last one will always be zero
  323. for i=1,6 do
  324. -- grab binary number from the char data array (this is why making it a 2D array would make it a pain)
  325. local binary_value = char[i]
  326.  
  327. -- inverts the bit if the sixth bit is enabled
  328. -- this is equivalent to our bitstream inversion from before
  329. if bit6 then
  330. binary_value = 1-binary_value
  331. end
  332.  
  333. -- calculates the value coefficient for the given digit.
  334. -- this essentially indicates how much value the current digit
  335. -- contributes to the whole thing
  336. local coefficient = base^(i-1)
  337.  
  338. -- turn a piece of the binary digit into the decimal part using the equation from before
  339. -- notice how we dont have to reverse the binary stream like we had to with tonumber
  340. -- this is because tonumber uses big endian and takes the first value as the largest one
  341. -- but if you look here the first value has the lowest i, which results in the first value
  342. -- actually having the lowest coefficient and thus small endian, no need for reversing!
  343. char_num = char_num + binary_value*coefficient
  344. end
  345. ```
  346. The same goes for the if statement implementation except we will hard code it for all the 6 values
  347. ```lua
  348. local char_num = 0
  349. char_num = char_num + char[1] * 2^(1-1)
  350. char_num = char_num + char[2] * 2^(2-1)
  351. char_num = char_num + char[3] * 2^(3-1)
  352. char_num = char_num + char[4] * 2^(4-1)
  353. char_num = char_num + char[5] * 2^(5-1)
  354. char_num = char_num + char[6] * 2^(6-1)
  355. ```
  356. This can be further simplified by precalculating the coefficients
  357. ```lua
  358. local char_num = 0
  359. char_num = char_num + char[1] * 1
  360. char_num = char_num + char[2] * 2
  361. char_num = char_num + char[3] * 4
  362. char_num = char_num + char[4] * 8
  363. char_num = char_num + char[5] * 16
  364. char_num = char_num + char[6] * 32
  365. ```
  366. We can also easily get rid of the multiplication by 1 by directly asigning char[2] to char_num
  367. ```lua
  368. local char_num = char[1]
  369. char_num = char_num + char[2] * 2
  370. char_num = char_num + char[3] * 4
  371. char_num = char_num + char[4] * 8
  372. char_num = char_num + char[5] * 16
  373. char_num = char_num + char[6] * 32
  374. ```
  375.  
  376. The rest of the code will be essentially the same as right now we got to our char number, all we need is to offset it by 128 and look it up into the charset
  377.  
  378. ###### example implementation
  379. ```lua
  380. -- character data
  381. local char = {
  382. 1,1,
  383. 1,0,
  384. 1,1
  385. }
  386.  
  387. local bit6 = char[6] == 1
  388.  
  389. -- flips the colors of foreground and background to correct for
  390. -- the inversion of the character
  391. if bit6 then
  392. local fg = term.getTextColor()
  393. local bg = term.getBackgroundColor()
  394.  
  395. term.setTextColor(bg)
  396. term.setBackgroundColor(fg)
  397. end
  398.  
  399. local char_num = 0
  400. local base = 2
  401. for i=1,6 do
  402. -- grab binary number from the char data array (this is why making it a 2D array would make it a pain)
  403. local binary_value = char[i]
  404.  
  405. -- inverts the bit if the sixth bit is enabled
  406. -- this is equivalent to our bitstream inversion from before
  407. if bit6 then
  408. binary_value = 1-binary_value
  409. end
  410.  
  411. -- calculates the value coefficient for the given digit.
  412. -- this essentially indicates how much value the current digit
  413. -- contributes to the whole thing
  414. local coefficient = base^(i-1)
  415.  
  416. -- turn a piece of the binary digit into the decimal part using the equation from before
  417. -- notice how we dont have to reverse the binary stream like we had to with tonumber
  418. -- this is because tonumber uses big endian and takes the first value as the largest one
  419. -- but if you look here the first value has the lowest i, which results in the first value
  420. -- actually having the lowest coefficient and thus small endian, no need for reversing!
  421. char_num = char_num + binary_value*coefficient
  422. end
  423.  
  424. -- calculate the position in the actual charset using an offset
  425. local char_index = char_num + 128
  426.  
  427. -- moves cursor pos to not have character touch edges of the screen
  428. term.setCursorPos(2,2)
  429.  
  430. -- get the character string from the number and display it
  431. print(string.char(char_index))
  432. ```
  433. And of course, version with the if statement binary conversion, since thats what we are going to be using from now on for the sake of efficiency
  434. ```lua
  435. -- character data
  436. local char = {
  437. 1,1,
  438. 1,0,
  439. 1,1
  440. }
  441.  
  442. local bit6 = char[6] == 1
  443.  
  444. if bit6 then
  445. local fg = term.getTextColor()
  446. local bg = term.getBackgroundColor()
  447.  
  448. term.setTextColor(bg)
  449. term.setBackgroundColor(fg)
  450. end
  451.  
  452. -- The binary conversion and flipping bit when bit6 is present is gonna be handled a bit differently
  453. -- instead of inverting the values when bit6 is present like this
  454. --[[
  455. local char_num = (bit6 and 1-char[1] or char[1])
  456. char_num = char_num + (bit6 and 1-char[2] or char[2]) * 2
  457. char_num = char_num + (bit6 and 1-char[3] or char[3]) * 4
  458. char_num = char_num + (bit6 and 1-char[4] or char[4]) * 8
  459. char_num = char_num + (bit6 and 1-char[5] or char[5]) * 16
  460. char_num = char_num + (bit6 and 1-char[6] or char[6]) * 32
  461. ]]
  462. -- lets rather take a look at a truth table for how these values change
  463. -- this truth table will have 2 values, bit and the sixth bith of the texel (int) and one output.
  464. -- so our table will have 4 (2^2) possible states,
  465. -- sixth;bit;result ((sixth == 1) and 1-bit or bit)
  466. -- 0 0 (false and 1-0 or 0) = 0
  467. -- 0 1 (false and 1-1 or 1) = 1
  468. -- 1 0 (true and 1-0 or 0) = 1
  469. -- 1 1 (true and 1-1 or 1) = 0
  470. -- you may notice that this is essentially an xnor
  471. -- but thats not the most important thing, the most important fact for us is that
  472. -- the value is only !0 when the values are different, this along with the fact
  473. -- that i add a value multiplied by this number to char_num means that
  474. -- i dont have to add anything at all when the values are same, as when they are the same
  475. -- it guarantees addition of a 0.
  476. -- Its also important to note that before we multiplied the coefficient by the bit value
  477. -- which automatically caused a 0 addition, instead i will just use some if statements
  478. -- i also dont have to add the sixth bit at all as its always equal to itself and thus will
  479. -- always result in a 0 addition. I will also replace the charset shift of 128 by
  480. -- just inicializing char_num at that value and thus have it be shifted since the start
  481. -- Here is a simple implementation
  482. local char_num = 128
  483. if char[1] ~= char[6] then char_num = char_num + 1 end
  484. if char[2] ~= char[6] then char_num = char_num + 2 end
  485. if char[3] ~= char[6] then char_num = char_num + 4 end
  486. if char[4] ~= char[6] then char_num = char_num + 8 end
  487. if char[5] ~= char[6] then char_num = char_num + 16 end
  488.  
  489. -- no actual addition needed cause offset is added at char_num's definition
  490. local char_index = char_num + 0
  491.  
  492. term.setCursorPos(2,2)
  493.  
  494. print(string.char(char_index))
  495. ```
  496.  
  497. #### Color handling
  498. The entirety of texel color handling lies in a few techniques.
  499. It mostly boils down to quantization,mapping and throwing data out!
  500.  
  501. ##### Issues with color handling in texels
  502. The main issue you will encounter when trying to process texel in color is the fact that each character (2x3 pixels) can only technically have 2 colors as you can only control the background and foreground color, while each character can have up to 6 different colors to it. How do you then pick the the bg and fg color along with the symbol? Turns out the only way to handle this is to discart some colors but that doesnt mean you cant get smart about how you deal with the positions of the discarted colors and picking the two colors to keep.
  503.  
  504. ##### Making a bitstream out of colors
  505. As you already know you need some sort of a [bitstream](#connection-of-binary-and-texels) or a list of ones and zeroes to calculate which drawing character you will want to use, previously every time ive used colors in any examples i just converted them to the bitstream by hand but we need to program something to do that for us as we wont be sitting there calculating this stuff by hand obviously
  506.  
  507. Here we will have an ideal scenario where we will only have 2 colors and thus we dont have to throw any data out and we can just process as is.
  508. All of this can be just handled using value mapping.
  509.  
  510. Specific example:
  511. ```yml
  512. white blue
  513. blue white
  514. white blue
  515. ```
  516.  
  517. Now to map it we will need a mapping list which we will explain how to create in later section which talks about quantization.
  518. For now lets just take this list
  519.  
  520. |key | value |
  521. |--- |-------- |
  522. | white | 0 |
  523. | blue | 1 |
  524.  
  525. **note that with this system 1 will always refer to the fg and 0 to the bg**, this is very important to know if you want to actually do colored rendering as we will be using term.blit later on!
  526.  
  527. ###### example implementation
  528. This can just be a lookup table, to show this i will make a simple implemention based on [code from before](#table-implementation)
  529. ```lua
  530. local char = {
  531. "white","blue",
  532. "blue" ,"white",
  533. "white","blue"
  534. }
  535.  
  536. -- define the list that will allow me to map the color codes onto the fg (1) and bg (0)
  537. local mapping_lookup = {
  538. ["white"] = 1,
  539. ["blue"] = 0
  540. }
  541.  
  542. -- apply the mapping iteratively to the list
  543. for i=1,#char do
  544. char[i] = mapping_lookup[char[i]]
  545. end
  546.  
  547. term.setTextColor(colors.white)
  548. term.setBackgroundColor(colors.blue)
  549.  
  550. local bit6 = char[6] == 1
  551.  
  552. if bit6 then
  553. local fg = term.getTextColor()
  554. local bg = term.getBackgroundColor()
  555.  
  556. term.setTextColor(bg)
  557. term.setBackgroundColor(fg)
  558. end
  559.  
  560. local char_num = 0
  561. local base = 2
  562. for i=1,6 do
  563. local binary_value = char[i]
  564.  
  565. if bit6 then binary_value = 1-binary_value end
  566.  
  567. char_num = char_num + binary_value*2^(i-1)
  568. end
  569.  
  570. local char_index = char_num + 128
  571. term.setCursorPos(2,2)
  572.  
  573. print(string.char(char_index))
  574. ```
  575. feel free to play around with the char list to see that this really does work!
  576.  
  577. [[
  578. First we need to make some sort of a list to be used for the value mapping, this simply consist of us making a list which defines which color maps to 1 or a 0.
  579. Here we can easily do it by iterating the list until we find 2 colors and every time we find a new color we can asign a 0 or a 1 to it depending on which one comes first.
  580. Like this we iterate
  581.  
  582. `index1 -> white` and we asign a 0 for example (the order doesnt actually matter)
  583.  
  584. `index2 -> blue` and now we got our second unique color so we can stop
  585. ]]
  586.  
  587. ##### Simple color processing
  588. Slightly more advanced color processint techniques, this function will be able to handle up up to 6 different color on its input and do some simple estimation for which character it should choose
  589.  
  590. It will mostly rely on a few things
  591. - finding the two most common colors
  592. - changing rest of the colors to one of the two common colors (quantization)
  593. - mapping into a bitstream and drawing (mapping)
  594. - terminal color settings and drawing
  595.  
  596. I will also put the code into an actual function as we will be referring to it later on.
  597.  
  598. ###### Finding the most common colors
  599. Lets start off by defining our character as a 1D list of colors
  600. ```lua
  601. local char = {
  602. colors.blue,colors.red,
  603. colors.green,colors.blue,
  604. colors.blue,colors.red
  605. }
  606. ```
  607. Now lets create a lookup with all of the colors present in the list
  608. ```lua
  609. local color_lookup = {}
  610. for i=1,#char do
  611. local color = char[i]
  612. color_lookup[color] = true
  613. end
  614. ```
  615. Since we want the color count and not just if its present or not (since we will later want to sort by the count) we will store a number instead of just a boolean for each color
  616. ```lua
  617. local color_lookup = {}
  618. for i=1,#char do
  619. local color = char[i]
  620.  
  621. -- check for colors existence to prevent arithmetic on nil
  622. if color_lookup[color] then
  623. color_lookup[color] = color_lookup[color] + 1
  624. else
  625. color_lookup[color] = 1
  626. end
  627. end
  628. ```
  629. To sort the table i will use table.sort, the only issue with that is that the list needs to be ordered from 1-n with no holes, the list ive made here obviously isnt that since its indexed by the color which is in the `2^n -1 < n < 16` range, so there will always be holes and its definitely not gonna be in order. To fix that we can just convert convert the color_lookup to into such a list.
  630. ```lua
  631. local sortable_colors = {}
  632. for k,v in pairs(color_lookup) do
  633. local color_data = {
  634. color = k,
  635. count = v
  636. }
  637.  
  638. sortable_colors[#sortable_colors+1] = color_data
  639. end
  640. ```
  641. Now to just sort it using table.sort and simple function which will make the values with the highest count be at the lowest index
  642. ```lua
  643. table.sort(sortable_colors,function(a,b)
  644. return a.count > b.count
  645. end)
  646. ```
  647. The final code for this will be
  648. ```lua
  649. local char = {
  650. colors.blue,colors.red,
  651. colors.green,colors.blue,
  652. colors.blue,colors.red
  653. }
  654.  
  655. local color_lookup = {}
  656.  
  657. for i=1,#char do
  658. local color = char[i]
  659.  
  660. if color_lookup[color] then
  661. color_lookup[color] = color_lookup[color] + 1
  662. else
  663. color_lookup[color] = 1
  664. end
  665. end
  666.  
  667. local sortable_colors = {}
  668. for k,v in pairs(color_lookup) do
  669. local color_data = {
  670. color = k,
  671. count = v
  672. }
  673.  
  674. sortable_colors[#sortable_colors+1] = color_data
  675. end
  676.  
  677. table.sort(sortable_colors,function(a,b)
  678. return a.count > b.count
  679. end)
  680. ```
  681.  
  682. ###### Quantization and mapping
  683. The next two steps can actually be handled in one step.
  684. As we can both turn the colors into a bitstream and map other colors to 2 most common ones at the same time
  685.  
  686. Lets start off by iterating all of the colors in the character like so, im going to be going off from the code above
  687. ```lua
  688. for i=1,6 do
  689. local subpixel_color = char[i]
  690. end
  691. ```
  692. Now to form the stream we can check if the color is either one of the most common colors
  693. and then asign it either a 1 or a 0 to the bitstream.
  694.  
  695. I am gonna take the sortable_colors list from the previous section
  696. ```lua
  697. local stream = {}
  698. for i=1,6 do
  699. local subpixel_color = char[i]
  700.  
  701. if subpixel_color == sortable_colors[1].color then
  702. stream[i] = 1
  703. elseif subpixel_color == sortable_colors[2].color then
  704. stream[i] = 0
  705. end
  706. end
  707. ```
  708. The last part left to handle is when find a color which is not among the 2 most common ones.
  709. There is really no right or wrong way to do this as its all just estimation after all
  710.  
  711. Here i will just use super simple method which will result in a dither-like effect. I will just make a table which will
  712. tell me which one of the 2 most common colors to sample depending on the texel sub-pixel index
  713.  
  714. Since i want a pattern effect like this
  715. ```lua
  716. 10
  717. 01
  718. 10
  719. ```
  720. The lookup will look something like this
  721. ```lua
  722. local sample_lookup = {
  723. 1,2,
  724. 2,1,
  725. 1,2
  726. }
  727. ```
  728. Considering we translate the sortable colors to 1s and 0s anyway we can just turn this into a 1 and 0 list
  729. (essentially preprocessing the mapping)
  730. ```lua
  731. local sample_lookup = {
  732. 1,0,
  733. 0,1,
  734. 1,0
  735. }
  736. ```
  737. Now lastly i will integrate this into the existing code, using `i` for the chracter index
  738. ```lua
  739. local stream = {}
  740. for i=1,6 do
  741. local subpixel_color = char[i]
  742.  
  743. if subpixel_color == sortable_colors[1].color then
  744. stream[i] = 1
  745. elseif subpixel_color == sortable_colors[2].color then
  746. stream[i] = 0
  747. else
  748. stream[i] = sample_lookup[i]
  749. end
  750. end
  751. ```
  752. ###### terminal configuration
  753. To have this character properly render we will have to set the foreground and background color accordingly depending on the
  754. 2 most common colors.
  755.  
  756. In our bitstream configuration `1` reffers to a foreground color reference and `0` to a background color reference.
  757. Given our current code `sortable_colors[1]` has to be the fg and `sortable_colors[2]` has to be the background color.
  758.  
  759. Also dont forget that you need to swap these two to correct for the bitstream flip if the sixth bit is enabled.
  760. This could be done with something like this
  761. ```lua
  762. if stream[6] == 0 then
  763. term.setTextColor (sortable_colors[1].color)
  764. term.setBackgroundColor(sortable_colors[2].color)
  765. elseif stream[6] == 1 then
  766. term.setTextColor (sortable_colors[2].color)
  767. term.setBackgroundColor(sortable_colors[1].color)
  768. end
  769. ```
  770.  
  771. ###### example implementation
  772. Here ive made a culmination of all the code above plus ive put it into a function so it can be used more easily
  773.  
  774. This should already be able to process a list of colors into a somewhat acceptable texel
  775. ```lua
  776. local sample_lookup = {
  777. 1,0,
  778. 0,1,
  779. 1,0
  780. }
  781.  
  782. local function process_texel(colors)
  783. -- figuring out the 2 most common colors
  784. local color_lookup = {}
  785.  
  786. for i=1,6 do
  787. local color = colors[i]
  788.  
  789. if color_lookup[color] then
  790. color_lookup[color] = color_lookup[color] + 1
  791. else
  792. color_lookup[color] = 1
  793. end
  794. end
  795.  
  796. local sortable_colors = {}
  797. for k,v in pairs(color_lookup) do
  798. local color_data = {
  799. color = k,
  800. count = v
  801. }
  802.  
  803. sortable_colors[#sortable_colors+1] = color_data
  804. end
  805.  
  806. table.sort(sortable_colors,function(a,b)
  807. return a.count > b.count
  808. end)
  809.  
  810. -- bitstream generation from colors
  811. local stream = {}
  812. for i=1,6 do
  813. local subpixel_color = colors[i]
  814.  
  815. if subpixel_color == sortable_colors[1].color then
  816. stream[i] = 1
  817. elseif subpixel_color == sortable_colors[2].color then
  818. stream[i] = 0
  819. else
  820. stream[i] = sample_lookup[i]
  821. end
  822. end
  823.  
  824. -- terminal color setting
  825. if stream[6] == 0 then
  826. term.setTextColor (sortable_colors[1].color)
  827. term.setBackgroundColor(sortable_colors[2].color)
  828. elseif stream[6] == 1 then
  829. term.setTextColor (sortable_colors[2].color)
  830. term.setBackgroundColor(sortable_colors[1].color)
  831. end
  832. W
  833. -- character generation from bitstream
  834. local char_num = 128
  835. if stream[1] ~= stream[6] then char_num = char_num + 1 end
  836. if stream[2] ~= stream[6] then char_num = char_num + 2 end
  837. if stream[3] ~= stream[6] then char_num = char_num + 4 end
  838. if stream[4] ~= stream[6] then char_num = char_num + 8 end
  839. if stream[5] ~= stream[6] then char_num = char_num + 16 end
  840.  
  841. -- rendering to term
  842. term.setCursorPos(2,2)
  843. print(string.char(char_num))
  844. end
  845.  
  846. local char = {
  847. colors.blue,colors.red,
  848. colors.green,colors.blue,
  849. colors.blue,colors.red
  850. }
  851.  
  852. process_texel(char)
  853. ```
  854. Well lets give this function a try on an actual image shall we.
  855. Lets try to render the original computercraft charset with it
  856. ![CC charset rendered with texels](resources/charser_texel.png)
  857. Or some oreos!
  858. ![oreos rendered with texels](resources/oreos.png)
  859.  
  860. ###### Complex quantization
  861. Not much is actually gonna change here. The main difference is how we will sample colors for pixels which are not among the 2 most common colors
  862.  
  863. Before i just used a simple pattern which made a dithering like effect
  864. ```lua
  865. local sample_lookup = {
  866. 1,0,
  867. 0,1,
  868. 1,0
  869. }
  870.  
  871. local stream = {}
  872. for i=1,6 do
  873. local subpixel_color = colors[i]
  874.  
  875. if subpixel_color == sortable_colors[1].color then
  876. stream[i] = 1
  877. elseif subpixel_color == sortable_colors[2].color then
  878. stream[i] = 0
  879. else
  880. stream[i] = sample_lookup[i]
  881. end
  882. end
  883. ```
  884. Instead of doing it like this i will try to handle it on a distance based approach. When i find a color which
  885. Is nowhere to be found in the 2 most colors and thus must be remapped to a different one i will check that colors
  886. neighbors in a specific order.
  887.  
  888. I will mostly follow 2 rules for this.
  889. - closest pixels, finding the closest pixels to the current one, if no suitable color is found then repeating that for those neighboring pixels aswell
  890. - clockwise iteration: when we get the 3 (or less) pixel neighbors of the current one we will iterate them in a clockwise order
  891.  
  892. Here you can see these rules being applied to two texels, one with the `i1` sampled and other with `i4` being sampled
  893.  
  894. ![Color sampling visual](resources/sampling_visual.png)
  895.  
  896. We could technically generate this for all positions using euclidean distance and some recursive algorithm but thats painful, and considering there
  897. is just 6 options (possible sample points) each with 5 values (5 alternate sample points) its probably gonna be the best idea to hardcode this
  898.  
  899. If you look at the example image where is started at `C1` you can see that it essentially just spreads to its neighbors.
  900. So your original sample point is always gonna be surrounded by `A1` and all `A1` sample points are gonna be surrounded by `A2` sample points.
  901. This slowly expands over the entirety of the texel (max of 3 search levels, aka A3).
  902.  
  903. We are gonna be looking for one of the 2 most commons in the texel and specifically prefer ones with the lowest search level (closest to the original sample points)
  904. And also if there are for example 2 of the most common colors right in the same search level we will search it in a clockwise manner
  905. To always have a predictable outcome, this clockwise type of search would be pretty hard to implement so as mentioned before we will just hardcode all of this
  906.  
  907. Lets start off by making the sampling lookup for our first texel pixel (C1), you can refer to the first texel in the visual above. What im gonna be mostly interested in is
  908. the B (iteration index) component of the subpixel, as thats what will set its weight/priority in the sampling process (the lower the more preferred it will be)
  909.  
  910. Note that i will go through the subpixels in order (given by the iter index component) and write down its subpixel index, the actual iter index is not
  911. written down anywhere and purely used for figuring out the subpixel indices.
  912.  
  913. ```lua
  914. local sampling_lookup = {
  915. -- [subpixel index] = {sample subpixel index from lowest iter index to highest}
  916. [1] = {2,3,4,5,6}
  917. }
  918. ```
  919.  
  920. Okay now lets do it for the second texel in the visual (the one where `C4` is being sampled)
  921. ```lua
  922. local sampling_lookup = {
  923. -- [subpixel index] = {sample subpixel index from lowest iter index to highest}
  924. [4] = {2,6,3,5,1}
  925. }
  926. ```
  927. Oh well all thats left is to do it for all the subpixels
  928. ```lua
  929. local sampling_lookup = {
  930. [1] = {2,3,4,5,6},
  931. [2] = {4,1,6,3,5},
  932. [3] = {1,4,5,2,6},
  933. [4] = {2,6,3,5,1},
  934. [5] = {3,6,1,4,2},
  935. [6] = {4,5,2,3,1}
  936. }
  937. ```
  938. That was rather straight forward.
  939. Now all thats left is to upgrade our original sampler
  940. ```lua
  941. else
  942. stream[i] = old_sample_lookup[i]
  943. end
  944. ```
  945. Instead of doing this we will look up the current subpixel in the sampling_lookup
  946. ```lua
  947. else
  948. local sample_points = sampling_lookup[i]
  949. end
  950. ```
  951. Then we iterate over all the sampling points until we find a color which is one of the 2 most common character colors (meaning we can actually use it during rendering).
  952. To do this you will have to up the actual color at that position in the character
  953. ```lua
  954. else
  955. local sample_points = sampling_lookup[i]
  956. for sample_index=1,5 do
  957. local sample_subpixel_index = sample_points[sample_index]
  958. local sample_color = colors[sample_subpixel_index]
  959.  
  960. local common_1 = sample_color == sortable_colors[1].color
  961. local common_2 = sample_color == sortable_colors[2].color
  962.  
  963. if common_1 or common_2 then
  964. print("Found suitable color!")
  965.  
  966. -- stop looking for colors once one has been alreay found
  967. break
  968. end
  969. end
  970. end
  971. ```
  972. The last thing that needs to be done is actually writing either a 1 or a zero into the bitstream depending on which one
  973. of the two common colors it actually is.
  974.  
  975. Since the original code considers the most **number 1** most common color to be a 1 in the bitstream and the **second most common** one
  976. to be a 0 zero in the bitstream (because of this piece of code)
  977. ```lua
  978. if subpixel_color == sortable_colors[1].color then
  979. stream[i] = 1
  980. elseif subpixel_color == sortable_colors[2].color then
  981. stream[i] = 0
  982. else
  983. ```
  984. We can easily go off of that and say that if the `sample_color` is equal to the most common color (`sortable_colors[1].color`) also called `common_1` then
  985. we should write a 1 into the bitstream, else we write a 0.
  986. Heres how that would be done
  987. ```lua
  988. else
  989. local sample_points = sampling_lookup[i]
  990. for sample_index=1,5 do
  991. local sample_subpixel_index = sample_points[sample_index]
  992. local sample_color = colors[sample_subpixel_index]
  993.  
  994. local common_1 = sample_color == sortable_colors[1].color
  995. local common_2 = sample_color == sortable_colors[2].color
  996.  
  997. if common_1 or common_2 then
  998. -- this can also be implemented with an if statement but i prefer the ternary version
  999. -- here is the if statement version for those who are not familiar with ternary anyway
  1000. -- if common_1 then
  1001. -- stream[i] = 1
  1002. -- else
  1003. -- stream[i] = 0
  1004. -- end
  1005.  
  1006. stream[i] = common_1 and 1 or 0
  1007.  
  1008. break
  1009. end
  1010. end
  1011. end
  1012. ```
  1013.  
  1014. Now we just have to put it all together!
  1015. ###### example implementation
  1016. ```lua
  1017. -- sampling lookup definition
  1018. local sampling_lookup = {
  1019. [1] = {2,3,4,5,6},
  1020. [2] = {4,1,6,3,5},
  1021. [3] = {1,4,5,2,6},
  1022. [4] = {2,6,3,5,1},
  1023. [5] = {3,6,1,4,2},
  1024. [6] = {4,5,2,3,1}
  1025. }
  1026.  
  1027. local function process_texel(colors)
  1028. -- figuring out the 2 most common colors
  1029. local color_lookup = {}
  1030.  
  1031. for i=1,6 do
  1032. local color = colors[i]
  1033.  
  1034. if color_lookup[color] then
  1035. color_lookup[color] = color_lookup[color] + 1
  1036. else
  1037. color_lookup[color] = 1
  1038. end
  1039. end
  1040.  
  1041. local sortable_colors = {}
  1042. for k,v in pairs(color_lookup) do
  1043. local color_data = {
  1044. color = k,
  1045. count = v
  1046. }
  1047.  
  1048. sortable_colors[#sortable_colors+1] = color_data
  1049. end
  1050.  
  1051. table.sort(sortable_colors,function(a,b)
  1052. return a.count > b.count
  1053. end)
  1054.  
  1055. -- bitstream generation from colors
  1056. local stream = {}
  1057. for i=1,6 do
  1058. local subpixel_color = colors[i]
  1059.  
  1060. if subpixel_color == sortable_colors[1].color then
  1061. stream[i] = 1
  1062. elseif subpixel_color == sortable_colors[2].color then
  1063. stream[i] = 0
  1064. else
  1065. -- more advanced missing color sampling
  1066. local sample_points = sampling_lookup[i]
  1067. for sample_index=1,5 do
  1068. local sample_subpixel_index = sample_points[sample_index]
  1069. local sample_color = colors[sample_subpixel_index]
  1070.  
  1071. local common_1 = sample_color == sortable_colors[1].color
  1072. local common_2 = sample_color == sortable_colors[2].color
  1073.  
  1074. if common_1 or common_2 then
  1075. stream[i] = common_1 and 1 or 0
  1076.  
  1077. break
  1078. end
  1079. end
  1080. end
  1081. end
  1082.  
  1083. -- terminal color setting
  1084. if stream[6] == 0 then
  1085. term.setTextColor (sortable_colors[1].color)
  1086. term.setBackgroundColor(sortable_colors[2].color)
  1087. elseif stream[6] == 1 then
  1088. term.setTextColor (sortable_colors[2].color)
  1089. term.setBackgroundColor(sortable_colors[1].color)
  1090. end
  1091.  
  1092. -- character generation from bitstream
  1093. local char_num = 128
  1094. if stream[1] ~= stream[6] then char_num = char_num + 1 end
  1095. if stream[2] ~= stream[6] then char_num = char_num + 2 end
  1096. if stream[3] ~= stream[6] then char_num = char_num + 4 end
  1097. if stream[4] ~= stream[6] then char_num = char_num + 8 end
  1098. if stream[5] ~= stream[6] then char_num = char_num + 16 end
  1099.  
  1100. -- rendering to term
  1101. term.setCursorPos(2,2)
  1102. print(string.char(char_num))
  1103. end
  1104.  
  1105. local char = {
  1106. colors.blue,colors.red,
  1107. colors.green,colors.blue,
  1108. colors.blue,colors.red
  1109. }
  1110.  
  1111. process_texel(char)
  1112. ```
  1113. Now lets again give this function a try with some images
  1114.  
  1115. ![Texel rendered cat (advanced)](resources/cat_texel_advanced.png)
  1116.  
  1117. Now lets try it on a larger screen
  1118.  
  1119. ![Texel rendered cat (advanced & large)](resources/cat_texel_advanced_large.png)
  1120.  
  1121. And of course some obligatory oreos
  1122.  
  1123. ![Texel rendered oreo (advanced)](resources/oreo_texel_advanced.png)
  1124.  
  1125. Now it can be kind hard to spot the differences from the sampler from before but close up there is actually a lot less noise!
  1126.  
  1127. ![Difference of noiser leves from the 2 samplers](resources/sampler_difference.png)
  1128.  
  1129. If you gotten this far congrats! you are now fully familiar with how to work with drawing characters!
  1130. The function we've built here is decently fast and should scale well aswell as being really readable.
  1131.  
  1132. However there is still a ton to improve! In the next sections i will show some methods which will allow you to
  1133. make this up to 6x faster! and even more extreme in some cases.
  1134.  
  1135.  
  1136.  
  1137. # credit
  1138. - [**ShadyDuck**](https://github.com/exerro/) - helping with understanding of graphics concepts and certain optimizations
  1139. - [**HaruCoded**](https://github.com/Kariaro) - idea and implementation of using color patterns for handling texel color quantization
  1140.  
  1141. ###### written by [**9551Dev**](https://github.com/9551-dev)
  1142. ###### part of the [**ComputerCraft Graphics Research**](https://github.com/ComputercraftGraphics) project
  1143. ###### licensed under the [**MIT LICENSE**](https://mit-license.org/)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement