← All posts

2D Visibility Polygons & Raymarched Lighting in WebGL 2.0

Master 2D visibility polygons and raymarched lighting in WebGL 2.0 to give your browser games stunning, dynamic shadows without frying budget laptops.

Top-down browser games are having a proper renaissance. Between tactical stealth roguelikes, neon-soaked zombie shooters, and cosy dungeon crawlers, players have moved far beyond static sprite stacks. Modern web gaming demands dramatic atmosphere, and nothing sells tension quite like realistic shadows snapping around corners as your character peeks down a dark corridor.

Pulling this off inside a browser tab, however, is a notorious performance trap. Push too many geometric shadow volumes to the CPU, and your frame rate tanks on an everyday office laptop. Throw brute-force pixel shaders at the canvas, and the player's cooling fan prepares for orbital launch.

Achieving snappy, cinema-grade dynamic lighting in WebGL 2.0 comes down to two distinct philosophies: geometric visibility polygons and screen-space raymarched 2D Signed Distance Fields (SDFs). Here is how they work, how they differ, and how to implement them without setting the user's browser on fire.


[ Light Source ]
       *  \  
       |   \   <- Radial Ray Sweep
       |    \
[Wall] |-----\======== (Hard Shadow Edge)
       |      \
       v       v
 [Visibility Mesh]

Direct Answer: Visibility Polygons vs. 2D Raymarching

  • A 2D Visibility Polygon is a geometric vector shape generated by sweeping radial rays from a central viewpoint towards the vertices of all local obstacles. The resulting sorted intersections form a mesh representing line-of-sight. It is mathematically exact, produces razor-sharp hard edges, and is ideal for stealth cones and line-of-sight fog-of-war.
  • 2D Raymarched Lighting is a fragment-shader technique where light paths step through an offscreen distance field or occluder texture directly on the GPU. It naturally calculates soft penumbras, ambient occlusion, and emissive materials at the cost of fill-rate overhead.

Method 1: The Radial Sweep Visibility Polygon

The classic algorithmic approach—famously popularised across developer forums and technical write-ups by Amit Patel at Red Blob Games—relies on sorting angular segments.

Instead of casting thousands of rays blindly into the void, you only cast rays towards the distinct corner points (vertices) of your level's occluding line segments.

How the Algorithm Works

1. Gather Segments: Collect all visible 2D wall segments within your light's radius.

2. Shoot Critical Rays: For every endpoint, cast three rays: one directly at the vertex, one angled slightly counter-clockwise ($-\epsilon$), and one slightly clockwise ($+\epsilon$). The offset rays allow visibility to 'shoot past' corners into open space.

3. Ray-Segment Intersections: Calculate the nearest collision point along each ray against all active wall segments.

4. Sort by Angle: Sort the resulting intersection points by their polar angle ($\theta = \text{atan2}(y, x)$) around the light source.

5. Draw the Fan: Connect adjacent sorted points to form a triangle fan (a TRIANGLE_FAN or indexed TRIANGLES buffer) in WebGL 2.0.

This approach gives you absolute precision. If a guard in a stealth game cannot see an enemy behind a crate, there is no fuzzy shader anti-aliasing guessing the outcome: the player sprite is simply outside the visibility mesh.


Method 2: 2D Raymarched Lighting via Distance Fields

While visibility polygons deliver crisp geometry, they fall short when you want soft, diffuse glows, coloured light mixing, and moody penumbras. If you want a flickering torch light that softens realistically across long hallways, you need the GPU to handle the heavy lifting.

With WebGL 2.0, we have universal access to floating-point textures, Multiple Render Targets (MRT), and texture arrays. This enables a screen-space approach:

1. Occlusion Pass: Render your scene’s walls and solid obstacles as solid black on a transparent background into a tiny, low-resolution framebuffer (often half or quarter-resolution of the canvas).

2. Jump Flood Algorithm (JFA): Run a quick compute-style pass across the occlusion buffer using fragment shaders to generate a 2D Signed Distance Field (SDF). Each pixel now stores the Euclidean distance to the nearest wall.

3. Raymarching Shader: For every screen pixel, trace a ray from the pixel back to the light source. Instead of marching pixel by pixel, take variable-sized steps based on the SDF values.


#version 300 es
precision highp float;

uniform sampler2D u_SDFTexture;
uniform vec2 u_LightPos;
uniform vec2 u_Resolution;

out vec4 fragColor;

void main() {
    vec2 uv = gl_FragCoord.xy / u_Resolution;
    vec2 rayDir = normalize(u_LightPos - uv);
    float distToLight = length(u_LightPos - uv);
    
    float traveled = 0.0;
    float shadow = 1.0;

    // Raymarch through the 2D SDF
    for (int i = 0; i < 32; i++) {
        vec2 samplePos = uv + rayDir * traveled;
        float sceneDist = texture(u_SDFTexture, samplePos).r;

        // If we hit an obstacle boundary, shadow out
        if (sceneDist < 0.001) {
            shadow = 0.0;
            break;
        }

        traveled += max(sceneDist, 0.005);
        if (traveled >= distToLight) break;
    }

    fragColor = vec4(vec3(shadow), 1.0);
}

Because you can sample distance variations along the ray, calculating penumbras (soft shadows) costs almost nothing extra mathematically—you simply track the minimum clearance distance throughout the march.


Architectural Comparison

FeatureDynamic Visibility Polygon2D Raymarched SDF
Primary ExecutionCPU-bound sorting, GPU triangle renderingFully GPU-bound fragment passes
Shadow VisualsRazor-sharp, vector-clean edgesSoft penumbras, realistic falloff
Dynamic LightsExpensive if multiple lights share scenesVery fast; multiple lights reuse one SDF
Gameplay AccuracyPerfect for direct line-of-sight checksVisual only; requires CPU readback for logic
Mobile & Integrated GPUsHighly performant on low-end hardwareCan cause fill-rate choking at 1080p+

Which One Should You Pick for Your Game?

The decision comes down to gameplay mechanics versus aesthetic goals:

  • Go with Visibility Polygons if you are designing a turn-based tactical title, a stealth game with rigid "spotted" states, or a retro roguelike. It delivers mathematical certainty, integrates directly into pathfinding logic, and runs effortlessly on every budget smartphone without drawing battery power.
  • Go with 2D Raymarching if you are crafting an immersive top-down action shooter, survival horror, or atmospheric adventure where moving lights, coloured neon signs, and moody silhouettes dominate the screen.

The sweet spot embraced by modern indie web games is a hybrid approach: calculate simple visibility polygons on the CPU for line-of-sight gameplay checks, then render screen-space SDF lighting for visual effects. That gives your browser game the tactical precision of a stealth classic alongside the visual punch of a modern PC indie.

Thanks for reading. Browse more from the Wobblox blog, or jump straight into all 100 free games.