Repository: pygfx/pygfx
Pull request #146 (2021-09-30 09:10:19)

Shader annotate uniform
This change is required with the new (upcoming) Naga (and already works with what we currently use).

Korijn: LGTM!

Patch:
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 6487364..02e74ea 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -9,6 +9,7 @@ twine
 
 # examples
 PyQt5
+PySide6
 imageio
 imageio-ffmpeg
 scikit-image
diff --git a/examples/spheres.py b/examples/spheres.py
new file mode 100644
index 0000000..5fef4df
--- /dev/null
+++ b/examples/spheres.py
@@ -0,0 +1,93 @@
+"""
+Example showing different types of geometric cylinders.
+"""
+
+import numpy as np
+import pygfx as gfx
+
+from PySide6 import QtWidgets, QtCore
+from wgpu.gui.qt import WgpuCanvas
+
+
+class WgpuCanvasWithInputEvents(WgpuCanvas):
+    _drag_modes = {QtCore.Qt.RightButton: "pan", QtCore.Qt.LeftButton: "rotate"}
+    _mode = None
+
+    def wheelEvent(self, event):  # noqa: N802
+        controls.zoom(2 ** (event.angleDelta().y() * 0.0015))
+
+    def mousePressEvent(self, event):  # noqa: N802
+        mode = self._drag_modes.get(event.button(), None)
+        if self._mode or not mode:
+            return
+        self._mode = mode
+        drag_start = (
+            controls.pan_start if self._mode == "pan" else controls.rotate_start
+        )
+        drag_start((event.x(), event.y()), self.get_logical_size(), camera)
+        app.setOverrideCursor(QtCore.Qt.ClosedHandCursor)
+
+    def mouseReleaseEvent(self, event):  # noqa: N802
+        if self._mode and self._mode == self._drag_modes.get(event.button(), None):
+            self._mode = None
+            drag_stop = (
+                controls.pan_stop if self._mode == "pan" else controls.rotate_stop
+            )
+            drag_stop()
+            app.restoreOverrideCursor()
+
+    def mouseMoveEvent(self, event):  # noqa: N802
+        if self._mode is not None:
+            drag_move = (
+                controls.pan_move if self._mode == "pan" else controls.rotate_move
+            )
+            drag_move((event.x(), event.y()))
+
+
+app = QtWidgets.QApplication([])
+
+canvas = WgpuCanvasWithInputEvents()
+renderer = gfx.renderers.WgpuRenderer(canvas)
+scene = gfx.Scene()
+
+spheres = [
+    (
+        (0, 0, -7.5),
+        (0, 0.65, 0, 1),
+        gfx.SphereGeometry(4.5, phi_length=np.pi * 1.5),
+    ),
+    ((0, 0, 7.5), (1, 1, 1, 1), gfx.SphereGeometry(4)),
+    (
+        (15, 0, -7.5),
+        (1, 0.3, 0.3, 1),
+        gfx.SphereGeometry(4, theta_start=np.pi * 0.25, theta_length=np.pi * 0.50),
+    ),
+    (
+        (15, 0, 7.5),
+        (0.35, 0, 0, 1),
+        gfx.SphereGeometry(5, width_segments=6),
+    ),
+    ((-15, 0, -7.5), (1, 1, 0.75, 1), gfx.SphereGeometry(7)),
+    ((-15, 0, 7.5), (1, 1, 0.75, 1), gfx.SphereGeometry(5, height_segments=8)),
+]
+for pos, color, geometry in spheres:
+    material = gfx.MeshFlatMaterial(color=color)
+    wobject = gfx.Mesh(geometry, material)
+    wobject.position.set(*pos)
+    scene.add(wobject)
+
+camera = gfx.PerspectiveCamera(70, 16 / 9)
+camera.position.set(6, 16, -22)
+controls = gfx.OrbitControls(camera.position.clone())
+
+
+def animate():
+    controls.update_camera(camera)
+
+    renderer.render(scene, camera)
+    canvas.request_draw()
+
+
+if __name__ == "__main__":
+    canvas.request_draw(animate)
+    app.exec_()
diff --git a/pygfx/geometries/__init__.py b/pygfx/geometries/__init__.py
index 0bbcc19..7ea5323 100644
--- a/pygfx/geometries/__init__.py
+++ b/pygfx/geometries/__init__.py
@@ -3,6 +3,7 @@
 from ._base import Geometry
 from ._box import BoxGeometry
 from ._cylinder import CylinderGeometry
+from ._sphere import SphereGeometry
 from ._plane import PlaneGeometry
 from ._toroidal import KleinBottleGeometry, TorusKnotGeometry
 
diff --git a/pygfx/geometries/_cylinder.py b/pygfx/geometries/_cylinder.py
index 9237509..6315b05 100644
--- a/pygfx/geometries/_cylinder.py
+++ b/pygfx/geometries/_cylinder.py
@@ -62,7 +62,7 @@ def generate_torso(
 
     # the face INDEX
     # the amount of vertices
-    indices = np.arange(n_rings * n_vertices).reshape((n_rings, n_vertices))
+    indices = np.arange(n_rings * n_vertices, dtype="u4").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="u4")
     # create a grid of initial indices for the panels
