GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — `VectorsGraphic` · Issue #560 · fastplotlib/fastplotlib

VectorsGraphic #560

New issue
Copy link
New issue
Copy link
Closed
#929
Closed
VectorsGraphic#560
#929
Copy link

Description

@kushalkolar
kushalkolar
opened on Jul 25, 2024
Issue body actions

Easy to do with PointsMarkerMaterial and the geometry. One thing I need do figure out is how to do the point rotations in 3D in model space.

with #913 :

see: #908 (comment)

# get uniform x, y positions
X, Y = np.meshgrid(np.arange(0, 2 * np.pi, .2), np.arange(0, 2 * np.pi, .2))

# positions of each vector as [n_points, 2] array
data = np.column_stack([X.ravel(), Y.ravel()])

# vectors, u and v are x and y components
U = np.cos(X)
V = np.sin(Y)

# angle of each vector
rotations = np.atan2(U, V)
# ravel so it maps `data` index -> rotation
rotations = rotations.ravel()

# sizes is magnitude
# ravel so it maps `data` index -> size
sizes = np.linalg.norm(np.dstack([U, V]), axis=-1).ravel() * 10

fig = fpl.Figure(size=(700, 600))

fig[0, 0].add_scatter(
    data, 
    point_rotation_mode="vertex",
    point_rotations=rotations, 
    sizes=sizes,
    markers="custom",
    uniform_marker=True,
    custom_sdf=vector_sdf,
    
)

fig.show()

Maybe an API like:

subplot.add_vector_field(
    positions=[n_vectors, 3], # position of vector in x, xy, or xyz
    vectors=[n_vectors, 3], # indicates direction vector is pointing
    shape=..., # option from a few predefined common shapes, uses some predefined sdfs
    shape_options={...}, # an optional dict of some arrow options in the predfined sdf
    custom_shape=... # custom sdf
    colors=..., # maps data index -> colors, can be uniform
    uniform_colors=...,
    cmap=...,
    cmap_transform=...,
    size_factor=..., # scales sizes by this factor
    size_space=..., # usual size space option, world, screen, model,
)

see what options other libs have too

The proposed API above will only require 2 new graphic features. One buffer manager to represent the vector directions which is actually a geo rotations buffer, and a uniform for the shape.

