.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "gallery/pygfx_example.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_gallery_pygfx_example.py: Pygfx example ============= This is a complex example from pygfx that includes compute shaders, PBR, imgui and even asset fetching. it only needed a small modification to work in pyodide. .. GENERATED FROM PYTHON SOURCE LINES 8-714 .. code-block:: Python import numpy as np import pygfx as gfx import wgpu import imageio.v3 as iio from rendercanvas.auto import RenderCanvas, loop from pygfx.utils.compute import ComputeShader from imgui_bundle import imgui from wgpu.utils.imgui import ImguiRenderer from wgpu.utils.imgui import Stats canvas = RenderCanvas( size=(1280, 720), update_mode="fastest", title="compute cloth", vsync=False ) renderer = gfx.WgpuRenderer(canvas) camera = gfx.PerspectiveCamera(45, 1280 / 720, depth_range=(0.01, 10)) scene = gfx.Scene() # cloth parameters cloth_width = 1.0 cloth_height = 1.0 cloth_segments_x = 30 cloth_segments_y = 30 sphere_radius = 0.15 # Verlet system parameters verlet_vertices = [] verlet_springs = [] vertex_columns = [] def setup_verlet_geometry(): global verlet_vertices, verlet_springs vertex_id = 0 # create Verlet vertices for x in range(cloth_segments_x + 1): column = [] for y in range(cloth_segments_y + 1): pos_x = x * (cloth_width / cloth_segments_x) - cloth_width * 0.5 pos_z = y * (cloth_height / cloth_segments_y) pos_y = cloth_height * 0.5 # fixed vertices at the top is_fixed = (y == 0) and (x % 5 == 0) vertex = { "id": vertex_id, "position": np.array([pos_x, pos_y, pos_z], dtype=np.float32), "is_fixed": is_fixed, "spring_ids": [], } verlet_vertices.append(vertex) column.append(vertex) vertex_id += 1 vertex_columns.append(column) # create Verlet springs spring_id = 0 def add_verlet_spring(vertex0, vertex1): nonlocal spring_id spring = { "id": spring_id, "vertex0_id": vertex0["id"], "vertex1_id": vertex1["id"], "rest_length": np.linalg.norm(vertex0["position"] - vertex1["position"]), } vertex0["spring_ids"].append(spring_id) vertex1["spring_ids"].append(spring_id) verlet_springs.append(spring) spring_id += 1 for x in range(cloth_segments_x + 1): for y in range(cloth_segments_y + 1): vertex0 = vertex_columns[x][y] if x > 0: add_verlet_spring(vertex0, vertex_columns[x - 1][y]) if y > 0: add_verlet_spring(vertex0, vertex_columns[x][y - 1]) if x > 0 and y > 0: add_verlet_spring(vertex0, vertex_columns[x - 1][y - 1]) if x > 0 and y < cloth_segments_y: add_verlet_spring(vertex0, vertex_columns[x - 1][y + 1]) def setup_verlet_buffers(): vertex_count = len(verlet_vertices) spring_count = len(verlet_springs) # Verlet vertex positions buffer positions = np.array([v["position"] for v in verlet_vertices], dtype=np.float32) position_buffer = gfx.Buffer( data=positions, usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST | wgpu.BufferUsage.VERTEX, ) # Verlet vertex forces buffer forces = np.zeros((vertex_count, 3), dtype=np.float32) force_buffer = gfx.Buffer( data=forces, usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST ) # Verlet vertex params buffer (is_fixed, spring_count, spring_pointer) spring_list = [] vertex_params = np.zeros((vertex_count, 3), dtype=np.uint32) for i, vertex in enumerate(verlet_vertices): vertex_params[i, 0] = 1 if vertex["is_fixed"] else 0 vertex_params[i, 1] = len(vertex["spring_ids"]) vertex_params[i, 2] = len(spring_list) spring_list.extend(vertex["spring_ids"]) vertex_params_buffer = gfx.Buffer( data=vertex_params, usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST ) # spring list buffer spring_list_buffer = gfx.Buffer( data=np.array(spring_list, dtype=np.uint32), usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST, ) # spring data buffers spring_vertex_ids = np.array( [[s["vertex0_id"], s["vertex1_id"]] for s in verlet_springs], dtype=np.uint32 ) spring_rest_lengths = np.array( [s["rest_length"] for s in verlet_springs], dtype=np.float32 ) spring_vertex_ids_buffer = gfx.Buffer( data=spring_vertex_ids, usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST, ) spring_rest_lengths_buffer = gfx.Buffer( data=spring_rest_lengths, usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST, ) # spring forces buffer spring_forces = np.zeros((spring_count, 3), dtype=np.float32) spring_force_buffer = gfx.Buffer( data=spring_forces, usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST ) # param buffer [dampening, sphere_pos(3), sphere_active, wind, stiffness, time] params_data = np.array([0.99, 0.0, 0.0, 0.0, 1.0, 1.0, 0.2, 0.0], dtype=np.float32) params_buffer = gfx.Buffer( data=params_data, usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST ) return ( position_buffer, force_buffer, vertex_params_buffer, spring_list_buffer, spring_vertex_ids_buffer, spring_rest_lengths_buffer, spring_force_buffer, params_buffer, vertex_count, spring_count, ) # setup Verlet geometry and buffers setup_verlet_geometry() ( position_buffer, force_buffer, vertex_params_buffer, spring_list_buffer, spring_vertex_ids_buffer, spring_rest_lengths_buffer, spring_force_buffer, params_buffer, vertex_count, spring_count, ) = setup_verlet_buffers() # todo: now, storage buffers (gfx.Buffer) are not supported array>, so we use array instead. # compute shaders for Verlet integration spring_compute_wgsl = """ struct Params { dampening: f32, sphere_x: f32, sphere_y: f32, sphere_z: f32, sphere_active: f32, wind: f32, stiffness: f32, time: f32, } @group(0) @binding(0) var params: Params; @group(0) @binding(1) var positions: array; @group(0) @binding(2) var spring_vertex_ids: array>; @group(0) @binding(3) var spring_rest_lengths: array; @group(0) @binding(4) var spring_forces: array; @compute @workgroup_size(64) fn compute_spring_forces(@builtin(global_invocation_id) global_id: vec3) { let index = global_id.x; if (index >= arrayLength(&spring_rest_lengths)) { return; } let vertex_ids = spring_vertex_ids[index]; let rest_length = spring_rest_lengths[index]; // let vertex0_pos = positions[vertex_ids.x]; let vertex0_pos = vec3(positions[vertex_ids.x * 3u], positions[vertex_ids.x * 3u + 1u], positions[vertex_ids.x * 3u + 2u]); // let vertex1_pos = positions[vertex_ids.y]; let vertex1_pos = vec3(positions[vertex_ids.y * 3u], positions[vertex_ids.y * 3u + 1u], positions[vertex_ids.y * 3u + 2u]); let delta = vertex1_pos - vertex0_pos; let dist = length(delta); let force = (dist - rest_length) * params.stiffness * delta * 0.5 / dist; // spring_forces[index] = force; spring_forces[index * 3u] = force.x; spring_forces[index * 3u + 1u] = force.y; spring_forces[index * 3u + 2u] = force.z; } """ vertex_compute_wgsl = """ struct Params { dampening: f32, sphere_x: f32, sphere_y: f32, sphere_z: f32, sphere_active: f32, wind: f32, stiffness: f32, time: f32, } @group(0) @binding(0) var params: Params; @group(0) @binding(1) var positions: array; @group(0) @binding(2) var forces: array; @group(0) @binding(3) var vertex_params: array; @group(0) @binding(4) var spring_list: array; @group(0) @binding(5) var spring_vertex_ids: array>; @group(0) @binding(6) var spring_forces: array; override SPHERE_RADIUS: f32 = 0.15; fn tri(x: f32) -> f32 { return abs(fract(x) - 0.5); } fn tri3(p: vec3) -> vec3 { return vec3( tri(p.z + tri(p.y * 1.0)), tri(p.z + tri(p.x * 1.0)), tri(p.y + tri(p.x * 1.0)) ); } fn triNoise3D(position: vec3, speed: f32, time: f32) -> f32 { var p = position; var z = 1.4; var rz = 0.0; var bp = position; for (var i = 0.0; i <= 3.0; i += 1.0) { let dg = tri3(bp * 2.0); p += dg + time * 0.1 * speed; bp *= 1.8; z *= 1.5; p *= 1.2; let t = tri(p.z + tri(p.x + tri(p.y))); rz += t / z; bp += vec3(0.14); } return rz; } @compute @workgroup_size(64) fn compute_vertex_forces(@builtin(global_invocation_id) global_id: vec3) { let index = global_id.x; if (index >= arrayLength(&positions)/3u) { return; } // let vertex_param = vertex_params[index]; let vertex_param = vec3(vertex_params[index * 3u], vertex_params[index * 3u + 1u], vertex_params[index * 3u + 2u]); let is_fixed = vertex_param.x; let spring_count = vertex_param.y; let spring_pointer = vertex_param.z; if (is_fixed == 1u) { return; } // let position = positions[index]; let position = vec3(positions[index * 3u], positions[index * 3u + 1u], positions[index * 3u + 2u]); // var force = forces[index]; var force = vec3(forces[index * 3u], forces[index * 3u + 1u], forces[index * 3u + 2u]); // dampening force *= params.dampening; // accumulate forces from springs for (var i = 0u; i < spring_count; i++) { let spring_id = spring_list[spring_pointer + i]; // let spring_force = spring_forces[spring_id]; let spring_force = vec3( spring_forces[spring_id * 3u], spring_forces[spring_id * 3u + 1u], spring_forces[spring_id * 3u + 2u] ); let spring_vertex_ids = spring_vertex_ids[spring_id]; let factor = select(-1.0, 1.0, spring_vertex_ids.x == index); force += spring_force * factor; } // gravity force.y -= 0.00005; // wind let noise_val = triNoise3D(position, 1.0, params.time) - 0.2; let wind_force = noise_val * 0.0001 * params.wind; force.z -= wind_force; // sphere interaction if (params.sphere_active > 0.0) { let sphere_pos = vec3(params.sphere_x, params.sphere_y, params.sphere_z); let new_pos = position + force; let delta_sphere = new_pos - sphere_pos; let dist = length(delta_sphere); if (dist < SPHERE_RADIUS) { let penetration = SPHERE_RADIUS - dist; let normal = delta_sphere / max(dist, 0.000001); force += normal * penetration; } } // forces[index] = force; forces[index * 3u] = force.x; forces[index * 3u + 1u] = force.y; forces[index * 3u + 2u] = force.z; // positions[index] += force; let new_position = position + force; positions[index * 3u] = new_position.x; positions[index * 3u + 1u] = new_position.y; positions[index * 3u + 2u] = new_position.z; } """ # create the Verlet geometry and buffers spring_shader = ComputeShader(spring_compute_wgsl, entry_point="compute_spring_forces") vertex_shader = ComputeShader(vertex_compute_wgsl, entry_point="compute_vertex_forces") spring_shader.set_resource(0, params_buffer) spring_shader.set_resource(1, position_buffer) spring_shader.set_resource(2, spring_vertex_ids_buffer) spring_shader.set_resource(3, spring_rest_lengths_buffer) spring_shader.set_resource(4, spring_force_buffer) vertex_shader.set_resource(0, params_buffer) vertex_shader.set_resource(1, position_buffer) vertex_shader.set_resource(2, force_buffer) vertex_shader.set_resource(3, vertex_params_buffer) vertex_shader.set_resource(4, spring_list_buffer) vertex_shader.set_resource(5, spring_vertex_ids_buffer) vertex_shader.set_resource(6, spring_force_buffer) vertex_shader.set_constant("SPHERE_RADIUS", sphere_radius) def create_cloth_mesh_buffer(): vertex_count = cloth_segments_x * cloth_segments_x verlet_vertex_ids = np.zeros((vertex_count, 4), dtype=np.uint32) indices = [] def get_index(x, y): return y * cloth_segments_x + x for x in range(cloth_segments_x): for y in range(cloth_segments_y): index = get_index(x, y) verlet_vertex_ids[index, 0] = vertex_columns[x][y]["id"] verlet_vertex_ids[index, 1] = vertex_columns[x + 1][y]["id"] verlet_vertex_ids[index, 2] = vertex_columns[x][y + 1]["id"] verlet_vertex_ids[index, 3] = vertex_columns[x + 1][y + 1]["id"] if x > 0 and y > 0: # two triangles for each quad indices.append( [get_index(x, y), get_index(x - 1, y), get_index(x - 1, y - 1)] ) indices.append( [get_index(x, y), get_index(x - 1, y - 1), get_index(x, y - 1)] ) verlet_vertex_ids_buffer = gfx.Buffer( data=verlet_vertex_ids, usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST, ) cloth_position_buffer = gfx.Buffer( data=np.zeros((vertex_count, 3), dtype=np.float32), usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST | wgpu.BufferUsage.VERTEX, ) cloth_normal_buffer = gfx.Buffer( data=np.zeros((vertex_count, 3), dtype=np.float32), usage=wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_DST, ) return ( cloth_position_buffer, verlet_vertex_ids_buffer, cloth_normal_buffer, np.array(indices, dtype=np.uint32), ) ( cloth_position_buffer, verlet_vertex_ids_buffer, cloth_normal_buffer, cloth_indices, ) = create_cloth_mesh_buffer() cloth_compute_wgsl = """ @group(0) @binding(0) var vertex_positions: array; @group(0) @binding(1) var vertex_ids: array; @group(0) @binding(2) var positions: array; @group(0) @binding(3) var normals: array; @compute @workgroup_size(64) fn compute_normals(@builtin(global_invocation_id) global_id: vec3) { let index = global_id.x; if (index >= arrayLength(&vertex_ids) / 4u) { return; } let vertex_id = vec4(vertex_ids[index * 4u], vertex_ids[index * 4u + 1u], vertex_ids[index * 4u + 2u], vertex_ids[index * 4u + 3u]); let v0 = vec3f(vertex_positions[vertex_id.x * 3u], vertex_positions[vertex_id.x * 3u + 1u], vertex_positions[vertex_id.x * 3u + 2u]); let v1 = vec3f(vertex_positions[vertex_id.y * 3u], vertex_positions[vertex_id.y * 3u + 1u], vertex_positions[vertex_id.y * 3u + 2u]); let v2 = vec3f(vertex_positions[vertex_id.z * 3u], vertex_positions[vertex_id.z * 3u + 1u], vertex_positions[vertex_id.z * 3u + 2u]); let v3 = vec3f(vertex_positions[vertex_id.w * 3u], vertex_positions[vertex_id.w * 3u + 1u], vertex_positions[vertex_id.w * 3u + 2u]); let top = v0 + v1; let right = v1 + v3; let bottom = v2 + v3; let left = v0 + v2; let tangent = normalize(right - left); let bitangent = normalize(bottom - top); let normal = normalize(cross(tangent, bitangent)); let position = (v0 + v1 + v2 + v3) / 4.0; // update normals normals[index * 3u] = normal.x; normals[index * 3u + 1u] = normal.y; normals[index * 3u + 2u] = normal.z; // update positions positions[index * 3u] = position.x; positions[index * 3u + 1u] = position.y; positions[index * 3u + 2u] = position.z; } """ cloth_buffer_shader = ComputeShader(cloth_compute_wgsl, entry_point="compute_normals") cloth_buffer_shader.set_resource(0, position_buffer) cloth_buffer_shader.set_resource(1, verlet_vertex_ids_buffer) cloth_buffer_shader.set_resource(2, cloth_position_buffer) cloth_buffer_shader.set_resource(3, cloth_normal_buffer) # only changed this part... # might be worth to upstream in imgageio: https://github.com/imageio/imageio/issues/1169 from pyodide.http import pyfetch from io import BytesIO response = await pyfetch("https://raw.githubusercontent.com/imageio/imageio-binaries/master/images/meadow_cube.jpg") env_img = iio.imread(BytesIO(await response.bytes())) # 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 ) cloth_geometry = gfx.Geometry( positions=cloth_position_buffer, indices=cloth_indices, normals=cloth_normal_buffer, ) cloth_material = gfx.MeshPhysicalMaterial( # color="#030d37", color=(0.2, 0.4, 0.8), side="both", opacity=0.85, alpha_mode="blend", env_map=env_tex, env_map_intensity=3.0, sheen=1.0, sheen_roughness=0.5, sheen_color=(1.0, 1.0, 1.0), roughness=1.0, ) cloth_mesh = gfx.Mesh(cloth_geometry, cloth_material) scene.add(cloth_mesh) def create_verlet_indices(): indices = [] for x in range(cloth_segments_x): for y in range(cloth_segments_y): v0 = x * (cloth_segments_y + 1) + y v1 = (x + 1) * (cloth_segments_y + 1) + y v2 = (x + 1) * (cloth_segments_y + 1) + y + 1 v3 = x * (cloth_segments_y + 1) + y + 1 indices.append([v0, v1, v2]) indices.append([v0, v2, v3]) indices.append([v0, v3, v1]) indices.append([v1, v3, v2]) return np.array(indices, dtype=np.uint32) verlet_system_geometry = gfx.Geometry( positions=position_buffer, indices=create_verlet_indices(), ) verlet_system_material = gfx.MeshBasicMaterial( side="both", wireframe=True, ) verlet_system_point_material = gfx.PointsMarkerMaterial( size=8.0, # size_space="model", marker=gfx.MarkerShape.square, ) verlet_system_mesh = gfx.Mesh(verlet_system_geometry, verlet_system_material) verlet_system_points = gfx.Points(verlet_system_geometry, verlet_system_point_material) verlet_system = gfx.Group() verlet_system.add(verlet_system_mesh) verlet_system.add(verlet_system_points) verlet_system.visible = False # Hide the Verlet system mesh by default scene.add(verlet_system) # create a sphere to interact with the cloth sphere_geometry = gfx.sphere_geometry(radius=sphere_radius * 0.95) sphere_material = gfx.MeshStandardMaterial(env_map=env_tex) sphere = gfx.Mesh(sphere_geometry, sphere_material) scene.add(sphere) scene.add(gfx.Background.from_color((0.1, 0.1, 0.1, 1))) camera.local.position = (-1.6, -0.1, -1.6) controller = gfx.OrbitController(camera, register_events=renderer, target=(0, -0.1, 0)) gui_renderer = ImguiRenderer(renderer.device, canvas) params_data = np.array([0.99, 0.0, 0.0, 0.0, 1.0, 1.0, 0.2, 0.0], dtype=np.float32) wireframe_mode = False show_sphere = True show_verlet_system = False def draw_ui(): global wireframe_mode, show_sphere, show_verlet_system imgui.set_next_window_size((350, 0), imgui.Cond_.always) imgui.set_next_window_pos((0, 0), imgui.Cond_.always) imgui.begin("Cloth Controls") _, params_data[6] = imgui.slider_float("Stiffness", params_data[6], 0.1, 0.5) _, params_data[5] = imgui.slider_float("Wind", params_data[5], 0.0, 5.0) _, wireframe_mode = imgui.checkbox("Wireframe", wireframe_mode) verlet_system_changed, show_verlet_system = imgui.checkbox( "Show Verlet Mass-Spring System", show_verlet_system ) if verlet_system_changed: verlet_system.visible = show_verlet_system cloth_mesh.visible = not show_verlet_system _, show_sphere = imgui.checkbox("Sphere", show_sphere) if imgui.collapsing_header("material", imgui.TreeNodeFlags_.default_open): _, cloth_material.color = imgui.color_edit3("Color", cloth_material.color.rgb) _, cloth_material.roughness = imgui.slider_float( "Roughness", cloth_material.roughness, 0.0, 1.0 ) _, cloth_material.sheen = imgui.slider_float( "Sheen", cloth_material.sheen, 0.0, 1.0 ) _, cloth_material.sheen_roughness = imgui.slider_float( "Sheen Roughness", cloth_material.sheen_roughness, 0.0, 1.0 ) _, cloth_material.sheen_color = imgui.color_edit3( "Sheen Color", cloth_material.sheen_color.rgb ) imgui.end() gui_renderer.set_gui(draw_ui) clock = gfx.Clock() timestamp = 0.0 time_since_last = 0.0 stat = Stats(align="right", device=renderer.device, canvas=canvas) def animate(): global wireframe_mode, show_sphere, timestamp, time_since_last with stat: dt = min(clock.get_delta(), 1 / 60) time_per_step = 1 / 360 time_since_last += dt while time_since_last >= time_per_step: # run the simulation step timestamp += time_per_step time_since_last -= time_per_step params_data[7] = timestamp # update sphere position sphere_x = np.sin(timestamp * 2.1) * 0.1 sphere_z = np.sin(timestamp * 0.8) sphere_y = 0.0 sphere.local.position = (sphere_x, sphere_y, sphere_z) params_data[1] = sphere_x params_data[2] = sphere_y params_data[3] = sphere_z params_data[4] = 1.0 if show_sphere else 0.0 # update parameters buffer params_buffer.set_data(params_data) # do the Verlet integration spring_shader.dispatch((spring_count + 63) // 64) vertex_shader.dispatch((vertex_count + 63) // 64) # update cloth mesh positions and normals cloth_buffer_shader.dispatch((cloth_segments_x * cloth_segments_x + 63) // 64) # update render state cloth_material.wireframe = wireframe_mode sphere.visible = show_sphere renderer.render(scene, camera) gui_renderer.render() canvas.request_draw() if __name__ == "__main__": renderer.request_draw(animate) loop.run() .. _sphx_glr_download_gallery_pygfx_example.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: pygfx_example.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: pygfx_example.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: pygfx_example.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_ .. only:: html Interactive example =================== This uses Pyodide. If this does not work, your browser may not have sufficient support for wasm/pyodide/wgpu (check your browser dev console). Stdout (print statements) will also appear in the browser console. .. raw:: html