Week 164: Voxel Cake

This week was dominated by strawbery shortcake.

Last week I released Apples and finished the shader feature on SugarcoatSo it was time to start something new!

Ayla (@bridgs) had recently asked me to test out her new library Simulsim. This library lets you make entity-based games and then simulate server-client behavior locally, with as many simulated clients as you like and with simulated latency.

So I came up with an entity-based game idea! Paku-Boisu would be a eat-all-you-can game in side view, with gravity, and each player would grow bigger as they eat things, until they eat another player which is smaller than them, at which point the eater would gain a point, but also regurgitate everything they ate and become small again.

And for the graphics of this game, I had a fancy idea!

cake_slice

The game’s food (to be mostly pastries and candy) would be rendered as 3D voxel models, much like my Pico-8 voxel experiments, except this time their would be… light!

Yes, I mean, I really want the graphics for this game to feel particularly sugar-y and shiny. Shiny in the literal sense. So I had no choice but to pull it off.

cake_slice5

Even though I already had an idea of how to achieve the lighting, the first concern was still to reimplement my voxel rendering in Love2D. (using Sugarcoat of course)

In fact, at first I wasn’t even going to go with voxels, but with sprite stacks instead! Sprite stacking is a technique consisting of superposing sprites which act as layers of a 3D model. Using y-axis offsets and rotation on the sprites, the result can be surprisingly smooth and satisfying to watch!

But the limitations of this technique became obvious quite fast. I wanted full 3D rotation on my models, thinking I could just make that y-axis offset into a xy vector, and using some clever squashing and rotation on the sprite layers. This sounded great, until I realized what a sprite squashed to a height of 0 looked like, or rather, didn’t. On a profile view, my sprite-stacked model was invisible. All the sprite layers had a height of 0, which made sense because of the 3D rotation, and were thus not displayed at all. Even without that, at certain angles, lines of the sprite would disappear, removing important details on the tiny models.

So sprite-stacking was not the way to go. No big deal, I happened to have dealt with fake 3D in the past, and here’s one technique I have particular experience with: voxels.

cake_slice8

And so I went through the usual process of getting something to render, then fix it, and then make it considerably faster via a variety of techniques.

The voxel models are taken from the layers drawn on a spritesheet (exactly like it was going to be sprite-stacked) and put into a 3-dimensional Lua table construct, consisting of a table of layers, themselves being tables of lines, which are tables of individual cells, simply storing the color indexes in the used palette. That data is then displayed on-screen using a bunch of for-iterators to go through the layers and lines, and some trigonometry, to draw tiny squares, or even just pixels, where the voxels are.

The first step of optimization consisted of not processing empty voxels, empty lines, and empty layers. In the voxel data initialization, any empty table (layer or line) or voxel value is to be set as ‘nil’. When rendering the voxels, we can check whether anything is nil with a simple ‘if data then’, which is fast and efficient.

Then it’s a matter of eliminating any voxel that would never show-up. My sprite layers were not originally drawn with optimization in mind, because that would be quite annoying to deal with. It’s much better to have the program take care of it, so I wrote some code that would check for surrounding cells for every single voxel in the model. If a cell has all its neighbors, it’s never going to show up on-screen, not at any angle, so it might as well not exist! Set it to nil and let the CPU enjoy its new-found free time. (for now)

Finally, caching values was the last optimization step. Since with have a for-loop in a for-loop in a for-loop, it can make quite a difference to move any calculations back into parent for-loops (or, best, outside for-loops completely) wherever possible. For example, the only trigonometry values we need throughout the rendering are the cosine and sine of all 3 axis rotations, put these 6 values in local variables at the start of the rendering function and never call trigonometry functions again during the rendering process. Likewise, ‘voxel_data[z][y][x]’ should never appear anywhere. Instead, you want this:

for z = z_start, z_end, z_dir do
  local layer = voxel_data[z]
  
  for y = y_start, y_end, y_dir do
    local line = layer[y]
    
    for x = x_start, x_end, x_dir do
      local cell = line[x]
      
      --[[ Render cell here ]]--
    end
  end
