Source code for pygfx.objects._more

import numpy as np

from ._base import WorldObject
from ..resources import Buffer
from ..utils import unpack_bitfield, array_from_shadertype, assert_type
from ..materials import BackgroundMaterial
from ..resources import Texture, TextureMap


class Group(WorldObject):
    """A group of objects.

    A Group is useful when manipulating the scene graph as children can be
    jointly moved/scaled/rotated. It has no visual properties.

    Parameters
    ----------
    visible : bool
        If true, the object and its children are visible.

    name : str
        The name of the group.

    """

[docs] def __init__(self, *, visible=True, name=""): super().__init__(visible=visible, name=name)
[docs] class Scene(Group): """Root of the scene graph. The scene holds scene-level information (background color, fog, environment map) as well as all objects that take part in the rendering process as either direct or indirect children/nested objects. Parameters ---------- environment : Texture | TextureMap The environment map for all physical materials in the scene. However, it's not possible to overwrite an existing map assigned to individual materials. Default is None. """
[docs] def __init__(self, environment=None, *args, **kwargs): super().__init__(*args, **kwargs) self.environment = environment
@property def environment(self): """The environment map for all physical materials in the scene. If a material has its own environment map set, it will override the scene's environment map. """ return self._store.environment @environment.setter def environment(self, environment): assert_type("environment", environment, None, Texture, TextureMap) if isinstance(environment, Texture): environment = TextureMap(environment) if environment is not None: # todo: for now, we only support cube maps if environment.texture.dim != 2 or environment.texture.size[2] != 6: raise ValueError("Environment map must be a Cube texture.") # todo: for now, we use normal mipmaps, but we should use a PMREM texture if not environment.texture.generate_mipmaps: raise ValueError("Environment map texture must generate mipmaps.") self._store.environment = environment
class Background(WorldObject): """The scene's background. Can be e.g. a gradient, a static image or a skybox. Parameters ---------- geometry : Geometry Must be ``None``. Exists for compliance with the generic WorldObject API. material : Material The material to use when rendering the background. kwargs : Any Additional kwargs are forwarded to the object's :class:`base class <pygfx.objects.WorldObject>`. """ def __init__(self, geometry=None, material=None, *, render_order=-1e6, **kwargs): if geometry is not None and material is None: raise TypeError("You need to instantiate using Background(None, material)") super().__init__(None, material, render_order=render_order, **kwargs) @classmethod def from_color(cls, *colors): """Create a background with a :class:`.BackgroundMaterial`, using 1 uniform color, 2 colors for a vertical gradient, or 4 colors (one for each corner). """ return cls(None, BackgroundMaterial(*colors)) class Grid(WorldObject): """A grid to help interpret spatial distances. The grid by default occupies 1x1 square in the xz plane. It can be scaled, rotated, and translated to move it into position. If the grid is infinite (``material.infinite``) then scale and in-plane translations are ignored. Parameters ---------- geometry : Geometry Must be ``None``. Exists for compliance with the generic WorldObject API. material : Material The material to use when rendering the background. orientation : str The (initial) grid rotation. Must be 'xy' (default), 'xz', or 'yz'. Simply rotates the object, e.g. for 'xz' will do ``self.local.euler_x = np.pi/2``. kwargs : Any Additional kwargs are forwarded to the object's :class:`base class <pygfx.objects.WorldObject>`. """ def __init__( self, geometry=None, material=None, *, orientation, render_order=-100, **kwargs ): if geometry is not None and material is None: raise TypeError("You need to instantiate using Grid(None, material)") super().__init__(None, material, render_order=render_order, **kwargs) if orientation is not None: if orientation == "xy": pass elif orientation == "xz": self.local.euler_x = np.pi / 2 elif orientation == "yz": self.local.euler_y = -np.pi / 2 else: raise ValueError( f"Invalid grid orientation: '{orientation}', must be 'xz', 'xy', or 'yz'." ) class Line(WorldObject): """An object representing a line using a list of vertices (3D positions). Some materials will render the line as a continuous line, while other materials will consider each pair of consecutive points a segment. The picking info of a Line (the result of ``renderer.get_pick_info()``) will for most materials include ``vertex_index`` (int) and ``segment_coord`` (float, sub-segment coordinate). Parameters ---------- geometry : Geometry The data defining the shape of the object. Must contain at least a "positions" buffer. Depending on the usage of the material, can also include buffers "texcoords", "colors", and "sizes". material : Material The data defining the appearance of the object. visible : bool Whether the object is visible. render_order : int Affects the order in which objects are rendered. position : Vector The position of the object in the world. Default (0, 0, 0). """ class Points(WorldObject): """A point cloud. An object consisting of points represented by vertices (3D positions). The picking info of a Points object (the result of ``renderer.get_pick_info()``) will for most materials include ``vertex_index`` (int) and ``point_coord`` (tuple of 2 float coordinates in logical pixels). Parameters ---------- geometry : Geometry The data defining the shape of the object. Must contain at least a "positions" buffer. Depending on the usage of the material, can also include buffers "texcoords", "colors", "sizes", "edge_colors", "rotations". material : Material The data defining the appearance of the object. visible : bool Whether the object is visible. render_order : int Affects the order in which objects are rendered. position : Vector The position of the object in the world. Default (0, 0, 0). """ class Mesh(WorldObject): """A mesh. An object consisting of triangular faces represented by a set of vertices (3D positions) and a set of vertex indices indicating which vertex triplets form mesh triangles. The picking info of a Mesh (the result of ``renderer.get_pick_info()``) will for most materials include ``instance_index`` (int), ``face_index`` (int), and ``face_coord`` (tuple of 3 floats). The latter are the barycentric coordinates for each vertex of the face (with values 0..1). Parameters ---------- geometry : Geometry The data defining the shape of the object. Must contain at least a "positions" and "indices" buffers. Depending on the usage of the material, can also include buffers "normals", "colors", "texcoords", "texcoords2", etc. Advanced use of meshes also support "tangents", "skin_indices", and "skin_weights". material : Material The data defining the appearance of the object. visible : bool Whether the object is visible. render_order : int Affects the order in which objects are rendered. position : Vector The position of the object in the world. Default (0, 0, 0). """
[docs] def __init__(self, geometry=None, material=None, *args, **kwargs): super().__init__(geometry, material, *args, **kwargs) self._morph_target_influences = None self._morph_target_names = []
@property def morph_target_influences(self): """ An ndarray of weights typically from 0-1 that specify how much of the morph is applied. Note: When using this attribute, its geometry needs to have the relevant attributes of morph targets. """ return self._morph_target_influences.data["influence"][:-1] @morph_target_influences.setter def morph_target_influences(self, value): value = np.asarray(value, dtype=np.float32) # Get the number of morph targets from the morph data on the geometry morph_attrs = [ getattr(self.geometry, name, None) for name in ["morph_positions", "morph_normals", "morph_colors"] ] morph_attrs = [x for x in morph_attrs if x is not None] if not morph_attrs: return morph_count = min(len(x) for x in morph_attrs) # Check with the size of the given data if len(value) != morph_count: raise ValueError( f"Length of morph target influences must match the number of morph targets. Expected {morph_count}, got {len(value)}." ) buffer_size = morph_count + 1 # add roon for base influence if ( self._morph_target_influences is None or self._morph_target_influences.nitems != buffer_size ): self._morph_target_influences = Buffer( array_from_shadertype( { "influence": "f4", }, buffer_size, ) ) if getattr(self.geometry, "morph_targets_relative", False): base_influence = 1.0 else: base_influence = 1 - value.sum() self._morph_target_influences.data["influence"][:-1] = value self._morph_target_influences.data["influence"][-1] = base_influence self._morph_target_influences.update_full() @property def morph_target_names(self): """ A list of names for the morph targets. """ return self._morph_target_names def _wgpu_get_pick_info(self, pick_value) -> dict: info = super()._wgpu_get_pick_info(pick_value) values = unpack_bitfield( pick_value, wobject_id=20, index=26, coord1=9, coord2=9 ) face_index = values["index"] face_coord = [ values["coord1"] / 511, values["coord2"] / 511, ] # The shader encodes only two of the barycentric coordinates. The third can be computed, as they add up to one. face_coord.append(1.0 - face_coord[0] - face_coord[1]) if ( self.geometry.indices.data is not None and self.geometry.indices.data.shape[-1] == 4 ): triangle_index = face_index % 2 face_index = face_index // 2 if triangle_index == 0: # The sub indices are 0, 1, 2, so we just add the zero for index 3. face_coord.append(0.0) else: # The sub indices are 0, 2, 3. The index 3 uses the # face_coord slot of index 1, (see meshshader.py), so # we put that at the end and put a zero in its place. face_coord = face_coord[0], 0.0, face_coord[2], face_coord[1] info["face_index"] = face_index info["face_coord"] = tuple(face_coord) return info class Image(WorldObject): """A 2D image. The geometry for this object consists only of ``geometry.grid``: a texture with the 2D data. If no colormap is applied to the material, the data are interpreted as colors in sRGB space. To use physical space instead, set the texture's colorspace property to ``"physical"``. The picking info of an Image (the result of ``renderer.get_pick_info()``) will for most materials include ``index`` (tuple of 2 int), and ``pixel_coord`` (tuple of float subpixel coordinates, zero at the center of the pixel and 0.5 at the edge). Parameters ---------- geometry : Geometry The data defining the shape of the object. Must contain at least a "grid" attribute for a 2D texture. material : Material The data defining the appearance of the object. visible : bool Whether the object is visible. render_order : int Affects the order in which objects are rendered. position : Vector The position of the object in the world. Default (0, 0, 0). """ def _wgpu_get_pick_info(self, pick_value) -> dict: info = super()._wgpu_get_pick_info(pick_value) tex = self.geometry.grid if hasattr(tex, "texture"): tex = tex.texture # tex was a view # This should match with the shader values = unpack_bitfield(pick_value, wobject_id=20, x=22, y=22) size = tex.size x = values["x"] / 4194303 * size[0] - 0.5 y = values["y"] / 4194303 * size[1] - 0.5 ix, iy = (min(int(x + 0.5), size[0] - 1), min(int(y + 0.5), size[1] - 1)) info["index"] = (ix, iy) info["pixel_coord"] = (x - ix, y - iy) return info class Volume(WorldObject): """A 3D image. The geometry for this object consists only of ``geometry.grid``: a texture with the 3D data. The picking info of a Volume (the result of ``renderer.get_pick_info()``) will for most materials include ``index`` (tuple of 3 int), and ``voxel_coord`` (tuple of float subpixel coordinates, zero at the center of the voxel and 0.5 at the edge). Parameters ---------- geometry : Geometry The data defining the shape of the object. Must contain at least a "grid" attribute for a 3D texture. material : Material The data defining the appearance of the object. visible : bool Whether the object is visible. render_order : int Affects the order in which objects are rendered. position : Vector The position of the object in the world. Default (0, 0, 0). """ def _wgpu_get_pick_info(self, pick_value) -> dict: info = super()._wgpu_get_pick_info(pick_value) tex = self.geometry.grid if hasattr(tex, "texture"): tex = tex.texture # tex was a view # This should match with the shader values = unpack_bitfield(pick_value, wobject_id=20, x=14, y=14, z=14) texcoords_encoded = values["x"], values["y"], values["z"] size = tex.size x, y, z = [ (v / 16383) * s - 0.5 for v, s in zip(texcoords_encoded, size, strict=True) ] ix, iy, iz = ( min(int(x + 0.5), size[0] - 1), min(int(y + 0.5), size[1] - 1), min(int(z + 0.5), size[2] - 1), ) info["index"] = (ix, iy, iz) info["voxel_coord"] = (x - ix, y - iy, z - iz) return info