Spaces:
Sleeping
Sleeping
| # 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); | |
| } | |
| """ | |
| 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, | |
| ) | |
| 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] | |
| 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 | |
| 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 | |
| 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] | |
| 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) | |
| 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] | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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") | |
| def paused(self): | |
| return self._paused | |
| def paused(self, value): | |
| self._paused = value | |
| if value: | |
| self.window.set_caption(f"{self._title} (paused)") | |
| else: | |
| self.window.set_caption(self._title) | |
| 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 | |
| def tiled_rendering(self): | |
| return self._tiled_rendering | |
| 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) | |
| 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) | |
| def num_tiles(self): | |
| return len(self._tile_instances) | |
| def tile_width(self): | |
| return self._tile_width | |
| def tile_height(self): | |
| return self._tile_height | |
| def num_shapes(self): | |
| return len(self._shapes) | |
| def num_instances(self): | |
| return self._instance_count | |
| def scaling(self): | |
| return self._scaling | |
| 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() | |
| 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) | |
| 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 | |
| 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) | |
| 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 | |
| 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 | |
| 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() | |