# How to Implement Custom Rendering Using Shaders and the ShaderWrapper in Manim

> Implement custom rendering in Manim using ShaderWrapper. Encapsulate GLSL shaders, vertex buffers, and uniforms for high-performance graphics without raw OpenGL boilerplate.

- Repository: [Grant Sanderson/manim](https://github.com/3b1b/manim)
- Tags: how-to-guide
- Published: 2026-02-28

---

**Use Manim's `ShaderWrapper` class to encapsulate custom GLSL shaders, vertex buffers, and uniforms, allowing high-performance custom rendering without managing raw OpenGL boilerplate.**

Manim’s rendering engine leverages ModernGL to communicate with the GPU, and the **ShaderWrapper** abstraction in the `3b1b/manim` repository provides a Pythonic interface for injecting custom shader code. By extending either the base `ShaderWrapper` for general-purpose rendering or `VShaderWrapper` for vectorized graphics, you can implement particle systems, custom lighting, or procedural geometry while Manim handles context management, buffer allocation, and the render loop automatically.

## Understanding the ShaderWrapper Architecture

The shader system centers on two primary wrapper classes and a suite of utility functions that manage GLSL compilation, vertex attribute binding, and efficient uniform updates.

### ShaderWrapper Base Class

The **`ShaderWrapper`** class in [`manimlib/shader_wrapper.py`](https://github.com/3b1b/manim/blob/main/manimlib/shader_wrapper.py) provides the foundation for custom GPU rendering. It creates a ModernGL `Program` from vertex, geometry, and fragment shader files, constructs a VBO/VAO pair for the supplied vertex data, and manages texture binding and uniform updates. The class orchestrates the full rendering lifecycle: `init_program_code()` loads GLSL sources, `init_program()` compiles the ModernGL `Program`, and `render()` executes the draw call.

### VShaderWrapper for VMobject Rendering

For complex vector graphics requiring fill and stroke rendering, **`VShaderWrapper`** (also in [`manimlib/shader_wrapper.py`](https://github.com/3b1b/manim/blob/main/manimlib/shader_wrapper.py)) extends the base class with specialized functionality. It compiles four separate shader programs—**stroke**, **fill**, **border**, and **depth**—and manages a shared off-screen framebuffer for the winding-number fill technique that Manim uses to render complex shapes with holes and self-intersections correctly.

### Utility Functions for Shader Management

Located in [`manimlib/utils/shaders.py`](https://github.com/3b1b/manim/blob/main/manimlib/utils/shaders.py), these helpers streamline shader development:

- **`get_shader_code_from_file`** – Locates shader files in the Manim shader directory and processes `#INSERT` directives for reusable code snippets.
- **`image_path_to_texture`** – Converts image files into ModernGL textures for fragment shader sampling.
- **`set_program_uniform`** – Updates uniform values while caching previous states in `PROGRAM_UNIFORM_MIRRORS` to avoid redundant GPU writes.

## Step-by-Step Implementation Guide

Implementing custom shaders requires defining structured vertex data, initializing the wrapper with shader paths, and managing uniforms through the scene loop.

### Creating a Custom ShaderWrapper

Instantiate `ShaderWrapper` with the ModernGL context from the camera, the path to a folder containing `vert.glsl` and `frag.glsl` files, and any initial uniforms:

```python
from manimlib.shader_wrapper import ShaderWrapper

self.shader = ShaderWrapper(
    ctx=self.camera.context,
    vert_data=self.vert_data,
    shader_folder="shaders/custom_effect",
    mobject_uniforms={"time": 0.0, "amplitude": 1.0},
    depth_test=False,
)

```

The wrapper automatically calls `init_program_code()` to load GLSL sources and `init_program()` to compile the ModernGL `Program`.

### Defining Vertex Data and Attributes

Manim uses **structured NumPy arrays** to define vertex attributes. The `moderngl.detect_format` function automatically translates NumPy dtypes into OpenGL attribute format strings:

```python
import numpy as np

dtype = [
    ("position", "3f"),
    ("color", "4f"),
    ("uv", "2f")
]
verts = np.zeros(100, dtype=dtype)
verts["position"] = np.random.rand(100, 3)
verts["color"] = np.random.rand(100, 4)

```

The `read_in()` method concatenates vertex data, while `generate_vaos()` creates `VertexArray` objects that bind the VBO to the shader program attributes. Ensure your GLSL attribute names match the NumPy field names exactly.

### Managing Uniforms and Textures

Update uniforms dynamically through the scene loop using the wrapper's `mobject_uniforms` dictionary:

```python
def update_uniforms(mobject, dt):
    mobject.shader.mobject_uniforms["time"] += dt
    mobject.shader.mobject_uniforms["resolution"] = [
        mobject.camera.frame_width,
        mobject.camera.frame_height
    ]

self.add_updater(update_uniforms)

```

For textures, provide a mapping of sampler names to image paths:

```python
self.shader = ShaderWrapper(
    # ... other arguments ...

    texture_paths={"noise_texture": "assets/noise.png"}
)

```

The `init_textures()` method loads these via `image_path_to_texture` and binds them to texture units before rendering. The `update_program_uniforms()` method efficiently updates GPU state while avoiding redundant writes through the `PROGRAM_UNIFORM_MIRRORS` cache.

## Complete Code Examples

### Example 1: Point Cloud with Custom Vertex Shader

This example creates a `PointCloudMobject` using the base `ShaderWrapper` to render 500 animated points with per-vertex colors:

```python
from manimlib import *
import numpy as np
from manimlib.shader_wrapper import ShaderWrapper

class PointCloudMobject(VMobject):
    def __init__(self, points, **kwargs):
        super().__init__(**kwargs)

        # Structured array: position (3f) and color (4f)

        dtype = [("position", "3f"), ("color", "4f")]
        self.vert_data = np.array(list(points), dtype=dtype)

        self.shader = ShaderWrapper(
            ctx=self.camera.context,
            vert_data=self.vert_data,
            shader_folder="shaders/simple_vert",
            mobject_uniforms={"time": 0.0},
            depth_test=False,
        )
        
        # Update time uniform each frame

        self.add_updater(lambda m, dt: m.shader.mobject_uniforms.update(time=m.time))

    def draw(self):
        self.shader.pre_render()
        self.shader.render()

class CustomShaderScene(Scene):
    def construct(self):
        # Generate 500 random points with random colors

        pts = [(np.random.rand(3), np.random.rand(4)) for _ in range(500)]
        point_cloud = PointCloudMobject(pts).scale(3)
        self.add(point_cloud)
        self.wait(5)

```

The `ShaderWrapper` automatically detects the vertex format from the NumPy dtype, creates the VBO and VAO, and handles the render loop integration through the `draw()` method.

### Example 2: Custom Fillable VMobject with VShaderWrapper

For objects requiring complex fills and strokes, use `VShaderWrapper` to leverage Manim’s winding-number fill technique:

```python
from manimlib import *
import numpy as np
from manimlib.shader_wrapper import VShaderWrapper

class WavyRect(VMobject):
    def __init__(self, width=4, height=2, **kwargs):
        super().__init__(**kwargs)

        # Define vertex structure for VMobject rendering

        verts = np.zeros(6, dtype=[
            ("point", "3f"),
            ("stroke_rgba", "4f"),
            ("stroke_width", "f4"),
            ("joint_angle", "f4"),
            ("fill_rgba", "4f"),
            ("base_normal", "3f"),
        ])
        
        # Populate rectangular mesh data

        verts["point"] = np.array([
            [-width/2, -height/2, 0],
            [width/2, -height/2, 0],
            [width/2, height/2, 0],
            [-width/2, height/2, 0],
            [-width/2, -height/2, 0],
            [-width/2, height/2, 0],
        ])
        verts["stroke_rgba"] = [1, 1, 1, 1]
        verts["fill_rgba"] = [0.3, 0.5, 0.8, 1.0]
        
        self.vert_data = verts

        self.shader = VShaderWrapper(
            ctx=self.camera.context,
            vert_data=self.vert_data,
            shader_folder="shaders/quadratic_bezier",
            mobject_uniforms={"wave_amp": 0.1},
            depth_test=True,
        )
        
        # Animate the wave amplitude uniform

        self.add_updater(lambda m, dt: m.shader.mobject_uniforms.update(
            wave_amp=0.1 * np.sin(m.time)
        ))

    def draw(self):
        self.shader.pre_render()
        self.shader.render()

class WaveFillScene(Scene):
    def construct(self):
        rect = WavyRect().shift(LEFT)
        self.add(rect)
        self.wait(6)

```

`VShaderWrapper` automatically creates the off-screen fill canvas, manages four separate shader programs, and composites the final image using the winding-number technique, enabling complex fillable shapes without manual framebuffer management.

## Key Source Files and References

| File | Role | Key Components |
|------|------|----------------|
| [`manimlib/shader_wrapper.py`](https://github.com/3b1b/manim/blob/main/manimlib/shader_wrapper.py) | Core wrapper implementations | `ShaderWrapper` class, `VShaderWrapper` class, `init_program()`, `generate_vaos()`, `render()` |
| [`manimlib/utils/shaders.py`](https://github.com/3b1b/manim/blob/main/manimlib/utils/shaders.py) | Shader loading utilities | `get_shader_code_from_file()`, `image_path_to_texture()`, `set_program_uniform()`, `PROGRAM_UNIFORM_MIRRORS` |
| `manimlib/shaders/simple_vert.glsl` | Minimal vertex shader example | Basic vertex/fragment pair for point rendering |
| `manimlib/shaders/quadratic_bezier/` | VMobject shader suite | `stroke/vert.glsl`, `fill/vert.glsl`, `border/frag.glsl` |
| [`manimlib/utils/directories.py`](https://github.com/3b1b/manim/blob/main/manimlib/utils/directories.py) | Path resolution | `get_shader_dir()` for locating shader files |

The `ShaderWrapper` system uses **ModernGL** to abstract OpenGL context management. When `read_in()` processes vertex data, it checks if the existing VBO can accommodate the new data or if it must allocate a new GPU buffer. The `generate_vaos()` method then binds these buffers to shader attributes using the format string detected from the NumPy dtype.

## Summary

- **`ShaderWrapper`** in [`manimlib/shader_wrapper.py`](https://github.com/3b1b/manim/blob/main/manimlib/shader_wrapper.py) encapsulates ModernGL programs, VBO/VAO management, and uniform updates, enabling custom GLSL rendering without low-level OpenGL code.
- **`VShaderWrapper`** extends this for vector graphics, automatically handling four shader passes (stroke, fill, border, depth) and off-screen framebuffers for the winding-number fill technique.
- **Vertex data** must be provided as structured NumPy arrays; `moderngl.detect_format` automatically maps these to GLSL attributes based on field names and dtypes.
- **Uniforms** are managed through the `mobject_uniforms` dictionary and efficiently updated via `set_program_uniform`, which caches values in `PROGRAM_UNIFORM_MIRRORS` to avoid redundant GPU writes.
- **Textures** are loaded via `image_path_to_texture` and bound to specific texture units, accessible in GLSL by the keys provided in the `texture_paths` dictionary.

## Frequently Asked Questions

### How do I update uniforms dynamically in a custom ShaderWrapper?

Access the `mobject_uniforms` dictionary on your wrapper instance and update values within a Manim updater. The wrapper automatically propagates these to the GPU during `pre_render()` using the cached uniform setter to avoid redundant writes. For time-based animations, add an updater like `self.add_updater(lambda m, dt: m.shader.mobject_uniforms.update(time=m.time))`.

### What is the difference between ShaderWrapper and VShaderWrapper?

**`ShaderWrapper`** is the general-purpose base class suitable for point clouds, particle systems, or any custom geometry requiring a single shader program. **`VShaderWrapper`** is a specialized subclass designed specifically for `VMobject` rendering, automatically managing four separate shader programs (stroke, fill, border, depth) and an off-screen framebuffer for the winding-number fill technique required for complex vector shapes.

### How does Manim handle vertex attribute formats automatically?

Manim uses `moderngl.detect_format` to inspect the NumPy structured array dtype provided to the wrapper. It translates field definitions like `("position", "3f")` or `("color", "4f")` into OpenGL attribute format strings (e.g., `'3f 4f'`). The `generate_vaos()` method then binds these attributes to the shader program, requiring that your GLSL attribute names match the NumPy field names exactly.

### Where should I place custom GLSL shader files?

Place your shader files in a folder containing `vert.glsl` and `frag.glsl` (and optionally `geom.glsl`), then pass the folder path to the `shader_folder` parameter. The utility `get_shader_code_from_file` in [`manimlib/utils/shaders.py`](https://github.com/3b1b/manim/blob/main/manimlib/utils/shaders.py) resolves these paths using `get_shader_dir()` from [`manimlib/utils/directories.py`](https://github.com/3b1b/manim/blob/main/manimlib/utils/directories.py), and supports `#INSERT` directives to include reusable code snippets from other files.