diff --git a/pygfx/geometries/_sphere.py b/pygfx/geometries/_sphere.py
new file mode 100644
index 0000000..4b494ca
--- /dev/null
+++ b/pygfx/geometries/_sphere.py
@@ -0,0 +1,97 @@
+import numpy as np
+
+from ..resources import Buffer
+from ._base import Geometry
+
+
+def generate_sphere(
+    radius,
+    width_segments,
+    height_segments,
+    phi_start,
+    phi_length,
+    theta_start,
+    theta_length,
+):
+    # create grid of spherical coordinates
+    nx = width_segments + 1
+    phi_end = phi_start + phi_length
+    phi = np.linspace(phi_start, phi_end, num=nx, dtype="f4")
+
+    ny = height_segments + 1
+    theta_end = theta_start + theta_length
+    theta = np.linspace(theta_start, theta_end, num=ny, dtype="f4")
+
+    # grid has shape (ny, nx)
+    phi_grid, theta_grid = np.meshgrid(phi, theta)
+
+    # convert to cartesian coordinates
+    theta_grid_sin = np.sin(theta_grid)
+    xx = np.cos(phi_grid) * theta_grid_sin * -1
+    yy = np.cos(theta_grid)
+    zz = np.sin(phi_grid) * theta_grid_sin
+
+    # construct normals and positions
+    normals = np.stack([xx, yy, zz], axis=-1)
+    positions = normals * radius
+
+    # construct texture coordinates
+    # u maps 0..1 to phi_start..phi_start+phi_length
+    # v maps 1..0 to theta_start..theta_start+theta_length
+    uu = (phi_grid - phi_start) / phi_length
+    vv = 1 - ((theta_grid - theta_start) / theta_length)
+    texcoords = np.stack([uu, vv], axis=-1)
+
+    # the face indices
+    # assign an index to every vertex on the grid
+    indices = np.arange(nx * ny, dtype="u4").reshape((ny, nx))
+    # for every panel (height_segments, width_segments) there is a quad (2, 3)
+    index = np.empty((height_segments, width_segments, 2, 3), dtype="u4")
+    # create a grid of initial indices for the panels
+    index[:, :, 0, 0] = indices[
+        np.arange(height_segments)[:, None], np.arange(width_segments)[None, :]
+    ]
+    # the remainder of the indices for every panel are relative
+    index[:, :, 0, 1] = index[:, :, 0, 0] + nx
+    index[:, :, 0, 2] = index[:, :, 0, 0] + 1
+    index[:, :, 1, 0] = index[:, :, 0, 0] + nx + 1
+    index[:, :, 1, 1] = index[:, :, 1, 0] - nx
+    index[:, :, 1, 2] = index[:, :, 1, 0] - 1
+
+    return (
+        positions.reshape((-1, 3)),
+        normals.reshape((-1, 3)),
+        texcoords.reshape((-1, 2)),
+        index.flatten(),
+    )
+
+
+class SphereGeometry(Geometry):
+    """A geometry defining a Sphere."""
+
+    def __init__(
+        self,
+        radius=1,
+        width_segments=32,
+        height_segments=16,
+        phi_start=0,
+        phi_length=np.pi * 2,
+        theta_start=0,
+        theta_length=np.pi,
+    ):
+        super().__init__()
+
+        vertices, normals, texcoords, indices = generate_sphere(
+            radius=radius,
+            width_segments=width_segments,
+            height_segments=height_segments,
+            phi_start=phi_start,
+            phi_length=phi_length,
+            theta_start=theta_start,
+            theta_length=theta_length,
+        )
+
+        self.positions = Buffer(vertices, usage="vertex|storage")
+        self.normals = Buffer(normals, usage="vertex|storage")
+        self.texcoords = Buffer(texcoords, usage="vertex|storage")
+        self.index = Buffer(indices, usage="index|storage")
diff --git a/pygfx/renderers/wgpu/_shadercomposer.py b/pygfx/renderers/wgpu/_shadercomposer.py
index 583942d..2ca2c84 100644
--- a/pygfx/renderers/wgpu/_shadercomposer.py
+++ b/pygfx/renderers/wgpu/_shadercomposer.py
@@ -132,7 +132,7 @@ class BaseShader:
         }};
 
         [[group({bindgroup}), binding({index})]]
-        var {name}: {structname};
+        var<uniform> {name}: {structname};
         """
         self._uniform_codes[name] = code
 


Test patch:
diff --git a/tests/renderers/test_shader_composer.py b/tests/renderers/test_shader_composer.py
index 3c473ac..481cf74 100644
--- a/tests/renderers/test_shader_composer.py
+++ b/tests/renderers/test_shader_composer.py
@@ -65,7 +65,7 @@ def test_uniform_definitions():
         };
 
         [[group(0), binding(0)]]
-        var zz: Struct_zz;
+        var<uniform> zz: Struct_zz;
     """.strip()
     )
 
@@ -82,7 +82,7 @@ def test_uniform_definitions():
         };
 
         [[group(0), binding(0)]]
-        var zz: Struct_zz;
+        var<uniform> zz: Struct_zz;
     """.strip()
     )
 
@@ -99,7 +99,7 @@ def test_uniform_definitions():
         };
 
         [[group(0), binding(0)]]
-        var zz: Struct_zz;
+        var<uniform> zz: Struct_zz;
     """.strip()
     )