Pixelated Roblox Part 4!

Alright I would like to preface this post with the link to a previous one on the same subject.

So, here’s why another forum post is absolutely essential, I have made an incredible breakthrough that has increased rendering performance by up to 10 times!!!

Here is the current version and a video and pic of it’s capabilities in action with MANY rendering techniques used to create both pixelation, and realism.



200x200 pixel resolution at 2fps, not even that bad!

This game is now in open source, and you can download it and edit it or implement it yourself!

Now, before we used to think that 40x40 resolution was great even at the cost of 20 fps. It’s true that there’s only so much you can optimize this when it all hinges of making hundreds of raycasts a frame; However, there was one more choke point that was contributing to major lag and that was the pixels.

The frames themselves, the for loops used to go between them, and just the sheer number was also a major contributor to the lag issue. I solved this by creating what I am now calling a Paxel, because it is a stack of pixels. Using the somewhat new UIGradient object inside of my frames, I’m now able to create 10 pixels for every frame used!

Now, there’s a lot more going on here than just my invention of the Paxel, I have set up in the test world several settings you can use to alter how you view the pixelation.

There’s:

  • Resolution
  • Pixel Density (How many pixels are in a paxel, the higher the better, but unfortunately 10 is the max possible.)
  • Render Distance
  • Material Shading
  • Lighting Shading
  • Shadows
  • Skybox
  • Line by Line Rendering (Making the rendering of each frame go by a little bit slower so that the actual program speed can stay high enough that you don’t crash.)

This is truly the innovation that was required to spark this pixelation idea again, I know that a few others were trying this with me, I hope they continue now with this idea of using UIGradients to create stacks of pixels that are easier to loop through.

Finally I want to ask a question to the community, do you have any ideas on how I could possibly take the cost of each from down even by a little? Are there any speculations I can make for the code that will allow it to run faster at all? I would even be willing to sacrifice some quality for higher FPS at higher resolutions.

99 Likes

Oh additional note, I’m pretty sure that 100p resolution would be just about perfect, like literally for any situation where you would want to pixelate roblox, I’m pretty sure 100p would be mid range for what you want.

Since I can, with my current system, get 20fps at 100p on lowest settings do any of you think you could implement this into a real game?

4 Likes

Oh haha I’m so sorry but I forgot an additional note that is material shading is persistent, as you can see in the video; However, it is more than statically persistent is is dynamic with the position and orientation of that part itself!!! You can rotate, move, and resize a part, but it’s material shading will persist in a way that you would imagine the texture of a part to stick to it.

This is really cool, I love this! Definitely helpful for those who want to make retro-style games!

1 Like

Finally I want to ask a question to the community, do you have any ideas on how I could possibly take the cost of each from down even by a little? Are there any speculations I can make for the code that will allow it to run faster at all? I would even be willing to sacrifice some quality for higher FPS at higher resolutions.

multithreading will make the process a few times faster per frame
wait for parallel lua to come out

4 Likes

OMG You are SO right!

I was looking into multi-threadding while making this actually. I found that it’s not a thing yet, and in fact coroutine and spawn do not grant a speed advantage.

coroutines aren’t multithreading. a single core is still utilized, only the code is ran independently
your compression method is also interesting, funny roblox gave us this advantage indirectly via uigradient

2 Likes

At last, after all these years, we can finally lower the quality of Roblox graphics even more!

In all seriousness: nice work! I could see this being implemented in retro 3D games. Hopefully you will find some way to further improve the performance.

Good luck!

1 Like

This would be amazing for so many games, really hope it goes open source

1 Like

I TOTALLY plan to make this an easy to implement open source project!

As a matter of fact it is currently literally just a single ScreenGui. When you put it in your game it just works, and it will stay that way! You’ll be able to plop this into your game and not even touch the code!

3 Likes

Yeah it really improves performance too! Yeah I knew it was all on one core, but I was hoping it would provide at least a little extra speed, it did not ahaha.

Alright, I’m totally ready to make this open source, and I would love to hear critique. If you can find even a single line that could be causing a fraction of lag please tell me!

