Realtime Raytracing + Stencil Shadows and Normal Mapping

Notice

This project is being reworked from scratch and in-progress updates and features can be found here.
The below post is the original version of the project!

Introduction

I recently started working on a custom, realtime rendering engine in Studio that uses basic raytracing, normal mapping, and hard shadows that come to together to mimic Roblox’s old graphics style. I plan on open-sourcing this upon completion, but am curious if anyone has recommendations or features to implement before release!

You can play the demo here. Keep in mind that performance may be impacted because of the resolution the game is running at, something that I did to improve performance in-game was locking my FPS around 120fps. Performance should still be decent even at the current resolution, but I plan to add an option to change this in the near future!

Features include…

  • Custom materials with albedo maps, normal maps, specular maps that fade in and out with distance.
  • Procedural skies and fog.
  • Parallel Luau with interlacing for performance improvements.
  • Global lighting with diffuse and specular shading.
  • Local light sources (point lights, spotlights, area lights).
  • Offline rendering mode.
  • 60fps gameplay at 128 x 100 pixels on my machine, though more optimizations to come!

Upcoming Features may include…

  • Mipmapping and antialiasing to reduce the visible noise caused by materials.
  • Textured skyboxes.
  • Portals and reflections.
  • Support for rendering avatar accessories.

Video Showcase:

Video Demo
46 Likes

Local light sources with shadows are implemented!
128x100 screen resolution:

12 Likes

Offline rendering mode!

6 Likes

Those look absolutely stunning.
(I bet this took a lot of hard work and time :sob::sob:)

But I always wonder if mipmapping would actually work and optimize this and maybe look the textures look a bit better cause of that moire pattern… Otherwise, magnificent work!!

3 Likes

Thank you! Mipmapping is totally possible but might have a performance hit due to sampling several texture reads in parallel depending on the number of mipmaps. I definitely want to try it out though!

3 Likes

This has to be the best ray tracer I have seen so far (especially open source), no lag, no artifacts, just stunning. The amount effort you put into this is unbelievable. Cant wait for release!

3 Likes

Demo out! WeirdRT - Roblox
Performance may not be great, but can be fixed by rendering the game at a lower resolution.

2 Likes

re-edited my comment [i was asking if it was gon be open sourced i didnt read :sob:]

could this be put on a surface gui tho?

1 Like

Yes it can! It’s functional with any ImageLabel.

This looks amazing! The demo is really impressive, and I love the offline renders!

How did you implement the interlacing? It’s a really nice effect, and it has so much style to it which I’d love to use in my own games (Despite being for performance improvements lol!)

1 Like

The idea is simple, but it can be a little tricky to implement at first! Each frame, the rendering engine splits the screen up into slices. I use the Y coordinate of each slice and check if it’s an odd or even number. The rendering engine switches to drawing odd slices, then even slices the next frame, and repeat. That way, the engine only has to render half of the entire screen every frame, then the other half the next frame, and repeat. The cool thing about this method is that you can also set whether you want to switch between odd and even slices each frame, or even every third or fourth slice, meaning you can increase your screen resolution even further while maintaining performance. The only downside is that pixels can warp slightly due to the fact that each group of slices are rendered in separate frames.

Before interlacing the engine drew each pixel on the screen individually, but with interlaced slices, you can allocate a parallel Actor to each individual slice, and draw the entire slice as a pixel buffer with editableImage:WritePixelsBuffer(…) in parallel.

1 Like

Update!

Project is very close to an open-source release! A few people that played the demo brought up the idea that making a custom renderer comes with a lot of potential for customizable lighting, so I ran with that idea and started working on an OpenGL-style customizable shader system!


Customizable “Shader” System

Right now, the per-pixel rendering pipeline looks something like Graphics Pipeline Overview (below). Only the default passes are implemented so far, but the system is modular and designed to allow developers to insert their own logic between any two stages.

By splitting the pipeline into distinct passes, users can hook in and write their own code that mimics how fragment shaders work in OpenGL or GLSL. You’ll be able to insert code at any step of the graphics pipeline.

This design makes it possible to write pre-processing shaders (that manipulate geometry or normals before lighting) and post-processing shaders (that modify final color or screen coordinates). For example, you could bend the screen with a barrel distortion post-processing shader for a cool CRT effect!

Graphics Pipeline Overview:


