Update content & design (#75)
All checks were successful
Deploy to Pages / build (push) Successful in 2m58s

Reviewed-on: https://home.schmelczer.dev/git/git/andras/schmelczer-dev/pulls/75
This commit is contained in:
Andras Schmelczer 2026-05-28 16:20:12 +01:00
parent 0be50b6c24
commit b554e92e9f
83 changed files with 2995 additions and 723 deletions

View file

@ -1,6 +1,6 @@
---
title: Tile-Based Optimization for 2D SDF Ray Tracing
description: How SDF-2D used signed distance fields, dynamic shaders, and tile-based rendering ideas to make 2D ray tracing run well in the browser.
title: A 2D Ray Tracer for the Browser, Tuned for the Phone in Your Pocket
description: 'My BSc thesis library. The mobile GPU constraint shaped the architecture: tile-based passes, deferred shading, shaders generated per scene and device.'
date: 2026-05-08
projectPeriod: 'Autumn-Winter 2020'
thumbnail:
@ -9,9 +9,10 @@ thumbnail:
tags: ['graphics', 'web', 'systems']
featuredOrder: 3
role: Library author
stack: ['TypeScript', 'WebGL', 'WebGL2', 'Signed distance fields']
scale: Browser library with mobile-oriented real-time rendering and reusable demos
outcome: Reusable NPM package and thesis project for efficient 2D SDF rendering
stack:
['TypeScript', 'WebGL', 'WebGL2', 'Signed distance fields', 'Dynamic shader generation']
scale: Browser library, mobile-targeted, real-time on consumer GPUs, both WebGL1 and WebGL2 paths
outcome: An NPM package and BSc thesis; the renderer behind the decla.red multiplayer game
audience: recruiter-relevant
links:
- label: NPM package
@ -25,43 +26,31 @@ media:
- type: image
src: ./_assets/sdf2d.jpg
alt: Browser demo page showing SDF-2D scenes rendered with soft lighting effects.
caption: SDF-2D was built as a reusable TypeScript library rather than a single demo.
caption: SDF-2D shipped as a TypeScript library, not a one-shot demo. That distinction shaped most of the design.
---
SDF-2D was my attempt to make a small, reusable browser library for 2D scenes rendered with ray-tracing techniques. The rendering is based on signed distance fields, where geometry can be represented as functions that return the distance to the nearest surface.
Winter 2020, BSc thesis deadline closing in, and the thing had to run acceptably on my advisor's laptop the day he graded it. That single shipping pressure exposed every lazy assumption in the architecture and picked the design: tile-based passes, deferred shading, shaders generated per scene and per device. A 2D ray tracer in the browser via signed distance fields: soft shadows, smooth reflections, no triangle mesh. The other half of the thesis was [decla.red](/articles/declared-shared-simulation-code/), the multiplayer game that proved the renderer survived a real game loop.
The interesting part was not the basic idea. Signed distance fields are a known technique. The interesting part was making the approach fast and reusable enough for browser demos, including on mobile devices.
## What "mobile GPU" actually meant
The project became one half of my BSc thesis, together with the multiplayer game `decla.red`, which used the rendering library in a real interactive setting.
A 2D SDF ray tracer is conceptually simple: for each pixel, march along a ray, sample the distance field, accumulate light. The implementation that works on a desktop NVIDIA card spends so much per pixel that a mobile GPU melts. So the design problem was never "can SDFs do soft shadows" (yes, easily), it was "what work can I avoid per pixel without giving up the look."
## The Problem
Three constraints did most of the design work:
Ray tracing and distance-field rendering can produce appealing 2D lighting and reflections, but a straightforward implementation spends too much work per pixel. A browser library also has to deal with device variation: WebGL capabilities, shader limits, mobile GPUs, and the overhead of generating scenes dynamically.
- **WebGL1 and WebGL2 both supported.** No "modern browser only" cheat. That ruled out anything that needed compute shaders or storage buffers.
- **No per-scene hand-tuned shader.** This is a library; users plug in their own scene descriptions. The renderer has to compile something appropriate at runtime.
- **Acceptable on a phone.** Not "good when the user owns the right hardware." It had to be acceptable on the laptop my advisor used to grade the thesis.
The goal was not to render one hand-tuned scene. The goal was a library with a simple API, reusable scene definitions, and real-time behavior.
## How it actually runs
## Constraints
- **Tile-based rendering.** Group pixels and reason about them together. Most regions of a frame share the same nearby geometry, so you can early-out enormous swathes of pixel work if you know the tile's bounds. This was the single biggest perf win.
- **Deferred shading.** Separate "find the surface" from "shade the surface." Shadow casting and reflections need the same geometry queries; doing them once per pixel and reusing the result was worth the extra texture bandwidth.
- **Generated shaders per scene and device.** If a scene has no reflective surfaces, the generated shader doesn't carry the reflection path. If the device only supports WebGL1, the shader doesn't reach for WebGL2 features. Static feature flags do this badly; runtime generation does it well.
- **TypeScript scene descriptions, no DSL.** I prototyped a small DSL for SDF authoring and threw it away. Pride's expensive. Users describe scenes in plain TypeScript and the library compiles them down. A DSL would have meant one more language to teach and one more compiler to debug.
The library had to support both WebGL and WebGL2. It had to run acceptably on phones. It had to avoid shipping scene-specific shader code by hand. And it had to expose an API that felt like a rendering library rather than a shader experiment.
## Held up, didn't hold up
Those constraints pushed the implementation toward generated shaders and capability-aware rendering paths.
## Design
The main optimization was inspired by tiled renderers. Instead of treating the entire screen uniformly, the renderer could reason about groups of pixels and avoid unnecessary work where possible.
That was paired with deferred shading and dynamic shader generation. Dynamic generation mattered because scenes and devices differ. If a feature or operation was not needed for a given scene or device, the generated shader could avoid carrying that cost.
The API was deliberately kept in TypeScript. That made the library easier to package, document, and reuse in projects that were already browser-first.
## What Worked
The project worked best when the library boundary was respected. A good demo can hide a messy implementation. A reusable package cannot. The API had to explain the rendering model without making every user think like a shader compiler.
The mobile constraint also improved the design. It forced performance work to be structural rather than cosmetic. When a technique works only on a powerful desktop GPU, it is easy to mistake headroom for good architecture.
## What I Would Change
Today I would write more instrumentation around shader variants and device behavior. The project had many optimizations, but stronger profiling output would have made tradeoffs easier to explain and compare.
I would also document the rendering pipeline with diagrams. The ideas are visual, and the explanation should be too.
- **Held up:** the mobile constraint forced structural perf work instead of cosmetic perf work. When something only runs on a desktop GPU you mistake headroom for good architecture, and the rude awakening comes from a user.
- **Held up:** keeping the library boundary clean. A demo can hide a messy implementation; a published package can't.
- **Didn't:** I had no instrumentation around shader variants. Today I'd ship a small `?debug=1` overlay that prints exactly which shader got compiled for that session and why.
- **Didn't:** the docs are words about ray marching. The ideas are visual; the explanation should have been too. Diagrams next time.