top of page
2aeMZRM.gif
GDWC_2025_Summer-Student-Finalist-white.png

Karolina
Motužytė

Graphics programmer

Hi! I am Karolina. I am a C++ programmer that specializes in DX12 graphics, which I study at Breda University of Applied Sciences. Currently I am looking for an internship since 2026 February.

Here you will be able to see my work, both in custom engines and UE5 :)

plus_logo_c_icon_214621 (1).png
images.png
hd-unreal-engine-white-logo-icon-png-7017516949697958pnkct2kiz.png
c-icon-1820x2048-2ys190xs.png
github.png
perforce_logo_icon_248875 (1).png
GDWC_2025_Summer-Student-Finalist-white.png

Check out my work!

About the project

I worked on this project using KRenderer as a framework. I tackled these topics during the 10 weeks I worked on it:

  • Monte-Carlo

  • Denoising with temporal accumulation

  • Soft shadows

  • ReSTIR DI

  • Several optimization methods:

    • Mesh instancing​

    • Frustum culling

    • Multithreading

The Monte Carlo algorithm

0th stage - Research. This project was started with intensive research into Monte Carlo and path tracing, as I had no experience with the topic before. I cross-checked multiple reputable sources, ranging from books like Raytracing gems and PBRT to SIGGRAPH talks and various blog posts from industry professionals, and made notes I could reference later.

Screenshot 2025-11-19 134246_edited.png

1st stage - Initial implementation. I got indirect rays shooting, but because of missing terms and incorrect calculations, rays were creating a reprojection of the scene onto surfaces.

2nd stage - Reviewing and debugging. After reviewing my notes, I managed to map the formula into my code and find the missing formula variables and steps in my implementation.

Screenshot 2025-11-25 183220.png

3rd stage - Confirming results. By putting a sphere with a color of 0.2f across all RGB channels in a scene with no light sources except a white void, I could use check pix to see that that the indirect illumination was working correctly by checking the color of the sphere (approx. 0.2f across pixels with some variance caused by noise)

Screenshot 2025-11-25 165411_edited.jpg

Shadow rays and soft shadows

Screenshot 2025-12-03 152348_edited.jpg
Screenshot 2025-12-03 152348_edited.jpg

Step 1 - Shadow rays. I started with a single shadow ray per light to establish a correct baseline, then debugged common artifacts (self-shadowing/“acne” and light leaks) by adding a small ray origin bias along the normal. This worked fine, but resulted in harsh shadows, which needed to be improved.

Picture1.png
Screenshot 2026-01-08 165211.png

Step 2 - Soft shadows. I upgraded to soft shadows by treating point and directional lights as area lights and sampling multiple shadow rays around the light direction; For point lights, I calculated the sampling cone using the light radius. For directional lights, I simply used the angular radius typically used for the sun (0.27°). 
Unfortunately, this made the shadows incredibly noisy, especially point  light shadows.

Directional light shadows

Point light shadows

Optimizing

image_2026-02-14_181830017.png

Profiling: Before moving  on to denoising, I first had to fix the issue of performance.

Profiling with PIX showed me that I was CPU bound and profiling with Visual Studio showed that the issue was in the model renderer, which is where I focused all optimizations.

image_2026-02-14_182557911.png

Optimizing: I optimized the mesh renderer in 3 ways:

  • Frustum culling - not calling draw for objects outside the camera view.

  • Mesh instancing - using one draw call for repeated meshes, whose only difference are transforms and material indices.

  • Multithreading draw calls - dividing draw calls into multiple command lists by using std::future and std::async to record commands asynchronously into the command lists. 

Results:  All these optimizations lowered frame time significantly for the rasterizer (e.g., ~10 ms → ~3.90 ms) and made performance more stable.

Basic denoising

  • Temporal accumulation (GI/DI): I reduced Monte-Carlo noise by accumulating lighting over multiple frames (“Monte Carlo over time”), keeping per-frame sample counts low while still converging toward a stable result.

  • History management / rejection: To prevent ghosting and incorrect buildup, I added simple but effective history invalidation (resetting accumulation when the camera/scene/lights change or on resize), so denoising stays stable during interaction.

  • By applying it to directional illumination, as well as global, I also got rid of noisy shadows.

Screenshot 2026-01-07 201532.png

Before denoising

image_2026-02-14_203504991.png

After denoising

ReSTIR DI

  • Once GI denoising was in place, I still had a major performance bottleneck: soft shadows made direct lighting scale poorly as the number of lights increased. After researching options, I chose ReSTIR DI—not because it was the only or “perfect” solution, but because it offered the best balance of impact and learning value. It let me solve the many-lights problem now, while also building the foundation to apply the same ReSTIR concepts to GI later.

  • At a high level, ReSTIR DI works by sampling a small set of candidate lights per pixel, selecting one good “representative” sample, and then reusing these good samples across time (previous frames) and space (neighboring pixels). This stabilizes direct lighting and soft shadows without requiring a large number of shadow rays every frame.

  • With this approach, I improved performance from ~40 FPS with 5 lights to ~60 FPS with 10 lights on a lower-end laptop.

ReSTIR DI before denoising

ReSTIR DI after denoising

bottom of page