Rxl-tech.art
Back to Home
Case Study·Graphics Programming / GPU Systems·Internal Tool

Custom Sports Scene Mockup Renderer

A lightweight custom renderer built for sports scene mockups and as a practical environment for developing lower-level real-time graphics skills outside of a game engine.

Shadow MappingSurface ShadingFramebuffersPost-Processing

My Contribution

  • Built the rendering pipeline in C++ / OpenGL / GLSL
  • Implemented custom material and sports-surface shaders
  • Implemented shadow mapping and PCF filtering
  • Built a multi-pass framebuffer and post-process pipeline
Football scene rendered in the custom sports scene mockup renderer
01Overview

A lightweight renderer for sports mockups

Creating marketing mockups directly from our Unity applications or DCCs was unnecessarily difficult for graphic artists. The applications contained far more systems than were needed to simply stage players, customize a sports scene, and export an image.

I built a smaller renderer around that specific workflow. Rather than trying to become a general-purpose engine, it stays intentionally focused on the requirements of sports scene rendering.

The renderer handles the complete path from imported geometry to the final image: GPU mesh buffers, materials, GLSL shading, shadow mapping, render targets, post-processing, and export.

That narrower scope also made it a practical environment for working directly with lower-level graphics concepts that are normally abstracted by Unity.

Impact
Internal Tool
Platforms
Custom renderer · Desktop
Tools
OpenGL · GLSL · C++
Focus
Shadow Mapping · Surface Shading · Framebuffers · Post-Processing
03Technical Breakdown
item.01

Rendering Architecture

  • Vertex data stores position, normal, UV, and tangent.
  • Meshes are uploaded through VAO / VBO / EBO. (.obj supported only)
  • Indexed submeshes provide separate material draw ranges.
  • Model, view, and projection transforms are handled explicitly.
  • Materials bind textures and shader parameters before each draw.
  • Tangents are passed through the pipeline for tangent-space normal mapping.

The intentionally small architecture makes it possible to trace a scene from CPU-side mesh data all the way to the resulting fragment.

Rendering diagram...

Mesh to frame path

item.02

Surface Shading

The main material shader implements the surface features required by the sports scenes rather than attempting to reproduce a complete engine material system.

  • Texture sampling and UV transforms
  • Point-light illumination
  • Tangent-space normal mapping
  • Overlay textures
  • Opacity
  • Unlit surfaces
  • Surface highlighting
  • Texture exposure / contrast / saturation controls
  • Shadow receiving

Building this directly in GLSL made the relationship between mesh attributes, interpolated vertex data, texture samples, and final lighting explicit rather than hidden behind an engine material abstraction.

item.03

Outfit Variation Pipeline

A useful part of this renderer supports changing the player outfits at runtime. Instead of treating outfits as fully fixed textures, I built a small pipeline that can generate reusable player outfit assets from templates, team colors, and text overlays.

To match the mockups' quick-change needs, including different teams, player names, and numbers, repainting textures outside the renderer would slow down iteration. Keeping it inside the tool makes scene setup faster and keeps the export workflow more consistent.

Current workflow:

  • The renderer reads reusable outfit template assets for shirts, shorts, and socks.
  • It injects team colors, player names, and player numbers into those templates.
  • The result is rasterized into textures that can be assigned directly to player materials.
  • Those choices can then be stored with the rest of the scene state, so the look is easy to reuse.
cppRuntime text overlay generation for shirt customization
// Allocate an RGBA bitmap that will become the shirt overlay texture.
if (!renderIntoBitmap(shirt.width, shirt.height, output, [&](CGContextRef) {})) {
    errorMessage = "Failed to allocate shirt text overlay texture.";
    return false;
}

// Draw the player name into the bitmap at the template-defined position.
drawPlacedTextIntoBitmap(
    output.width,
    output.height,
    output.pixels,
    customization.displayName,
    scaleTextPlacement(activeNamePlacement, scaleX, scaleY),
    customization.textColor
);

// Draw the player number into the same bitmap.
// The resulting pixel buffer can then be used as a texture in the renderer.
drawPlacedTextIntoBitmap(
    output.width,
    output.height,
    output.pixels,
    customization.displayNumber,
    scaleTextPlacement(activeNumberPlacement, scaleX, scaleY),
    customization.textColor
);
item.04

Shadow Mapping

Dynamic player shadows are rendered through a dedicated depth pass.

The implementation helped expose several practical shadow-mapping problems directly:

  • Depth bias
  • Shadow acne
  • Peter-panning
  • Resolution and aliasing trade-offs
  • Light-space projection
  • PCF filtering cost
Rendering diagram...

Shadow mapping pipeline

A simple 3x3 PCF kernel softens the raw shadow-map result by sampling neighboring depth values.

glsl3x3 PCF filtering on the shadow map
vec2 texelSize = 1.0 / vec2(textureSize(shadowMap, 0));

for (int x = -1; x <= 1; ++x)
{
    for (int y = -1; y <= 1; ++y)
    {
        float depth = texture(
            shadowMap,
            projCoords.xy + vec2(x, y) * texelSize
        ).r;

        shadow += currentDepth - bias > depth ? 1.0 : 0.0;
    }
}

shadow /= 9.0;
item.05

Render Targets & Post-Processing

The final image is produced through multiple framebuffer stages rather than rendering directly to the application window.

Rendering diagram...

Framebuffer and post-process path

The export pipeline uses the same rendered scene data while allowing presentation-specific processing to happen as a final GPU pass.

  • Shadow depth render target
  • Multisampled main-scene framebuffer
  • Resolved scene texture
  • Fullscreen post-process pass
  • Dedicated screenshot and export targets
  • UI compositing
  • Color correction (contrast, saturation, exposure)
  • Background replacement for transparent export handling
glslFullscreen color correction before display or export
vec4 color = texture(sceneTexture, texCoord);

vec3 corrected = color.rgb * exp2(exposure);

float luminance =
    dot(corrected, vec3(0.2126, 0.7152, 0.0722));

corrected = mix(
    vec3(luminance),
    corrected,
    saturation
);

corrected = (corrected - 0.5) * contrast + 0.5;