PASS PixelPass DESCRIPTION
DEFAULT PASS 1 PixelPass(…) ← Determines the worldSpacePixelPosition.
STAGE 1 Pass1(…), Pass2(…), . . . , PassN(…) ← User-written passes that take in relevant parameters from the last pass. Same for all Stages.
DEFAULT PASS 2 VisibilityPass(…) ← Determines what objects each pixel on the screen can see using a raycast.
STAGE 2 Pass1(…), Pass2(…), . . . , PassN(…)
DEFAULT PASS 3 FogPass(…) ← Calculates fog on the current pixel given user-defined and pre-calculated parameters.
STAGE 3 Pass1(…), Pass2(…), . . . , PassN(…)
DEFAULT PASS 4 GlobalShadingPass(…) ← Determines pixel visibility from global lights (the sun) and simple shading using a raycast.
STAGE 4 Pass1(…), Pass2(…), . . . , PassN(…)
DEFAULT PASS 5 GlobalLightingPass(…) ← Calculates diffuse, specular, and normal-mapped lighting from the global light source on the current pixel based on user-defined materials.
STAGE 5 Pass1(…), Pass2(…), . . . , PassN(…)
DEFAULT PASS 6 LocalShadingPass(…) ← Determines pixel visibility from local lights (pointlights) and simple shading using raycasts.
STAGE 6 Pass1(…), Pass2(…), . . . , PassN(…)
DEFAULT PASS 7 LocalLightingPass(…) ← Calculates diffuse, specular, and normal-mapped lighting on the current pixel from local light sources based on user-defined materials.
STAGE 7 Pass1(…), Pass2(…), . . . , PassN(…)
DEFAULT PASS 8 SkyPass(…) ← Calculates the procedural sky color for the current pixel based on user-defined parameters.
STAGE 8 Pass1(…), Pass2(…), . . . , PassN(…) ← This acts as the post-processing pass for the current pixel!
7 Likes

You are Literally the most talented developer I have ever seen. I don’t think people realize how incredible this is, as it literally makes all the payed raytracers look like a joke.
I beg you, please continue working on this, I can’t wait for the public release!

7 Likes

this is exceedingly impressive tbh

2 Likes

I’m so glad to hear you’re looking forward to the release! However, it’s good to keep in mind that we don’t know the situations developers of the paid raytracers face that result in their pricing decisions! I work on open source projects during my free time, but many devs make a living off of their software!

4 Likes

this looks really nice with the almost future lighting tech paired with roblox’s old stencil shadows type of appearance

howd you get it so fast on a cpu? cause im writing a path tracer and only pull 3 fps on a screen with the same resolution

3 Likes

Thank you! A lot of the optimizations are a result of multithreading, trying my best to prevent high-cost memory operations, and doing some image manipulation so that the raytracer doesn’t have to render each pixel on the screen every frame. During each frame, the engine splits up the screen (EditableImage) into rows, and attaches one parallel Actor per row. With interlacing enabled, you can include every row with an odd Y coordinate each frame, then switch to the even rows on the next frame, meaning the engine only has to render half of the rows it normally would when interlacing is off for the current frame. Each Actor then renders the pixels for that row, puts the data inside of a Buffer, and then writes the entire buffer to the current row with WritePixelsBuffer() (you could also write each pixel to the screen individually, but it will impact performance). When the frame is finished rendering, you get one half of the interlaced rows drawn on the screen, and in the next frame, the other half, so it gives the illusion that it’s all being rendered in one frame while keeping performance manageable. Another optimization technique is using native codegen and type checking whenever possible. It’s really cool that you’re making a pathtracer, but it also might be difficult to optimize enough to a playable frame rate. Right now, I’m averaging around 115fps, but it usually drops to 60-80fps depending on the screen resolution when multiple light sources are visible on the screen, and that’s typically only with 2-3 rays per pixel. You may want to look into other optimization methods that lower noise and the amount of rays that are typically fired for indirect lighting, like spherical harmonics or radiance cascades!

question: why are there studs on every single surface in the demo?

2 Likes

Good question! The engine has a custom material system that uses an underlying roblox material to render a custom material on top. Right now I have a “Studs” material that is drawn in place of the “Plastic” material. Other materials are supported as well, but it currently gets drawn on top of every surface of a part. I could add a surface property on top of each material to allow you to customize this!

3 Likes

That looks very good and it runs on my pc nicely, around 130-180 FPS.

edit: i noticed the studs disappear in shadows

1 Like