| name | blender-core-gpu |
| description | Guides GPU drawing in Blender Python using the gpu module, built-in shaders, batch rendering, gpu.state management, SpaceView3D draw handlers, and offscreen rendering. Covers BGL-to-gpu migration. Activates when drawing custom overlays, creating viewport visualizations, or migrating from deprecated bgl module. |
| license | MIT |
| compatibility | Designed for Claude Code. Requires Blender 3.x/4.x/5.x with Python. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
blender-core-gpu
Quick Reference
Critical Warnings
NEVER use the bgl module — it is deprecated since Blender 3.5 and completely removed in Blender 5.0. ALL drawing code MUST use the gpu module.
NEVER use shader name 3D_UNIFORM_COLOR or 3D_FLAT_COLOR in Blender 4.0+ — the 3D_ prefix was removed. Use UNIFORM_COLOR, POLYLINE_UNIFORM_COLOR, etc.
NEVER forget to restore gpu.state to defaults at the end of draw callbacks — failing to restore state corrupts rendering for ALL subsequent draw handlers and Blender's own UI.
NEVER create gpu.types.GPUOffScreen outside a valid OpenGL/GPU context — offscreen buffers require a graphics context (viewport draw callback or bpy.types.SpaceView3D).
NEVER call gpu.shader.from_builtin() or create batches from a background thread — ALL GPU operations MUST execute on the main thread.
ALWAYS remove draw handlers in addon unregister() — leaked handlers cause crashes or persistent ghost overlays.
ALWAYS set viewportSize and lineWidth uniforms when using POLYLINE_* shaders — these are REQUIRED uniforms (Blender 4.0+).
ALWAYS call offscreen.free() when done with offscreen buffers to prevent GPU memory leaks.
Module Overview
| Module | Purpose |
|---|
gpu | Core GPU module — shader creation, state management |
gpu.shader | Shader compilation, built-in shader access |
gpu.types | GPU types: GPUShader, GPUBatch, GPUOffScreen, GPUTexture |
gpu.state | OpenGL-like state: blend, depth, line width, viewport |
gpu.texture | Texture creation from images |
gpu.matrix | Model-view-projection matrix stack |
gpu.select | GPU-based selection utilities |
gpu.platform | GPU backend info (vendor, renderer, version) |
gpu_extras.batch | batch_for_shader() helper |
gpu_extras.presets | draw_circle_2d(), draw_texture_2d() |
Built-in Shaders (Blender 4.0+)
| Shader Name | Geometry | Use Case |
|---|
'UNIFORM_COLOR' | Triangles, points | Flat colored filled geometry |
'POLYLINE_UNIFORM_COLOR' | Lines | Lines with configurable width |
'FLAT_COLOR' | Triangles | Per-vertex colored (flat shaded) |
'POLYLINE_FLAT_COLOR' | Lines | Per-vertex colored lines |
'SMOOTH_COLOR' | Triangles | Per-vertex colored (smooth shaded) |
'POLYLINE_SMOOTH_COLOR' | Lines | Smooth per-vertex colored lines |
'IMAGE' | Triangles | Textured quads/geometry |
'IMAGE_COLOR' | Triangles | Textured with color tint |
Shader Name Version Map
| Blender 3.x Name | Blender 4.0+ Name |
|---|
'3D_UNIFORM_COLOR' | 'UNIFORM_COLOR' |
'3D_FLAT_COLOR' | 'FLAT_COLOR' |
'3D_SMOOTH_COLOR' | 'SMOOTH_COLOR' |
'3D_IMAGE' | 'IMAGE' |
'2D_UNIFORM_COLOR' | 'UNIFORM_COLOR' |
'2D_FLAT_COLOR' | 'FLAT_COLOR' |
'2D_SMOOTH_COLOR' | 'SMOOTH_COLOR' |
'2D_IMAGE' | 'IMAGE' |
In Blender 4.0+, the 2D_/3D_ distinction is removed. A single 'UNIFORM_COLOR' works in both 2D and 3D contexts.
Essential Patterns
Pattern 1: Basic Line Drawing with POLYLINE Shader
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
def draw_lines():
coords = [(0, 0, 0), (5, 0, 0), (5, 5, 0), (0, 5, 0), (0, 0, 0)]
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": coords})
gpu.state.blend_set('ALPHA')
gpu.state.line_width_set(2.0)
shader.bind()
region = bpy.context.region
shader.uniform_float("viewportSize", (region.width, region.height))
shader.uniform_float("lineWidth", 2.0)
shader.uniform_float("color", (1.0, 0.0, 0.0, 0.8))
batch.draw(shader)
gpu.state.blend_set('NONE')
gpu.state.line_width_set(1.0)
_handle = bpy.types.SpaceView3D.draw_handler_add(draw_lines, (), 'WINDOW', 'POST_VIEW')
Pattern 2: Filled Triangles with UNIFORM_COLOR
import gpu
from gpu_extras.batch import batch_for_shader
def draw_filled():
verts = [(0, 0, 0), (5, 0, 0), (5, 5, 0), (0, 5, 0)]
indices = ((0, 1, 2), (2, 3, 0))
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
batch = batch_for_shader(shader, 'TRIS', {"pos": verts}, indices=indices)
gpu.state.blend_set('ALPHA')
shader.bind()
shader.uniform_float("color", (0.0, 0.5, 1.0, 0.3))
batch.draw(shader)
gpu.state.blend_set('NONE')
Pattern 3: Draw Handler Registration/Removal
import bpy
_draw_handle = None
def register():
global _draw_handle
_draw_handle = bpy.types.SpaceView3D.draw_handler_add(
draw_callback, (), 'WINDOW', 'POST_VIEW'
)
def unregister():
global _draw_handle
if _draw_handle is not None:
bpy.types.SpaceView3D.draw_handler_remove(_draw_handle, 'WINDOW')
_draw_handle = None
Draw Handler Layer Options
| Layer | When | Coordinate Space |
|---|
'PRE_VIEW' | Before 3D scene | 3D world space |
'POST_VIEW' | After 3D scene, before gizmos | 3D world space |
'POST_PIXEL' | After everything, on top | 2D pixel space |
Pattern 4: Version-Compatible Shader Selection
import bpy
import gpu
def get_line_shader():
if bpy.app.version >= (4, 0, 0):
return gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
else:
return gpu.shader.from_builtin('3D_UNIFORM_COLOR')
def get_flat_shader():
if bpy.app.version >= (4, 0, 0):
return gpu.shader.from_builtin('UNIFORM_COLOR')
else:
return gpu.shader.from_builtin('3D_UNIFORM_COLOR')
Pattern 5: Offscreen Rendering
import gpu
offscreen = gpu.types.GPUOffScreen(512, 512)
with offscreen.bind():
fb = gpu.state.active_framebuffer_get()
fb.clear(color=(0.0, 0.0, 0.0, 0.0))
texture = offscreen.texture_color
offscreen.free()
Pattern 6: GPU State Management
import gpu
def draw_with_state():
gpu.state.blend_set('ALPHA')
gpu.state.depth_test_set('LESS_EQUAL')
gpu.state.depth_mask_set(True)
gpu.state.line_width_set(2.0)
gpu.state.point_size_set(5.0)
gpu.state.face_culling_set('BACK')
gpu.state.blend_set('NONE')
gpu.state.depth_test_set('NONE')
gpu.state.depth_mask_set(False)
gpu.state.line_width_set(1.0)
gpu.state.point_size_set(1.0)
gpu.state.face_culling_set('NONE')
Common Operations
Drawing 2D Overlays (POST_PIXEL)
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
def draw_2d_overlay():
verts = [(10, 10), (200, 10), (200, 50), (10, 50)]
indices = ((0, 1, 2), (2, 3, 0))
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
batch = batch_for_shader(shader, 'TRIS', {"pos": verts}, indices=indices)
gpu.state.blend_set('ALPHA')
shader.bind()
shader.uniform_float("color", (0.0, 0.0, 0.0, 0.5))
batch.draw(shader)
gpu.state.blend_set('NONE')
_handle = bpy.types.SpaceView3D.draw_handler_add(
draw_2d_overlay, (), 'WINDOW', 'POST_PIXEL'
)
Drawing Textured Quads
import gpu
from gpu_extras.batch import batch_for_shader
def draw_image(texture):
shader = gpu.shader.from_builtin('IMAGE')
batch = batch_for_shader(
shader, 'TRI_FAN',
{
"pos": ((0, 0), (200, 0), (200, 200), (0, 200)),
"texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)),
},
)
shader.bind()
shader.uniform_sampler("image", texture)
batch.draw(shader)
Per-Vertex Colored Geometry
import gpu
from gpu_extras.batch import batch_for_shader
shader = gpu.shader.from_builtin('SMOOTH_COLOR')
batch = batch_for_shader(shader, 'TRIS', {
"pos": [(0, 0, 0), (5, 0, 0), (2.5, 5, 0)],
"color": [(1, 0, 0, 1), (0, 1, 0, 1), (0, 0, 1, 1)],
})
shader.bind()
batch.draw(shader)
Loading Image as GPU Texture
import bpy
import gpu
image = bpy.data.images.load("/path/to/image.png")
texture = gpu.texture.from_image(image)
BGL-to-gpu Migration Checklist
When migrating code from bgl to gpu:
- Remove ALL
import bgl statements
- Replace
bgl.glEnable(bgl.GL_BLEND) → gpu.state.blend_set('ALPHA')
- Replace
bgl.glDisable(bgl.GL_BLEND) → gpu.state.blend_set('NONE')
- Replace
bgl.glLineWidth(w) → gpu.state.line_width_set(w)
- Replace
bgl.glEnable(bgl.GL_DEPTH_TEST) → gpu.state.depth_test_set('LESS_EQUAL')
- Replace
bgl.glDisable(bgl.GL_DEPTH_TEST) → gpu.state.depth_test_set('NONE')
- Replace
bgl.glDepthMask(GL_TRUE) → gpu.state.depth_mask_set(True)
- Replace
bgl.glPointSize(s) → gpu.state.point_size_set(s)
- Replace
bgl.glEnable(bgl.GL_LINE_SMOOTH) → use POLYLINE_* shaders (no direct replacement)
- Replace
image.gl_load() / bgl.glBindTexture() → gpu.texture.from_image(image)
- Replace
3D_UNIFORM_COLOR → POLYLINE_UNIFORM_COLOR (for lines) or UNIFORM_COLOR (for triangles)
- Replace
3D_FLAT_COLOR → POLYLINE_FLAT_COLOR (for lines) or FLAT_COLOR (for triangles)
- Add
viewportSize and lineWidth uniforms for ALL POLYLINE_* shaders
- Add state restoration at end of every draw callback
- Test on Apple Silicon (Metal backend) — bgl was already non-functional there
BGL-to-gpu API Mapping
| bgl (REMOVED in 5.0) | gpu replacement (3.5+) | Notes |
|---|
bgl.glEnable(bgl.GL_BLEND) | gpu.state.blend_set('ALPHA') | Options: 'ALPHA', 'ADDITIVE', 'ADDITIVE_PREMUL', 'MULTIPLY' |
bgl.glDisable(bgl.GL_BLEND) | gpu.state.blend_set('NONE') | |
bgl.glLineWidth(w) | gpu.state.line_width_set(w) | Also set lineWidth uniform for POLYLINE shaders |
bgl.glEnable(bgl.GL_DEPTH_TEST) | gpu.state.depth_test_set('LESS_EQUAL') | Options: 'NONE', 'LESS', 'LESS_EQUAL', 'EQUAL', 'GREATER' |
bgl.glDisable(bgl.GL_DEPTH_TEST) | gpu.state.depth_test_set('NONE') | |
bgl.glDepthMask(GL_TRUE) | gpu.state.depth_mask_set(True) | |
bgl.glPointSize(s) | gpu.state.point_size_set(s) | |
bgl.glEnable(bgl.GL_LINE_SMOOTH) | (no direct replacement) | Use POLYLINE_* shaders for antialiased lines |
image.gl_load() / bgl.glBindTexture() | gpu.texture.from_image(image) | Completely different API |
Version Timeline
| Version | Change |
|---|
| Blender 3.0 | gpu module stable, gpu_extras.batch available |
| Blender 3.5 | bgl module officially deprecated |
| Blender 4.0 | 3D_/2D_ shader name prefixes removed; POLYLINE_* shaders added |
| Blender 5.0 | bgl module completely removed; gpu is the ONLY drawing API |
Reference Links
Cross-References
Official Sources