# Copyright (c) 2022 NVIDIA CORPORATION. All rights reserved. # NVIDIA CORPORATION and its licensors retain all intellectual property # and proprietary rights in and to this software, related documentation # and any modifications thereto. Any use, reproduction, disclosure or # distribution of this software and related documentation without an express # license agreement from NVIDIA CORPORATION is strictly prohibited. import sys import time import warp as wp from .utils import tab10_color_map from collections import defaultdict from typing import List, Tuple, Union, Optional from enum import Enum import numpy as np import ctypes Mat44 = Union[List[float], List[List[float]], np.ndarray] wp.set_module_options({"enable_backward": False}) shape_vertex_shader = """ #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoord; // column vectors of the instance transform matrix layout (location = 3) in vec4 aInstanceTransform0; layout (location = 4) in vec4 aInstanceTransform1; layout (location = 5) in vec4 aInstanceTransform2; layout (location = 6) in vec4 aInstanceTransform3; // colors to use for the checkerboard pattern layout (location = 7) in vec3 aObjectColor1; layout (location = 8) in vec3 aObjectColor2; uniform mat4 view; uniform mat4 model; uniform mat4 projection; out vec3 Normal; out vec3 FragPos; out vec2 TexCoord; out vec3 ObjectColor1; out vec3 ObjectColor2; void main() { mat4 transform = model * mat4(aInstanceTransform0, aInstanceTransform1, aInstanceTransform2, aInstanceTransform3); vec4 worldPos = transform * vec4(aPos, 1.0); gl_Position = projection * view * worldPos; FragPos = vec3(worldPos); Normal = mat3(transpose(inverse(transform))) * aNormal; TexCoord = aTexCoord; ObjectColor1 = aObjectColor1; ObjectColor2 = aObjectColor2; } """ shape_fragment_shader = """ #version 330 core out vec4 FragColor; in vec3 Normal; in vec3 FragPos; in vec2 TexCoord; in vec3 ObjectColor1; in vec3 ObjectColor2; uniform vec3 viewPos; uniform vec3 lightColor; uniform vec3 sunDirection; void main() { float ambientStrength = 0.3; vec3 ambient = ambientStrength * lightColor; vec3 norm = normalize(Normal); float diff = max(dot(norm, sunDirection), 0.0); vec3 diffuse = diff * lightColor; vec3 lightDir2 = normalize(vec3(1.0, 0.3, -0.3)); diff = max(dot(norm, lightDir2), 0.0); diffuse += diff * lightColor * 0.3; float specularStrength = 0.5; vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-sunDirection, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); vec3 specular = specularStrength * spec * lightColor; reflectDir = reflect(-lightDir2, norm); spec = pow(max(dot(viewDir, reflectDir), 0.0), 64); specular += specularStrength * spec * lightColor * 0.3; // checkerboard pattern float u = TexCoord.x; float v = TexCoord.y; // blend the checkerboard pattern dependent on the gradient of the texture coordinates // to void Moire patterns vec2 grad = abs(dFdx(TexCoord)) + abs(dFdy(TexCoord)); float blendRange = 1.5; float blendFactor = max(grad.x, grad.y) * blendRange; float scale = 2.0; float checker = mod(floor(u * scale) + floor(v * scale), 2.0); checker = mix(checker, 0.5, smoothstep(0.0, 1.0, blendFactor)); vec3 checkerColor = mix(ObjectColor1, ObjectColor2, checker); vec3 result = (ambient + diffuse + specular) * checkerColor; FragColor = vec4(result, 1.0); } """ grid_vertex_shader = """ #version 330 core uniform mat4 view; uniform mat4 model; uniform mat4 projection; in vec3 position; void main() { gl_Position = projection * view * model * vec4(position, 1.0); } """ # Fragment shader source code grid_fragment_shader = """ #version 330 core out vec4 outColor; void main() { outColor = vec4(0.5, 0.5, 0.5, 1.0); } """ sky_vertex_shader = """ #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoord; uniform mat4 view; uniform mat4 model; uniform mat4 projection; uniform vec3 viewPos; out vec3 FragPos; out vec2 TexCoord; void main() { vec4 worldPos = vec4(aPos + viewPos, 1.0); gl_Position = projection * view * worldPos; FragPos = vec3(worldPos); TexCoord = aTexCoord; } """ sky_fragment_shader = """ #version 330 core out vec4 FragColor; in vec3 FragPos; in vec2 TexCoord; uniform vec3 color1; uniform vec3 color2; uniform float farPlane; uniform vec3 sunDirection; void main() { float y = tanh(FragPos.y/farPlane*10.0)*0.5+0.5; float height = sqrt(1.0-y); float s = pow(0.5, 1.0 / 10.0); s = 1.0 - clamp(s, 0.75, 1.0); vec3 haze = mix(vec3(1.0), color2 * 1.3, s); vec3 sky = mix(color1, haze, height / 1.3); float diff = max(dot(sunDirection, normalize(FragPos)), 0.0); vec3 sun = pow(diff, 32) * vec3(1.0, 0.8, 0.6) * 0.5; FragColor = vec4(sky + sun, 1.0); } """ frame_vertex_shader = """ #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aTexCoord; out vec2 TexCoord; void main() { gl_Position = vec4(aPos, 1.0); TexCoord = aTexCoord; } """ frame_fragment_shader = """ #version 330 core in vec2 TexCoord; out vec4 FragColor; uniform sampler2D textureSampler; void main() { FragColor = texture(textureSampler, TexCoord); } """ frame_depth_fragment_shader = """ #version 330 core in vec2 TexCoord; out vec4 FragColor; uniform sampler2D textureSampler; vec3 bourkeColorMap(float v) { vec3 c = vec3(1.0, 1.0, 1.0); v = clamp(v, 0.0, 1.0); // Ensures v is between 0 and 1 if (v < 0.25) { c.r = 0.0; c.g = 4.0 * v; } else if (v < 0.5) { c.r = 0.0; c.b = 1.0 + 4.0 * (0.25 - v); } else if (v < 0.75) { c.r = 4.0 * (v - 0.5); c.b = 0.0; } else { c.g = 1.0 + 4.0 * (0.75 - v); c.b = 0.0; } return c; } void main() { float depth = texture(textureSampler, TexCoord).r; FragColor = vec4(bourkeColorMap(sqrt(1.0 - depth)), 1.0); } """ @wp.kernel def update_vbo_transforms( instance_id: wp.array(dtype=int), instance_body: wp.array(dtype=int), instance_transforms: wp.array(dtype=wp.transform), instance_scalings: wp.array(dtype=wp.vec3), body_q: wp.array(dtype=wp.transform), # outputs vbo_transforms: wp.array(dtype=wp.mat44), ): tid = wp.tid() i = instance_id[tid] X_ws = instance_transforms[i] if instance_body: body = instance_body[i] if body >= 0: if body_q: X_ws = body_q[body] * X_ws else: return p = wp.transform_get_translation(X_ws) q = wp.transform_get_rotation(X_ws) s = instance_scalings[i] rot = wp.quat_to_matrix(q) # transposed definition vbo_transforms[tid] = wp.mat44( rot[0, 0] * s[0], rot[1, 0] * s[0], rot[2, 0] * s[0], 0.0, rot[0, 1] * s[1], rot[1, 1] * s[1], rot[2, 1] * s[1], 0.0, rot[0, 2] * s[2], rot[1, 2] * s[2], rot[2, 2] * s[2], 0.0, p[0], p[1], p[2], 1.0, ) @wp.kernel def update_vbo_vertices( points: wp.array(dtype=wp.vec3), # outputs vbo_vertices: wp.array(dtype=float, ndim=2), ): tid = wp.tid() p = points[tid] vbo_vertices[tid, 0] = p[0] vbo_vertices[tid, 1] = p[1] vbo_vertices[tid, 2] = p[2] @wp.kernel def update_points_positions( instance_positions: wp.array(dtype=wp.vec3), instance_scalings: wp.array(dtype=wp.vec3), # outputs vbo_transforms: wp.array(dtype=wp.mat44), ): tid = wp.tid() p = instance_positions[tid] s = wp.vec3(1.0) if instance_scalings: s = instance_scalings[tid] # transposed definition # fmt: off vbo_transforms[tid] = wp.mat44( s[0], 0.0, 0.0, 0.0, 0.0, s[1], 0.0, 0.0, 0.0, 0.0, s[2], 0.0, p[0], p[1], p[2], 1.0) # fmt: on @wp.kernel def update_line_transforms( lines: wp.array(dtype=wp.vec3, ndim=2), # outputs vbo_transforms: wp.array(dtype=wp.mat44), ): tid = wp.tid() p0 = lines[tid, 0] p1 = lines[tid, 1] p = 0.5 * (p0 + p1) d = p1 - p0 s = wp.length(d) axis = wp.normalize(d) y_up = wp.vec3(0.0, 1.0, 0.0) angle = wp.acos(wp.dot(axis, y_up)) axis = wp.normalize(wp.cross(axis, y_up)) q = wp.quat_from_axis_angle(axis, -angle) rot = wp.quat_to_matrix(q) # transposed definition # fmt: off vbo_transforms[tid] = wp.mat44( rot[0, 0], rot[1, 0], rot[2, 0], 0.0, s * rot[0, 1], s * rot[1, 1], s * rot[2, 1], 0.0, rot[0, 2], rot[1, 2], rot[2, 2], 0.0, p[0], p[1], p[2], 1.0, ) # fmt: on @wp.kernel def compute_gfx_vertices( indices: wp.array(dtype=int, ndim=2), vertices: wp.array(dtype=wp.vec3, ndim=1), # outputs gfx_vertices: wp.array(dtype=float, ndim=2), ): tid = wp.tid() v0 = vertices[indices[tid, 0]] v1 = vertices[indices[tid, 1]] v2 = vertices[indices[tid, 2]] i = tid * 3 j = i + 1 k = i + 2 gfx_vertices[i, 0] = v0[0] gfx_vertices[i, 1] = v0[1] gfx_vertices[i, 2] = v0[2] gfx_vertices[j, 0] = v1[0] gfx_vertices[j, 1] = v1[1] gfx_vertices[j, 2] = v1[2] gfx_vertices[k, 0] = v2[0] gfx_vertices[k, 1] = v2[1] gfx_vertices[k, 2] = v2[2] n = wp.normalize(wp.cross(v1 - v0, v2 - v0)) gfx_vertices[i, 3] = n[0] gfx_vertices[i, 4] = n[1] gfx_vertices[i, 5] = n[2] gfx_vertices[j, 3] = n[0] gfx_vertices[j, 4] = n[1] gfx_vertices[j, 5] = n[2] gfx_vertices[k, 3] = n[0] gfx_vertices[k, 4] = n[1] gfx_vertices[k, 5] = n[2] @wp.kernel def compute_average_normals( indices: wp.array(dtype=int, ndim=2), vertices: wp.array(dtype=wp.vec3), # outputs normals: wp.array(dtype=wp.vec3), faces_per_vertex: wp.array(dtype=int), ): tid = wp.tid() i = indices[tid, 0] j = indices[tid, 1] k = indices[tid, 2] v0 = vertices[i] v1 = vertices[j] v2 = vertices[k] n = wp.normalize(wp.cross(v1 - v0, v2 - v0)) wp.atomic_add(normals, i, n) wp.atomic_add(faces_per_vertex, i, 1) wp.atomic_add(normals, j, n) wp.atomic_add(faces_per_vertex, j, 1) wp.atomic_add(normals, k, n) wp.atomic_add(faces_per_vertex, k, 1) @wp.kernel def assemble_gfx_vertices( vertices: wp.array(dtype=wp.vec3, ndim=1), normals: wp.array(dtype=wp.vec3), faces_per_vertex: wp.array(dtype=int), # outputs gfx_vertices: wp.array(dtype=float, ndim=2), ): tid = wp.tid() v = vertices[tid] n = normals[tid] / float(faces_per_vertex[tid]) gfx_vertices[tid, 0] = v[0] gfx_vertices[tid, 1] = v[1] gfx_vertices[tid, 2] = v[2] gfx_vertices[tid, 3] = n[0] gfx_vertices[tid, 4] = n[1] gfx_vertices[tid, 5] = n[2] @wp.kernel def copy_rgb_frame( input_img: wp.array(dtype=wp.uint8), width: int, height: int, # outputs output_img: wp.array(dtype=float, ndim=3), ): w, v = wp.tid() pixel = v * width + w pixel *= 3 r = float(input_img[pixel + 0]) g = float(input_img[pixel + 1]) b = float(input_img[pixel + 2]) # flip vertically (OpenGL coordinates start at bottom) v = height - v - 1 output_img[v, w, 0] = r / 255.0 output_img[v, w, 1] = g / 255.0 output_img[v, w, 2] = b / 255.0 @wp.kernel def copy_depth_frame( input_img: wp.array(dtype=wp.float32), width: int, height: int, near: float, far: float, # outputs output_img: wp.array(dtype=wp.float32, ndim=3), ): w, v = wp.tid() pixel = v * width + w # flip vertically (OpenGL coordinates start at bottom) v = height - v - 1 d = 2.0 * input_img[pixel] - 1.0 d = 2.0 * near * far / ((far - near) * d - near - far) output_img[v, w, 0] = -d @wp.kernel def copy_rgb_frame_tiles( input_img: wp.array(dtype=wp.uint8), positions: wp.array(dtype=int, ndim=2), screen_width: int, screen_height: int, tile_height: int, # outputs output_img: wp.array(dtype=float, ndim=4), ): tile, x, y = wp.tid() p = positions[tile] qx = x + p[0] qy = y + p[1] pixel = qy * screen_width + qx # flip vertically (OpenGL coordinates start at bottom) y = tile_height - y - 1 if qx >= screen_width or qy >= screen_height: output_img[tile, y, x, 0] = 0.0 output_img[tile, y, x, 1] = 0.0 output_img[tile, y, x, 2] = 0.0 return # prevent out-of-bounds access pixel *= 3 r = float(input_img[pixel + 0]) g = float(input_img[pixel + 1]) b = float(input_img[pixel + 2]) output_img[tile, y, x, 0] = r / 255.0 output_img[tile, y, x, 1] = g / 255.0 output_img[tile, y, x, 2] = b / 255.0 @wp.kernel def copy_depth_frame_tiles( input_img: wp.array(dtype=wp.float32), positions: wp.array(dtype=int, ndim=2), screen_width: int, screen_height: int, tile_height: int, near: float, far: float, # outputs output_img: wp.array(dtype=wp.float32, ndim=4), ): tile, x, y = wp.tid() p = positions[tile] qx = x + p[0] qy = y + p[1] pixel = qy * screen_width + qx # flip vertically (OpenGL coordinates start at bottom) y = tile_height - y - 1 if qx >= screen_width or qy >= screen_height: output_img[tile, y, x, 0] = far return # prevent out-of-bounds access d = 2.0 * input_img[pixel] - 1.0 d = 2.0 * near * far / ((far - near) * d - near - far) output_img[tile, y, x, 0] = -d @wp.kernel def copy_rgb_frame_tile( input_img: wp.array(dtype=wp.uint8), offset_x: int, offset_y: int, screen_width: int, screen_height: int, tile_height: int, # outputs output_img: wp.array(dtype=float, ndim=4), ): tile, x, y = wp.tid() qx = x + offset_x qy = y + offset_y pixel = qy * screen_width + qx # flip vertically (OpenGL coordinates start at bottom) y = tile_height - y - 1 if qx >= screen_width or qy >= screen_height: output_img[tile, y, x, 0] = 0.0 output_img[tile, y, x, 1] = 0.0 output_img[tile, y, x, 2] = 0.0 return # prevent out-of-bounds access pixel *= 3 r = float(input_img[pixel + 0]) g = float(input_img[pixel + 1]) b = float(input_img[pixel + 2]) output_img[tile, y, x, 0] = r / 255.0 output_img[tile, y, x, 1] = g / 255.0 output_img[tile, y, x, 2] = b / 255.0 def check_gl_error(): from pyglet import gl error = gl.glGetError() if error != gl.GL_NO_ERROR: print(f"OpenGL error: {error}") class ShapeInstancer: """ Handles instanced rendering for a mesh. Note the vertices must be in the 8-dimensional format: [3D point, 3D normal, UV texture coordinates] """ def __init__(self, shape_shader, device): self.shape_shader = shape_shader self.device = device self.face_count = 0 self.vao = None self.instance_transform_gl_buffer = None self.instance_color1_buffer = None self.instance_color2_buffer = None self.color1 = (1.0, 1.0, 1.0) self.color2 = (0.0, 0.0, 0.0) self.num_instances = 0 self.transforms = None self.scalings = None self._instance_transform_cuda_buffer = None def __del__(self): from pyglet import gl if self.instance_transform_gl_buffer is not None: try: gl.glDeleteBuffers(1, self.instance_transform_gl_buffer) gl.glDeleteBuffers(1, self.instance_color1_buffer) gl.glDeleteBuffers(1, self.instance_color2_buffer) except gl.GLException: pass if self.vao is not None: try: gl.glDeleteVertexArrays(1, self.vao) gl.glDeleteBuffers(1, self.vbo) gl.glDeleteBuffers(1, self.ebo) except gl.GLException: pass def register_shape(self, vertices, indices, color1=(1.0, 1.0, 1.0), color2=(0.0, 0.0, 0.0)): from pyglet import gl if color1 is not None and color2 is None: color2 = np.clip(np.array(color1) + 0.25, 0.0, 1.0) self.color1 = color1 self.color2 = color2 gl.glUseProgram(self.shape_shader.id) # Create VAO, VBO, and EBO self.vao = gl.GLuint() gl.glGenVertexArrays(1, self.vao) gl.glBindVertexArray(self.vao) self.vbo = gl.GLuint() gl.glGenBuffers(1, self.vbo) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vbo) gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices.ctypes.data, gl.GL_STATIC_DRAW) self.ebo = gl.GLuint() gl.glGenBuffers(1, self.ebo) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.ebo) gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices.ctypes.data, gl.GL_STATIC_DRAW) # Set up vertex attributes vertex_stride = vertices.shape[1] * vertices.itemsize # positions gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(0)) gl.glEnableVertexAttribArray(0) # normals gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(3 * vertices.itemsize)) gl.glEnableVertexAttribArray(1) # uv coordinates gl.glVertexAttribPointer(2, 2, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(6 * vertices.itemsize)) gl.glEnableVertexAttribArray(2) gl.glBindVertexArray(0) self.face_count = len(indices) def allocate_instances(self, positions, rotations=None, colors1=None, colors2=None, scalings=None): from pyglet import gl gl.glBindVertexArray(self.vao) self.num_instances = len(positions) # Create instance buffer and bind it as an instanced array if self.instance_transform_gl_buffer is None: self.instance_transform_gl_buffer = gl.GLuint() gl.glGenBuffers(1, self.instance_transform_gl_buffer) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_transform_gl_buffer) self.instance_ids = wp.array(np.arange(self.num_instances), dtype=wp.int32, device=self.device) if rotations is None: self.instance_transforms = wp.array( [(*pos, 0.0, 0.0, 0.0, 1.0) for pos in positions], dtype=wp.transform, device=self.device ) else: self.instance_transforms = wp.array( [(*pos, *rot) for pos, rot in zip(positions, rotations)], dtype=wp.transform, device=self.device ) if scalings is None: self.instance_scalings = wp.array( np.tile((1.0, 1.0, 1.0), (self.num_instances, 1)), dtype=wp.vec3, device=self.device ) else: self.instance_scalings = wp.array(scalings, dtype=wp.vec3, device=self.device) vbo_transforms = wp.zeros(dtype=wp.mat44, shape=(self.num_instances,), device=self.device) wp.launch( update_vbo_transforms, dim=self.num_instances, inputs=[ self.instance_ids, None, self.instance_transforms, self.instance_scalings, None, ], outputs=[ vbo_transforms, ], device=self.device, ) vbo_transforms = vbo_transforms.numpy() gl.glBufferData(gl.GL_ARRAY_BUFFER, vbo_transforms.nbytes, vbo_transforms.ctypes.data, gl.GL_DYNAMIC_DRAW) # Create CUDA buffer for instance transforms self._instance_transform_cuda_buffer = wp.RegisteredGLBuffer( int(self.instance_transform_gl_buffer.value), self.device ) if colors1 is None: colors1 = np.tile(self.color1, (self.num_instances, 1)) if colors2 is None: colors2 = np.tile(self.color2, (self.num_instances, 1)) colors1 = np.array(colors1, dtype=np.float32) colors2 = np.array(colors2, dtype=np.float32) # create buffer for checkerboard colors if self.instance_color1_buffer is None: self.instance_color1_buffer = gl.GLuint() gl.glGenBuffers(1, self.instance_color1_buffer) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_color1_buffer) gl.glBufferData(gl.GL_ARRAY_BUFFER, colors1.nbytes, colors1.ctypes.data, gl.GL_STATIC_DRAW) if self.instance_color2_buffer is None: self.instance_color2_buffer = gl.GLuint() gl.glGenBuffers(1, self.instance_color2_buffer) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_color2_buffer) gl.glBufferData(gl.GL_ARRAY_BUFFER, colors2.nbytes, colors2.ctypes.data, gl.GL_STATIC_DRAW) # Set up instance attribute pointers matrix_size = vbo_transforms[0].nbytes gl.glBindVertexArray(self.vao) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_transform_gl_buffer) # we can only send vec4s to the shader, so we need to split the instance transforms matrix into its column vectors for i in range(4): gl.glVertexAttribPointer( 3 + i, 4, gl.GL_FLOAT, gl.GL_FALSE, matrix_size, ctypes.c_void_p(i * matrix_size // 4) ) gl.glEnableVertexAttribArray(3 + i) gl.glVertexAttribDivisor(3 + i, 1) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_color1_buffer) gl.glVertexAttribPointer(7, 3, gl.GL_FLOAT, gl.GL_FALSE, colors1[0].nbytes, ctypes.c_void_p(0)) gl.glEnableVertexAttribArray(7) gl.glVertexAttribDivisor(7, 1) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_color2_buffer) gl.glVertexAttribPointer(8, 3, gl.GL_FLOAT, gl.GL_FALSE, colors2[0].nbytes, ctypes.c_void_p(0)) gl.glEnableVertexAttribArray(8) gl.glVertexAttribDivisor(8, 1) gl.glBindVertexArray(0) def update_instances(self, transforms: wp.array = None, scalings: wp.array = None, colors1=None, colors2=None): from pyglet import gl if transforms is not None: if transforms.device.is_cuda: wp_transforms = transforms else: wp_transforms = transforms.to(self.device) self.transforms = wp_transforms if scalings is not None: if transforms.device.is_cuda: wp_scalings = scalings else: wp_scalings = scalings.to(self.device) self.scalings = wp_scalings if transforms is not None or scalings is not None: gl.glBindVertexArray(self.vao) vbo_transforms = self._instance_transform_cuda_buffer.map(dtype=wp.mat44, shape=(self.num_instances,)) wp.launch( update_vbo_transforms, dim=self.num_instances, inputs=[ self.instance_ids, None, self.instance_transforms, self.instance_scalings, None, ], outputs=[ vbo_transforms, ], device=self.device, ) self._instance_transform_cuda_buffer.unmap() def render(self): from pyglet import gl gl.glUseProgram(self.shape_shader.id) gl.glBindVertexArray(self.vao) gl.glDrawElementsInstanced(gl.GL_TRIANGLES, self.face_count, gl.GL_UNSIGNED_INT, None, self.num_instances) gl.glBindVertexArray(0) # scope exposes VBO transforms to be set directly by a warp kernel def __enter__(self): from pyglet import gl gl.glBindVertexArray(self.vao) self.vbo_transforms = self._instance_transform_cuda_buffer.map(dtype=wp.mat44, shape=(self.num_instances,)) return self def __exit__(self, exc_type, exc_value, traceback): self._instance_transform_cuda_buffer.unmap() def str_buffer(string: str): return ctypes.c_char_p(string.encode("utf-8")) def arr_pointer(arr: np.ndarray): return arr.astype(np.float32).ctypes.data_as(ctypes.POINTER(ctypes.c_float)) class OpenGLRenderer: """ OpenGLRenderer is a simple OpenGL renderer for rendering 3D shapes and meshes. """ # number of segments to use for rendering spheres, capsules, cones and cylinders default_num_segments = 32 def __init__( self, title="Warp sim", scaling=1.0, fps=60, up_axis="Y", screen_width=1024, screen_height=768, near_plane=1.0, far_plane=100.0, camera_fov=45.0, camera_pos=(0.0, 2.0, 10.0), camera_front=(0.0, 0.0, -1.0), camera_up=(0.0, 1.0, 0.0), background_color=(0.53, 0.8, 0.92), draw_grid=True, draw_sky=True, draw_axis=True, show_info=True, render_wireframe=False, render_depth=False, axis_scale=1.0, vsync=False, headless=False, enable_backface_culling=True, ): try: import pyglet # disable error checking for performance pyglet.options["debug_gl"] = False from pyglet import gl from pyglet.math import Vec3 as PyVec3 from pyglet.graphics.shader import Shader, ShaderProgram except ImportError: raise Exception("OpenGLRenderer requires pyglet (version >= 2.0) to be installed.") self.camera_near_plane = near_plane self.camera_far_plane = far_plane self.camera_fov = camera_fov self.background_color = background_color self.draw_grid = draw_grid self.draw_sky = draw_sky self.draw_axis = draw_axis self.show_info = show_info self.render_wireframe = render_wireframe self.render_depth = render_depth self.enable_backface_culling = enable_backface_culling self._device = wp.get_cuda_device() self._title = title self.window = pyglet.window.Window( width=screen_width, height=screen_height, caption=title, resizable=True, vsync=vsync, visible=not headless ) self.app = pyglet.app # making window current opengl rendering context self.window.switch_to() self.screen_width, self.screen_height = self.window.get_framebuffer_size() self._camera_pos = PyVec3(*camera_pos) self._camera_front = PyVec3(*camera_front) self._camera_up = PyVec3(*camera_up) self._camera_speed = 0.04 if isinstance(up_axis, int): self._camera_axis = up_axis else: self._camera_axis = "XYZ".index(up_axis.upper()) self._yaw, self._pitch = -90.0, 0.0 self._last_x, self._last_y = self.screen_width // 2, self.screen_height // 2 self._first_mouse = True self._left_mouse_pressed = False self._keys_pressed = defaultdict(bool) self._key_callbacks = [] self.update_view_matrix() self.update_projection_matrix() self._frame_dt = 1.0 / fps self.time = 0.0 self._start_time = time.time() self.clock_time = 0.0 self._paused = False self._frame_speed = 0.0 self.skip_rendering = False self._skip_frame_counter = 0 self._fps_update = 0.0 self._fps_render = 0.0 self._fps_alpha = 0.1 # low pass filter rate to update FPS stats self._body_name = {} self._shapes = [] self._shape_geo_hash = {} self._shape_gl_buffers = {} self._shape_instances = defaultdict(list) self._instances = {} self._instance_shape = {} self._instance_gl_buffers = {} self._instance_transform_gl_buffer = None self._instance_transform_cuda_buffer = None self._instance_color1_buffer = None self._instance_color2_buffer = None self._instance_count = 0 self._wp_instance_ids = None self._instance_ids = None self._inverse_instance_ids = None self._wp_instance_transforms = None self._wp_instance_scalings = None self._wp_instance_bodies = None self._update_shape_instances = False self._add_shape_instances = False # additional shape instancer used for points and line rendering self._shape_instancers = {} # instancer for the arrow shapes sof the coordinate system axes self._axis_instancer = None # toggle tiled rendering self._tiled_rendering = False self._tile_instances = None self._tile_ncols = 0 self._tile_nrows = 0 self._tile_width = 0 self._tile_height = 0 self._tile_viewports = None self._tile_view_matrices = None self._tile_projection_matrices = None self._frame_texture = None self._frame_depth_texture = None self._frame_fbo = None self._frame_pbo = None self.window.push_handlers(on_draw=self._draw) self.window.push_handlers(on_resize=self._window_resize_callback) self.window.push_handlers(on_key_press=self._key_press_callback) self._key_handler = pyglet.window.key.KeyStateHandler() self.window.push_handlers(self._key_handler) self.window.on_mouse_scroll = self._scroll_callback self.window.on_mouse_drag = self._mouse_drag_callback gl.glClearColor(*self.background_color, 1) gl.glEnable(gl.GL_DEPTH_TEST) gl.glDepthMask(True) gl.glDepthRange(0.0, 1.0) self._shape_shader = ShaderProgram( Shader(shape_vertex_shader, "vertex"), Shader(shape_fragment_shader, "fragment") ) self._grid_shader = ShaderProgram( Shader(grid_vertex_shader, "vertex"), Shader(grid_fragment_shader, "fragment") ) self._sun_direction = np.array((-0.2, 0.8, 0.3)) self._sun_direction /= np.linalg.norm(self._sun_direction) with self._shape_shader: gl.glUniform3f( gl.glGetUniformLocation(self._shape_shader.id, str_buffer("sunDirection")), *self._sun_direction ) gl.glUniform3f(gl.glGetUniformLocation(self._shape_shader.id, str_buffer("lightColor")), 1, 1, 1) self._loc_shape_model = gl.glGetUniformLocation(self._shape_shader.id, str_buffer("model")) self._loc_shape_view = gl.glGetUniformLocation(self._shape_shader.id, str_buffer("view")) self._loc_shape_projection = gl.glGetUniformLocation(self._shape_shader.id, str_buffer("projection")) self._loc_shape_view_pos = gl.glGetUniformLocation(self._shape_shader.id, str_buffer("viewPos")) gl.glUniform3f(self._loc_shape_view_pos, 0, 0, 10) # create grid data limit = 10.0 ticks = np.linspace(-limit, limit, 21) grid_vertices = [] for i in ticks: if self._camera_axis == 0: grid_vertices.extend([0, -limit, i, 0, limit, i]) grid_vertices.extend([0, i, -limit, 0, i, limit]) elif self._camera_axis == 1: grid_vertices.extend([-limit, 0, i, limit, 0, i]) grid_vertices.extend([i, 0, -limit, i, 0, limit]) elif self._camera_axis == 2: grid_vertices.extend([-limit, i, 0, limit, i, 0]) grid_vertices.extend([i, -limit, 0, i, limit, 0]) grid_vertices = np.array(grid_vertices, dtype=np.float32) self._grid_vertex_count = len(grid_vertices) // 3 with self._grid_shader: self._grid_vao = gl.GLuint() gl.glGenVertexArrays(1, self._grid_vao) gl.glBindVertexArray(self._grid_vao) self._grid_vbo = gl.GLuint() gl.glGenBuffers(1, self._grid_vbo) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._grid_vbo) gl.glBufferData(gl.GL_ARRAY_BUFFER, grid_vertices.nbytes, grid_vertices.ctypes.data, gl.GL_STATIC_DRAW) self._loc_grid_view = gl.glGetUniformLocation(self._grid_shader.id, str_buffer("view")) self._loc_grid_model = gl.glGetUniformLocation(self._grid_shader.id, str_buffer("model")) self._loc_grid_projection = gl.glGetUniformLocation(self._grid_shader.id, str_buffer("projection")) self._loc_grid_pos_attribute = gl.glGetAttribLocation(self._grid_shader.id, str_buffer("position")) gl.glVertexAttribPointer(self._loc_grid_pos_attribute, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None) gl.glEnableVertexAttribArray(self._loc_grid_pos_attribute) # create sky data self._sky_shader = ShaderProgram(Shader(sky_vertex_shader, "vertex"), Shader(sky_fragment_shader, "fragment")) with self._sky_shader: self._loc_sky_view = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("view")) self._loc_sky_model = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("model")) self._loc_sky_projection = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("projection")) self._loc_sky_color1 = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("color1")) self._loc_sky_color2 = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("color2")) self._loc_sky_far_plane = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("farPlane")) gl.glUniform3f(self._loc_sky_color1, *background_color) # glUniform3f(self._loc_sky_color2, *np.clip(np.array(background_color)+0.5, 0.0, 1.0)) gl.glUniform3f(self._loc_sky_color2, 0.8, 0.4, 0.05) gl.glUniform1f(self._loc_sky_far_plane, self.camera_far_plane) self._loc_sky_view_pos = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("viewPos")) gl.glUniform3f( gl.glGetUniformLocation(self._sky_shader.id, str_buffer("sunDirection")), *self._sun_direction ) # create VAO, VBO, and EBO self._sky_vao = gl.GLuint() gl.glGenVertexArrays(1, self._sky_vao) gl.glBindVertexArray(self._sky_vao) vertices, indices = self._create_sphere_mesh(self.camera_far_plane * 0.9, 32, 32, reverse_winding=True) self._sky_tri_count = len(indices) self._sky_vbo = gl.GLuint() gl.glGenBuffers(1, self._sky_vbo) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._sky_vbo) gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices.ctypes.data, gl.GL_STATIC_DRAW) self._sky_ebo = gl.GLuint() gl.glGenBuffers(1, self._sky_ebo) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self._sky_ebo) gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices.ctypes.data, gl.GL_STATIC_DRAW) # set up vertex attributes vertex_stride = vertices.shape[1] * vertices.itemsize # positions gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(0)) gl.glEnableVertexAttribArray(0) # normals gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(3 * vertices.itemsize)) gl.glEnableVertexAttribArray(1) # uv coordinates gl.glVertexAttribPointer(2, 2, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(6 * vertices.itemsize)) gl.glEnableVertexAttribArray(2) gl.glBindVertexArray(0) self._last_time = time.time() self._last_begin_frame_time = self._last_time self._last_end_frame_time = self._last_time # create arrow shapes for the coordinate system axes vertices, indices = self._create_arrow_mesh( base_radius=0.02 * axis_scale, base_height=0.85 * axis_scale, cap_height=0.15 * axis_scale ) self._axis_instancer = ShapeInstancer(self._shape_shader, self._device) self._axis_instancer.register_shape(vertices, indices) sqh = np.sqrt(0.5) self._axis_instancer.allocate_instances( positions=[(0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)], rotations=[(0.0, 0.0, 0.0, 1.0), (0.0, 0.0, -sqh, sqh), (sqh, 0.0, 0.0, sqh)], colors1=[(0.0, 1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)], colors2=[(0.0, 1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)], ) # create frame buffer for rendering to a texture self._frame_texture = None self._frame_depth_texture = None self._frame_fbo = None self._setup_framebuffer() # fmt: off # set up VBO for the quad that is rendered to the user window with the texture self._frame_vertices = np.array([ # Positions TexCoords -1.0, -1.0, 0.0, 0.0, 1.0, -1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0.0, 1.0 ], dtype=np.float32) # fmt: on self._frame_indices = np.array([0, 1, 2, 2, 3, 0], dtype=np.uint32) self._frame_vao = gl.GLuint() gl.glGenVertexArrays(1, self._frame_vao) gl.glBindVertexArray(self._frame_vao) self._frame_vbo = gl.GLuint() gl.glGenBuffers(1, self._frame_vbo) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._frame_vbo) gl.glBufferData( gl.GL_ARRAY_BUFFER, self._frame_vertices.nbytes, self._frame_vertices.ctypes.data, gl.GL_STATIC_DRAW ) self._frame_ebo = gl.GLuint() gl.glGenBuffers(1, self._frame_ebo) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self._frame_ebo) gl.glBufferData( gl.GL_ELEMENT_ARRAY_BUFFER, self._frame_indices.nbytes, self._frame_indices.ctypes.data, gl.GL_STATIC_DRAW ) gl.glVertexAttribPointer(0, 2, gl.GL_FLOAT, gl.GL_FALSE, 4 * self._frame_vertices.itemsize, ctypes.c_void_p(0)) gl.glEnableVertexAttribArray(0) gl.glVertexAttribPointer( 1, 2, gl.GL_FLOAT, gl.GL_FALSE, 4 * self._frame_vertices.itemsize, ctypes.c_void_p(2 * vertices.itemsize) ) gl.glEnableVertexAttribArray(1) self._frame_shader = ShaderProgram( Shader(frame_vertex_shader, "vertex"), Shader(frame_fragment_shader, "fragment") ) gl.glUseProgram(self._frame_shader.id) self._frame_loc_texture = gl.glGetUniformLocation(self._frame_shader.id, str_buffer("textureSampler")) self._frame_depth_shader = ShaderProgram( Shader(frame_vertex_shader, "vertex"), Shader(frame_depth_fragment_shader, "fragment") ) gl.glUseProgram(self._frame_depth_shader.id) self._frame_loc_depth_texture = gl.glGetUniformLocation( self._frame_depth_shader.id, str_buffer("textureSampler") ) # unbind the VBO and VAO gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) gl.glBindVertexArray(0) # update model matrix self.scaling = scaling check_gl_error() # create text to render stats on the screen self._info_label = pyglet.text.Label( "", font_name="Arial", font_size=12, color=(255, 255, 255, 255), x=10, y=10, anchor_x="left", anchor_y="top", multiline=True, width=400, ) # set up our own event handling so we can synchronously render frames # by calling update() in a loop from pyglet.window import Window Window._enable_event_queue = False self.window.switch_to() self.window.dispatch_pending_events() platform_event_loop = self.app.platform_event_loop platform_event_loop.start() # start event loop self.app.event_loop.dispatch_event("on_enter") @property def paused(self): return self._paused @paused.setter def paused(self, value): self._paused = value if value: self.window.set_caption(f"{self._title} (paused)") else: self.window.set_caption(self._title) @property def has_exit(self): return self.app.event_loop.has_exit def clear(self): from pyglet import gl self.app.event_loop.dispatch_event("on_exit") self.app.platform_event_loop.stop() if self._instance_transform_gl_buffer is not None: try: gl.glDeleteBuffers(1, self._instance_transform_gl_buffer) gl.glDeleteBuffers(1, self._instance_color1_buffer) gl.glDeleteBuffers(1, self._instance_color2_buffer) except gl.GLException: pass for vao, vbo, ebo, _, vertex_cuda_buffer in self._shape_gl_buffers.values(): try: gl.glDeleteVertexArrays(1, vao) gl.glDeleteBuffers(1, vbo) gl.glDeleteBuffers(1, ebo) except gl.GLException: pass self._body_name.clear() self._shapes.clear() self._shape_geo_hash.clear() self._shape_gl_buffers.clear() self._shape_instances.clear() self._instances.clear() self._instance_shape.clear() self._instance_gl_buffers.clear() self._instance_transform_gl_buffer = None self._instance_transform_cuda_buffer = None self._instance_color1_buffer = None self._instance_color2_buffer = None self._wp_instance_ids = None self._wp_instance_transforms = None self._wp_instance_scalings = None self._wp_instance_bodies = None self._update_shape_instances = False @property def tiled_rendering(self): return self._tiled_rendering @tiled_rendering.setter def tiled_rendering(self, value): if value: assert self._tile_instances is not None, "Tiled rendering is not set up. Call setup_tiled_rendering first." self._tiled_rendering = value def setup_tiled_rendering( self, instances: List[List[int]], rescale_window: bool = False, tile_width: Optional[int] = None, tile_height: Optional[int] = None, tile_ncols: Optional[int] = None, tile_nrows: Optional[int] = None, tile_positions: Optional[List[Tuple[int]]] = None, tile_sizes: Optional[List[Tuple[int]]] = None, projection_matrices: Optional[List[Mat44]] = None, view_matrices: Optional[List[Mat44]] = None, ): """ Set up tiled rendering where the render buffer is split into multiple tiles that can visualize different shape instances of the scene with different view and projection matrices. See `get_pixels` which allows to retrieve the pixels of for each tile. :param instances: A list of lists of shape instance ids. Each list of shape instance ids will be rendered into a separate tile. :param rescale_window: If True, the window will be resized to fit the tiles. :param tile_width: The width of each tile in pixels (optional). :param tile_height: The height of each tile in pixels (optional). :param tile_ncols: The number of tiles rendered horizontally (optional). Will be considered if `tile_width` is set to compute the tile positions, unless `tile_positions` is defined. :param tile_positions: A list of (x, y) tuples specifying the position of each tile in pixels. If None, the tiles will be arranged in a square grid, or, if `tile_ncols` and `tile_nrows` is set, in a grid with the specified number of columns and rows. :param tile_sizes: A list of (width, height) tuples specifying the size of each tile in pixels. If None, the tiles will have the same size as specified by `tile_width` and `tile_height`. :param projection_matrices: A list of projection matrices for each tile (each view matrix is either a flattened 16-dimensional array or a 4x4 matrix). If the entire array is None, or only a view instances, the projection matrices for all, or these instances, respectively, will be derived from the current render settings. :param view_matrices: A list of view matrices for each tile (each view matrix is either a flattened 16-dimensional array or a 4x4 matrix). If the entire array is None, or only a view instances, the view matrices for all, or these instances, respectively, will be derived from the current camera settings and be updated when the camera is moved. """ assert len(instances) > 0 and all(isinstance(i, list) for i in instances), "Invalid tile instances." self._tile_instances = instances n = len(self._tile_instances) if tile_positions is None or tile_sizes is None: if tile_ncols is None or tile_nrows is None: # try to fit the tiles into a square self._tile_ncols = int(np.ceil(np.sqrt(n))) self._tile_nrows = int(np.ceil(n / float(self._tile_ncols))) else: self._tile_ncols = tile_ncols self._tile_nrows = tile_nrows self._tile_width = tile_width or max(32, self.screen_width // self._tile_ncols) self._tile_height = tile_height or max(32, self.screen_height // self._tile_nrows) self._tile_viewports = [ (i * self._tile_width, j * self._tile_height, self._tile_width, self._tile_height) for i in range(self._tile_ncols) for j in range(self._tile_nrows) ] if rescale_window: self.window.set_size(self._tile_width * self._tile_ncols, self._tile_height * self._tile_nrows) else: assert ( len(tile_positions) == n and len(tile_sizes) == n ), "Number of tiles does not match number of instances." self._tile_ncols = None self._tile_nrows = None self._tile_width = None self._tile_height = None if all([tile_sizes[i][0] == tile_sizes[0][0] for i in range(n)]): # tiles all have the same width self._tile_width = tile_sizes[0][0] if all([tile_sizes[i][1] == tile_sizes[0][1] for i in range(n)]): # tiles all have the same height self._tile_height = tile_sizes[0][1] self._tile_viewports = [(x, y, w, h) for (x, y), (w, h) in zip(tile_positions, tile_sizes)] if projection_matrices is None: projection_matrices = [None] * n self._tile_projection_matrices = [] for i, p in enumerate(projection_matrices): if p is None: w, h = self._tile_viewports[i][2:] self._tile_projection_matrices.append( self.compute_projection_matrix( self.camera_fov, w / h, self.camera_near_plane, self.camera_far_plane ) ) else: self._tile_projection_matrices.append(np.array(p).flatten()) if view_matrices is None: self._tile_view_matrices = [None] * n else: self._tile_view_matrices = [np.array(m).flatten() for m in view_matrices] self._tiled_rendering = True def update_tile( self, tile_id, instances: Optional[List[int]] = None, projection_matrix: Optional[Mat44] = None, view_matrix: Optional[Mat44] = None, tile_size: Optional[Tuple[int]] = None, tile_position: Optional[Tuple[int]] = None, ): """ Update the shape instances, projection matrix, view matrix, tile size, or tile position for a given tile given its index. :param tile_id: The index of the tile to update. :param instances: A list of shape instance ids (optional). :param projection_matrix: A projection matrix (optional). :param view_matrix: A view matrix (optional). :param tile_size: A (width, height) tuple specifying the size of the tile in pixels (optional). :param tile_position: A (x, y) tuple specifying the position of the tile in pixels (optional). """ assert self._tile_instances is not None, "Tiled rendering is not set up. Call setup_tiled_rendering first." assert tile_id < len(self._tile_instances), "Invalid tile id." if instances is not None: self._tile_instances[tile_id] = instances if projection_matrix is not None: self._tile_projection_matrices[tile_id] = np.array(projection_matrix).flatten() if view_matrix is not None: self._tile_view_matrices[tile_id] = np.array(view_matrix).flatten() (x, y, w, h) = self._tile_viewports[tile_id] if tile_size is not None: w, h = tile_size if tile_position is not None: x, y = tile_position self._tile_viewports[tile_id] = (x, y, w, h) def _setup_framebuffer(self): from pyglet import gl if self._frame_texture is None: self._frame_texture = gl.GLuint() gl.glGenTextures(1, self._frame_texture) if self._frame_depth_texture is None: self._frame_depth_texture = gl.GLuint() gl.glGenTextures(1, self._frame_depth_texture) # set up RGB texture gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0) gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_texture) gl.glTexImage2D( gl.GL_TEXTURE_2D, 0, gl.GL_RGB, self.screen_width, self.screen_height, 0, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, None, ) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) # set up depth texture gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_depth_texture) gl.glTexImage2D( gl.GL_TEXTURE_2D, 0, gl.GL_DEPTH_COMPONENT32, self.screen_width, self.screen_height, 0, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT, None, ) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) gl.glBindTexture(gl.GL_TEXTURE_2D, 0) # create a framebuffer object (FBO) if self._frame_fbo is None: self._frame_fbo = gl.GLuint() gl.glGenFramebuffers(1, self._frame_fbo) gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._frame_fbo) # attach the texture to the FBO as its color attachment gl.glFramebufferTexture2D( gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self._frame_texture, 0 ) # attach the depth texture to the FBO as its depth attachment gl.glFramebufferTexture2D( gl.GL_FRAMEBUFFER, gl.GL_DEPTH_ATTACHMENT, gl.GL_TEXTURE_2D, self._frame_depth_texture, 0 ) if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE: print("Framebuffer is not complete!") gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) sys.exit(1) # unbind the FBO (switch back to the default framebuffer) gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) if self._frame_pbo is None: self._frame_pbo = gl.GLuint() gl.glGenBuffers(1, self._frame_pbo) # generate 1 buffer reference gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, self._frame_pbo) # binding to this buffer # allocate memory for PBO rgb_bytes_per_pixel = 3 depth_bytes_per_pixel = 4 pixels = np.zeros( (self.screen_height, self.screen_width, rgb_bytes_per_pixel + depth_bytes_per_pixel), dtype=np.uint8 ) gl.glBufferData(gl.GL_PIXEL_PACK_BUFFER, pixels.nbytes, pixels.ctypes.data, gl.GL_DYNAMIC_DRAW) gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, 0) @staticmethod def compute_projection_matrix( fov: float, aspect_ratio: float, near_plane: float, far_plane: float, ) -> Mat44: """ Compute a projection matrix given the field of view, aspect ratio, near plane, and far plane. :param fov: The field of view in degrees. :param aspect_ratio: The aspect ratio (width / height). :param near_plane: The near plane. :param far_plane: The far plane. :return: A projection matrix. """ from pyglet.math import Mat4 as PyMat4 return np.array(PyMat4.perspective_projection(aspect_ratio, near_plane, far_plane, fov)) def update_projection_matrix(self): if self.screen_height == 0: return aspect_ratio = self.screen_width / self.screen_height self._projection_matrix = self.compute_projection_matrix( self.camera_fov, aspect_ratio, self.camera_near_plane, self.camera_far_plane ) def update_view_matrix(self): from pyglet.math import Mat4 as PyMat4 cam_pos = self._camera_pos self._view_matrix = np.array(PyMat4.look_at(cam_pos, cam_pos + self._camera_front, self._camera_up)) def update_model_matrix(self): from pyglet import gl # fmt: off if self._camera_axis == 0: self._model_matrix = np.array(( 0, 0, self._scaling, 0, self._scaling, 0, 0, 0, 0, self._scaling, 0, 0, 0, 0, 0, 1 )) elif self._camera_axis == 2: self._model_matrix = np.array(( -self._scaling, 0, 0, 0, 0, 0, self._scaling, 0, 0, self._scaling, 0, 0, 0, 0, 0, 1 )) else: self._model_matrix = np.array(( self._scaling, 0, 0, 0, 0, self._scaling, 0, 0, 0, 0, self._scaling, 0, 0, 0, 0, 1 )) # fmt: on ptr = arr_pointer(self._model_matrix) gl.glUseProgram(self._shape_shader.id) gl.glUniformMatrix4fv(self._loc_shape_model, 1, gl.GL_FALSE, ptr) gl.glUseProgram(self._grid_shader.id) gl.glUniformMatrix4fv(self._loc_grid_model, 1, gl.GL_FALSE, ptr) gl.glUseProgram(self._sky_shader.id) gl.glUniformMatrix4fv(self._loc_sky_model, 1, gl.GL_FALSE, ptr) @property def num_tiles(self): return len(self._tile_instances) @property def tile_width(self): return self._tile_width @property def tile_height(self): return self._tile_height @property def num_shapes(self): return len(self._shapes) @property def num_instances(self): return self._instance_count @property def scaling(self): return self._scaling @scaling.setter def scaling(self, scaling): self._scaling = scaling self.update_model_matrix() def begin_frame(self, t: float = None): self._last_begin_frame_time = time.time() self.time = t or self.clock_time def end_frame(self): self._last_end_frame_time = time.time() if self._add_shape_instances: self.allocate_shape_instances() if self._update_shape_instances: self.update_shape_instances() self.update() while self.paused and self.is_running(): self.update() def update(self): self.clock_time = time.time() - self._start_time update_duration = self.clock_time - self._last_time frame_duration = self._last_end_frame_time - self._last_begin_frame_time self._last_time = self.clock_time self._frame_speed = update_duration * 100.0 # self.app.event_loop.idle() self.app.platform_event_loop.step(self._frame_dt * 1e-3) if not self.skip_rendering: self._skip_frame_counter += 1 if self._skip_frame_counter > 100: self._skip_frame_counter = 0 if frame_duration > 0.0: if self._fps_update is None: self._fps_update = 1.0 / frame_duration else: update = 1.0 / frame_duration self._fps_update = (1.0 - self._fps_alpha) * self._fps_update + self._fps_alpha * update if update_duration > 0.0: if self._fps_render is None: self._fps_render = 1.0 / update_duration else: update = 1.0 / update_duration self._fps_render = (1.0 - self._fps_alpha) * self._fps_render + self._fps_alpha * update self.app.event_loop._redraw_windows(self._frame_dt * 1e-3) def _draw(self): from pyglet import gl # catch key hold events self._process_inputs() if self.enable_backface_culling: gl.glEnable(gl.GL_CULL_FACE) else: gl.glDisable(gl.GL_CULL_FACE) if self._frame_fbo is not None: gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._frame_fbo) gl.glClearColor(*self.background_color, 1) gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) gl.glBindVertexArray(0) if not self._tiled_rendering: if self.draw_grid: self._draw_grid() if self.draw_sky: self._draw_sky() view_mat_ptr = arr_pointer(self._view_matrix) projection_mat_ptr = arr_pointer(self._projection_matrix) gl.glUseProgram(self._shape_shader.id) gl.glUniformMatrix4fv(self._loc_shape_view, 1, gl.GL_FALSE, view_mat_ptr) gl.glUniform3f(self._loc_shape_view_pos, *self._camera_pos) gl.glUniformMatrix4fv(self._loc_shape_view, 1, gl.GL_FALSE, view_mat_ptr) gl.glUniformMatrix4fv(self._loc_shape_projection, 1, gl.GL_FALSE, projection_mat_ptr) if self.render_wireframe: gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE) if self._tiled_rendering: self._render_scene_tiled() else: self._render_scene() gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL) gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0) gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) gl.glViewport(0, 0, self.screen_width, self.screen_height) # render frame buffer texture to screen if self._frame_fbo is not None: if self.render_depth: with self._frame_depth_shader: gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_depth_texture) gl.glUniform1i(self._frame_loc_depth_texture, 0) gl.glBindVertexArray(self._frame_vao) gl.glDrawElements(gl.GL_TRIANGLES, len(self._frame_indices), gl.GL_UNSIGNED_INT, None) gl.glBindVertexArray(0) gl.glBindTexture(gl.GL_TEXTURE_2D, 0) else: with self._frame_shader: gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_texture) gl.glUniform1i(self._frame_loc_texture, 0) gl.glBindVertexArray(self._frame_vao) gl.glDrawElements(gl.GL_TRIANGLES, len(self._frame_indices), gl.GL_UNSIGNED_INT, None) gl.glBindVertexArray(0) gl.glBindTexture(gl.GL_TEXTURE_2D, 0) # check for OpenGL errors # check_gl_error() if self.show_info: gl.glClear(gl.GL_DEPTH_BUFFER_BIT) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glEnable(gl.GL_BLEND) text = f"""Sim Time: {self.time:.1f} Update FPS: {self._fps_update:.1f} Render FPS: {self._fps_render:.1f} Shapes: {len(self._shapes)} Instances: {len(self._instances)}""" if self.paused: text += "\nPaused (press space to resume)" self._info_label.text = text self._info_label.y = self.screen_height - 5 self._info_label.draw() def _draw_grid(self, is_tiled=False): from pyglet import gl if not is_tiled: gl.glUseProgram(self._grid_shader.id) gl.glUniformMatrix4fv(self._loc_grid_view, 1, gl.GL_FALSE, arr_pointer(self._view_matrix)) gl.glUniformMatrix4fv(self._loc_grid_projection, 1, gl.GL_FALSE, arr_pointer(self._projection_matrix)) gl.glBindVertexArray(self._grid_vao) gl.glDrawArrays(gl.GL_LINES, 0, self._grid_vertex_count) gl.glBindVertexArray(0) def _draw_sky(self, is_tiled=False): from pyglet import gl if not is_tiled: gl.glUseProgram(self._sky_shader.id) gl.glUniformMatrix4fv(self._loc_sky_view, 1, gl.GL_FALSE, arr_pointer(self._view_matrix)) gl.glUniformMatrix4fv(self._loc_sky_projection, 1, gl.GL_FALSE, arr_pointer(self._projection_matrix)) gl.glUniform3f(self._loc_sky_view_pos, *self._camera_pos) gl.glBindVertexArray(self._sky_vao) gl.glDrawElements(gl.GL_TRIANGLES, self._sky_tri_count, gl.GL_UNSIGNED_INT, None) gl.glBindVertexArray(0) def _render_scene(self): from pyglet import gl start_instance_idx = 0 for shape, (vao, _, _, tri_count, _) in self._shape_gl_buffers.items(): num_instances = len(self._shape_instances[shape]) gl.glBindVertexArray(vao) gl.glDrawElementsInstancedBaseInstance( gl.GL_TRIANGLES, tri_count, gl.GL_UNSIGNED_INT, None, num_instances, start_instance_idx ) start_instance_idx += num_instances if self.draw_axis: self._axis_instancer.render() for instancer in self._shape_instancers.values(): instancer.render() gl.glBindVertexArray(0) def _render_scene_tiled(self): from pyglet import gl for i, viewport in enumerate(self._tile_viewports): projection_matrix_ptr = arr_pointer(self._tile_projection_matrices[i]) view_matrix_ptr = arr_pointer(self._tile_view_matrices[i] or self._view_matrix) gl.glViewport(*viewport) if self.draw_grid: gl.glUseProgram(self._grid_shader.id) gl.glUniformMatrix4fv(self._loc_grid_projection, 1, gl.GL_FALSE, projection_matrix_ptr) gl.glUniformMatrix4fv(self._loc_grid_view, 1, gl.GL_FALSE, view_matrix_ptr) self._draw_grid(is_tiled=True) if self.draw_sky: gl.glUseProgram(self._sky_shader.id) gl.glUniformMatrix4fv(self._loc_sky_projection, 1, gl.GL_FALSE, projection_matrix_ptr) gl.glUniformMatrix4fv(self._loc_sky_view, 1, gl.GL_FALSE, view_matrix_ptr) self._draw_sky(is_tiled=True) gl.glUseProgram(self._shape_shader.id) gl.glUniformMatrix4fv(self._loc_shape_projection, 1, gl.GL_FALSE, projection_matrix_ptr) gl.glUniformMatrix4fv(self._loc_shape_view, 1, gl.GL_FALSE, view_matrix_ptr) instances = self._tile_instances[i] for instance in instances: shape = self._instance_shape[instance] vao, _, _, tri_count, _ = self._shape_gl_buffers[shape] start_instance_idx = self._inverse_instance_ids[instance] gl.glBindVertexArray(vao) gl.glDrawElementsInstancedBaseInstance( gl.GL_TRIANGLES, tri_count, gl.GL_UNSIGNED_INT, None, 1, start_instance_idx ) if self.draw_axis: self._axis_instancer.render() for instancer in self._shape_instancers.values(): instancer.render() gl.glBindVertexArray(0) def _mouse_drag_callback(self, x, y, dx, dy, buttons, modifiers): import pyglet if buttons & pyglet.window.mouse.LEFT: sensitivity = 0.1 dx *= sensitivity dy *= sensitivity self._yaw += dx self._pitch += dy self._pitch = max(min(self._pitch, 89.0), -89.0) self._camera_front.x = np.cos(np.deg2rad(self._yaw)) * np.cos(np.deg2rad(self._pitch)) self._camera_front.y = np.sin(np.deg2rad(self._pitch)) self._camera_front.z = np.sin(np.deg2rad(self._yaw)) * np.cos(np.deg2rad(self._pitch)) self._camera_front = self._camera_front.normalize() self.update_view_matrix() def _scroll_callback(self, x, y, scroll_x, scroll_y): self.camera_fov -= scroll_y self.camera_fov = max(min(self.camera_fov, 90.0), 15.0) self.update_projection_matrix() def _process_inputs(self): import pyglet from pyglet.math import Vec3 as PyVec3 if self._key_handler[pyglet.window.key.W] or self._key_handler[pyglet.window.key.UP]: self._camera_pos += self._camera_front * (self._camera_speed * self._frame_speed) self.update_view_matrix() if self._key_handler[pyglet.window.key.S] or self._key_handler[pyglet.window.key.DOWN]: self._camera_pos -= self._camera_front * (self._camera_speed * self._frame_speed) self.update_view_matrix() if self._key_handler[pyglet.window.key.A] or self._key_handler[pyglet.window.key.LEFT]: camera_side = PyVec3.cross(self._camera_front, self._camera_up).normalize() self._camera_pos -= camera_side * (self._camera_speed * self._frame_speed) self.update_view_matrix() if self._key_handler[pyglet.window.key.D] or self._key_handler[pyglet.window.key.RIGHT]: camera_side = PyVec3.cross(self._camera_front, self._camera_up).normalize() self._camera_pos += camera_side * (self._camera_speed * self._frame_speed) self.update_view_matrix() def _key_press_callback(self, symbol, modifiers): import pyglet if symbol == pyglet.window.key.ESCAPE: self.window.close() if symbol == pyglet.window.key.SPACE: self.paused = not self.paused if symbol == pyglet.window.key.TAB: self.skip_rendering = not self.skip_rendering if symbol == pyglet.window.key.C: self.draw_axis = not self.draw_axis if symbol == pyglet.window.key.G: self.draw_grid = not self.draw_grid if symbol == pyglet.window.key.I: self.show_info = not self.show_info if symbol == pyglet.window.key.X: self.render_wireframe = not self.render_wireframe if symbol == pyglet.window.key.T: self.render_depth = not self.render_depth if symbol == pyglet.window.key.B: self.enable_backface_culling = not self.enable_backface_culling for cb in self._key_callbacks: cb(symbol, modifiers) def register_key_press_callback(self, callback): self._key_callbacks.append(callback) def _window_resize_callback(self, width, height): self._first_mouse = True self.screen_width, self.screen_height = self.window.get_framebuffer_size() self.update_projection_matrix() self._setup_framebuffer() def register_shape(self, geo_hash, vertices, indices, color1=None, color2=None): from pyglet import gl shape = len(self._shapes) if color1 is None: color1 = tab10_color_map(len(self._shape_geo_hash)) if color2 is None: color2 = np.clip(np.array(color1) + 0.25, 0.0, 1.0) # TODO check if we actually need to store the shape data self._shapes.append((vertices, indices, color1, color2, geo_hash)) self._shape_geo_hash[geo_hash] = shape gl.glUseProgram(self._shape_shader.id) # Create VAO, VBO, and EBO vao = gl.GLuint() gl.glGenVertexArrays(1, vao) gl.glBindVertexArray(vao) vbo = gl.GLuint() gl.glGenBuffers(1, vbo) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo) gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices.ctypes.data, gl.GL_STATIC_DRAW) vertex_cuda_buffer = wp.RegisteredGLBuffer(int(vbo.value), self._device) ebo = gl.GLuint() gl.glGenBuffers(1, ebo) gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, ebo) gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices.ctypes.data, gl.GL_STATIC_DRAW) # Set up vertex attributes vertex_stride = vertices.shape[1] * vertices.itemsize # positions gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(0)) gl.glEnableVertexAttribArray(0) # normals gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(3 * vertices.itemsize)) gl.glEnableVertexAttribArray(1) # uv coordinates gl.glVertexAttribPointer(2, 2, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(6 * vertices.itemsize)) gl.glEnableVertexAttribArray(2) gl.glBindVertexArray(0) self._shape_gl_buffers[shape] = (vao, vbo, ebo, len(indices), vertex_cuda_buffer) return shape def add_shape_instance( self, name: str, shape: int, body, pos, rot, scale=(1.0, 1.0, 1.0), color1=None, color2=None ): if color1 is None: color1 = self._shapes[shape][2] if color2 is None: color2 = self._shapes[shape][3] instance = len(self._instances) self._shape_instances[shape].append(instance) body = self._resolve_body_id(body) self._instances[name] = (instance, body, shape, [*pos, *rot], scale, color1, color2) self._instance_shape[instance] = shape self._add_shape_instances = True self._instance_count = len(self._instances) return instance def allocate_shape_instances(self): from pyglet import gl self._add_shape_instances = False self._wp_instance_transforms = wp.array( [instance[3] for instance in self._instances.values()], dtype=wp.transform, device=self._device ) self._wp_instance_scalings = wp.array( [instance[4] for instance in self._instances.values()], dtype=wp.vec3, device=self._device ) self._wp_instance_bodies = wp.array( [instance[1] for instance in self._instances.values()], dtype=wp.int32, device=self._device ) gl.glUseProgram(self._shape_shader.id) if self._instance_transform_gl_buffer is not None: gl.glDeleteBuffers(1, self._instance_transform_gl_buffer) gl.glDeleteBuffers(1, self._instance_color1_buffer) gl.glDeleteBuffers(1, self._instance_color2_buffer) # create instance buffer and bind it as an instanced array self._instance_transform_gl_buffer = gl.GLuint() gl.glGenBuffers(1, self._instance_transform_gl_buffer) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_transform_gl_buffer) transforms = np.tile(np.diag(np.ones(4, dtype=np.float32)), (len(self._instances), 1, 1)) gl.glBufferData(gl.GL_ARRAY_BUFFER, transforms.nbytes, transforms.ctypes.data, gl.GL_DYNAMIC_DRAW) # create CUDA buffer for instance transforms self._instance_transform_cuda_buffer = wp.RegisteredGLBuffer( int(self._instance_transform_gl_buffer.value), self._device ) colors1, colors2 = [], [] all_instances = list(self._instances.values()) for shape, instances in self._shape_instances.items(): for i in instances: if i >= len(all_instances): continue instance = all_instances[i] colors1.append(instance[5]) colors2.append(instance[6]) colors1 = np.array(colors1, dtype=np.float32) colors2 = np.array(colors2, dtype=np.float32) # create buffer for checkerboard colors self._instance_color1_buffer = gl.GLuint() gl.glGenBuffers(1, self._instance_color1_buffer) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color1_buffer) gl.glBufferData(gl.GL_ARRAY_BUFFER, colors1.nbytes, colors1.ctypes.data, gl.GL_STATIC_DRAW) self._instance_color2_buffer = gl.GLuint() gl.glGenBuffers(1, self._instance_color2_buffer) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color2_buffer) gl.glBufferData(gl.GL_ARRAY_BUFFER, colors2.nbytes, colors2.ctypes.data, gl.GL_STATIC_DRAW) # set up instance attribute pointers matrix_size = transforms[0].nbytes instance_ids = [] inverse_instance_ids = {} instance_count = 0 for shape, (vao, vbo, ebo, tri_count, vertex_cuda_buffer) in self._shape_gl_buffers.items(): gl.glBindVertexArray(vao) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_transform_gl_buffer) # we can only send vec4s to the shader, so we need to split the instance transforms matrix into its column vectors for i in range(4): gl.glVertexAttribPointer( 3 + i, 4, gl.GL_FLOAT, gl.GL_FALSE, matrix_size, ctypes.c_void_p(i * matrix_size // 4) ) gl.glEnableVertexAttribArray(3 + i) gl.glVertexAttribDivisor(3 + i, 1) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color1_buffer) gl.glVertexAttribPointer(7, 3, gl.GL_FLOAT, gl.GL_FALSE, colors1[0].nbytes, ctypes.c_void_p(0)) gl.glEnableVertexAttribArray(7) gl.glVertexAttribDivisor(7, 1) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color2_buffer) gl.glVertexAttribPointer(8, 3, gl.GL_FLOAT, gl.GL_FALSE, colors2[0].nbytes, ctypes.c_void_p(0)) gl.glEnableVertexAttribArray(8) gl.glVertexAttribDivisor(8, 1) instance_ids.extend(self._shape_instances[shape]) for i in self._shape_instances[shape]: inverse_instance_ids[i] = instance_count instance_count += 1 # trigger update to the instance transforms self._update_shape_instances = True self._wp_instance_ids = wp.array(instance_ids, dtype=wp.int32, device=self._device) self._instance_ids = instance_ids self._inverse_instance_ids = inverse_instance_ids gl.glBindVertexArray(0) def update_shape_instance(self, name, pos, rot, color1=None, color2=None): """Update the instance transform of the shape Args: name: The name of the shape pos: The position of the shape rot: The rotation of the shape """ if name in self._instances: i, body, shape, _, scale, old_color1, old_color2 = self._instances[name] self._instances[name] = (i, body, shape, [*pos, *rot], scale, color1 or old_color1, color2 or old_color2) self._update_shape_instances = True return True return False def update_shape_instances(self): with self._shape_shader: self._update_shape_instances = False self._wp_instance_transforms = wp.array( [instance[3] for instance in self._instances.values()], dtype=wp.transform, device=self._device ) self.update_body_transforms(None) def update_body_transforms(self, body_tf: wp.array): if self._instance_transform_cuda_buffer is None: return body_q = None if body_tf is not None: if body_tf.device.is_cuda: body_q = body_tf else: body_q = body_tf.to(self._device) vbo_transforms = self._instance_transform_cuda_buffer.map(dtype=wp.mat44, shape=(self._instance_count,)) wp.launch( update_vbo_transforms, dim=self._instance_count, inputs=[ self._wp_instance_ids, self._wp_instance_bodies, self._wp_instance_transforms, self._wp_instance_scalings, body_q, ], outputs=[ vbo_transforms, ], device=self._device, ) self._instance_transform_cuda_buffer.unmap() def register_body(self, name): # register body name and return its ID if name not in self._body_name: self._body_name[name] = len(self._body_name) return self._body_name[name] def _resolve_body_id(self, body): if body is None: return -1 if isinstance(body, int): return body return self._body_name[body] def is_running(self): return not self.app.event_loop.has_exit def save(self): # save just keeps the window open to allow the user to interact with the scene while not self.app.event_loop.has_exit: self.update() if self.app.event_loop.has_exit: self.clear() self.app.event_loop.exit() def get_pixels(self, target_image: wp.array, split_up_tiles=True, mode="rgb"): """ Read the pixels from the frame buffer (RGB or depth are supported) into the given array. If `split_up_tiles` is False, array must be of shape (screen_height, screen_width, 3) for RGB mode or (screen_height, screen_width, 1) for depth mode. If `split_up_tiles` is True, the pixels will be split up into tiles (see :attr:`tile_width` and :attr:`tile_height` for dimensions): array must be of shape (num_tiles, tile_height, tile_width, 3) for RGB mode or (num_tiles, tile_height, tile_width, 1) for depth mode. Args: target_image (array): The array to read the pixels into. Must have float32 as dtype and be on a CUDA device. split_up_tiles (bool): Whether to split up the viewport into tiles, see :meth:`setup_tiled_rendering`. mode (str): can be either "rgb" or "depth" Returns: bool: Whether the pixels were successfully read. """ from pyglet import gl channels = 3 if mode == "rgb" else 1 if split_up_tiles: assert ( self._tile_width is not None and self._tile_height is not None ), "Tile width and height are not set, tiles must all have the same size" assert all( vp[2] == self._tile_width for vp in self._tile_viewports ), "Tile widths do not all equal global tile_width, use `get_tile_pixels` instead to retrieve pixels for a single tile" assert all( vp[3] == self._tile_height for vp in self._tile_viewports ), "Tile heights do not all equal global tile_height, use `get_tile_pixels` instead to retrieve pixels for a single tile" assert target_image.shape == ( self.num_tiles, self._tile_height, self._tile_width, channels, ), f"Shape of `target_image` array does not match {self.num_tiles} x {self.screen_height} x {self.screen_width} x {channels}" else: assert target_image.shape == ( self.screen_height, self.screen_width, channels, ), f"Shape of `target_image` array does not match {self.screen_height} x {self.screen_width} x {channels}" gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, self._frame_pbo) if mode == "rgb": gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_texture) if mode == "depth": gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_depth_texture) try: # read screen texture into PBO if mode == "rgb": gl.glGetTexImage(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, ctypes.c_void_p(0)) elif mode == "depth": gl.glGetTexImage(gl.GL_TEXTURE_2D, 0, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT, ctypes.c_void_p(0)) except gl.GLException: # this can happen if the window is closed/being moved to a different display gl.glBindTexture(gl.GL_TEXTURE_2D, 0) gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, 0) return False gl.glBindTexture(gl.GL_TEXTURE_2D, 0) gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, 0) pbo_buffer = wp.RegisteredGLBuffer( int(self._frame_pbo.value), self._device, wp.RegisteredGLBuffer.WRITE_DISCARD ) screen_size = self.screen_height * self.screen_width if mode == "rgb": img = pbo_buffer.map(dtype=wp.uint8, shape=(screen_size * channels)) elif mode == "depth": img = pbo_buffer.map(dtype=wp.float32, shape=(screen_size * channels)) img = img.to(target_image.device) if split_up_tiles: positions = wp.array(self._tile_viewports, ndim=2, dtype=wp.int32, device=target_image.device) if mode == "rgb": wp.launch( copy_rgb_frame_tiles, dim=(self.num_tiles, self._tile_width, self._tile_height), inputs=[img, positions, self.screen_width, self.screen_height, self._tile_height], outputs=[target_image], device=target_image.device, ) elif mode == "depth": wp.launch( copy_depth_frame_tiles, dim=(self.num_tiles, self._tile_width, self._tile_height), inputs=[ img, positions, self.screen_width, self.screen_height, self._tile_height, self.camera_near_plane, self.camera_far_plane, ], outputs=[target_image], device=target_image.device, ) else: if mode == "rgb": wp.launch( copy_rgb_frame, dim=(self.screen_width, self.screen_height), inputs=[img, self.screen_width, self.screen_height], outputs=[target_image], device=target_image.device, ) elif mode == "depth": wp.launch( copy_depth_frame, dim=(self.screen_width, self.screen_height), inputs=[img, self.screen_width, self.screen_height, self.camera_near_plane, self.camera_far_plane], outputs=[target_image], device=target_image.device, ) pbo_buffer.unmap() return True # def create_image_texture(self, file_path): # from PIL import Image # img = Image.open(file_path) # img_data = np.array(list(img.getdata()), np.uint8) # texture = glGenTextures(1) # glBindTexture(GL_TEXTURE_2D, texture) # glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img.width, img.height, 0, GL_RGB, GL_UNSIGNED_BYTE, img_data) # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) # return texture # def create_check_texture(self, color1=(0, 0.5, 1.0), color2=None, width=default_texture_size, height=default_texture_size): # if width == 1 and height == 1: # pixels = np.array([np.array(color1)*255], dtype=np.uint8) # else: # pixels = np.zeros((width, height, 3), dtype=np.uint8) # half_w = width // 2 # half_h = height // 2 # color1 = np.array(np.array(color1)*255, dtype=np.uint8) # pixels[0:half_w, 0:half_h] = color1 # pixels[half_w:width, half_h:height] = color1 # if color2 is None: # color2 = np.array(np.clip(np.array(color1, dtype=np.float32) + 50, 0, 255), dtype=np.uint8) # else: # color2 = np.array(np.array(color2)*255, dtype=np.uint8) # pixels[half_w:width, 0:half_h] = color2 # pixels[0:half_w, half_h:height] = color2 # texture = glGenTextures(1) # glBindTexture(GL_TEXTURE_2D, texture) # glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels.flatten()) # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) # return texture def render_plane( self, name: str, pos: tuple, rot: tuple, width: float, length: float, color: tuple = (1.0, 1.0, 1.0), color2=None, parent_body: str = None, is_template: bool = False, u_scaling=1.0, v_scaling=1.0, ): """Add a plane for visualization Args: name: The name of the plane pos: The position of the plane rot: The rotation of the plane width: The width of the plane length: The length of the plane color: The color of the plane texture: The texture of the plane (optional) """ geo_hash = hash(("plane", width, length)) if geo_hash in self._shape_geo_hash: shape = self._shape_geo_hash[geo_hash] if self.update_shape_instance(name, pos, rot): return shape else: faces = np.array([0, 1, 2, 2, 3, 0], dtype=np.uint32) normal = (0.0, 1.0, 0.0) width = width if width > 0.0 else 100.0 length = length if length > 0.0 else 100.0 aspect = width / length u = width * aspect * u_scaling v = length * v_scaling gfx_vertices = np.array( [ [-width, 0.0, -length, *normal, 0.0, 0.0], [-width, 0.0, length, *normal, 0.0, v], [width, 0.0, length, *normal, u, v], [width, 0.0, -length, *normal, u, 0.0], ], dtype=np.float32, ) shape = self.register_shape(geo_hash, gfx_vertices, faces, color1=color, color2=color2) if not is_template: body = self._resolve_body_id(parent_body) self.add_shape_instance(name, shape, body, pos, rot) return shape def render_ground(self, size: float = 100.0): """Add a ground plane for visualization Args: size: The size of the ground plane """ color1 = (200 / 255, 200 / 255, 200 / 255) color2 = (150 / 255, 150 / 255, 150 / 255) sqh = np.sqrt(0.5) if self._camera_axis == 0: q = (0.0, 0.0, -sqh, sqh) elif self._camera_axis == 1: q = (0.0, 0.0, 0.0, 1.0) elif self._camera_axis == 2: q = (sqh, 0.0, 0.0, sqh) return self.render_plane( "ground", (0.0, 0.0, 0.0), q, size, size, color1, color2=color2, u_scaling=1.0, v_scaling=1.0, ) def render_sphere( self, name: str, pos: tuple, rot: tuple, radius: float, parent_body: str = None, is_template: bool = False ): """Add a sphere for visualization Args: pos: The position of the sphere radius: The radius of the sphere name: A name for the USD prim on the stage """ geo_hash = hash(("sphere", radius)) if geo_hash in self._shape_geo_hash: shape = self._shape_geo_hash[geo_hash] if self.update_shape_instance(name, pos, rot): return shape else: vertices, indices = self._create_sphere_mesh(radius) shape = self.register_shape(geo_hash, vertices, indices) if not is_template: body = self._resolve_body_id(parent_body) self.add_shape_instance(name, shape, body, pos, rot) return shape def render_capsule( self, name: str, pos: tuple, rot: tuple, radius: float, half_height: float, parent_body: str = None, is_template: bool = False, up_axis: int = 1, ): """Add a capsule for visualization Args: pos: The position of the capsule radius: The radius of the capsule half_height: The half height of the capsule name: A name for the USD prim on the stage up_axis: The axis of the capsule that points up (0: x, 1: y, 2: z) """ geo_hash = hash(("capsule", radius, half_height)) if geo_hash in self._shape_geo_hash: shape = self._shape_geo_hash[geo_hash] if self.update_shape_instance(name, pos, rot): return shape else: vertices, indices = self._create_capsule_mesh(radius, half_height, up_axis=up_axis) shape = self.register_shape(geo_hash, vertices, indices) if not is_template: body = self._resolve_body_id(parent_body) self.add_shape_instance(name, shape, body, pos, rot) return shape def render_cylinder( self, name: str, pos: tuple, rot: tuple, radius: float, half_height: float, parent_body: str = None, is_template: bool = False, up_axis: int = 1, ): """Add a cylinder for visualization Args: pos: The position of the cylinder radius: The radius of the cylinder half_height: The half height of the cylinder name: A name for the USD prim on the stage up_axis: The axis of the cylinder that points up (0: x, 1: y, 2: z) """ geo_hash = hash(("cylinder", radius, half_height)) if geo_hash in self._shape_geo_hash: shape = self._shape_geo_hash[geo_hash] if self.update_shape_instance(name, pos, rot): return shape else: vertices, indices = self._create_cylinder_mesh(radius, half_height, up_axis=up_axis) shape = self.register_shape(geo_hash, vertices, indices) if not is_template: body = self._resolve_body_id(parent_body) self.add_shape_instance(name, shape, body, pos, rot) return shape def render_cone( self, name: str, pos: tuple, rot: tuple, radius: float, half_height: float, parent_body: str = None, is_template: bool = False, up_axis: int = 1, ): """Add a cone for visualization Args: pos: The position of the cone radius: The radius of the cone half_height: The half height of the cone name: A name for the USD prim on the stage up_axis: The axis of the cone that points up (0: x, 1: y, 2: z) """ geo_hash = hash(("cone", radius, half_height)) if geo_hash in self._shape_geo_hash: shape = self._shape_geo_hash[geo_hash] if self.update_shape_instance(name, pos, rot): return shape else: vertices, indices = self._create_cone_mesh(radius, half_height, up_axis=up_axis) shape = self.register_shape(geo_hash, vertices, indices) if not is_template: body = self._resolve_body_id(parent_body) self.add_shape_instance(name, shape, body, pos, rot) return shape def render_box( self, name: str, pos: tuple, rot: tuple, extents: tuple, parent_body: str = None, is_template: bool = False ): """Add a box for visualization Args: pos: The position of the box extents: The extents of the box name: A name for the USD prim on the stage """ geo_hash = hash(("box", tuple(extents))) if geo_hash in self._shape_geo_hash: shape = self._shape_geo_hash[geo_hash] if self.update_shape_instance(name, pos, rot): return shape else: vertices, indices = self._create_box_mesh(extents) shape = self.register_shape(geo_hash, vertices, indices) if not is_template: body = self._resolve_body_id(parent_body) self.add_shape_instance(name, shape, body, pos, rot) return shape def render_mesh( self, name: str, points, indices, colors=None, pos=(0.0, 0.0, 0.0), rot=(0.0, 0.0, 0.0, 1.0), scale=(1.0, 1.0, 1.0), update_topology=False, parent_body: str = None, is_template: bool = False, smooth_shading: bool = True, ): """Add a mesh for visualization Args: points: The points of the mesh indices: The indices of the mesh colors: The colors of the mesh pos: The position of the mesh rot: The rotation of the mesh scale: The scale of the mesh name: A name for the USD prim on the stage smooth_shading: Whether to average face normals at each vertex or introduce additional vertices for each face """ if colors is None: colors = np.ones((len(points), 3), dtype=np.float32) else: colors = np.array(colors, dtype=np.float32) points = np.array(points, dtype=np.float32) * np.array(scale, dtype=np.float32) indices = np.array(indices, dtype=np.int32).reshape((-1, 3)) if name in self._instances: self.update_shape_instance(name, pos, rot) shape = self._instances[name][2] self.update_shape_vertices(shape, points) return geo_hash = hash((points.tobytes(), indices.tobytes(), colors.tobytes())) if geo_hash in self._shape_geo_hash: shape = self._shape_geo_hash[geo_hash] if self.update_shape_instance(name, pos, rot): return shape else: if smooth_shading: normals = wp.zeros(len(points), dtype=wp.vec3) vertices = wp.array(points, dtype=wp.vec3) faces_per_vertex = wp.zeros(len(points), dtype=int) wp.launch( compute_average_normals, dim=len(indices), inputs=[wp.array(indices, dtype=int), vertices], outputs=[normals, faces_per_vertex], ) gfx_vertices = wp.zeros((len(points), 8), dtype=float) wp.launch( assemble_gfx_vertices, dim=len(points), inputs=[vertices, normals, faces_per_vertex], outputs=[gfx_vertices], ) gfx_vertices = gfx_vertices.numpy() gfx_indices = indices.flatten() else: gfx_vertices = wp.zeros((len(indices) * 3, 8), dtype=float) wp.launch( compute_gfx_vertices, dim=len(indices), inputs=[wp.array(indices, dtype=int), wp.array(points, dtype=wp.vec3)], outputs=[gfx_vertices], ) gfx_vertices = gfx_vertices.numpy() gfx_indices = np.arange(len(indices) * 3) shape = self.register_shape(geo_hash, gfx_vertices, gfx_indices) if not is_template: body = self._resolve_body_id(parent_body) self.add_shape_instance(name, shape, body, pos, rot) return shape def render_arrow( self, name: str, pos: tuple, rot: tuple, base_radius: float, base_height: float, cap_radius: float = None, cap_height: float = None, parent_body: str = None, is_template: bool = False, up_axis: int = 1, color: Tuple[float, float, float] = None, ): """Add a arrow for visualization Args: pos: The position of the arrow base_radius: The radius of the cylindrical base of the arrow base_height: The height of the cylindrical base of the arrow cap_radius: The radius of the conical cap of the arrow cap_height: The height of the conical cap of the arrow name: A name for the USD prim on the stage up_axis: The axis of the arrow that points up (0: x, 1: y, 2: z) """ geo_hash = hash(("arrow", base_radius, base_height, cap_radius, cap_height)) if geo_hash in self._shape_geo_hash: shape = self._shape_geo_hash[geo_hash] if self.update_shape_instance(name, pos, rot): return shape else: vertices, indices = self._create_arrow_mesh( base_radius, base_height, cap_radius, cap_height, up_axis=up_axis ) shape = self.register_shape(geo_hash, vertices, indices) if not is_template: body = self._resolve_body_id(parent_body) self.add_shape_instance(name, shape, body, pos, rot, color1=color, color2=color) return shape def render_ref(self, name: str, path: str, pos: tuple, rot: tuple, scale: tuple): """ Create a reference (instance) with the given name to the given path. """ if path in self._instances: _, body, shape, _, original_scale, color1, color2 = self._instances[path] self.add_shape_instance(name, shape, body, pos, rot, scale or original_scale, color1, color2) return raise Exception("Cannot create reference to path: " + path) def render_points(self, name: str, points, radius, colors=None): """Add a set of points Args: points: The points to render radius: The radius of the points (scalar or list) colors: The colors of the points name: A name for the USD prim on the stage """ if len(points) == 0: return if isinstance(points, wp.array): wp_points = points else: wp_points = wp.array(points, dtype=wp.vec3, device=self._device) if name not in self._shape_instancers: np_points = points.numpy() if isinstance(points, wp.array) else points instancer = ShapeInstancer(self._shape_shader, self._device) radius_is_scalar = np.isscalar(radius) if radius_is_scalar: vertices, indices = self._create_sphere_mesh(radius) else: vertices, indices = self._create_sphere_mesh(1.0) if colors is None: color = tab10_color_map(len(self._shape_geo_hash)) else: color = colors[0] instancer.register_shape(vertices, indices, color, color) scalings = None if radius_is_scalar else np.tile(radius, (3, 1)).T instancer.allocate_instances(np_points, colors1=colors, colors2=colors, scalings=scalings) self._shape_instancers[name] = instancer else: instancer = self._shape_instancers[name] if len(points) != instancer.num_instances: np_points = points.numpy() if isinstance(points, wp.array) else points instancer.allocate_instances(np_points) with instancer: wp.launch( update_points_positions, dim=len(points), inputs=[wp_points, instancer.instance_scalings], outputs=[instancer.vbo_transforms], device=self._device, ) def _render_lines(self, name: str, lines, color: tuple, radius: float = 0.01): if len(lines) == 0: return if name not in self._shape_instancers: instancer = ShapeInstancer(self._shape_shader, self._device) vertices, indices = self._create_capsule_mesh(radius, 0.5) if color is None or isinstance(color, list) and len(color) > 0 and isinstance(color[0], list): color = tab10_color_map(len(self._shape_geo_hash)) instancer.register_shape(vertices, indices, color, color) instancer.allocate_instances(np.zeros((len(lines), 3))) self._shape_instancers[name] = instancer else: instancer = self._shape_instancers[name] if len(lines) != instancer.num_instances: instancer.allocate_instances(np.zeros((len(lines), 3))) lines_wp = wp.array(lines, dtype=wp.vec3, ndim=2, device=self._device) with instancer: wp.launch( update_line_transforms, dim=len(lines), inputs=[lines_wp], outputs=[instancer.vbo_transforms], device=self._device, ) def render_line_list(self, name, vertices, indices, color, radius): """Add a line list as a set of capsules Args: vertices: The vertices of the line-list indices: The indices of the line-list color: The color of the line radius: The radius of the line """ lines = [] for i in range(len(indices) // 2): lines.append((vertices[indices[2 * i]], vertices[indices[2 * i + 1]])) lines = np.array(lines) self._render_lines(name, lines, color, radius) def render_line_strip(self, name: str, vertices, color: tuple, radius: float = 0.01): """Add a line strip as a set of capsules Args: vertices: The vertices of the line-strip color: The color of the line radius: The radius of the line """ lines = [] for i in range(len(vertices) - 1): lines.append((vertices[i], vertices[i + 1])) lines = np.array(lines) self._render_lines(name, lines, color, radius) def update_shape_vertices(self, shape, points): if isinstance(points, wp.array): wp_points = points.to(self._device) else: wp_points = wp.array(points, dtype=wp.vec3, device=self._device) cuda_buffer = self._shape_gl_buffers[shape][4] vertices_shape = self._shapes[shape][0].shape vbo_vertices = cuda_buffer.map(dtype=wp.float32, shape=vertices_shape) wp.launch( update_vbo_vertices, dim=vertices_shape[0], inputs=[wp_points], outputs=[vbo_vertices], device=self._device, ) cuda_buffer.unmap() @staticmethod def _create_sphere_mesh( radius=1.0, num_latitudes=default_num_segments, num_longitudes=default_num_segments, reverse_winding=False, ): vertices = [] indices = [] for i in range(num_latitudes + 1): theta = i * np.pi / num_latitudes sin_theta = np.sin(theta) cos_theta = np.cos(theta) for j in range(num_longitudes + 1): phi = j * 2 * np.pi / num_longitudes sin_phi = np.sin(phi) cos_phi = np.cos(phi) x = cos_phi * sin_theta y = cos_theta z = sin_phi * sin_theta u = float(j) / num_longitudes v = float(i) / num_latitudes vertices.append([x * radius, y * radius, z * radius, x, y, z, u, v]) for i in range(num_latitudes): for j in range(num_longitudes): first = i * (num_longitudes + 1) + j second = first + num_longitudes + 1 if reverse_winding: indices.extend([first, second, first + 1, second, second + 1, first + 1]) else: indices.extend([first, first + 1, second, second, first + 1, second + 1]) return np.array(vertices, dtype=np.float32), np.array(indices, dtype=np.uint32) @staticmethod def _create_capsule_mesh(radius, half_height, up_axis=1, segments=default_num_segments): vertices = [] indices = [] x_dir, y_dir, z_dir = ((1, 2, 0), (2, 0, 1), (0, 1, 2))[up_axis] up_vector = np.zeros(3) up_vector[up_axis] = half_height for i in range(segments + 1): theta = i * np.pi / segments sin_theta = np.sin(theta) cos_theta = np.cos(theta) for j in range(segments + 1): phi = j * 2 * np.pi / segments sin_phi = np.sin(phi) cos_phi = np.cos(phi) z = cos_phi * sin_theta y = cos_theta x = sin_phi * sin_theta u = cos_theta * 0.5 + 0.5 v = cos_phi * sin_theta * 0.5 + 0.5 xyz = x, y, z x, y, z = xyz[x_dir], xyz[y_dir], xyz[z_dir] xyz = np.array((x, y, z), dtype=np.float32) * radius if j < segments // 2: xyz += up_vector else: xyz -= up_vector vertices.append([*xyz, x, y, z, u, v]) nv = len(vertices) for i in range(segments + 1): for j in range(segments + 1): first = (i * (segments + 1) + j) % nv second = (first + segments + 1) % nv indices.extend([first, second, (first + 1) % nv, second, (second + 1) % nv, (first + 1) % nv]) vertex_data = np.array(vertices, dtype=np.float32) index_data = np.array(indices, dtype=np.uint32) return vertex_data, index_data @staticmethod def _create_cone_mesh(radius, half_height, up_axis=1, segments=default_num_segments): # render it as a cylinder with zero top radius so we get correct normals on the sides return OpenGLRenderer._create_cylinder_mesh(radius, half_height, up_axis, segments, 0.0) @staticmethod def _create_cylinder_mesh(radius, half_height, up_axis=1, segments=default_num_segments, top_radius=None): if up_axis not in (0, 1, 2): raise ValueError("up_axis must be between 0 and 2") x_dir, y_dir, z_dir = ( (1, 2, 0), (0, 1, 2), (2, 0, 1), )[up_axis] indices = [] cap_vertices = [] side_vertices = [] # create center cap vertices position = np.array([0, -half_height, 0])[[x_dir, y_dir, z_dir]] normal = np.array([0, -1, 0])[[x_dir, y_dir, z_dir]] cap_vertices.append([*position, *normal, 0.5, 0.5]) cap_vertices.append([*-position, *-normal, 0.5, 0.5]) if top_radius is None: top_radius = radius side_slope = -np.arctan2(top_radius - radius, 2 * half_height) # create the cylinder base and top vertices for j in (-1, 1): center_index = max(j, 0) if j == 1: radius = top_radius for i in range(segments): theta = 2 * np.pi * i / segments cos_theta = np.cos(theta) sin_theta = np.sin(theta) x = cos_theta y = j * half_height z = sin_theta position = np.array([radius * x, y, radius * z]) normal = np.array([x, side_slope, z]) normal = normal / np.linalg.norm(normal) uv = (i / (segments - 1), (j + 1) / 2) vertex = np.hstack([position[[x_dir, y_dir, z_dir]], normal[[x_dir, y_dir, z_dir]], uv]) side_vertices.append(vertex) normal = np.array([0, j, 0]) uv = (cos_theta * 0.5 + 0.5, sin_theta * 0.5 + 0.5) vertex = np.hstack([position[[x_dir, y_dir, z_dir]], normal[[x_dir, y_dir, z_dir]], uv]) cap_vertices.append(vertex) cs = center_index * segments indices.extend([center_index, i + cs + 2, (i + 1) % segments + cs + 2][::-j]) # create the cylinder side indices for i in range(segments): index1 = len(cap_vertices) + i + segments index2 = len(cap_vertices) + ((i + 1) % segments) + segments index3 = len(cap_vertices) + i index4 = len(cap_vertices) + ((i + 1) % segments) indices.extend([index1, index2, index3, index2, index4, index3]) vertex_data = np.array(np.vstack((cap_vertices, side_vertices)), dtype=np.float32) index_data = np.array(indices, dtype=np.uint32) return vertex_data, index_data @staticmethod def _create_arrow_mesh( base_radius, base_height, cap_radius=None, cap_height=None, up_axis=1, segments=default_num_segments ): if up_axis not in (0, 1, 2): raise ValueError("up_axis must be between 0 and 2") if cap_radius is None: cap_radius = base_radius * 1.8 if cap_height is None: cap_height = base_height * 0.18 up_vector = np.array([0, 0, 0]) up_vector[up_axis] = 1 base_vertices, base_indices = OpenGLRenderer._create_cylinder_mesh( base_radius, base_height / 2, up_axis, segments ) cap_vertices, cap_indices = OpenGLRenderer._create_cone_mesh(cap_radius, cap_height / 2, up_axis, segments) base_vertices[:, :3] += base_height / 2 * up_vector # move cap slightly lower to avoid z-fighting cap_vertices[:, :3] += (base_height + cap_height / 2 - 1e-3 * base_height) * up_vector vertex_data = np.vstack((base_vertices, cap_vertices)) index_data = np.hstack((base_indices, cap_indices + len(base_vertices))) return vertex_data, index_data @staticmethod def _create_box_mesh(extents): x_extent, y_extent, z_extent = extents vertices = [ # Position Normal UV [-x_extent, -y_extent, -z_extent, -1, 0, 0, 0, 0], [-x_extent, -y_extent, z_extent, -1, 0, 0, 1, 0], [-x_extent, y_extent, z_extent, -1, 0, 0, 1, 1], [-x_extent, y_extent, -z_extent, -1, 0, 0, 0, 1], [x_extent, -y_extent, -z_extent, 1, 0, 0, 0, 0], [x_extent, -y_extent, z_extent, 1, 0, 0, 1, 0], [x_extent, y_extent, z_extent, 1, 0, 0, 1, 1], [x_extent, y_extent, -z_extent, 1, 0, 0, 0, 1], [-x_extent, -y_extent, -z_extent, 0, -1, 0, 0, 0], [-x_extent, -y_extent, z_extent, 0, -1, 0, 1, 0], [x_extent, -y_extent, z_extent, 0, -1, 0, 1, 1], [x_extent, -y_extent, -z_extent, 0, -1, 0, 0, 1], [-x_extent, y_extent, -z_extent, 0, 1, 0, 0, 0], [-x_extent, y_extent, z_extent, 0, 1, 0, 1, 0], [x_extent, y_extent, z_extent, 0, 1, 0, 1, 1], [x_extent, y_extent, -z_extent, 0, 1, 0, 0, 1], [-x_extent, -y_extent, -z_extent, 0, 0, -1, 0, 0], [-x_extent, y_extent, -z_extent, 0, 0, -1, 1, 0], [x_extent, y_extent, -z_extent, 0, 0, -1, 1, 1], [x_extent, -y_extent, -z_extent, 0, 0, -1, 0, 1], [-x_extent, -y_extent, z_extent, 0, 0, 1, 0, 0], [-x_extent, y_extent, z_extent, 0, 0, 1, 1, 0], [x_extent, y_extent, z_extent, 0, 0, 1, 1, 1], [x_extent, -y_extent, z_extent, 0, 0, 1, 0, 1], ] # fmt: off indices = [ 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 8, 10, 9, 8, 11, 10, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 22, 21, 20, 23, 22, ] # fmt: on return np.array(vertices, dtype=np.float32), np.array(indices, dtype=np.uint32) if __name__ == "__main__": wp.init() renderer = OpenGLRenderer()