In this reply I will be demonstrating the use of each script and documenting how they’re written and perform.

image
This image depicts everything there is with this, and the Settings and FPS parts are entirely optional, you can use them to find the settings you want, then delete them.

The GuiRemover script just get’s rid of all default roblox UI, this can also be removed if you really want.

The Background Frame itself really just serves to be a black background, but again this can also be removed and will not affect the pixelation at all. Really you can think of it as a filter on the screen, as long as you still have that Main Frame, it will function.

Getting into the Main Frame!

There are two scripts, one is a regular local script, and the other is a module script used for creating and saving your screen settings.

The ModuleScriptFrameCreator’ has the following properties and methods:

Properties: [Property Datatype] FrameCreator.Property (Limits)

  • [Integer] FrameCreator.Resolution (Current Limits: 1 to 200)
    The Resolution of the image to be rendered, it is in the form of X by X pixel resolution, X being the number given. It is read only! There is a method to change it, use that!

  • [Number] FrameCreator.RenderDist
    The length that each raycast for each pixel will travel before stopping and defaulting to sky. It is read only! There is a method to change it, use that!

  • [Integer] FrameCreator.PaxelSize (Unfortunate Limits: 1 to 10)
    The number of pixels that will exist in each paxel, this number is automatically calculated currently, it will set itself to the highest number between it’s limits that is a modulo of the Resolution. It is read only and updates automatically!

  • [Number] FrameCreator.Center
    The exact center of the Main Frame, used to generate the PixelToWorldspace that provides the direction of the raycast. It is read only and updates automatically!

  • [Boolean] FrameCreator.Materialization
    Dictates whether or not to perform the color calculations to generate persistent materials onto the projected parts, Materials are currently performed by a random number generator whos seed is determined by a calculation based on the position and orientation of the part and raycast position. The system is very good, but will take a lot of number playing to make all of the materials look good. This property is Read and Write and will update as soon as it’s changed without needing to recreate the frame.

  • [Boolean] FrameCreator.Shading
    Dictates whether or not to calculate the proper shading for each pixel based on it’s cross product of the angle of the normal and the direction toward the sun, it takes close to no additional processing power, and adds a lot of detail. This property is Read and Write and will update as soon as it’s changed without needing to recreate the frame.

  • [Boolean] FrameCreator.Shadows
    Dictates whether or not to cast an additional ray for each pixel that hits a part to test if it is blocking the sun, in which case a shadow will be drawn by making that pixel darker. This takes a considerable amount of processing power, but less than the power it takes to calculate each ray regardless, I’m unsure why. This property is Read and Write and will update as soon as it’s changed without needing to recreate the frame.

  • [Boolean] FrameCreator.LineByLine
    Dictates whether or not to partition the pixel calculations between Stepped loop intervals. It will, instead of calculating each frame and then drawing it all at once, calculate 10 lines of the frame 10 I have found to be the best number in this case for performance at a time and then display them in order until reaching the bottom and restarting at the top. This allows for higher Game FPS at the expense of the FPS of the Main Frame. In short, it will allow you to not crash regardless of what you make the settings. This property is Read and Write and will update as soon as it’s changed without needing to recreate the frame.

  • [Boolean] FrameCreator.Skybox
    Dictates whether or not to calculate a gradient color to represent the sky when a raycast did not hit a part, similarly to Shading, it takes very little processing power, and adds good detail. I am considering also making the sun visible, and maybe clouds as well. This property is Read and Write and will update as soon as it’s changed without needing to recreate the frame.

