Raytracing on a Graphing Calculator (again)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
This image you’re looking at was  rendered completely on a TI84 calculator.   No tricks, no “I rendered it on my  PC and copied to the calculator.”   This is the real deal, a raytracing engine that  runs on a calculator that supports textures,   reflection, HDR, gamma-correct rendering,  dithering, and global illumination.  Now, most people familiar with my older catalog  of videos probably know that a while back,   I already wrote a raytracing program on a  graphing calculator. But that one had a few   limitations that I’d like to address. Mainly,  my first raytracer wasn’t all that fast. It   took almost 6 hours to render this image  here, which works out to about .0000485 FPS,   and less than 5 pixels per second. To be fair,  raytracing is not something that can easily be   pulled off in realtime. Each pixel involves a  whole boatload of computations to determine what   is visible and how it should be illuminated. Even  now, you need a beefy GPU to pull it off. But,   even for the cheap little CPU in these  calculators, 6 hours for this is just pitiful.  The reason everything is so slow is because  TI-BASIC, the built-in language I used,   is interpreted. In short, the processor  in the calculator doesn’t run my program,   it runs a program that runs my program. This  makes sense for a calculator, since the whole   process is sandboxed, and a lot harder to crash,  but for my code, it means every math operation I   do involves more work than it really needs to,  and this overhead adds up. TI-BASIC also limits   you to 15 colors over a fraction of the full  screen, and probably the biggest killer for   a project like this: TI-BASIC only lets you use  single letter variable names. There are only 26   places to store values in the program, and the  code is practically unreadable because of it.  This time, instead of TiBASIC,  I’ll be writing this project in C++   using the toolchain provided by Matt Waltz and the  other contributors to the CE Programming project.   C is really nice compared to BASIC. It’s compiled  so the programs run directly on the CPU, code is   a lot easier to document and reuse, and I can  poke around in memory basically anywhere I want,   including writing to every pixel. Best of  all, I can write and test code right on my   computer through emulation, which has the added  bonus of being faster than the real hardware.  After taking a look at some of the sample code  in the toolchain repository, I figured the best   way to get started would be to rewrite the  original version of the program, so I more or   less ported it over. Here’s a fun fact, did you  know that these calculators have a 32,768 color   320x240 display? That’s on par with the Super  Nintendo, and a whole lot better than the TiBASIC   graphical specs. Even this recreation of the old  program already looks a whole lot better than the   original. As for speed? Well, the old version  took just under 6 hours to draw this image.   The new version takes about 3 minutes. That’s  nearly 400 pixels per second or an 80x speedup.   It’s a great start, but this video would be pretty  short if I just stopped here. We can go faster.  At its core, raytracing is really just a bunch of  math to model how light travels through a scene,   and to do all that rendering math in my  code, I’m using floating point numbers,   which are like scientific notation but for binary  numbers. Floats are a lot better than integers for   general purpose computation since they can work  on really big numbers and really small numbers,   but the tradeoff is that generally math  operations on floating point numbers are a lot   slower than integers, and on the Z80 processor  inside the TI84 this is definitely the case.   There aren’t any hardware optimizations  for floats like you would find on any   modern processor or GPU, which means that  even for something as simple as adding two   numbers together: integers take 2 CPU  cycles, and floats take a few thousand.  One of the reasons floats are so expensive is  because you have to account for the fact that the   decimal in two floating point numbers might not be  in the same place. Before we can add two numbers,   we have to make sure all their digits are lined up  properly, but if we can be sure that the decimals   are always in the same spot, we can treat these  numbers like integers. This approach is called   Fixed Point, since instead of floating around, the  decimal point is always in the same place. A great   example is US Dollars. As long as we assume you  can’t have a fraction of a cent, you can write   out any amount of money as an integer number of  cents rather than a fractional number of dollars.  Fun fact time again! I actually lied about the  processor that these calculators use. I said   that they use the Zilog Z80, which is an 8-bit  processor from all the way back in 1974. Well,   that’s partially true. Up to and including the  Plus C Silver Edition, everything in the TI84 line   was using a groovy 70s-era architecture, which,  fair enough, for what they do these calculators   don’t need to be all that powerful. But, the model  I’m using here, the 2015 skinny-legend TI84 Plus   CE finally gave the CPU an update. Now, it has  a Zilog eZ80 under the hood. A whole lot faster,   plus a better instruction set AND 24-bit wide  registers. Yes, you heard me right, 24-bit,   not something more typical like 16, 32 or 64, this  thing is 24-bit. Your guess is as good as mine.  Anyways, I can make my fixed point numbers have  24 bits, 12 below the decimal, and 12 above.   This can represent numbers as large as 2047.999,  and as small as .0002, not perfectly precise,   but good enough for graphics. Plus, these  fixed point numbers are way fast. I was able   to render one test scene in less than  a quarter of the time! An improvement   to be sure, but still, not good enough… Besides speed, one thing I really wanted to   improve in this raytracer was the complexity  of the scenes it draws. The first one had   a reflective sphere, a tile floor, and a  single shadow, all centered in the view.   Simple scenes like that can offer some  mathematical shortcuts, but this time I   want to make things more complicated. So let's  add a few walls, a ceiling, and another sphere.  The whole idea for ray-tracing is that we take  some sight ray, and figure out what objects in the   scene it intersects with. Depending on the answer,  we shade that pixel in the output a certain way.   But, if we have an efficient way to find out  if a ray intersects something in the scene,   we can use this information to figure  out other ways light is traveling.   For example, if there’s something in the way of  a ray pointing from a surface to a light source,   we know that that point will be in shadow. There are two other properties of light that we’ll   also want to model here. The first is Lambert’s  Law: light intensity on an object falls off the   more a surface points away from the light source.  And the second is the inverse-square law, light   intensity falls off the further away you get from  the source. The two of these have to do with the   angular size of the surface from the perspective  of the light source. If we assume the lamp gives   out equal energy in all directions, the bigger  the solid angle our surface takes up, the more   light it’s gonna reflect. Turn the surface away  from the lamp, and the solid angle gets smaller.   Move the object twice as far away, and now it  takes 4 times as much area to take up the same   angular space. In both cases, the surface  will get less light, and appear dimmer.  When you put these two effects together with the  shadows, things already look a whole lot better.   But, computer renders have this awkward  tendency to look a little too clean,   and you can’t get much cleaner than a room with  two perfect spheres and solid-color planes, so,   let’s look into texturing. All we have to do is  to take an existing picture, and to project it   down onto an object in the scene when rendering.  In our case, whenever we’re rendering a pixel on   the floor, we’ll want to use the color from a  pixel in some image like these hardwood planks.   Since the floor is an axis-aligned  plane, that task is pretty simple:   just take the x and z coordinates where  we hit, use those to pick a pixel in the   texture, and then interpolate between any  neighboring pixels to hide any jagged edges.  But here’s something else that doesn’t look  too great. If you look closely at any surface,   you’ll see bands of color rather than a smooth  gradient. As unnecessarily good as the display   hardware in these calculators is, we are still  bound by 15-bit color. In the red, green, and   blue channels there are only 32 different levels  of brightness to choose from, and so in areas   where color is changing slowly, the transition  between the two closest colors stands out.  Thankfully, dithering is an easy fix to this  problem. By mixing together a few pixels of each   color along a transition, we can get the illusion  of a bigger palette at the expense of a little   pixel noise. There’s no shortage of dithering  algorithms to choose from depending on if you want   to prioritize speed, image quality, or noise, but  here, I’m doing arguably the simplest technique.   Taking whatever error there is from the last  pixel, and just adding it onto the next one in   the row. Even though there’s not much to it, this  technique helps a lot to smooth out the colors.  While we’re on the subject of color, I gotta  confess, my renderer still is a little bit   entirely wrong. My code all operates on the  assumption that light intensity in the renderer   scales linearly with the RGB value I set the  pixels to. But in the real world, that’s not the   case. There’s a great MinutePhysics video talking  about why this is, but long story short, standard   RGB values are grouped so that darker colors  are very close together in terms of brightness,   but brighter colors are more spread out. If  we’re assuming a linear mapping from actual   intensity to sRGB, things are going to look  darker than they should. There is a conversion   function between the two, but, well, that looks  hard. So instead, I added it as a lookup table.   The nice thing about the lookup table, is  that I can use it to add Tone Mapping for HDR   at no extra cost, by adjusting the  values according to this function.   Now the renderer preserves detail in  areas that would normally appear too dim,   or so bright that they look washed out. Accurate color representation goes a long   way towards photorealism, but there’s still  one glaring issue with this image. Anything   in shadow isn’t just dark, it’s pitch black, and  in the real world, you don’t see this very often.   Even if something isn’t being lit directly  by the sun, light can bounce off of something   else into the shadow. This is called global  illumination, as opposed to direct illumination,   and it shows up all over the place. Under trees,  along sidewalks, inside a room on a sunny day.   But compared to direct illumination, global  illumination is a lot harder to simulate. Instead   of just one or two light sources, the illumination  an object receives comes in from all directions.  Professional renderers generally use  random sampling, since if you take the   average of enough light samples, you’ll  eventually get close to the right answer,   but, since I’m not in the mood to wait a week for  the render, I’m gonna use a different technique   called radiosity. The radiosity algorithm computes  a lightmap for each object in the scene, which   is like a texture that keeps track of how much  light is hitting the object at a couple points.   First, we get the direct illumination for  every patch, and then we simulate light   bouncing around the scene by adding up the light  contributed to each patch by all the others.   Repeat that enough times to account for  the light paths with multiple bounces,   and we’ll eventually converge on a more  accurate representation of how the scene is lit.  I still render direct illumination individually  for every pixel, since the resolution of the   lightmaps make shadows look a bit chunky. But,  I can also turn off the direct illumination   completely and render the scene with just light  that’s reflected off the walls. One effect I   think helps with photorealism is color bleeding.  You can see in the global image that the red wall   contributes a bit of red light to everything else,  and so does the blue wall and the wood floor.   When we look in the fully rendered image, this  means that the spheres have a bit of a colored   tint on the sides. And this is something we can  also see in the real world. If a brightly colored   object like this box reflects a lot of light,  things around it will have a similar color.   It’s a subtle effect, but once you know  about it, you’ll see it everywhere…  For one last cherry on top, it wouldn’t be  a raytracing demo without a mirror ball,   so I threw together some code to reflect a  sight ray off of the one sphere, and then   render the scene from that direction. This is one  of the things raytracing gives almost for free.   We already have all the code necessary  to render sight rays from the camera,   all we have to do now is change  the direction of the ray.  And, there you have it. A raytraced scene with  reflection, global illumination, HDR, gamma   correct rendering, point lights and textures, all  still on a TI84 calculator, in case you forgot.   For fun, I tried my best to recreate the scene in  Blender and did a quick render on my computer to   compare. For the most part the calculator isn’t  that far off. I noticed in some areas of the   image, the Blender illumination is a bit different  from the calculator’s, notice how the mirror ball   is dimmer here, and the back wall has a bit more  red reflecting from the side. It’s not perfect,   but remember this is still night and day compared  to the graphics this calculator normally draws.  On my computer, this render took a couple  minutes to perform using a modern multicore   processor, but, how long does it take for the  calculator? Get ready for the quickness...   14 minutes. Granted, the calculator is performing  a simpler algorithm than Blender, but time-wise,   we’re in the same ballpark. The original version  took 6 hours for a lot less. This version runs at   roughly 90 pixels per second which is almost  20 times faster. Altogether we’ve got better   results in less time. I’d count that as a win. As always, there are still ways it could be   improved. When writing some of the more complex  fixed point functions, I got a little lazy   and just used the floating point library  instead of writing it myself in assembly.   I mean, I wrote my own fast multiplication and  squaring routines in ASM since those get used   pretty often, but for something like square roots,  I only use it a few times per pixel, so garbage   like this is good enough for me. There are a few  redundant calculations my renderer performs which   could probably be stored and reused, mostly  during radiosity. Cutting out all this work   would make things faster, but, there are memory  limits to consider, and what I have works for now.  It’d be really cool to add support for triangle  meshes to render more complex shapes, but that   comes at the cost of way more compute time per  pixel. Other forms of lighting like directional   light and surface lights would also make for  some interesting renders, as would refractive   materials like glass. I might get around to it  in the future and give this video a sequel (no…   three-quel? idk) so uh, subscribe or whatever.  But, if you’re the impatient type, the source   code is all going up on Github, and you’re more  than welcome to add these or other features.  I’ve already spent enough time on this as is.  So, in the interest of getting this video out,   I’m gonna have to be happy with what I’ve written.  Ti84 calculators can now do raytracing faster,   and with more photorealistic results. I don’t  know about you, but I for one am excited that   we’re now one step closer to real-time  raytraced Minecraft on a calculator...
Info
Channel: The Science Elf
Views: 1,985,209
Rating: undefined out of 5
Keywords:
Id: rY413t5fArw
Channel Id: undefined
Length: 13min 38sec (818 seconds)
Published: Thu Feb 10 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.