GeezerVison is showing github.com — `VectorsGraphic` · Issue #560 · fastplotlib/fastplotlib
DescriptionEasy 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. Activitychanged the title [-]Vector graphic[/-] [+]`VectorGraphic`[/+] on Jul 25, 2024changed the title [-]`VectorGraphic`[/-] [+]`VectorFieldGraphic`[/+] on Oct 12, 2025kushalkolar commented on Oct 18, 2025 Should use instanced meshes with cones so we can have 3d vector fields. kushalkolar commented on Oct 21, 2025 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()changed the title [-]`VectorFieldGraphic`[/-] [+]`VectorsGraphic`[/+] on Nov 2, 2025
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|