end

Table accessing can have quite an impact when you’re doing them 3 * 16^3 = 12280times per model render, reducing it is hardly an option. Also note that the for-loop initializers (z_start, z_end, etc) were also cached ahead of this piece of code, since they will be consistent throughout the rendering.

cake_slice13

Now we can have plenty of cake on the screen! So it’s time to move on, finally, to lighting.

My idea was this:

  • On the voxel data initialization, check for each voxel whether its direct neighbors, on the 3 axes, exist and make up a bitmask from that. The bitmask would have 6 bits for the 6 direct neighbors, with 2 bits left for storing the cell’s color.
  • Before the rendering for-loops, calculate a light value for each of the 6 facing directions, using the model’s rotation. Then for every bitmask available (256 since we’re using all 8 bits)add the light values for all the facing directions where a neighbor doesn’t exist, and use palette swaps to correspond the bitmask to a color in a lighted color-ramp, using that light value.
  • Draw the voxels using their bitmask as color and the palette swaps will take care of the rest.
  • Profit???

And… this worked out. Perfectly. When I got the first colored result, with light values way too intense, I was literally jumping with joy. My idea was working, it looked just like I wanted, there was no major issue of any sort with any of the steps, and even the performance was still pretty good!

cake_slice21

Having 2 bits for color in the voxels, I can use up to 4 color ramps for each model, and then use whichever I want when rendering them. Using additional color ramps (btw I’m using the Lux3K palette from @ThisIsLux) I could get different colors of strawberries for example. But I could just as well change the color of the cream or the dough, and even add an additional color-ramp since this model only uses 3!

This is what most of the week was spent working toward. And I’m really happy with it! Now… I just have to make the rest of the game!

You can check out more gifs of the process towards the final result in this little Twitter tour.

 

After all this, on Friday afternoon I made a small update for CursorPainters! I added a palette editor, using Castle’s new integrated UI panel, and a cool shader using Sugarcoat’s shader integration.

Castle’s UI panel feature is really interesting to set up. Basically you just have to implement the ‘castle.uiupdate()’ function where you call for example ‘castle.ui.button(label, { onClick = callback })’ to get the panel to show up with a functional button in it. You can check out the API for this UI panel feature over here. I believe it might be subject to minor changes as it is still work-in-progress, but it already feels very solid all around.

The shader is a variation of the one I made for @Liquidream last week! I turned the scanline effect into a… scansquare (?) effect by taking both y and x coordinates into the maths. I also removed the distortion on the moving ray and toned down the glow effect quite a bit. I think it feels like some sort of cool old terminal and I really like it!

You can still play CursorPainters over there!

And that’s it for this week! It was a good week!

Thank you so so much to my Patreon supporters! Here are the names of all the Accredited tier supporters and up!

★Joseph White, ★Spaceling, rotatetranslate, Anne Le Clech, bbsamurai, HJS, Austin East, Meru, Paul Nguyen, Dan Lewis, Dan Rees-Jones, Reza Esmaili, Joel Jorgensen, Marty Kovach, Flo Devaux, Thomas Wright, HERVAN, berkfrei, Tim and Alexandra Swast, Jearl, Michael Leonardi, Johnathan Roatch, Raphael Gaschignard, Eiyeron, Sam Loeschen, Andrew Reitano, amy, Andrea D’Amico, Simon Stålhandske, yunowadidis-musik, slono, Max Cahill, hushcoil, Gruber, Pierre B., Sean S. LeBlanc, Andrew Reist, Paul Nicholas, vaporstack, Jakub Wasilewski

This new week, I’m working on making the actual game which will use my new voxel tech! It should be a rather simple game so I’m not expecting it to take forever to make. I’m even tempted to say it will be finished by the end of the week, but I really wouldn’t trust myself with that. So we’ll see!

Have a superb week!

Take care!

TRASEVOL_DOG

Leave a comment

Blog at WordPress.com.

Up ↑