Activity

  1. kushalkolar
    changed the title [-]Vector graphic[/-] [+]`VectorGraphic`[/+] on Jul 25, 2024
  2. kushalkolar
    changed the title [-]`VectorGraphic`[/-] [+]`VectorFieldGraphic`[/+] on Oct 12, 2025
  3. kushalkolar commented on Oct 18, 2025

    @kushalkolar
    kushalkolar
    on Oct 18, 2025
    MemberAuthor
    More actions

    Should use instanced meshes with cones so we can have 3d vector fields.

  4. kushalkolar commented on Oct 21, 2025

    @kushalkolar
    kushalkolar
    on Oct 21, 2025
    MemberAuthor
    More actions

    OK with instanced meshes I have a starting point:

    """
    Look At (Instanced Mesh)
    ========================
    
    This example shows how the look_at function can be used with instanced mesh.
    """
    
    # sphinx_gallery_pygfx_docs = 'animate 3s'
    # sphinx_gallery_pygfx_test = 'run'
    
    from time import perf_counter
    import numpy as np
    from typing import Sequence
    import fastplotlib as fpl
    
    import pygfx as pygfx
    from pygfx.geometries.utils import merge
    import pylinalg as la
    
    def generate_torso(
        radius_bottom,
        radius_top,
        height,
        radial_segments,
        height_segments,
        theta_start,
        theta_length,
        z_offset=0.0,
    ):
        # compute POSITIONS assuming x-y horizontal plane and z up axis
    
        # radius for each vertex ring from bottom to top
        n_rings = height_segments + 1
        radii = np.linspace(radius_bottom, radius_top, num=n_rings, dtype=np.float32)
    
        # height for each vertex ring from bottom to top
        half_height = height / 2
        heights = np.linspace(-half_height, half_height, num=n_rings, dtype=np.float32)
    
        # to enable texture mapping to fully wrap around the cylinder,
        # we can't close the geometry and need a degenerate vertex
        n_vertices = radial_segments + 1
    
        # xy coordinates on unit circle for a single vertex ring
        theta = np.linspace(
            theta_start, theta_start + theta_length, num=n_vertices, dtype=np.float32
        )
        ring_xy = np.column_stack([np.cos(theta), np.sin(theta)])
    
        # put all the rings together
        positions = np.empty((n_rings, n_vertices, 3), dtype=np.float32)
        positions[..., :2] = ring_xy[None, ...] * radii[:, None, None]
        positions[..., 2] = heights[:, None] - z_offset
    
        # the NORMALS are the same for every ring, so compute for only one ring
        # and then repeat
        slope = (radius_bottom - radius_top) / height
        ring_normals = np.empty(positions.shape[1:], dtype=np.float32)
        ring_normals[..., :2] = ring_xy
        ring_normals[..., 2] = slope
        ring_normals /= np.linalg.norm(ring_normals, axis=-1)[:, None]
        normals = np.empty_like(positions)
        normals[:] = ring_normals[None, ...]
    
        # the TEXTURE COORDS
        # u maps 0..1 to theta_start..theta_start+theta_length
        # v maps 0..1 to -height/2..height/2
        ring_u = (theta - theta_start) / theta_length
        ring_v = (heights / height) + 0.5
        texcoords = np.empty((n_rings, n_vertices, 2), dtype=np.float32)
        texcoords[..., 0] = ring_u[None, :]
        texcoords[..., 1] = ring_v[:, None]
    
        # the face INDEX
        # the amount of vertices
        indices = np.arange(n_rings * n_vertices, dtype=np.uint32).reshape(
            (n_rings, n_vertices)
        )
        # for every panel (height_segments, radial_segments) there is a quad (2, 3)
        index = np.empty((height_segments, radial_segments, 2, 3), dtype=np.uint32)
        # create a grid of initial indices for the panels
        index[:, :, 0, 0] = indices[
            np.arange(height_segments)[:, None], np.arange(radial_segments)[None, :]
        ]
        # the remainder of the indices for every panel are relative
        index[:, :, 0, 1] = index[:, :, 0, 0] + 1
        index[:, :, 0, 2] = index[:, :, 0, 0] + n_vertices
        index[:, :, 1, 0] = index[:, :, 0, 0] + n_vertices + 1
        index[:, :, 1, 1] = index[:, :, 1, 0] - 1
        index[:, :, 1, 2] = index[:, :, 1, 0] - n_vertices
    
        return (
            positions.reshape((-1, 3)),
            normals.reshape((-1, 3)),
            texcoords.reshape((-1, 2)),
            index.flatten(),
        )
    
    
    def generate_cap(radius, height, radial_segments, theta_start, theta_length, up=True):
        # compute POSITIONS assuming x-y horizontal plane and z up axis
    
        # to enable texture mapping to fully wrap around the cylinder,
        # we can't close the geometry and need a degenerate vertex
        n_vertices = radial_segments + 1
    
        # xy coordinates on unit circle for vertex ring
        theta = np.linspace(
            theta_start, theta_start + theta_length, num=n_vertices, dtype=np.float32
        )
        ring_xy = np.column_stack([np.cos(theta), np.sin(theta)])
    
        # put the vertices together, inserting a center vertex at the start
        positions = np.empty((1 + n_vertices, 3), dtype=np.float32)
        positions[0, :2] = [0.0, 0.0]
        positions[1:, :2] = ring_xy * radius
        positions[..., 2] = height
    
        # the NORMALS
        normals = np.zeros_like(positions, dtype=np.float32)
        sign = int(up) * 2.0 - 1.0
        normals[..., 2] = sign
    
        # the TEXTURE COORDS
        # uv etches out a circle from the [0..1, 0..1] range
        # direction is reversed for up=False
        texcoords = np.empty((1 + n_vertices, 2), dtype=np.float32)
        texcoords[0] = [0.5, 0.5]
        texcoords[1:, 0] = ring_xy[:, 0] * 0.5 + 0.5
        texcoords[1:, 1] = ring_xy[:, 1] * 0.5 * sign + 0.5
    
        # the face INDEX
        indices = np.arange(n_vertices) + 1
        # for every radial segment there is a triangle (3)
        index = np.empty((radial_segments, 3), dtype=np.uint32)
        # create a grid of initial indices for the panels
        index[:, 0] = indices[np.arange(radial_segments)]
        # the remainder of the indices for every panel are relative
        index[:, 1 + int(up)] = n_vertices
        index[:, 2 - int(up)] = index[:, 0] + 1
    
        return (
            positions,
            normals,
            texcoords,
            index.flatten(),
        )
    
    
    def create_vector_geometry(
            spacing: float,
            color: str | pygfx.Color | Sequence[float] | np.ndarray = "w",
            cone_cap_color: str | pygfx.Color | Sequence[float] | np.ndarray | None = None,
            cone_radius_divisor: float = 10.0,
            cone_height_divisor: float = 4.0,
            stalk_radius_divisor: float = 30.0,
            stalk_height_divisor: float = 4.0,
            segments: int = 24,
    ):
        cone_radius = spacing / cone_radius_divisor
        stalk_radius = spacing / stalk_radius_divisor
        radius_top = 0
    
        cone_height = spacing / cone_height_divisor
        stalk_height = spacing / stalk_height_divisor
        radial_segments = segments
    
        height_segments = 1
        theta_start = 0.0
        theta_length = np.pi * 2
    
        # create cone
        cone = generate_torso(
            cone_radius,
            radius_top,
            cone_height,
            radial_segments,
            height_segments,
            theta_start,
            theta_length,
        )
    
        groups = [cone]
    
        cone_cap_start_ix = len(cone[0])
    
        # create bottom cap
        cone_cap = generate_cap(
            cone_radius,
            -cone_height / 2,
            radial_segments,
            theta_start,
            theta_length,
            up=False,
        )
    
        cone_cap_stop_ix = cone_cap_start_ix + len(cone_cap[0])
    
        groups.append(cone_cap)
    
        stalk = generate_torso(
            stalk_radius,
            stalk_radius,
            stalk_height,
            radial_segments,
            height_segments,
            theta_start,
            theta_length,
            z_offset=cone_height,
        )
    
        groups.append(stalk)
    
        stalk_cap = generate_cap(
            stalk_radius,
            -stalk_radius / 2,
            radial_segments,
            theta_start,
            theta_length,
            up=False,
        )
    
        groups.append(stalk_cap)
    
        merged = merge(groups)
    
        positions, normals, texcoords, indices = merged
    
        color = np.array(pygfx.Color(color).rgb, dtype=np.float32)
    
        # color the cone cap in a different color
        if cone_cap_color is not None:
            cone_cap_color = np.array(pygfx.Color(cone_cap_color).rgb, dtype=np.float32)
        else:
            # make the cone cap a slightly darker version of the cone color
            cone_cap_color = (color - np.array([0.25, 0.25, 0.25], dtype=np.float32)).clip(0)
    
        colors = np.repeat([color], repeats=len(positions), axis=0)
    
        colors[cone_cap_start_ix:cone_cap_stop_ix, :] = cone_cap_color
    
        return pygfx.Geometry(
            indices=indices.reshape((-1, 3)),
            positions=positions,
            normals=normals,
            texcoords=texcoords,
            colors=colors,
        )
    
    
    spacing = 2
    
    # get uniform x, y positions
    
    geometry = create_vector_geometry(spacing / 20, color="r")
    material = pygfx.MeshBasicMaterial(color="w")
    cones = pygfx.Group()
    
    X, Y = np.meshgrid(np.arange(0, 2 * np.pi, spacing), np.arange(0, 2 * np.pi, spacing))
    X = np.arange(-10, 10, spacing)
    Y = np.arange(-10, 10, spacing)
    Z = np.arange(-10, 10, spacing)
    X, Y, Z = np.meshgrid(X, Y, Z)
    # positions of each vector as [n_points, 2] array
    positions = np.column_stack([X.ravel(), Y.ravel(), Z.ravel()])
    
    # vectors, u and v are x and y components
    U = np.cos(X.ravel())
    V = np.sin(Y.ravel())
    U, V = X.ravel(), Y.ravel()
    W = Z.ravel()
    
    rotations = np.column_stack([U.ravel(), V.ravel(), W.ravel()])
    #
    # x, y, z = np.meshgrid(np.arange(-0.8, 1, 0.2),
    #                       np.arange(-0.8, 1, 0.2),
    #                       np.arange(-0.8, 1, 0.8))
    #
    # u = np.sin(np.pi * x) * np.cos(np.pi * y) * np.cos(np.pi * z)
    # v = -np.cos(np.pi * x) * np.sin(np.pi * y) * np.cos(np.pi * z)
    # w = (np.sqrt(2.0 / 3.0) * np.cos(np.pi * x) * np.cos(np.pi * y) *
    #      np.sin(np.pi * z))
    #
    # positions = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
    #
    # rotations = np.column_stack([u.ravel(), v.ravel(), w.ravel()])
    
    instance_count = positions.shape[0]
    
    mesh = pygfx.InstancedMesh(geometry, material, instance_count)
    
    start_rot = np.array([0, 0, 1])
    
    for i in range(instance_count):
        # vector start direction is [0, 0, 1]
        rot = la.quat_from_vecs(start_rot, rotations[i])
        transform = la.mat_compose(
            positions[i], rot, np.linalg.norm(rotations[i], ord=2)
        )
    
        mesh.set_matrix_at(i, transform)
    
    fig = fpl.Figure()
    
    fig[0, 0].scene.add(mesh)
    
    fig.show()
    
    fpl.loop.run()
  5. kushalkolar
    changed the title [-]`VectorFieldGraphic`[/-] [+]`VectorsGraphic`[/+] on Nov 2, 2025
  6. kushalkolar
    closed this as completedin #929on Nov 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.