@@ -53,6 +53,27 @@ def check_gl_error():
5353 print (f"Called from: { '' .join (stack [- 2 :- 1 ])} " )
5454
5555
56+ def _texture_tile_uv_rect (
57+ tile_index : int ,
58+ tile_width : int ,
59+ tile_height : int ,
60+ texture_width : int ,
61+ texture_height : int ,
62+ atlas_cols : int ,
63+ ) -> tuple [float , float , float , float ]:
64+ """Return a V-flipped UV rect for a row-major image atlas tile."""
65+ atlas_cols = max (1 , int (atlas_cols ))
66+ atlas_row , atlas_col = divmod (tile_index , atlas_cols )
67+ u_step = tile_width / float (texture_width )
68+ v_step = tile_height / float (texture_height )
69+ return (
70+ atlas_col * u_step ,
71+ (atlas_row + 1 ) * v_step ,
72+ (atlas_col + 1 ) * u_step ,
73+ atlas_row * v_step ,
74+ )
75+
76+
5677def _upload_texture_from_file (gl , texture_image : np .ndarray ) -> int :
5778 image = normalize_texture (
5879 texture_image ,
@@ -1334,6 +1355,108 @@ def render(self, camera, objects, lines=None, wireframe_shapes=None, arrows=None
13341355 err = gl .glGetError ()
13351356 assert err == gl .GL_NO_ERROR , hex (err )
13361357
1358+ def render_texture (
1359+ self ,
1360+ texture_id : int | None ,
1361+ texture_width : int ,
1362+ texture_height : int ,
1363+ * ,
1364+ tile_count : int = 1 ,
1365+ tile_width : int | None = None ,
1366+ tile_height : int | None = None ,
1367+ atlas_cols : int = 1 ,
1368+ spacing_px : float = 2.0 ,
1369+ clear_color : tuple [float , float , float , float ] = (0.0 , 0.0 , 0.0 , 1.0 ),
1370+ ):
1371+ """Draw a texture directly to the window without rendering the 3D scene.
1372+
1373+ Args:
1374+ texture_id: OpenGL texture id to draw, or ``None`` to only clear.
1375+ texture_width: Source texture width in pixels.
1376+ texture_height: Source texture height in pixels.
1377+ tile_count: Number of source tiles packed into the texture atlas.
1378+ tile_width: Width of each atlas tile in pixels. Defaults to
1379+ ``texture_width`` for single-image textures.
1380+ tile_height: Height of each atlas tile in pixels. Defaults to
1381+ ``texture_height`` for single-image textures.
1382+ atlas_cols: Number of atlas columns used to pack the source tiles.
1383+ spacing_px: Spacing between displayed tiles in pixels.
1384+ clear_color: Window clear color.
1385+ """
1386+ gl = RendererGL .gl
1387+ self ._make_current ()
1388+
1389+ screen_w = max (int (self ._screen_width ), 1 )
1390+ screen_h = max (int (self ._screen_height ), 1 )
1391+
1392+ gl .glBindFramebuffer (gl .GL_FRAMEBUFFER , 0 )
1393+ gl .glClearColor (* clear_color )
1394+ gl .glDisable (gl .GL_DEPTH_TEST )
1395+ gl .glDepthMask (True )
1396+ gl .glDisable (gl .GL_BLEND )
1397+ gl .glViewport (0 , 0 , screen_w , screen_h )
1398+ gl .glClear (gl .GL_COLOR_BUFFER_BIT | gl .GL_DEPTH_BUFFER_BIT )
1399+ gl .glDepthMask (False )
1400+
1401+ if texture_id is None or texture_id == 0 or texture_width <= 0 or texture_height <= 0 :
1402+ gl .glDepthMask (True )
1403+ return
1404+
1405+ from .image_logger import compute_grid_layout # noqa: PLC0415
1406+
1407+ tile_count = max (1 , int (tile_count ))
1408+ tile_width = int (tile_width or texture_width )
1409+ tile_height = int (tile_height or texture_height )
1410+ atlas_cols = max (1 , int (atlas_cols ))
1411+ spacing_px = max (0.0 , float (spacing_px if tile_count > 1 else 0.0 ))
1412+
1413+ rows , cols , cell_w , cell_h = compute_grid_layout (
1414+ tile_count ,
1415+ tile_height / float (max (tile_width , 1 )),
1416+ float (screen_w ),
1417+ float (screen_h ),
1418+ spacing_x = spacing_px ,
1419+ spacing_y = spacing_px ,
1420+ )
1421+ grid_w = cols * cell_w + max (0 , cols - 1 ) * spacing_px
1422+ grid_h = rows * cell_h + max (0 , rows - 1 ) * spacing_px
1423+ origin_x = (screen_w - grid_w ) * 0.5
1424+ origin_y = (screen_h - grid_h ) * 0.5
1425+
1426+ gl .glActiveTexture (gl .GL_TEXTURE0 )
1427+ gl .glBindTexture (gl .GL_TEXTURE_2D , int (texture_id ))
1428+ with self ._frame_shader :
1429+ for i in range (tile_count ):
1430+ display_row , display_col = divmod (i , cols )
1431+ draw_x = max (0 , int (round (origin_x + display_col * (cell_w + spacing_px ))))
1432+ draw_y = max (
1433+ 0 ,
1434+ int (round (origin_y + (rows - 1 - display_row ) * (cell_h + spacing_px ))),
1435+ )
1436+ draw_w = max (1 , int (round (cell_w )))
1437+ draw_h = max (1 , int (round (cell_h )))
1438+ uv_rect = _texture_tile_uv_rect (
1439+ i ,
1440+ tile_width ,
1441+ tile_height ,
1442+ texture_width ,
1443+ texture_height ,
1444+ atlas_cols ,
1445+ )
1446+
1447+ gl .glViewport (draw_x , draw_y , draw_w , draw_h )
1448+ self ._frame_shader .update (0 , uv_rect )
1449+ gl .glBindVertexArray (self ._frame_vao )
1450+ gl .glDrawElements (gl .GL_TRIANGLES , len (self ._frame_indices ), gl .GL_UNSIGNED_INT , None )
1451+ gl .glBindVertexArray (0 )
1452+ gl .glBindTexture (gl .GL_TEXTURE_2D , 0 )
1453+
1454+ gl .glViewport (0 , 0 , screen_w , screen_h )
1455+ gl .glDepthMask (True )
1456+
1457+ err = gl .glGetError ()
1458+ assert err == gl .GL_NO_ERROR , hex (err )
1459+
13371460 def present (self ):
13381461 if not self .headless :
13391462 if self ._dwm_flush is not None and self .window ._interval :
0 commit comments