From 75c88bbd66bcbdeb03218e1cd07331c17a7c5cc9 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 21 Mar 2024 09:38:09 +0100 Subject: [PATCH] More examples with wgpu --- examples/webgpu/__init__.py | 0 examples/webgpu/pbr2.py | 116 ++++++++++ examples/webgpu/pbr2_embed.py | 213 ++++++++++++++++++ examples/webgpu/pop.py | 90 ++++++++ .../bindings/ScriptJuceGuiBasicsBindings.h | 32 +-- .../ScriptJuceGuiEntryPointsBindings.cpp | 24 +- 6 files changed, 447 insertions(+), 28 deletions(-) create mode 100644 examples/webgpu/__init__.py create mode 100644 examples/webgpu/pbr2.py create mode 100644 examples/webgpu/pbr2_embed.py diff --git a/examples/webgpu/__init__.py b/examples/webgpu/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/webgpu/pbr2.py b/examples/webgpu/pbr2.py new file mode 100644 index 0000000000..72a8fc951e --- /dev/null +++ b/examples/webgpu/pbr2.py @@ -0,0 +1,116 @@ +""" + +PBR Rendering 2 +=============== + +This example shows the lighting rendering affect of materials with different +metalness and roughness. Every second sphere has an IBL environment map on it. +""" + +# run_example = false +# sphinx_gallery_pygfx_animate = True +# sphinx_gallery_pygfx_duration = 3 +# sphinx_gallery_pygfx_lossless = False + +import math +from colorsys import hls_to_rgb +from itertools import count + +import imageio.v3 as iio +import numpy as np +from pop import WgpuCanvas, run + +import pygfx as gfx +import pylinalg as la + + +def main(canvas): + frame_idx = count() # counter for animation + renderer = gfx.renderers.WgpuRenderer(canvas) + scene = gfx.Scene() + + # Lights + scene.add(gfx.AmbientLight("#fff", 0.2)) + directional_light = gfx.DirectionalLight("#fff", 3) + directional_light.local.position = la.vec_normalize((1, 1, 1)) + scene.add(directional_light) + point_light = gfx.PointLight("#fff", 3) + scene.add(point_light) + point_light.add(gfx.PointLightHelper(4)) + + # Read cube image and turn it into a 3D image (a 4d array) + env_img = iio.imread("imageio:meadow_cube.jpg") + cube_size = env_img.shape[1] + env_img.shape = 6, cube_size, cube_size, env_img.shape[-1] + + # Create environment map + env_tex = gfx.Texture( + env_img, dim=2, size=(cube_size, cube_size, 6), generate_mipmaps=True + ) + + # Apply env map to skybox + background = gfx.Background(None, gfx.BackgroundSkyboxMaterial(map=env_tex)) + scene.add(background) + + # Now create spheres ... + cube_width = 400 + numbers_per_side = 5 + sphere_radius = (cube_width / numbers_per_side) * 0.8 * 0.5 + step_size = 1.0 / numbers_per_side + + geometry = gfx.sphere_geometry(sphere_radius, 32, 16) + + index = 0 + alpha = 0.0 + while alpha <= 1.0: + beta = 0.0 + while beta <= 1.0: + gamma = 0.0 + while gamma <= 1.0: + material = gfx.MeshStandardMaterial( + color=hls_to_rgb(alpha, 0.5, gamma * 0.5 + 0.1), + metalness=beta, + roughness=1.0 - alpha, + ) + + if index % 2 != 0: + material.env_map = env_tex + + mesh = gfx.Mesh(geometry, material) + + mesh.local.position = ( + alpha * 400 - 200, + beta * 400 - 200, + gamma * 400 - 200, + ) + scene.add(mesh) + index += 1 + + gamma += step_size + beta += step_size + alpha += step_size + + # Create camera and controls + camera = gfx.PerspectiveCamera(45, 640 / 480) + camera.show_object(scene, view_dir=(-2, -1.5, -3), scale=1.2) + + controller = gfx.OrbitController(camera, register_events=renderer) + + def animate(): + timer = next(frame_idx) / 30 + + point_light.local.position = ( + math.sin(timer / 30 * (2 * np.pi)) * 300, + math.cos(timer * 2 / 30 * (2 * np.pi)) * 400, + math.cos(timer / 30 * (2 * np.pi)) * 300, + ) + + renderer.render(scene, camera) + renderer.request_draw() + + renderer.request_draw(animate) + + +if __name__ == "__main__": + canvas = WgpuCanvas(size=(640, 480), title="gfx_pbr") + run(canvas, main) diff --git a/examples/webgpu/pbr2_embed.py b/examples/webgpu/pbr2_embed.py new file mode 100644 index 0000000000..dba40fd999 --- /dev/null +++ b/examples/webgpu/pbr2_embed.py @@ -0,0 +1,213 @@ +""" + +PBR Rendering 2 +=============== + +This example shows the lighting rendering affect of materials with different +metalness and roughness. Every second sphere has an IBL environment map on it. +""" + +# run_example = false +# sphinx_gallery_pygfx_animate = True +# sphinx_gallery_pygfx_duration = 3 +# sphinx_gallery_pygfx_lossless = False + +import math +from colorsys import hls_to_rgb +from itertools import count + +import imageio.v3 as iio +import numpy as np +from pop import WgpuWidget + +import popsicle as juce +import pygfx as gfx +import pylinalg as la + + +class WgpuComponent(juce.Component): + canvas = None + + def __init__(self): + super().__init__() + + self.canvas = WgpuWidget() + self.addAndMakeVisible(self.canvas) + + self.slider = juce.Slider() + self.slider.setRange(1.0, 100.0, 0.1) + self.slider.onValueChange = lambda: self.sliderChanged() + self.addAndMakeVisible(self.slider) + + self.setSize(640, 480) + self.setVisible(True) + + def __del__(self): + if self.renderer: + del self.renderer + + if self.canvas: + del self.canvas + + def resized(self): + bounds = self.getLocalBounds() + + sliderBounds = bounds.removeFromBottom(40).reduced(10) + self.slider.setBounds(sliderBounds) + + self.canvas.setBounds(bounds.reduced(10)) + + def sliderChanged(self): + value = self.slider.getValue() + + if self.point_light: + self.point_light.intensity = value + + def attachToWindow(self, window): + self.canvas.reparentToWindow(window) + + self.frame_idx = count() # counter for animation + self.renderer = gfx.renderers.WgpuRenderer(self.canvas) + self.scene = gfx.Scene() + + # Lights + self.scene.add(gfx.AmbientLight("#fff", 0.2)) + self.directional_light = gfx.DirectionalLight("#fff", 3) + self.directional_light.local.position = la.vec_normalize((1, 1, 1)) + self.scene.add(self.directional_light) + self.point_light = gfx.PointLight("#fff", 3) + self.scene.add(self.point_light) + self.point_light.add(gfx.PointLightHelper(4)) + + # Read cube image and turn it into a 3D image (a 4d array) + env_img = iio.imread("imageio:meadow_cube.jpg") + self.cube_size = env_img.shape[1] + env_img.shape = 6, self.cube_size, self.cube_size, env_img.shape[-1] + + # Create environment map + env_tex = gfx.Texture(env_img, dim=2, size=(self.cube_size, self.cube_size, 6), generate_mipmaps=True) + + # Apply env map to skybox + self.background = gfx.Background(None, gfx.BackgroundSkyboxMaterial(map=env_tex)) + self.scene.add(self.background) + + # Now create spheres ... + cube_width = 400 + numbers_per_side = 5 + sphere_radius = (cube_width / numbers_per_side) * 0.8 * 0.5 + step_size = 1.0 / numbers_per_side + + self.geometry = gfx.sphere_geometry(sphere_radius, 32, 16) + + index = 0 + alpha = 0.0 + while alpha <= 1.0: + beta = 0.0 + while beta <= 1.0: + gamma = 0.0 + while gamma <= 1.0: + material = gfx.MeshStandardMaterial( + color=hls_to_rgb(alpha, 0.5, gamma * 0.5 + 0.1), + metalness=beta, + roughness=1.0 - alpha, + ) + + if index % 2 != 0: + material.env_map = env_tex + + mesh = gfx.Mesh(self.geometry, material) + + mesh.local.position = ( + alpha * 400 - 200, + beta * 400 - 200, + gamma * 400 - 200, + ) + + self.scene.add(mesh) + index += 1 + + gamma += step_size + beta += step_size + alpha += step_size + + # Create camera and controls + self.camera = gfx.PerspectiveCamera(45, self.getWidth() / self.getHeight()) + self.camera.show_object(self.scene, view_dir=(-2, -1.5, -3), scale=1.2) + self.controller = gfx.OrbitController(self.camera, register_events=self.renderer) + + def animate(): + timer = next(self.frame_idx) / 30 + + self.point_light.local.position = ( + math.sin(timer / 30 * (2 * np.pi)) * 300, + math.cos(timer * 2 / 30 * (2 * np.pi)) * 400, + math.cos(timer / 30 * (2 * np.pi)) * 300, + ) + + self.renderer.render(self.scene, self.camera) + self.renderer.request_draw() + + self.renderer.request_draw(animate) + + def detach(self): + if self.canvas: + self.canvas.removeFromDesktop() + + +class WgpuWindow(juce.DocumentWindow): + component = None + + def __init__(self): + super().__init__( + "wgpu triangle embedded in a popsicle app", + juce.Desktop.getInstance().getDefaultLookAndFeel() + .findColour(juce.ResizableWindow.backgroundColourId), + juce.DocumentWindow.allButtons, + True) + + self.component = WgpuComponent() + + self.setUsingNativeTitleBar(True) + self.setResizable(True, True) + self.setContentNonOwned(self.component, True) + self.centreWithSize(self.component.getWidth(), self.component.getHeight()) + self.setVisible(True) + + self.component.attachToWindow(self) + self.getContentComponent().resized() + + def __del__(self): + self.clearContentComponent() + + if self.component: + self.component.detach() + del self.component + + def closeButtonPressed(self): + juce.JUCEApplication.getInstance().systemRequestedQuit() + + +class WgpuApplication(juce.JUCEApplication): + window = None + + def getApplicationName(self): + return "triangle-embed-juce" + + def getApplicationVersion(self): + return "1.0" + + def initialise(self, commandLineParameters: str): + self.window = WgpuWindow() + + juce.MessageManager.callAsync(lambda: juce.Process.makeForegroundProcess()) + + def shutdown(self): + if self.window: + del self.window + + def systemRequestedQuit(self): + self.quit() + + +if __name__ == "__main__": + juce.START_JUCE_APPLICATION(WgpuApplication) diff --git a/examples/webgpu/pop.py b/examples/webgpu/pop.py index 80a95e170b..2f88410dcd 100644 --- a/examples/webgpu/pop.py +++ b/examples/webgpu/pop.py @@ -27,6 +27,36 @@ def enable_hidpi(): enable_hidpi() +def convert_buttons(event: juce.MouseEvent): + button = 0 + buttons = [] + if event.mods.isLeftButtonDown(): + button = 1 + buttons.append(1) + if event.mods.isRightButtonDown(): + button = 2 + buttons.append(2) + if event.mods.isMiddleButtonDown(): + button = 3 + buttons.append(3) + + modifiers = [] + if event.mods.getCurrentModifiers() == juce.ModifierKeys.altModifier: + modifiers.append("Alt") + if event.mods.getCurrentModifiers() == juce.ModifierKeys.shiftModifier: + modifiers.append("Shift") + if event.mods.getCurrentModifiers() == juce.ModifierKeys.ctrlModifier: + modifiers.append("Control") + if event.mods.getCurrentModifiers() == juce.ModifierKeys.commandModifier: + modifiers.append("Meta") + + return button, buttons, modifiers + + +def call_later(delay, callback, *args): + juce.MessageManager.callAsync(lambda: callback(*args)) # int(delay * 1000) + + class JUCECallbackTimer(juce.Timer): def __init__(self, callback): juce.Timer.__init__(self) @@ -115,6 +145,66 @@ def _request_draw(self): def is_closed(self): return not self.isVisible() + # User events to jupyter_rfb events + + def _mouse_event(self, event_type, event, touches=True): + button, buttons, modifiers = convert_buttons(event) + + ev = { + "event_type": event_type, + "x": event.getPosition().x, + "y": event.getPosition().y, + "button": button, + "buttons": buttons, + "modifiers": modifiers, + } + + if touches: + ev.update({ + "ntouches": 0, + "touches": {} + }) + + if event_type == "pointer_move": + match_keys = {"buttons", "modifiers", "ntouches"} + accum_keys = {} + self._handle_event_rate_limited(ev, call_later, match_keys, accum_keys) + else: + self._handle_event_and_flush(ev) + + def mouseDown(self, event): # noqa: N802 + self._mouse_event("pointer_down", event) + + def mouseMove(self, event): # noqa: N802 + self._mouse_event("pointer_move", event) + + def mouseDrag(self, event): # noqa: N802 + self._mouse_event("pointer_move", event) + + def mouseUp(self, event): # noqa: N802 + self._mouse_event("pointer_up", event) + + def mouseDoubleClick(self, event): # noqa: N802 + self._mouse_event("double_click", event, touches=False) + + def mouseWheelMove(self, event, wheel): + _, buttons, modifiers = convert_buttons(event) + + ev = { + "event_type": "wheel", + "dx": -wheel.deltaX * 200, + "dy": -wheel.deltaY * 200, + "x": event.getPosition().x, + "y": event.getPosition().y, + "buttons": buttons, + "modifiers": modifiers + } + + match_keys = {"modifiers"} + accum_keys = {"dx", "dy"} + + self._handle_event_rate_limited(ev, call_later, match_keys, accum_keys) + # Custom methods to make Component work when used standalone def reparentToWindow(self, window): diff --git a/modules/juce_python/bindings/ScriptJuceGuiBasicsBindings.h b/modules/juce_python/bindings/ScriptJuceGuiBasicsBindings.h index aedea3da8a..e89a806020 100644 --- a/modules/juce_python/bindings/ScriptJuceGuiBasicsBindings.h +++ b/modules/juce_python/bindings/ScriptJuceGuiBasicsBindings.h @@ -121,29 +121,31 @@ struct PyJUCEApplication : juce::JUCEApplication return; } - if (globalOptions().catchExceptionsAndContinue) + if (pyEx != nullptr) { - pybind11::print (100, ex->what()); + pybind11::print (ex->what()); + traceback.attr ("print_tb")(pyEx->trace()); - if (pyEx != nullptr) + if (pyEx->matches (PyExc_KeyboardInterrupt) || PyErr_CheckSignals() != 0) { - traceback.attr ("print_tb")(pyEx->trace()); - - if (pyEx->matches (PyExc_KeyboardInterrupt)) - globalOptions().caughtKeyboardInterrupt = true; - } - else - { - traceback.attr ("print_stack")(); - - if (PyErr_CheckSignals() != 0) - globalOptions().caughtKeyboardInterrupt = true; + globalOptions().caughtKeyboardInterrupt = true; + return; } } else { - std::terminate(); + pybind11::print (ex->what()); + traceback.attr ("print_stack")(); + + if (PyErr_CheckSignals() != 0) + { + globalOptions().caughtKeyboardInterrupt = true; + return; + } } + + if (! globalOptions().catchExceptionsAndContinue) + std::terminate(); } void memoryWarningReceived() override diff --git a/modules/juce_python/bindings/ScriptJuceGuiEntryPointsBindings.cpp b/modules/juce_python/bindings/ScriptJuceGuiEntryPointsBindings.cpp index 34062aebc0..f975911a92 100644 --- a/modules/juce_python/bindings/ScriptJuceGuiEntryPointsBindings.cpp +++ b/modules/juce_python/bindings/ScriptJuceGuiEntryPointsBindings.cpp @@ -66,24 +66,22 @@ void runApplication (JUCEApplicationBase* application, int milliseconds) while (! MessageManager::getInstance()->hasStopMessageBeenSent()) { - if (globalOptions().catchExceptionsAndContinue) + try { - try - { - py::gil_scoped_release release; + py::gil_scoped_release release; - MessageManager::getInstance()->runDispatchLoopUntil (milliseconds); - } - catch (const py::error_already_set& e) + MessageManager::getInstance()->runDispatchLoopUntil (milliseconds); + } + catch (const py::error_already_set& e) + { + if (globalOptions().catchExceptionsAndContinue) { Helpers::printPythonException (e); } - } - else - { - py::gil_scoped_release release; - - MessageManager::getInstance()->runDispatchLoopUntil (milliseconds); + else + { + throw e; + } } if (globalOptions().caughtKeyboardInterrupt)