Methods: [Return Datatype] FrameCreator.MethodName([Parameter Datatype])

  • [Table of ColorSequenceKeypoint] FrameCreator.CreateKeyPoints(Table of Color3)
    When given a table of 1 to 10 Color3 values it will return a table of ColorSequenceKeypoint values that will allow you to create a ColorSequence value of distinct sharp pixels, hard edges and no gradient. It’s used to calculate the color for each paxel.

  • [Sync Void] FrameCreator.SetResolution([Integer]Res, [Number]Render)
    When given a resolution or Res, as well as an optional render distance or Render, it will set the Resolution and RenderDist accordingly as well as recalculate the PaxelSize and Center and then it will delete the current frame and re create it with our new settings. This method is also how a first frame is initialized. Additionally, it’s synchronous; I did this so that you could be certain the code beneath calling this method won’t run until the frame is ready.

  • [BaseGui Frame] FrameCreator.GetPaxel([Integer]X, [Integer]Y)
    When given an X in the paxel space, not pixel, and a Y assuming these numbers are within the bounds of the frame it will return the BaseGui Frame Object of that paxel, this can then be used later to set each pixel color.

  • [Async Void] FrameCreator.SetPaxelColors([BaseGui Frame]Paxel, [ColorSequence]ColorSeq)
    When given a paxel or Frame in the form of a BaseGui Frame Object obtained via the GetPaxel() method as well as a ColorSequence or ColorSeq obtained via the CreateKeyPoints() method this method will set the color of the paxle’s UIGradient object to ColorSeq. This is used to set the color of each paxel and each pixel individually. It is not synchronous, code will not wait for it to finish to continue.

Alright! That pretty much covers everything there is worth knowing about the FrameCreator module.

The only thing left to go over is the RayCaster script which does all of the heavy lifting and calculations for each pixel on the screen as well as handles what to do with all of the settings given in FrameCreator.

Really it’s not as formatted as FrameCreator is and frankly not worth me going over is as much detail, but there are some major point I want to explain in case people are confused.

The Material Persistence Calculations:

local MaterialColorOffset = {
    {Enum.Material.Brick, -10, 0, 10, nil, function(Position)
	    return Position.X * Position.Y * Position.Z
    end},
    {Enum.Material.Grass, -10, 0, 0.01, nil, function(Position)
	    return Position.X + Position.Y + Position.Z
    end},
    {Enum.Material.Cobblestone, -10, 0, 1, {31, 16, 6}},
    {Enum.Material.Pebble, -20, 0, 10, nil, function(Position)
    	return Position.X * Position.Y * Position.Z
    end},
    {Enum.Material.Concrete, -5, 0, 5, nil, function(Position)
    	return Position.X * Position.Y * Position.Z
    end},
    {Enum.Material.Metal, -3, 0, 3, {-30, -30, -30}, function(Position)
    	return Position.X + Position.Y + Position.Z
    end},
    {Enum.Material.Neon, 0, 0, 1, {81, 81, 81}, function(Position)
    	return 0
    end},
    {Enum.Material.Sand, -10, 0, 0.1, nil, function(Position)
    	return math.sin(Position.X + Position.Z) * 5
    end},
    {Enum.Material.WoodPlanks, -20, 0, 20, nil, function(Position)
    	return Position.X
    end},
}

This lookup table defines how each material will behave and be calculated. It isn’t a dictionary so it must be hard to understand, allow me to explain.

The MaterialColorOffset table contains tables of the following format to describe the calculation behavior of each material:

{
    Material,
    Lowest Brightness Offset,
    Highest Brightness Offset,
    Brightness Offset Step Size,
    {
        Red Offset,
        Green Offset,
        Blue Offset
    },
    Material Seed Calculation()
}
  • The first value given is the Material that this data refers to.

  • The second value is the Lowest Brightness Offset. The Brightness offset is how materials are performed, using math to calculate particular areas to shade in order to recreate each material. This value represents the lowest it will randomly generate to subtract from the brightness.

  • The third value is the Highest Brightness Offset. This value represents the highest it will randomly generate to subtract from the brightness of each pixel.

  • The fourth value is the Brightness Offset Step Size. This is essentially in what steps can the random number between Lowest Brightness Offset and Highest Brightness Offset be calculated, assume that your lowest is -10, and your highest is 0, if your step size is 1 then you will get 10 possible random numbers, -10 through 0; However, if your step size were 5 then you would only have 3 possible random numbers, -10, -5, and 0. These numbers are in the RGB 0 to 255 space.

  • The fifth value is that Color Offset. This is represented by a table of 3 numbers all of which are in the RGB 0 to 255 space. They represent the R, G, and B offset respectively, they will be added to the final color to tint the material, as some roblox materials change the color of the part slightly.

  • The sixth and final value is the Material Seed Calculation. This is a function that will receive a parameter in the form of a Vector3 position value. This position value is calculated with the following code:
    Raycast.Instance.CFrame:inverse() * CFrame.new(Raycast.Position)
    ‘Racycast’ being the current cast for each pixel. Using the calculation, the material shading is able to be persistent with both position and orientation of any part.

