Coverage for trimesh/rendering.py: 87%
125 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2rendering.py
3--------------
5Functions to convert trimesh objects to pyglet/opengl objects.
6"""
8import numpy as np
10from . import util
12# avoid importing pyglet or pyglet.gl
13# as pyglet does things on import
14GL_POINTS, GL_LINES, GL_TRIANGLES = (0, 1, 4)
17def convert_to_vertexlist(geometry, **kwargs):
18 """
19 Try to convert various geometry objects to the constructor
20 args for a pyglet indexed vertex list.
22 Parameters
23 ------------
24 obj : Trimesh, Path2D, Path3D, (n,2) float, (n,3) float
25 Object to render
27 Returns
28 ------------
29 args : tuple
30 Args to be passed to pyglet indexed vertex list
31 constructor.
32 """
33 if util.is_instance_named(geometry, "Trimesh"):
34 return mesh_to_vertexlist(geometry, **kwargs)
35 elif util.is_instance_named(geometry, "Path"):
36 # works for Path3D and Path2D
37 # both of which inherit from Path
38 return path_to_vertexlist(geometry, **kwargs)
39 elif util.is_instance_named(geometry, "PointCloud"):
40 # pointcloud objects contain colors
41 return points_to_vertexlist(geometry.vertices, colors=geometry.colors, **kwargs)
42 elif util.is_instance_named(geometry, "ndarray"):
43 # (n,2) or (n,3) points
44 return points_to_vertexlist(geometry, **kwargs)
45 elif util.is_instance_named(geometry, "VoxelGrid"):
46 # for voxels view them as a bunch of boxes
47 return mesh_to_vertexlist(geometry.as_boxes(**kwargs), **kwargs)
48 else:
49 raise ValueError("Geometry passed is not a viewable type!")
52def mesh_to_vertexlist(mesh, group=None, smooth=True, smooth_threshold=60000):
53 """
54 Convert a Trimesh object to arguments for an
55 indexed vertex list constructor.
57 Parameters
58 -------------
59 mesh : trimesh.Trimesh
60 Mesh to be rendered
61 group : str
62 Rendering group for the vertex list
63 smooth : bool
64 Should we try to smooth shade the mesh
65 smooth_threshold : int
66 Maximum number of faces to smooth shade
68 Returns
69 --------------
70 args : (7,) tuple
71 Args for vertex list constructor
73 """
74 # nominally support 2D vertices
75 if len(mesh.vertices.shape) == 2 and mesh.vertices.shape[1] == 2:
76 vertices = np.column_stack((mesh.vertices, np.zeros(len(mesh.vertices))))
77 else:
78 vertices = mesh.vertices
80 if hasattr(mesh.visual, "uv"):
81 # if the mesh has texture defined pass it to pyglet
82 vertex_count = len(vertices)
83 normals = mesh.vertex_normals
84 faces = mesh.faces
86 # get the per-vertex UV coordinates
87 uv = mesh.visual.uv
89 # shortcut for the material
90 material = mesh.visual.material
91 if hasattr(material, "image"):
92 # does the material actually have an image specified
93 no_image = material.image is None
94 elif hasattr(material, "baseColorTexture"):
95 no_image = material.baseColorTexture is None
96 else:
97 no_image = True
99 # didn't get valid texture so skip it
100 if uv is None or no_image or len(uv) != vertex_count:
101 # if no UV coordinates on material, just set face colors
102 # to the diffuse color of the material
103 color_gl = colors_to_gl(material.main_color, vertex_count)
104 else:
105 # if someone passed (n, 3) UVR cut it off here
106 if uv.shape[1] > 2:
107 uv = uv[:, :2]
108 # texcoord as (2,) float
109 color_gl = ("t2f/static", uv.astype(np.float64).reshape(-1).tolist())
111 elif smooth and len(mesh.faces) < smooth_threshold:
112 # if we have a small number of faces and colors defined
113 # smooth the mesh by merging vertices of faces below
114 # the threshold angle
115 smooth = mesh.smooth_shaded
116 vertices = smooth.vertices
117 vertex_count = len(vertices)
118 normals = smooth.vertex_normals
119 faces = smooth.faces
120 color_gl = colors_to_gl(smooth.visual.vertex_colors, vertex_count)
121 else:
122 # we don't have textures or want to smooth so
123 # send a polygon soup of disconnected triangles to opengl
124 vertex_count = len(mesh.faces) * 3
125 normals = np.tile(mesh.face_normals, (1, 3))
126 vertices = vertices[mesh.faces]
127 faces = np.arange(vertex_count, dtype=np.int64)
128 colors = np.tile(mesh.visual.face_colors, (1, 3)).reshape((-1, 4))
129 color_gl = colors_to_gl(colors, vertex_count)
131 # create the ordered tuple for pyglet, use like:
132 # `batch.add_indexed(*args)`
133 args = (
134 vertex_count, # number of vertices
135 GL_TRIANGLES, # mode
136 group, # group
137 faces.reshape(-1).tolist(), # indices
138 ("v3f/static", vertices.reshape(-1).tolist()),
139 ("n3f/static", normals.reshape(-1).tolist()),
140 color_gl,
141 )
143 return args
146def path_to_vertexlist(path, group=None, **kwargs):
147 """
148 Convert a Path3D object to arguments for a
149 pyglet indexed vertex list constructor.
151 Parameters
152 -------------
153 path : trimesh.path.Path3D object
154 Mesh to be rendered
155 group : str
156 Rendering group for the vertex list
158 Returns
159 --------------
160 args : (7,) tuple
161 Args for vertex list constructor
162 """
163 # avoid cache check inside tight loop
164 vertices = path.vertices
166 # get (n, 2, (2|3)) lines
167 stacked = [util.stack_lines(e.discrete(vertices)) for e in path.entities]
168 lines = util.vstack_empty(stacked)
169 count = len(lines)
171 # stack zeros for 2D lines
172 if util.is_shape(vertices, (-1, 2)):
173 lines = lines.reshape((-1, 2))
174 lines = np.column_stack((lines, np.zeros(len(lines))))
175 # index for GL is one per point
176 index = np.arange(count).tolist()
177 # convert from entity color to the color of
178 # each vertex in the line segments
179 colors = path.colors
180 if colors is not None:
181 colors = np.vstack(
182 [
183 (np.ones((len(s), 4)) * c).astype(np.uint8)
184 for s, c in zip(stacked, path.colors)
185 ]
186 )
187 # convert to gl-friendly colors
188 gl_colors = colors_to_gl(colors, count=count)
190 # collect args for vertexlist constructor
191 args = (
192 count, # number of lines
193 GL_LINES, # mode
194 group, # group
195 index, # indices
196 ("v3f/static", lines.reshape(-1)),
197 gl_colors,
198 )
199 return args
202def points_to_vertexlist(points, colors=None, group=None, **kwargs):
203 """
204 Convert a numpy array of 3D points to args for
205 a vertex list constructor.
207 Parameters
208 -------------
209 points : (n, 3) float
210 Points to be rendered
211 colors : (n, 3) or (n, 4) float
212 Colors for each point
213 group : str
214 Rendering group for the vertex list
216 Returns
217 --------------
218 args : (7,) tuple
219 Args for vertex list constructor
220 """
221 points = np.asanyarray(points, dtype=np.float64)
223 if util.is_shape(points, (-1, 2)):
224 points = np.column_stack((points, np.zeros(len(points))))
225 elif not util.is_shape(points, (-1, 3)):
226 raise ValueError("Pointcloud must be (n,3)!")
228 index = np.arange(len(points)).tolist()
230 args = (
231 len(points), # number of vertices
232 GL_POINTS, # mode
233 group, # group
234 index, # indices
235 ("v3f/static", points.reshape(-1)),
236 colors_to_gl(colors, len(points)),
237 )
238 return args
241def colors_to_gl(colors, count):
242 """
243 Given a list of colors (or None) return a GL-acceptable
244 list of colors.
246 Parameters
247 ------------
248 colors: (count, (3 or 4)) float
249 Input colors as an array
251 Returns
252 ---------
253 colors_type : str
254 Color type
255 colors_gl : (count,) list
256 Colors to pass to pyglet
257 """
259 colors = np.asanyarray(colors)
260 count = int(count)
261 # get the GL kind of color we have
262 colors_dtypes = {"f": "f", "i": "B", "u": "B"}
264 if colors.dtype.kind in colors_dtypes:
265 dtype = colors_dtypes[colors.dtype.kind]
266 else:
267 dtype = None
269 if dtype is not None and util.is_shape(colors, (count, (3, 4))):
270 # save the shape and dtype for opengl color string
271 colors_type = f"c{colors.shape[1]}{dtype}/static"
272 # reshape the 2D array into a 1D one and then convert to a python list
273 gl_colors = colors.reshape(-1).tolist()
274 elif dtype is not None and colors.shape in [(3,), (4,)]:
275 # we've been passed a single color so tile them
276 gl_colors = (
277 (np.ones((count, colors.size), dtype=colors.dtype) * colors)
278 .reshape(-1)
279 .tolist()
280 )
281 # we know we're tiling
282 colors_type = f"c{colors.size}{dtype}/static"
283 else:
284 # case where colors are wrong shape
285 # use black as the default color
286 gl_colors = np.tile([0.0, 0.0, 0.0], (count, 1)).reshape(-1).tolist()
287 # we're returning RGB float colors
288 colors_type = "c3f/static"
290 return colors_type, gl_colors
293def material_to_texture(material, upsize=True):
294 """
295 Convert a trimesh.visual.texture.Material object into
296 a pyglet-compatible texture object.
298 Parameters
299 --------------
300 material : trimesh.visual.texture.Material
301 Material to be converted
302 upsize: bool
303 If True, will upscale textures to their nearest power
304 of two resolution to avoid weirdness
306 Returns
307 ---------------
308 texture : pyglet.image.Texture
309 Texture loaded into pyglet form
310 """
311 import pyglet
313 # try to extract a PIL image from material
314 if hasattr(material, "image"):
315 img = material.image
316 elif hasattr(material, "baseColorTexture"):
317 img = material.baseColorTexture
318 else:
319 return None
321 # if no images in texture return now
322 if img is None:
323 return None
325 # if we're not powers of two upsize
326 if upsize:
327 from .visual.texture import power_resize
329 img = power_resize(img)
331 # use a PNG export to exchange into pyglet
332 # probably a way to do this with a PIL converter
333 with util.BytesIO() as f:
334 # export PIL image as PNG
335 img.save(f, format="png")
336 f.seek(0)
337 # filename used for format guess
338 gl_image = pyglet.image.load(filename=".png", file=f)
340 # turn image into pyglet texture
341 texture = gl_image.get_texture()
343 return texture
346def matrix_to_gl(matrix):
347 """
348 Convert a numpy row-major homogeneous transformation matrix
349 to a flat column-major GLfloat transformation.
351 Parameters
352 -------------
353 matrix : (4,4) float
354 Row-major homogeneous transform
356 Returns
357 -------------
358 glmatrix : (16,) gl.GLfloat
359 Transform in pyglet format
360 """
361 from pyglet import gl
363 # convert to GLfloat, switch to column major and flatten to (16,)
364 return (gl.GLfloat * 16)(*np.array(matrix, dtype=np.float32).T.ravel())
367def vector_to_gl(array, *args):
368 """
369 Convert an array and an optional set of args into a
370 flat vector of gl.GLfloat
371 """
372 from pyglet import gl
374 array = np.array(array)
375 if len(args) > 0:
376 array = np.append(array, args)
377 vector = (gl.GLfloat * len(array))(*array)
378 return vector
381def light_to_gl(light, transform, lightN):
382 """
383 Convert trimesh.scene.lighting.Light objects into
384 args for gl.glLightFv calls
386 Parameters
387 --------------
388 light : trimesh.scene.lighting.Light
389 Light object to be converted to GL
390 transform : (4, 4) float
391 Transformation matrix of light
392 lightN : int
393 Result of gl.GL_LIGHT0, gl.GL_LIGHT1, etc
395 Returns
396 --------------
397 multiarg : [tuple]
398 List of args to pass to gl.glLightFv eg:
399 [gl.glLightfb(*a) for a in multiarg]
400 """
401 from pyglet import gl
403 # convert color to opengl
404 gl_color = vector_to_gl(light.color.astype(np.float64) / 255.0)
405 assert len(gl_color) == 4
407 # cartesian translation from matrix
408 gl_position = vector_to_gl(transform[:3, 3])
410 # create the different position and color arguments
411 args = [
412 (lightN, gl.GL_POSITION, gl_position),
413 (lightN, gl.GL_SPECULAR, gl_color),
414 (lightN, gl.GL_DIFFUSE, gl_color),
415 (lightN, gl.GL_AMBIENT, gl_color),
416 ]
417 return args