That is all folks!
To anyone that actually read all of that, uhm great now you’re 100% equipped to work on this with me!
To everyone else, the majority of you, I hope that my use of bold and italics will help you skim through it when you need specific information.

Thanks all, and I hope we can make this efficient enough to put into any project!

6 Likes

I have so many ideas how to use and abuse this hehehehe

1 Like

Okay so I really hate to drag this post on and keep bumping it, but I don’t really have a place to post updates so this reply will serve as a board for me to add stuff as it is found.

From this point forward all updates will be added as edits to this reply so that this board can stay as clean as possible, thanks for your patience.

Updates:

  • Raycasts are not as expensive as you think! I found this out by mimicking raycast results with a simple table of pure results. I found that with these static results performance was only increased by about 40%. This means that 60% of the work right now is just in rendering and calculations, most of which are for the ray casts but still. It makes sense considering my paxels took this idea from a funny joke to a viable pixel filter for games. What needs to be managed now is optimizing the renderer. I need to find a way to set each pixel in a faster, less loopy manner. I believe making the renderer more efficient is the only way to proceed, if I can get it to a better order then I can reiterate my calculations, maybe even make them less expensive. [11/5/20 - 1:54am]

  • I’ve added transparency! It took me so long to figure out all of the color combining and everything, I’m so glad it’s done with.image image
    Thank you to this post for basically solving my issue with the colors. Fixed bug where overlapping transparencies would infinite loop!

  • Added fog as well as the sun. When Skybox is enabled you’ll be able to see the sun, and now you can alter fog settings.

  • Upgraded the sun. I made the sun look a little bit better.

  • Implemented @Ethanthegrand14’s reflectance!! Now mirrors are a simple click away, and it’s very cost efficient!

6 Likes

Now that is awesome. I haven’t tried this out yet. But it looks much better and faster than my old raycast pixelater.

It’s unbelievable that you got this to run so smoothly. You even implement textures into it! I mean almost everyone thought that renderers would cause too much lag or would NEVER be this fast.

Keep up the great work!

1 Like

This is so interesting! I hope this can be used in games in the future

I have found a bug with your renderer.

image

It appears the script breaks when I attempt to look at these parts that I created:


ALSO


You should make it so that transparent objects work with this renderer.

Hey! Uh so quick thing here sorry. That’s not an actual bug haha, I’m working on it at this very second and happen to have some things disabled, causing the bug you’re experiencing.

Sorry about that!!

2 Likes

Oh, lol. What are you working on?

I’m currently trying to make it scale with FOV. You probably haven’t tested this yet, but this only works with 70 FOV so far. Actually could you help me out with this one?

So there is a simple calculation I’m doing to get the world position of a pixel, it goes like this:

Camera.CFrame * CFrame.new((x + i) - FC.Center, FC.Center - y, -(FC.Resolution / 1.4))

x being the xth paxel in the row.
y being the yth pixel in the column.
i being the ith pixel in the paxel.
FC.Center being the center of the Frame.
FC.Resolution being the X by X pixel resolution.

finally 1.4 It is the number that I found via brute force a while ago that works with 70 FOV.

It’s unfortunately not linear, as in the number that replaces 1.4 does not scale linearly with the FOV.
Here is my data from a few tests.

FOV     | Divider
_________________
100     |    2.38
90      |       2
80      |    1.68
70      |     1.4
60      |    1.15
52.25   |    0.98
50      |    0.93
40      |   0.725
35      |    0.63
30      |   0.535
20      |   0.353
17.25   |   0.304
10      |   0.175

Any chance you can figure out the correlation among these numbers?