warp-lang 1.9.1__py3-none-manylinux_2_34_aarch64.whl → 1.10.0rc2__py3-none-manylinux_2_34_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of warp-lang might be problematic. Click here for more details.

Files changed (346) hide show
  1. warp/__init__.py +301 -287
  2. warp/__init__.pyi +794 -305
  3. warp/_src/__init__.py +14 -0
  4. warp/_src/autograd.py +1075 -0
  5. warp/_src/build.py +618 -0
  6. warp/_src/build_dll.py +640 -0
  7. warp/{builtins.py → _src/builtins.py} +1382 -377
  8. warp/_src/codegen.py +4359 -0
  9. warp/{config.py → _src/config.py} +178 -169
  10. warp/_src/constants.py +57 -0
  11. warp/_src/context.py +8294 -0
  12. warp/_src/dlpack.py +462 -0
  13. warp/_src/fabric.py +355 -0
  14. warp/_src/fem/__init__.py +14 -0
  15. warp/_src/fem/adaptivity.py +508 -0
  16. warp/_src/fem/cache.py +687 -0
  17. warp/_src/fem/dirichlet.py +188 -0
  18. warp/{fem → _src/fem}/domain.py +40 -30
  19. warp/_src/fem/field/__init__.py +131 -0
  20. warp/_src/fem/field/field.py +701 -0
  21. warp/{fem → _src/fem}/field/nodal_field.py +30 -15
  22. warp/{fem → _src/fem}/field/restriction.py +1 -1
  23. warp/{fem → _src/fem}/field/virtual.py +53 -27
  24. warp/_src/fem/geometry/__init__.py +32 -0
  25. warp/{fem → _src/fem}/geometry/adaptive_nanogrid.py +77 -163
  26. warp/_src/fem/geometry/closest_point.py +97 -0
  27. warp/{fem → _src/fem}/geometry/deformed_geometry.py +14 -22
  28. warp/{fem → _src/fem}/geometry/element.py +32 -10
  29. warp/{fem → _src/fem}/geometry/geometry.py +48 -20
  30. warp/{fem → _src/fem}/geometry/grid_2d.py +12 -23
  31. warp/{fem → _src/fem}/geometry/grid_3d.py +12 -23
  32. warp/{fem → _src/fem}/geometry/hexmesh.py +40 -63
  33. warp/{fem → _src/fem}/geometry/nanogrid.py +255 -248
  34. warp/{fem → _src/fem}/geometry/partition.py +121 -63
  35. warp/{fem → _src/fem}/geometry/quadmesh.py +26 -45
  36. warp/{fem → _src/fem}/geometry/tetmesh.py +40 -63
  37. warp/{fem → _src/fem}/geometry/trimesh.py +26 -45
  38. warp/{fem → _src/fem}/integrate.py +164 -158
  39. warp/_src/fem/linalg.py +383 -0
  40. warp/_src/fem/operator.py +396 -0
  41. warp/_src/fem/polynomial.py +229 -0
  42. warp/{fem → _src/fem}/quadrature/pic_quadrature.py +15 -20
  43. warp/{fem → _src/fem}/quadrature/quadrature.py +95 -47
  44. warp/_src/fem/space/__init__.py +248 -0
  45. warp/{fem → _src/fem}/space/basis_function_space.py +20 -11
  46. warp/_src/fem/space/basis_space.py +679 -0
  47. warp/{fem → _src/fem}/space/dof_mapper.py +3 -3
  48. warp/{fem → _src/fem}/space/function_space.py +14 -13
  49. warp/{fem → _src/fem}/space/grid_2d_function_space.py +4 -7
  50. warp/{fem → _src/fem}/space/grid_3d_function_space.py +4 -4
  51. warp/{fem → _src/fem}/space/hexmesh_function_space.py +4 -10
  52. warp/{fem → _src/fem}/space/nanogrid_function_space.py +3 -9
  53. warp/{fem → _src/fem}/space/partition.py +117 -60
  54. warp/{fem → _src/fem}/space/quadmesh_function_space.py +4 -10
  55. warp/{fem → _src/fem}/space/restriction.py +66 -33
  56. warp/_src/fem/space/shape/__init__.py +152 -0
  57. warp/{fem → _src/fem}/space/shape/cube_shape_function.py +9 -9
  58. warp/{fem → _src/fem}/space/shape/shape_function.py +8 -9
  59. warp/{fem → _src/fem}/space/shape/square_shape_function.py +6 -6
  60. warp/{fem → _src/fem}/space/shape/tet_shape_function.py +3 -3
  61. warp/{fem → _src/fem}/space/shape/triangle_shape_function.py +3 -3
  62. warp/{fem → _src/fem}/space/tetmesh_function_space.py +3 -9
  63. warp/_src/fem/space/topology.py +459 -0
  64. warp/{fem → _src/fem}/space/trimesh_function_space.py +3 -9
  65. warp/_src/fem/types.py +112 -0
  66. warp/_src/fem/utils.py +486 -0
  67. warp/_src/jax.py +186 -0
  68. warp/_src/jax_experimental/__init__.py +14 -0
  69. warp/_src/jax_experimental/custom_call.py +387 -0
  70. warp/_src/jax_experimental/ffi.py +1284 -0
  71. warp/_src/jax_experimental/xla_ffi.py +656 -0
  72. warp/_src/marching_cubes.py +708 -0
  73. warp/_src/math.py +414 -0
  74. warp/_src/optim/__init__.py +14 -0
  75. warp/_src/optim/adam.py +163 -0
  76. warp/_src/optim/linear.py +1606 -0
  77. warp/_src/optim/sgd.py +112 -0
  78. warp/_src/paddle.py +406 -0
  79. warp/_src/render/__init__.py +14 -0
  80. warp/_src/render/imgui_manager.py +289 -0
  81. warp/_src/render/render_opengl.py +3636 -0
  82. warp/_src/render/render_usd.py +937 -0
  83. warp/_src/render/utils.py +160 -0
  84. warp/_src/sparse.py +2716 -0
  85. warp/_src/tape.py +1206 -0
  86. warp/{thirdparty → _src/thirdparty}/unittest_parallel.py +9 -2
  87. warp/_src/torch.py +391 -0
  88. warp/_src/types.py +5870 -0
  89. warp/_src/utils.py +1693 -0
  90. warp/autograd.py +12 -1054
  91. warp/bin/warp-clang.so +0 -0
  92. warp/bin/warp.so +0 -0
  93. warp/build.py +8 -588
  94. warp/build_dll.py +6 -721
  95. warp/codegen.py +6 -4251
  96. warp/constants.py +6 -39
  97. warp/context.py +12 -8062
  98. warp/dlpack.py +6 -444
  99. warp/examples/distributed/example_jacobi_mpi.py +4 -5
  100. warp/examples/fem/example_adaptive_grid.py +1 -1
  101. warp/examples/fem/example_apic_fluid.py +1 -1
  102. warp/examples/fem/example_burgers.py +8 -8
  103. warp/examples/fem/example_diffusion.py +1 -1
  104. warp/examples/fem/example_distortion_energy.py +1 -1
  105. warp/examples/fem/example_mixed_elasticity.py +2 -2
  106. warp/examples/fem/example_navier_stokes.py +1 -1
  107. warp/examples/fem/example_nonconforming_contact.py +7 -7
  108. warp/examples/fem/example_stokes.py +1 -1
  109. warp/examples/fem/example_stokes_transfer.py +1 -1
  110. warp/examples/fem/utils.py +2 -2
  111. warp/examples/interop/example_jax_callable.py +1 -1
  112. warp/examples/interop/example_jax_ffi_callback.py +1 -1
  113. warp/examples/interop/example_jax_kernel.py +1 -1
  114. warp/examples/tile/example_tile_mcgp.py +191 -0
  115. warp/fabric.py +6 -337
  116. warp/fem/__init__.py +159 -97
  117. warp/fem/adaptivity.py +7 -489
  118. warp/fem/cache.py +9 -648
  119. warp/fem/dirichlet.py +6 -184
  120. warp/fem/field/__init__.py +8 -109
  121. warp/fem/field/field.py +7 -652
  122. warp/fem/geometry/__init__.py +7 -18
  123. warp/fem/geometry/closest_point.py +11 -77
  124. warp/fem/linalg.py +18 -366
  125. warp/fem/operator.py +11 -369
  126. warp/fem/polynomial.py +9 -209
  127. warp/fem/space/__init__.py +5 -211
  128. warp/fem/space/basis_space.py +6 -662
  129. warp/fem/space/shape/__init__.py +41 -118
  130. warp/fem/space/topology.py +6 -437
  131. warp/fem/types.py +6 -81
  132. warp/fem/utils.py +11 -444
  133. warp/jax.py +8 -165
  134. warp/jax_experimental/__init__.py +14 -1
  135. warp/jax_experimental/custom_call.py +8 -365
  136. warp/jax_experimental/ffi.py +17 -873
  137. warp/jax_experimental/xla_ffi.py +5 -605
  138. warp/marching_cubes.py +5 -689
  139. warp/math.py +16 -393
  140. warp/native/array.h +385 -37
  141. warp/native/builtin.h +314 -37
  142. warp/native/bvh.cpp +43 -9
  143. warp/native/bvh.cu +62 -27
  144. warp/native/bvh.h +310 -309
  145. warp/native/clang/clang.cpp +102 -97
  146. warp/native/coloring.cpp +0 -1
  147. warp/native/crt.h +208 -0
  148. warp/native/exports.h +156 -0
  149. warp/native/hashgrid.cu +2 -0
  150. warp/native/intersect.h +24 -1
  151. warp/native/intersect_tri.h +44 -35
  152. warp/native/mat.h +1456 -276
  153. warp/native/mesh.cpp +4 -4
  154. warp/native/mesh.cu +4 -2
  155. warp/native/mesh.h +176 -61
  156. warp/native/quat.h +0 -52
  157. warp/native/scan.cu +2 -0
  158. warp/native/sparse.cu +7 -3
  159. warp/native/spatial.h +12 -0
  160. warp/native/tile.h +681 -89
  161. warp/native/tile_radix_sort.h +1 -1
  162. warp/native/tile_reduce.h +394 -46
  163. warp/native/tile_scan.h +4 -4
  164. warp/native/vec.h +469 -0
  165. warp/native/version.h +23 -0
  166. warp/native/volume.cpp +1 -1
  167. warp/native/volume.cu +1 -0
  168. warp/native/volume.h +1 -1
  169. warp/native/volume_builder.cu +2 -0
  170. warp/native/warp.cpp +57 -29
  171. warp/native/warp.cu +253 -171
  172. warp/native/warp.h +11 -8
  173. warp/optim/__init__.py +6 -3
  174. warp/optim/adam.py +6 -145
  175. warp/optim/linear.py +14 -1585
  176. warp/optim/sgd.py +6 -94
  177. warp/paddle.py +6 -388
  178. warp/render/__init__.py +8 -4
  179. warp/render/imgui_manager.py +7 -267
  180. warp/render/render_opengl.py +6 -3618
  181. warp/render/render_usd.py +6 -919
  182. warp/render/utils.py +6 -142
  183. warp/sparse.py +37 -2563
  184. warp/tape.py +6 -1188
  185. warp/tests/__main__.py +1 -1
  186. warp/tests/cuda/test_async.py +4 -4
  187. warp/tests/cuda/test_conditional_captures.py +1 -1
  188. warp/tests/cuda/test_multigpu.py +1 -1
  189. warp/tests/cuda/test_streams.py +58 -1
  190. warp/tests/geometry/test_bvh.py +157 -22
  191. warp/tests/geometry/test_marching_cubes.py +0 -1
  192. warp/tests/geometry/test_mesh.py +5 -3
  193. warp/tests/geometry/test_mesh_query_aabb.py +5 -12
  194. warp/tests/geometry/test_mesh_query_point.py +5 -2
  195. warp/tests/geometry/test_mesh_query_ray.py +15 -3
  196. warp/tests/geometry/test_volume_write.py +5 -5
  197. warp/tests/interop/test_dlpack.py +14 -14
  198. warp/tests/interop/test_jax.py +772 -49
  199. warp/tests/interop/test_paddle.py +1 -1
  200. warp/tests/test_adam.py +0 -1
  201. warp/tests/test_arithmetic.py +9 -9
  202. warp/tests/test_array.py +527 -100
  203. warp/tests/test_array_reduce.py +3 -3
  204. warp/tests/test_atomic.py +12 -8
  205. warp/tests/test_atomic_bitwise.py +209 -0
  206. warp/tests/test_atomic_cas.py +4 -4
  207. warp/tests/test_bool.py +2 -2
  208. warp/tests/test_builtins_resolution.py +5 -571
  209. warp/tests/test_codegen.py +33 -14
  210. warp/tests/test_conditional.py +1 -1
  211. warp/tests/test_context.py +6 -6
  212. warp/tests/test_copy.py +242 -161
  213. warp/tests/test_ctypes.py +3 -3
  214. warp/tests/test_devices.py +24 -2
  215. warp/tests/test_examples.py +16 -84
  216. warp/tests/test_fabricarray.py +35 -35
  217. warp/tests/test_fast_math.py +0 -2
  218. warp/tests/test_fem.py +56 -10
  219. warp/tests/test_fixedarray.py +3 -3
  220. warp/tests/test_func.py +8 -5
  221. warp/tests/test_generics.py +1 -1
  222. warp/tests/test_indexedarray.py +24 -24
  223. warp/tests/test_intersect.py +39 -9
  224. warp/tests/test_large.py +1 -1
  225. warp/tests/test_lerp.py +3 -1
  226. warp/tests/test_linear_solvers.py +1 -1
  227. warp/tests/test_map.py +35 -4
  228. warp/tests/test_mat.py +52 -62
  229. warp/tests/test_mat_constructors.py +4 -5
  230. warp/tests/test_mat_lite.py +1 -1
  231. warp/tests/test_mat_scalar_ops.py +121 -121
  232. warp/tests/test_math.py +34 -0
  233. warp/tests/test_module_aot.py +4 -4
  234. warp/tests/test_modules_lite.py +28 -2
  235. warp/tests/test_print.py +11 -11
  236. warp/tests/test_quat.py +93 -58
  237. warp/tests/test_runlength_encode.py +1 -1
  238. warp/tests/test_scalar_ops.py +38 -10
  239. warp/tests/test_smoothstep.py +1 -1
  240. warp/tests/test_sparse.py +126 -15
  241. warp/tests/test_spatial.py +105 -87
  242. warp/tests/test_special_values.py +6 -6
  243. warp/tests/test_static.py +7 -7
  244. warp/tests/test_struct.py +13 -2
  245. warp/tests/test_triangle_closest_point.py +48 -1
  246. warp/tests/test_types.py +27 -15
  247. warp/tests/test_utils.py +52 -52
  248. warp/tests/test_vec.py +29 -29
  249. warp/tests/test_vec_constructors.py +5 -5
  250. warp/tests/test_vec_scalar_ops.py +97 -97
  251. warp/tests/test_version.py +75 -0
  252. warp/tests/tile/test_tile.py +178 -0
  253. warp/tests/tile/test_tile_atomic_bitwise.py +403 -0
  254. warp/tests/tile/test_tile_cholesky.py +7 -4
  255. warp/tests/tile/test_tile_load.py +26 -2
  256. warp/tests/tile/test_tile_mathdx.py +3 -3
  257. warp/tests/tile/test_tile_matmul.py +1 -1
  258. warp/tests/tile/test_tile_mlp.py +2 -4
  259. warp/tests/tile/test_tile_reduce.py +214 -13
  260. warp/tests/unittest_suites.py +6 -14
  261. warp/tests/unittest_utils.py +10 -9
  262. warp/tests/walkthrough_debug.py +3 -1
  263. warp/torch.py +6 -373
  264. warp/types.py +29 -5764
  265. warp/utils.py +10 -1659
  266. {warp_lang-1.9.1.dist-info → warp_lang-1.10.0rc2.dist-info}/METADATA +46 -99
  267. warp_lang-1.10.0rc2.dist-info/RECORD +468 -0
  268. warp_lang-1.10.0rc2.dist-info/licenses/licenses/Gaia-LICENSE.txt +6 -0
  269. warp_lang-1.10.0rc2.dist-info/licenses/licenses/appdirs-LICENSE.txt +22 -0
  270. warp_lang-1.10.0rc2.dist-info/licenses/licenses/asset_pixel_jpg-LICENSE.txt +3 -0
  271. warp_lang-1.10.0rc2.dist-info/licenses/licenses/cuda-LICENSE.txt +1582 -0
  272. warp_lang-1.10.0rc2.dist-info/licenses/licenses/dlpack-LICENSE.txt +201 -0
  273. warp_lang-1.10.0rc2.dist-info/licenses/licenses/fp16-LICENSE.txt +28 -0
  274. warp_lang-1.10.0rc2.dist-info/licenses/licenses/libmathdx-LICENSE.txt +220 -0
  275. warp_lang-1.10.0rc2.dist-info/licenses/licenses/llvm-LICENSE.txt +279 -0
  276. warp_lang-1.10.0rc2.dist-info/licenses/licenses/moller-LICENSE.txt +16 -0
  277. warp_lang-1.10.0rc2.dist-info/licenses/licenses/nanovdb-LICENSE.txt +2 -0
  278. warp_lang-1.10.0rc2.dist-info/licenses/licenses/nvrtc-LICENSE.txt +1592 -0
  279. warp_lang-1.10.0rc2.dist-info/licenses/licenses/svd-LICENSE.txt +23 -0
  280. warp_lang-1.10.0rc2.dist-info/licenses/licenses/unittest_parallel-LICENSE.txt +21 -0
  281. warp_lang-1.10.0rc2.dist-info/licenses/licenses/usd-LICENSE.txt +213 -0
  282. warp_lang-1.10.0rc2.dist-info/licenses/licenses/windingnumber-LICENSE.txt +21 -0
  283. warp/examples/assets/cartpole.urdf +0 -110
  284. warp/examples/assets/crazyflie.usd +0 -0
  285. warp/examples/assets/nv_ant.xml +0 -92
  286. warp/examples/assets/nv_humanoid.xml +0 -183
  287. warp/examples/assets/quadruped.urdf +0 -268
  288. warp/examples/optim/example_bounce.py +0 -266
  289. warp/examples/optim/example_cloth_throw.py +0 -228
  290. warp/examples/optim/example_drone.py +0 -870
  291. warp/examples/optim/example_inverse_kinematics.py +0 -182
  292. warp/examples/optim/example_inverse_kinematics_torch.py +0 -191
  293. warp/examples/optim/example_softbody_properties.py +0 -400
  294. warp/examples/optim/example_spring_cage.py +0 -245
  295. warp/examples/optim/example_trajectory.py +0 -227
  296. warp/examples/sim/example_cartpole.py +0 -143
  297. warp/examples/sim/example_cloth.py +0 -225
  298. warp/examples/sim/example_cloth_self_contact.py +0 -316
  299. warp/examples/sim/example_granular.py +0 -130
  300. warp/examples/sim/example_granular_collision_sdf.py +0 -202
  301. warp/examples/sim/example_jacobian_ik.py +0 -244
  302. warp/examples/sim/example_particle_chain.py +0 -124
  303. warp/examples/sim/example_quadruped.py +0 -203
  304. warp/examples/sim/example_rigid_chain.py +0 -203
  305. warp/examples/sim/example_rigid_contact.py +0 -195
  306. warp/examples/sim/example_rigid_force.py +0 -133
  307. warp/examples/sim/example_rigid_gyroscopic.py +0 -115
  308. warp/examples/sim/example_rigid_soft_contact.py +0 -140
  309. warp/examples/sim/example_soft_body.py +0 -196
  310. warp/examples/tile/example_tile_walker.py +0 -327
  311. warp/sim/__init__.py +0 -74
  312. warp/sim/articulation.py +0 -793
  313. warp/sim/collide.py +0 -2570
  314. warp/sim/graph_coloring.py +0 -307
  315. warp/sim/import_mjcf.py +0 -791
  316. warp/sim/import_snu.py +0 -227
  317. warp/sim/import_urdf.py +0 -579
  318. warp/sim/import_usd.py +0 -898
  319. warp/sim/inertia.py +0 -357
  320. warp/sim/integrator.py +0 -245
  321. warp/sim/integrator_euler.py +0 -2000
  322. warp/sim/integrator_featherstone.py +0 -2101
  323. warp/sim/integrator_vbd.py +0 -2487
  324. warp/sim/integrator_xpbd.py +0 -3295
  325. warp/sim/model.py +0 -4821
  326. warp/sim/particles.py +0 -121
  327. warp/sim/render.py +0 -431
  328. warp/sim/utils.py +0 -431
  329. warp/tests/sim/disabled_kinematics.py +0 -244
  330. warp/tests/sim/test_cloth.py +0 -863
  331. warp/tests/sim/test_collision.py +0 -743
  332. warp/tests/sim/test_coloring.py +0 -347
  333. warp/tests/sim/test_inertia.py +0 -161
  334. warp/tests/sim/test_model.py +0 -226
  335. warp/tests/sim/test_sim_grad.py +0 -287
  336. warp/tests/sim/test_sim_grad_bounce_linear.py +0 -212
  337. warp/tests/sim/test_sim_kinematics.py +0 -98
  338. warp/thirdparty/__init__.py +0 -0
  339. warp_lang-1.9.1.dist-info/RECORD +0 -456
  340. /warp/{fem → _src/fem}/quadrature/__init__.py +0 -0
  341. /warp/{tests/sim → _src/thirdparty}/__init__.py +0 -0
  342. /warp/{thirdparty → _src/thirdparty}/appdirs.py +0 -0
  343. /warp/{thirdparty → _src/thirdparty}/dlpack.py +0 -0
  344. {warp_lang-1.9.1.dist-info → warp_lang-1.10.0rc2.dist-info}/WHEEL +0 -0
  345. {warp_lang-1.9.1.dist-info → warp_lang-1.10.0rc2.dist-info}/licenses/LICENSE.md +0 -0
  346. {warp_lang-1.9.1.dist-info → warp_lang-1.10.0rc2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,3636 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from __future__ import annotations
17
+
18
+ import ctypes
19
+ import sys
20
+ import time
21
+ from collections import defaultdict
22
+ from typing import List, Union
23
+
24
+ import numpy as np
25
+
26
+ import warp as wp
27
+
28
+ from .utils import tab10_color_map
29
+
30
+ Mat44 = Union[List[float], List[List[float]], np.ndarray]
31
+
32
+
33
+ wp.set_module_options({"enable_backward": False})
34
+
35
+ shape_vertex_shader = """
36
+ #version 330 core
37
+ layout (location = 0) in vec3 aPos;
38
+ layout (location = 1) in vec3 aNormal;
39
+ layout (location = 2) in vec2 aTexCoord;
40
+
41
+ // column vectors of the instance transform matrix
42
+ layout (location = 3) in vec4 aInstanceTransform0;
43
+ layout (location = 4) in vec4 aInstanceTransform1;
44
+ layout (location = 5) in vec4 aInstanceTransform2;
45
+ layout (location = 6) in vec4 aInstanceTransform3;
46
+
47
+ // colors to use for the checkerboard pattern
48
+ layout (location = 7) in vec3 aObjectColor1;
49
+ layout (location = 8) in vec3 aObjectColor2;
50
+
51
+ uniform mat4 view;
52
+ uniform mat4 model;
53
+ uniform mat4 projection;
54
+
55
+ out vec3 Normal;
56
+ out vec3 FragPos;
57
+ out vec2 TexCoord;
58
+ out vec3 ObjectColor1;
59
+ out vec3 ObjectColor2;
60
+
61
+ void main()
62
+ {
63
+ mat4 transform = model * mat4(aInstanceTransform0, aInstanceTransform1, aInstanceTransform2, aInstanceTransform3);
64
+ vec4 worldPos = transform * vec4(aPos, 1.0);
65
+ gl_Position = projection * view * worldPos;
66
+ FragPos = vec3(worldPos);
67
+ Normal = mat3(transpose(inverse(transform))) * aNormal;
68
+ TexCoord = aTexCoord;
69
+ ObjectColor1 = aObjectColor1;
70
+ ObjectColor2 = aObjectColor2;
71
+ }
72
+ """
73
+
74
+ shape_fragment_shader = """
75
+ #version 330 core
76
+ out vec4 FragColor;
77
+
78
+ in vec3 Normal;
79
+ in vec3 FragPos;
80
+ in vec2 TexCoord;
81
+ in vec3 ObjectColor1;
82
+ in vec3 ObjectColor2;
83
+
84
+ uniform vec3 viewPos;
85
+ uniform vec3 lightColor;
86
+ uniform vec3 sunDirection;
87
+
88
+ void main()
89
+ {
90
+ float ambientStrength = 0.3;
91
+ vec3 ambient = ambientStrength * lightColor;
92
+ vec3 norm = normalize(Normal);
93
+
94
+ float diff = max(dot(norm, sunDirection), 0.0);
95
+ vec3 diffuse = diff * lightColor;
96
+
97
+ vec3 lightDir2 = normalize(vec3(1.0, 0.3, -0.3));
98
+ diff = max(dot(norm, lightDir2), 0.0);
99
+ diffuse += diff * lightColor * 0.3;
100
+
101
+ float specularStrength = 0.5;
102
+ vec3 viewDir = normalize(viewPos - FragPos);
103
+
104
+ vec3 reflectDir = reflect(-sunDirection, norm);
105
+ float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
106
+ vec3 specular = specularStrength * spec * lightColor;
107
+
108
+ reflectDir = reflect(-lightDir2, norm);
109
+ spec = pow(max(dot(viewDir, reflectDir), 0.0), 64);
110
+ specular += specularStrength * spec * lightColor * 0.3;
111
+
112
+ // checkerboard pattern
113
+ float u = TexCoord.x;
114
+ float v = TexCoord.y;
115
+ // blend the checkerboard pattern dependent on the gradient of the texture coordinates
116
+ // to void Moire patterns
117
+ vec2 grad = abs(dFdx(TexCoord)) + abs(dFdy(TexCoord));
118
+ float blendRange = 1.5;
119
+ float blendFactor = max(grad.x, grad.y) * blendRange;
120
+ float scale = 2.0;
121
+ float checker = mod(floor(u * scale) + floor(v * scale), 2.0);
122
+ checker = mix(checker, 0.5, smoothstep(0.0, 1.0, blendFactor));
123
+ vec3 checkerColor = mix(ObjectColor1, ObjectColor2, checker);
124
+
125
+ vec3 result = (ambient + diffuse + specular) * checkerColor;
126
+ FragColor = vec4(result, 1.0);
127
+ }
128
+ """
129
+
130
+ grid_vertex_shader = """
131
+ #version 330 core
132
+
133
+ uniform mat4 view;
134
+ uniform mat4 model;
135
+ uniform mat4 projection;
136
+
137
+ in vec3 position;
138
+
139
+ void main() {
140
+ gl_Position = projection * view * model * vec4(position, 1.0);
141
+ }
142
+ """
143
+
144
+ # Fragment shader source code
145
+ grid_fragment_shader = """
146
+ #version 330 core
147
+
148
+ out vec4 outColor;
149
+
150
+ void main() {
151
+ outColor = vec4(0.5, 0.5, 0.5, 1.0);
152
+ }
153
+ """
154
+
155
+ sky_vertex_shader = """
156
+ #version 330 core
157
+
158
+ layout (location = 0) in vec3 aPos;
159
+ layout (location = 1) in vec3 aNormal;
160
+ layout (location = 2) in vec2 aTexCoord;
161
+
162
+ uniform mat4 view;
163
+ uniform mat4 inv_model;
164
+ uniform mat4 projection;
165
+ uniform vec3 viewPos;
166
+
167
+ out vec3 FragPos;
168
+ out vec2 TexCoord;
169
+
170
+ void main()
171
+ {
172
+ vec4 worldPos = vec4(aPos + viewPos, 1.0);
173
+ gl_Position = projection * view * inv_model * worldPos;
174
+
175
+ FragPos = vec3(worldPos);
176
+ TexCoord = aTexCoord;
177
+ }
178
+ """
179
+
180
+ sky_fragment_shader = """
181
+ #version 330 core
182
+
183
+ out vec4 FragColor;
184
+
185
+ in vec3 FragPos;
186
+ in vec2 TexCoord;
187
+
188
+ uniform vec3 color1;
189
+ uniform vec3 color2;
190
+ uniform float farPlane;
191
+
192
+ uniform vec3 sunDirection;
193
+
194
+ void main()
195
+ {
196
+ float y = tanh(FragPos.y/farPlane*10.0)*0.5+0.5;
197
+ float height = sqrt(1.0-y);
198
+
199
+ float s = pow(0.5, 1.0 / 10.0);
200
+ s = 1.0 - clamp(s, 0.75, 1.0);
201
+
202
+ vec3 haze = mix(vec3(1.0), color2 * 1.3, s);
203
+ vec3 sky = mix(color1, haze, height / 1.3);
204
+
205
+ float diff = max(dot(sunDirection, normalize(FragPos)), 0.0);
206
+ vec3 sun = pow(diff, 32) * vec3(1.0, 0.8, 0.6) * 0.5;
207
+
208
+ FragColor = vec4(sky + sun, 1.0);
209
+ }
210
+ """
211
+
212
+ frame_vertex_shader = """
213
+ #version 330 core
214
+ layout (location = 0) in vec3 aPos;
215
+ layout (location = 1) in vec2 aTexCoord;
216
+
217
+ out vec2 TexCoord;
218
+
219
+ void main() {
220
+ gl_Position = vec4(aPos, 1.0);
221
+ TexCoord = aTexCoord;
222
+ }
223
+ """
224
+
225
+ frame_fragment_shader = """
226
+ #version 330 core
227
+ in vec2 TexCoord;
228
+
229
+ out vec4 FragColor;
230
+
231
+ uniform sampler2D textureSampler;
232
+
233
+ void main() {
234
+ FragColor = texture(textureSampler, TexCoord);
235
+ }
236
+ """
237
+
238
+ frame_depth_fragment_shader = """
239
+ #version 330 core
240
+ in vec2 TexCoord;
241
+
242
+ out vec4 FragColor;
243
+
244
+ uniform sampler2D textureSampler;
245
+
246
+ vec3 bourkeColorMap(float v) {
247
+ vec3 c = vec3(1.0, 1.0, 1.0);
248
+
249
+ v = clamp(v, 0.0, 1.0); // Ensures v is between 0 and 1
250
+
251
+ if (v < 0.25) {
252
+ c.r = 0.0;
253
+ c.g = 4.0 * v;
254
+ } else if (v < 0.5) {
255
+ c.r = 0.0;
256
+ c.b = 1.0 + 4.0 * (0.25 - v);
257
+ } else if (v < 0.75) {
258
+ c.r = 4.0 * (v - 0.5);
259
+ c.b = 0.0;
260
+ } else {
261
+ c.g = 1.0 + 4.0 * (0.75 - v);
262
+ c.b = 0.0;
263
+ }
264
+
265
+ return c;
266
+ }
267
+
268
+ void main() {
269
+ float depth = texture(textureSampler, TexCoord).r;
270
+ FragColor = vec4(bourkeColorMap(sqrt(1.0 - depth)), 1.0);
271
+ }
272
+ """
273
+
274
+
275
+ @wp.kernel
276
+ def update_vbo_transforms(
277
+ instance_id: wp.array(dtype=int),
278
+ instance_body: wp.array(dtype=int),
279
+ instance_transforms: wp.array(dtype=wp.transform),
280
+ instance_scalings: wp.array(dtype=wp.vec3),
281
+ body_q: wp.array(dtype=wp.transform),
282
+ # outputs
283
+ vbo_transforms: wp.array(dtype=wp.mat44),
284
+ ):
285
+ tid = wp.tid()
286
+ i = instance_id[tid]
287
+ X_ws = instance_transforms[i]
288
+ if instance_body:
289
+ body = instance_body[i]
290
+ if body >= 0:
291
+ if body_q:
292
+ X_ws = body_q[body] * X_ws
293
+ else:
294
+ return
295
+ p = wp.transform_get_translation(X_ws)
296
+ q = wp.transform_get_rotation(X_ws)
297
+ s = instance_scalings[i]
298
+ rot = wp.quat_to_matrix(q)
299
+ # transposed definition
300
+ vbo_transforms[tid] = wp.mat44(
301
+ rot[0, 0] * s[0],
302
+ rot[1, 0] * s[0],
303
+ rot[2, 0] * s[0],
304
+ 0.0,
305
+ rot[0, 1] * s[1],
306
+ rot[1, 1] * s[1],
307
+ rot[2, 1] * s[1],
308
+ 0.0,
309
+ rot[0, 2] * s[2],
310
+ rot[1, 2] * s[2],
311
+ rot[2, 2] * s[2],
312
+ 0.0,
313
+ p[0],
314
+ p[1],
315
+ p[2],
316
+ 1.0,
317
+ )
318
+
319
+
320
+ @wp.kernel
321
+ def update_vbo_vertices(
322
+ points: wp.array(dtype=wp.vec3),
323
+ # outputs
324
+ vbo_vertices: wp.array(dtype=float, ndim=2),
325
+ ):
326
+ tid = wp.tid()
327
+ p = points[tid]
328
+ vbo_vertices[tid, 0] = p[0]
329
+ vbo_vertices[tid, 1] = p[1]
330
+ vbo_vertices[tid, 2] = p[2]
331
+
332
+
333
+ @wp.kernel
334
+ def update_points_positions(
335
+ instance_positions: wp.array(dtype=wp.vec3),
336
+ instance_scalings: wp.array(dtype=wp.vec3),
337
+ # outputs
338
+ vbo_transforms: wp.array(dtype=wp.mat44),
339
+ ):
340
+ tid = wp.tid()
341
+ p = instance_positions[tid]
342
+ s = wp.vec3(1.0)
343
+ if instance_scalings:
344
+ s = instance_scalings[tid]
345
+ # transposed definition
346
+ # fmt: off
347
+ vbo_transforms[tid] = wp.mat44(
348
+ s[0], 0.0, 0.0, 0.0,
349
+ 0.0, s[1], 0.0, 0.0,
350
+ 0.0, 0.0, s[2], 0.0,
351
+ p[0], p[1], p[2], 1.0)
352
+ # fmt: on
353
+
354
+
355
+ @wp.kernel
356
+ def update_line_transforms(
357
+ lines: wp.array(dtype=wp.vec3, ndim=2),
358
+ # outputs
359
+ vbo_transforms: wp.array(dtype=wp.mat44),
360
+ ):
361
+ tid = wp.tid()
362
+ p0 = lines[tid, 0]
363
+ p1 = lines[tid, 1]
364
+ p = 0.5 * (p0 + p1)
365
+ d = p1 - p0
366
+ s = wp.length(d)
367
+ axis = wp.normalize(d)
368
+ y_up = wp.vec3(0.0, 1.0, 0.0)
369
+ angle = wp.acos(wp.dot(axis, y_up))
370
+ axis = wp.normalize(wp.cross(axis, y_up))
371
+ q = wp.quat_from_axis_angle(axis, -angle)
372
+ rot = wp.quat_to_matrix(q)
373
+ # transposed definition
374
+ # fmt: off
375
+ vbo_transforms[tid] = wp.mat44(
376
+ rot[0, 0], rot[1, 0], rot[2, 0], 0.0,
377
+ s * rot[0, 1], s * rot[1, 1], s * rot[2, 1], 0.0,
378
+ rot[0, 2], rot[1, 2], rot[2, 2], 0.0,
379
+ p[0], p[1], p[2], 1.0,
380
+ )
381
+ # fmt: on
382
+
383
+
384
+ @wp.kernel
385
+ def compute_gfx_vertices(
386
+ indices: wp.array(dtype=int, ndim=2),
387
+ vertices: wp.array(dtype=wp.vec3, ndim=1),
388
+ scale: wp.vec3,
389
+ # outputs
390
+ gfx_vertices: wp.array(dtype=float, ndim=2),
391
+ ):
392
+ tid = wp.tid()
393
+ v0 = wp.cw_mul(vertices[indices[tid, 0]], scale)
394
+ v1 = wp.cw_mul(vertices[indices[tid, 1]], scale)
395
+ v2 = wp.cw_mul(vertices[indices[tid, 2]], scale)
396
+ i = tid * 3
397
+ j = i + 1
398
+ k = i + 2
399
+ gfx_vertices[i, 0] = v0[0]
400
+ gfx_vertices[i, 1] = v0[1]
401
+ gfx_vertices[i, 2] = v0[2]
402
+ gfx_vertices[j, 0] = v1[0]
403
+ gfx_vertices[j, 1] = v1[1]
404
+ gfx_vertices[j, 2] = v1[2]
405
+ gfx_vertices[k, 0] = v2[0]
406
+ gfx_vertices[k, 1] = v2[1]
407
+ gfx_vertices[k, 2] = v2[2]
408
+ n = wp.normalize(wp.cross(v1 - v0, v2 - v0))
409
+ gfx_vertices[i, 3] = n[0]
410
+ gfx_vertices[i, 4] = n[1]
411
+ gfx_vertices[i, 5] = n[2]
412
+ gfx_vertices[j, 3] = n[0]
413
+ gfx_vertices[j, 4] = n[1]
414
+ gfx_vertices[j, 5] = n[2]
415
+ gfx_vertices[k, 3] = n[0]
416
+ gfx_vertices[k, 4] = n[1]
417
+ gfx_vertices[k, 5] = n[2]
418
+
419
+
420
+ @wp.kernel
421
+ def compute_average_normals(
422
+ indices: wp.array(dtype=int, ndim=2),
423
+ vertices: wp.array(dtype=wp.vec3),
424
+ scale: wp.vec3,
425
+ # outputs
426
+ normals: wp.array(dtype=wp.vec3),
427
+ faces_per_vertex: wp.array(dtype=int),
428
+ ):
429
+ tid = wp.tid()
430
+ i = indices[tid, 0]
431
+ j = indices[tid, 1]
432
+ k = indices[tid, 2]
433
+ v0 = wp.cw_mul(vertices[i], scale)
434
+ v1 = wp.cw_mul(vertices[j], scale)
435
+ v2 = wp.cw_mul(vertices[k], scale)
436
+ n = wp.normalize(wp.cross(v1 - v0, v2 - v0))
437
+ wp.atomic_add(normals, i, n)
438
+ wp.atomic_add(faces_per_vertex, i, 1)
439
+ wp.atomic_add(normals, j, n)
440
+ wp.atomic_add(faces_per_vertex, j, 1)
441
+ wp.atomic_add(normals, k, n)
442
+ wp.atomic_add(faces_per_vertex, k, 1)
443
+
444
+
445
+ @wp.kernel
446
+ def assemble_gfx_vertices(
447
+ vertices: wp.array(dtype=wp.vec3, ndim=1),
448
+ normals: wp.array(dtype=wp.vec3),
449
+ faces_per_vertex: wp.array(dtype=int),
450
+ scale: wp.vec3,
451
+ # outputs
452
+ gfx_vertices: wp.array(dtype=float, ndim=2),
453
+ ):
454
+ tid = wp.tid()
455
+ v = vertices[tid]
456
+ n = normals[tid] / float(faces_per_vertex[tid])
457
+ gfx_vertices[tid, 0] = v[0] * scale[0]
458
+ gfx_vertices[tid, 1] = v[1] * scale[1]
459
+ gfx_vertices[tid, 2] = v[2] * scale[2]
460
+ gfx_vertices[tid, 3] = n[0]
461
+ gfx_vertices[tid, 4] = n[1]
462
+ gfx_vertices[tid, 5] = n[2]
463
+
464
+
465
+ @wp.kernel
466
+ def copy_rgb_frame(
467
+ input_img: wp.array(dtype=wp.uint8),
468
+ width: int,
469
+ height: int,
470
+ # outputs
471
+ output_img: wp.array(dtype=float, ndim=3),
472
+ ):
473
+ w, v = wp.tid()
474
+ pixel = v * width + w
475
+ pixel *= 3
476
+ r = float(input_img[pixel + 0])
477
+ g = float(input_img[pixel + 1])
478
+ b = float(input_img[pixel + 2])
479
+ # flip vertically (OpenGL coordinates start at bottom)
480
+ v = height - v - 1
481
+ output_img[v, w, 0] = r / 255.0
482
+ output_img[v, w, 1] = g / 255.0
483
+ output_img[v, w, 2] = b / 255.0
484
+
485
+
486
+ @wp.kernel
487
+ def copy_rgb_frame_uint8(
488
+ input_img: wp.array(dtype=wp.uint8),
489
+ width: int,
490
+ height: int,
491
+ # outputs
492
+ output_img: wp.array(dtype=wp.uint8, ndim=3),
493
+ ):
494
+ w, v = wp.tid()
495
+ pixel = v * width + w
496
+ pixel *= 3
497
+ # flip vertically (OpenGL coordinates start at bottom)
498
+ v = height - v - 1
499
+ output_img[v, w, 0] = input_img[pixel + 0]
500
+ output_img[v, w, 1] = input_img[pixel + 1]
501
+ output_img[v, w, 2] = input_img[pixel + 2]
502
+
503
+
504
+ @wp.kernel
505
+ def copy_depth_frame(
506
+ input_img: wp.array(dtype=wp.float32),
507
+ width: int,
508
+ height: int,
509
+ near: float,
510
+ far: float,
511
+ # outputs
512
+ output_img: wp.array(dtype=wp.float32, ndim=3),
513
+ ):
514
+ w, v = wp.tid()
515
+ pixel = v * width + w
516
+ # flip vertically (OpenGL coordinates start at bottom)
517
+ v = height - v - 1
518
+ d = 2.0 * input_img[pixel] - 1.0
519
+ d = 2.0 * near * far / ((far - near) * d - near - far)
520
+ output_img[v, w, 0] = -d
521
+
522
+
523
+ @wp.kernel
524
+ def copy_rgb_frame_tiles(
525
+ input_img: wp.array(dtype=wp.uint8),
526
+ positions: wp.array(dtype=int, ndim=2),
527
+ screen_width: int,
528
+ screen_height: int,
529
+ tile_height: int,
530
+ # outputs
531
+ output_img: wp.array(dtype=float, ndim=4),
532
+ ):
533
+ tile, x, y = wp.tid()
534
+ p = positions[tile]
535
+ qx = x + p[0]
536
+ qy = y + p[1]
537
+ pixel = qy * screen_width + qx
538
+ # flip vertically (OpenGL coordinates start at bottom)
539
+ y = tile_height - y - 1
540
+ if qx >= screen_width or qy >= screen_height:
541
+ output_img[tile, y, x, 0] = 0.0
542
+ output_img[tile, y, x, 1] = 0.0
543
+ output_img[tile, y, x, 2] = 0.0
544
+ return # prevent out-of-bounds access
545
+ pixel *= 3
546
+ r = float(input_img[pixel + 0])
547
+ g = float(input_img[pixel + 1])
548
+ b = float(input_img[pixel + 2])
549
+ output_img[tile, y, x, 0] = r / 255.0
550
+ output_img[tile, y, x, 1] = g / 255.0
551
+ output_img[tile, y, x, 2] = b / 255.0
552
+
553
+
554
+ @wp.kernel
555
+ def copy_rgb_frame_tiles_uint8(
556
+ input_img: wp.array(dtype=wp.uint8),
557
+ positions: wp.array(dtype=int, ndim=2),
558
+ screen_width: int,
559
+ screen_height: int,
560
+ tile_height: int,
561
+ # outputs
562
+ output_img: wp.array(dtype=wp.uint8, ndim=4),
563
+ ):
564
+ tile, x, y = wp.tid()
565
+ p = positions[tile]
566
+ qx = x + p[0]
567
+ qy = y + p[1]
568
+ pixel = qy * screen_width + qx
569
+ # flip vertically (OpenGL coordinates start at bottom)
570
+ y = tile_height - y - 1
571
+ if qx >= screen_width or qy >= screen_height:
572
+ output_img[tile, y, x, 0] = wp.uint8(0)
573
+ output_img[tile, y, x, 1] = wp.uint8(0)
574
+ output_img[tile, y, x, 2] = wp.uint8(0)
575
+ return # prevent out-of-bounds access
576
+ pixel *= 3
577
+ output_img[tile, y, x, 0] = input_img[pixel + 0]
578
+ output_img[tile, y, x, 1] = input_img[pixel + 1]
579
+ output_img[tile, y, x, 2] = input_img[pixel + 2]
580
+
581
+
582
+ @wp.kernel
583
+ def copy_depth_frame_tiles(
584
+ input_img: wp.array(dtype=wp.float32),
585
+ positions: wp.array(dtype=int, ndim=2),
586
+ screen_width: int,
587
+ screen_height: int,
588
+ tile_height: int,
589
+ near: float,
590
+ far: float,
591
+ # outputs
592
+ output_img: wp.array(dtype=wp.float32, ndim=4),
593
+ ):
594
+ tile, x, y = wp.tid()
595
+ p = positions[tile]
596
+ qx = x + p[0]
597
+ qy = y + p[1]
598
+ pixel = qy * screen_width + qx
599
+ # flip vertically (OpenGL coordinates start at bottom)
600
+ y = tile_height - y - 1
601
+ if qx >= screen_width or qy >= screen_height:
602
+ output_img[tile, y, x, 0] = far
603
+ return # prevent out-of-bounds access
604
+ d = 2.0 * input_img[pixel] - 1.0
605
+ d = 2.0 * near * far / ((far - near) * d - near - far)
606
+ output_img[tile, y, x, 0] = -d
607
+
608
+
609
+ @wp.kernel
610
+ def copy_rgb_frame_tile(
611
+ input_img: wp.array(dtype=wp.uint8),
612
+ offset_x: int,
613
+ offset_y: int,
614
+ screen_width: int,
615
+ screen_height: int,
616
+ tile_height: int,
617
+ # outputs
618
+ output_img: wp.array(dtype=float, ndim=4),
619
+ ):
620
+ tile, x, y = wp.tid()
621
+ qx = x + offset_x
622
+ qy = y + offset_y
623
+ pixel = qy * screen_width + qx
624
+ # flip vertically (OpenGL coordinates start at bottom)
625
+ y = tile_height - y - 1
626
+ if qx >= screen_width or qy >= screen_height:
627
+ output_img[tile, y, x, 0] = 0.0
628
+ output_img[tile, y, x, 1] = 0.0
629
+ output_img[tile, y, x, 2] = 0.0
630
+ return # prevent out-of-bounds access
631
+ pixel *= 3
632
+ r = float(input_img[pixel + 0])
633
+ g = float(input_img[pixel + 1])
634
+ b = float(input_img[pixel + 2])
635
+ output_img[tile, y, x, 0] = r / 255.0
636
+ output_img[tile, y, x, 1] = g / 255.0
637
+ output_img[tile, y, x, 2] = b / 255.0
638
+
639
+
640
+ @wp.kernel
641
+ def copy_rgb_frame_tile_uint8(
642
+ input_img: wp.array(dtype=wp.uint8),
643
+ offset_x: int,
644
+ offset_y: int,
645
+ screen_width: int,
646
+ screen_height: int,
647
+ tile_height: int,
648
+ # outputs
649
+ output_img: wp.array(dtype=wp.uint8, ndim=4),
650
+ ):
651
+ tile, x, y = wp.tid()
652
+ qx = x + offset_x
653
+ qy = y + offset_y
654
+ pixel = qy * screen_width + qx
655
+ # flip vertically (OpenGL coordinates start at bottom)
656
+ y = tile_height - y - 1
657
+ if qx >= screen_width or qy >= screen_height:
658
+ output_img[tile, y, x, 0] = wp.uint8(0)
659
+ output_img[tile, y, x, 1] = wp.uint8(0)
660
+ output_img[tile, y, x, 2] = wp.uint8(0)
661
+ return # prevent out-of-bounds access
662
+ pixel *= 3
663
+ output_img[tile, y, x, 0] = input_img[pixel + 0]
664
+ output_img[tile, y, x, 1] = input_img[pixel + 1]
665
+ output_img[tile, y, x, 2] = input_img[pixel + 2]
666
+
667
+
668
+ def check_gl_error():
669
+ from pyglet import gl
670
+
671
+ error = gl.glGetError()
672
+ if error != gl.GL_NO_ERROR:
673
+ print(f"OpenGL error: {error}")
674
+
675
+
676
+ class ShapeInstancer:
677
+ """
678
+ Handles instanced rendering for a mesh.
679
+ Note the vertices must be in the 8-dimensional format:
680
+ [3D point, 3D normal, UV texture coordinates]
681
+ """
682
+
683
+ gl = None # Class-level variable to hold the imported module
684
+
685
+ @classmethod
686
+ def initialize_gl(cls):
687
+ if cls.gl is None: # Only import if not already imported
688
+ from pyglet import gl
689
+
690
+ cls.gl = gl
691
+
692
+ def __new__(cls, *args, **kwargs):
693
+ instance = super().__new__(cls)
694
+ instance.instance_transform_gl_buffer = None
695
+ instance.vao = None
696
+ return instance
697
+
698
+ def __init__(self, shape_shader, device):
699
+ self.shape_shader = shape_shader
700
+ self.device = device
701
+ self.face_count = 0
702
+ self.instance_color1_buffer = None
703
+ self.instance_color2_buffer = None
704
+ self.color1 = (1.0, 1.0, 1.0)
705
+ self.color2 = (0.0, 0.0, 0.0)
706
+ self.num_instances = 0
707
+ self.transforms = None
708
+ self.scalings = None
709
+ self._instance_transform_cuda_buffer = None
710
+
711
+ ShapeInstancer.initialize_gl()
712
+
713
+ def __del__(self):
714
+ gl = ShapeInstancer.gl
715
+
716
+ if self.instance_transform_gl_buffer is not None:
717
+ try:
718
+ gl.glDeleteBuffers(1, self.instance_transform_gl_buffer)
719
+ gl.glDeleteBuffers(1, self.instance_color1_buffer)
720
+ gl.glDeleteBuffers(1, self.instance_color2_buffer)
721
+ except gl.GLException:
722
+ pass
723
+ if self.vao is not None:
724
+ try:
725
+ gl.glDeleteVertexArrays(1, self.vao)
726
+ gl.glDeleteBuffers(1, self.vbo)
727
+ gl.glDeleteBuffers(1, self.ebo)
728
+ except gl.GLException:
729
+ pass
730
+
731
+ def register_shape(self, vertices, indices, color1=(1.0, 1.0, 1.0), color2=(0.0, 0.0, 0.0)):
732
+ gl = ShapeInstancer.gl
733
+
734
+ if color1 is not None and color2 is None:
735
+ color2 = np.clip(np.array(color1) + 0.25, 0.0, 1.0)
736
+ self.color1 = color1
737
+ self.color2 = color2
738
+
739
+ gl.glUseProgram(self.shape_shader.id)
740
+
741
+ # Create VAO, VBO, and EBO
742
+ self.vao = gl.GLuint()
743
+ gl.glGenVertexArrays(1, self.vao)
744
+ gl.glBindVertexArray(self.vao)
745
+
746
+ self.vbo = gl.GLuint()
747
+ gl.glGenBuffers(1, self.vbo)
748
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vbo)
749
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices.ctypes.data, gl.GL_STATIC_DRAW)
750
+
751
+ self.ebo = gl.GLuint()
752
+ gl.glGenBuffers(1, self.ebo)
753
+ gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.ebo)
754
+ gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices.ctypes.data, gl.GL_STATIC_DRAW)
755
+
756
+ # Set up vertex attributes
757
+ vertex_stride = vertices.shape[1] * vertices.itemsize
758
+ # positions
759
+ gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(0))
760
+ gl.glEnableVertexAttribArray(0)
761
+ # normals
762
+ gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(3 * vertices.itemsize))
763
+ gl.glEnableVertexAttribArray(1)
764
+ # uv coordinates
765
+ gl.glVertexAttribPointer(2, 2, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(6 * vertices.itemsize))
766
+ gl.glEnableVertexAttribArray(2)
767
+
768
+ gl.glBindVertexArray(0)
769
+
770
+ self.face_count = len(indices)
771
+
772
+ def update_colors(self, colors1, colors2):
773
+ gl = ShapeInstancer.gl
774
+
775
+ if colors1 is None:
776
+ colors1 = np.tile(self.color1, (self.num_instances, 1))
777
+ if colors2 is None:
778
+ colors2 = np.tile(self.color2, (self.num_instances, 1))
779
+ if np.shape(colors1) != (self.num_instances, 3):
780
+ colors1 = np.tile(colors1, (self.num_instances, 1))
781
+ if np.shape(colors2) != (self.num_instances, 3):
782
+ colors2 = np.tile(colors2, (self.num_instances, 1))
783
+ colors1 = np.array(colors1, dtype=np.float32)
784
+ colors2 = np.array(colors2, dtype=np.float32)
785
+
786
+ gl.glBindVertexArray(self.vao)
787
+
788
+ # create buffer for checkerboard colors
789
+ if self.instance_color1_buffer is None:
790
+ self.instance_color1_buffer = gl.GLuint()
791
+ gl.glGenBuffers(1, self.instance_color1_buffer)
792
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_color1_buffer)
793
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, colors1.nbytes, colors1.ctypes.data, gl.GL_STATIC_DRAW)
794
+
795
+ if self.instance_color2_buffer is None:
796
+ self.instance_color2_buffer = gl.GLuint()
797
+ gl.glGenBuffers(1, self.instance_color2_buffer)
798
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_color2_buffer)
799
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, colors2.nbytes, colors2.ctypes.data, gl.GL_STATIC_DRAW)
800
+
801
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_color1_buffer)
802
+ gl.glVertexAttribPointer(7, 3, gl.GL_FLOAT, gl.GL_FALSE, colors1[0].nbytes, ctypes.c_void_p(0))
803
+ gl.glEnableVertexAttribArray(7)
804
+ gl.glVertexAttribDivisor(7, 1)
805
+
806
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_color2_buffer)
807
+ gl.glVertexAttribPointer(8, 3, gl.GL_FLOAT, gl.GL_FALSE, colors2[0].nbytes, ctypes.c_void_p(0))
808
+ gl.glEnableVertexAttribArray(8)
809
+ gl.glVertexAttribDivisor(8, 1)
810
+
811
+ def allocate_instances(self, positions, rotations=None, colors1=None, colors2=None, scalings=None):
812
+ gl = ShapeInstancer.gl
813
+
814
+ gl.glBindVertexArray(self.vao)
815
+
816
+ self.num_instances = len(positions)
817
+
818
+ # Create instance buffer and bind it as an instanced array
819
+ if self.instance_transform_gl_buffer is None:
820
+ self.instance_transform_gl_buffer = gl.GLuint()
821
+ gl.glGenBuffers(1, self.instance_transform_gl_buffer)
822
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_transform_gl_buffer)
823
+
824
+ self.instance_ids = wp.array(np.arange(self.num_instances), dtype=wp.int32, device=self.device)
825
+ if rotations is None:
826
+ self.instance_transforms = wp.array(
827
+ [(*pos, 0.0, 0.0, 0.0, 1.0) for pos in positions], dtype=wp.transform, device=self.device
828
+ )
829
+ else:
830
+ self.instance_transforms = wp.array(
831
+ [(*pos, *rot) for pos, rot in zip(positions, rotations)],
832
+ dtype=wp.transform,
833
+ device=self.device,
834
+ )
835
+
836
+ if scalings is None:
837
+ self.instance_scalings = wp.array(
838
+ np.tile((1.0, 1.0, 1.0), (self.num_instances, 1)), dtype=wp.vec3, device=self.device
839
+ )
840
+ else:
841
+ self.instance_scalings = wp.array(scalings, dtype=wp.vec3, device=self.device)
842
+
843
+ vbo_transforms = wp.zeros(dtype=wp.mat44, shape=(self.num_instances,), device=self.device)
844
+
845
+ wp.launch(
846
+ update_vbo_transforms,
847
+ dim=self.num_instances,
848
+ inputs=[
849
+ self.instance_ids,
850
+ None,
851
+ self.instance_transforms,
852
+ self.instance_scalings,
853
+ None,
854
+ ],
855
+ outputs=[
856
+ vbo_transforms,
857
+ ],
858
+ device=self.device,
859
+ record_tape=False,
860
+ )
861
+
862
+ vbo_transforms = vbo_transforms.numpy()
863
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, vbo_transforms.nbytes, vbo_transforms.ctypes.data, gl.GL_DYNAMIC_DRAW)
864
+
865
+ # Create CUDA buffer for instance transforms
866
+ self._instance_transform_cuda_buffer = wp.RegisteredGLBuffer(
867
+ int(self.instance_transform_gl_buffer.value), self.device
868
+ )
869
+
870
+ self.update_colors(colors1, colors2)
871
+
872
+ # Set up instance attribute pointers
873
+ matrix_size = vbo_transforms[0].nbytes
874
+
875
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.instance_transform_gl_buffer)
876
+
877
+ # we can only send vec4s to the shader, so we need to split the instance transforms matrix into its column vectors
878
+ for i in range(4):
879
+ gl.glVertexAttribPointer(
880
+ 3 + i, 4, gl.GL_FLOAT, gl.GL_FALSE, matrix_size, ctypes.c_void_p(i * matrix_size // 4)
881
+ )
882
+ gl.glEnableVertexAttribArray(3 + i)
883
+ gl.glVertexAttribDivisor(3 + i, 1)
884
+
885
+ gl.glBindVertexArray(0)
886
+
887
+ def update_instances(self, transforms: wp.array = None, scalings: wp.array = None, colors1=None, colors2=None):
888
+ gl = ShapeInstancer.gl
889
+
890
+ if transforms is not None:
891
+ if transforms.device.is_cuda:
892
+ wp_transforms = transforms
893
+ else:
894
+ wp_transforms = transforms.to(self.device)
895
+ self.transforms = wp_transforms
896
+ if scalings is not None:
897
+ if transforms.device.is_cuda:
898
+ wp_scalings = scalings
899
+ else:
900
+ wp_scalings = scalings.to(self.device)
901
+ self.scalings = wp_scalings
902
+
903
+ if transforms is not None or scalings is not None:
904
+ gl.glBindVertexArray(self.vao)
905
+ vbo_transforms = self._instance_transform_cuda_buffer.map(dtype=wp.mat44, shape=(self.num_instances,))
906
+
907
+ wp.launch(
908
+ update_vbo_transforms,
909
+ dim=self.num_instances,
910
+ inputs=[
911
+ self.instance_ids,
912
+ None,
913
+ self.instance_transforms,
914
+ self.instance_scalings,
915
+ None,
916
+ ],
917
+ outputs=[
918
+ vbo_transforms,
919
+ ],
920
+ device=self.device,
921
+ record_tape=False,
922
+ )
923
+
924
+ self._instance_transform_cuda_buffer.unmap()
925
+
926
+ if colors1 is not None or colors2 is not None:
927
+ self.update_colors(colors1, colors2)
928
+
929
+ def render(self):
930
+ gl = ShapeInstancer.gl
931
+
932
+ gl.glUseProgram(self.shape_shader.id)
933
+
934
+ gl.glBindVertexArray(self.vao)
935
+ gl.glDrawElementsInstanced(gl.GL_TRIANGLES, self.face_count, gl.GL_UNSIGNED_INT, None, self.num_instances)
936
+ gl.glBindVertexArray(0)
937
+
938
+ # scope exposes VBO transforms to be set directly by a warp kernel
939
+ def __enter__(self):
940
+ gl = ShapeInstancer.gl
941
+
942
+ gl.glBindVertexArray(self.vao)
943
+ self.vbo_transforms = self._instance_transform_cuda_buffer.map(dtype=wp.mat44, shape=(self.num_instances,))
944
+ return self
945
+
946
+ def __exit__(self, exc_type, exc_value, traceback):
947
+ self._instance_transform_cuda_buffer.unmap()
948
+
949
+
950
+ def str_buffer(string: str):
951
+ return ctypes.c_char_p(string.encode("utf-8"))
952
+
953
+
954
+ def arr_pointer(arr: np.ndarray):
955
+ return arr.astype(np.float32).ctypes.data_as(ctypes.POINTER(ctypes.c_float))
956
+
957
+
958
+ class OpenGLRenderer:
959
+ """
960
+ OpenGLRenderer is a simple OpenGL renderer for rendering 3D shapes and meshes.
961
+ """
962
+
963
+ # number of segments to use for rendering spheres, capsules, cones and cylinders
964
+ default_num_segments = 32
965
+
966
+ gl = None # Class-level variable to hold the imported module
967
+
968
+ @classmethod
969
+ def initialize_gl(cls):
970
+ if cls.gl is None: # Only import if not already imported
971
+ from pyglet import gl
972
+
973
+ cls.gl = gl
974
+
975
+ def __init__(
976
+ self,
977
+ title="Warp",
978
+ scaling=1.0,
979
+ fps=60,
980
+ up_axis="Y",
981
+ screen_width=1024,
982
+ screen_height=768,
983
+ near_plane=1.0,
984
+ far_plane=100.0,
985
+ camera_fov=45.0,
986
+ camera_pos=(0.0, 2.0, 10.0),
987
+ camera_front=(0.0, 0.0, -1.0),
988
+ camera_up=(0.0, 1.0, 0.0),
989
+ background_color=(0.53, 0.8, 0.92),
990
+ draw_grid=True,
991
+ draw_sky=True,
992
+ draw_axis=True,
993
+ show_info=True,
994
+ render_wireframe=False,
995
+ render_depth=False,
996
+ axis_scale=1.0,
997
+ vsync=False,
998
+ headless=None,
999
+ enable_backface_culling=True,
1000
+ enable_mouse_interaction=True,
1001
+ enable_keyboard_interaction=True,
1002
+ device=None,
1003
+ use_legacy_opengl: bool | None = None,
1004
+ ):
1005
+ """
1006
+ Args:
1007
+
1008
+ title (str): The window title.
1009
+ scaling (float): The scaling factor for the scene.
1010
+ fps (int): The target frames per second.
1011
+ up_axis (str): The up axis of the scene. Can be "X", "Y", or "Z".
1012
+ screen_width (int): The width of the window.
1013
+ screen_height (int): The height of the window.
1014
+ near_plane (float): The near clipping plane.
1015
+ far_plane (float): The far clipping plane.
1016
+ camera_fov (float): The camera field of view in degrees.
1017
+ camera_pos (tuple): The initial camera position.
1018
+ camera_front (tuple): The initial camera front direction.
1019
+ camera_up (tuple): The initial camera up direction.
1020
+ background_color (tuple): The background color of the scene.
1021
+ draw_grid (bool): Whether to draw a grid indicating the ground plane.
1022
+ draw_sky (bool): Whether to draw a sky sphere.
1023
+ draw_axis (bool): Whether to draw the coordinate system axes.
1024
+ show_info (bool): Whether to overlay rendering information.
1025
+ render_wireframe (bool): Whether to render scene shapes as wireframes.
1026
+ render_depth (bool): Whether to show the depth buffer instead of the RGB image.
1027
+ axis_scale (float): The scale of the coordinate system axes being rendered (only if ``draw_axis`` is True).
1028
+ vsync (bool): Whether to enable vertical synchronization.
1029
+ headless (bool): Whether to run in headless mode (no window is created). If None, the value is determined by the Pyglet configuration defined in ``pyglet.options["headless"]``.
1030
+ enable_backface_culling (bool): Whether to enable backface culling.
1031
+ enable_mouse_interaction (bool): Whether to enable mouse interaction.
1032
+ enable_keyboard_interaction (bool): Whether to enable keyboard interaction.
1033
+ device (Devicelike): Where to store the internal data.
1034
+ use_legacy_opengl (bool | None): Whether to use a legacy OpenGL implementation that is more compatible with macOS. If ``None``, it will be automatically detected based on the operating system.
1035
+
1036
+ Note:
1037
+
1038
+ :class:`OpenGLRenderer` requires Pyglet (version >= 2.0, known to work on 2.0.7) to be installed.
1039
+
1040
+ Headless rendering is supported via EGL on UNIX operating systems. To enable headless rendering, set the following pyglet options before importing ``warp.render``:
1041
+
1042
+ .. code-block:: python
1043
+
1044
+ import pyglet
1045
+
1046
+ pyglet.options["headless"] = True
1047
+
1048
+ import warp.render
1049
+
1050
+ # OpenGLRenderer is instantiated with headless=True by default
1051
+ renderer = warp.render.OpenGLRenderer()
1052
+ """
1053
+ try:
1054
+ import pyglet
1055
+
1056
+ # disable error checking for performance
1057
+ pyglet.options["debug_gl"] = False
1058
+
1059
+ from pyglet.graphics.shader import Shader, ShaderProgram
1060
+ from pyglet.math import Vec3 as PyVec3
1061
+
1062
+ OpenGLRenderer.initialize_gl()
1063
+ gl = OpenGLRenderer.gl
1064
+ except ImportError as e:
1065
+ raise Exception("OpenGLRenderer requires pyglet (version >= 2.0) to be installed.") from e
1066
+
1067
+ self.camera_near_plane = near_plane
1068
+ self.camera_far_plane = far_plane
1069
+ self.camera_fov = camera_fov
1070
+
1071
+ self.background_color = background_color
1072
+ self.draw_grid = draw_grid
1073
+ self.draw_sky = draw_sky
1074
+ self.draw_axis = draw_axis
1075
+ self.show_info = show_info
1076
+ self.render_wireframe = render_wireframe
1077
+ self.render_depth = render_depth
1078
+ self.enable_backface_culling = enable_backface_culling
1079
+
1080
+ if use_legacy_opengl is None:
1081
+ self.use_legacy_opengl = sys.platform == "darwin"
1082
+ else:
1083
+ self.use_legacy_opengl = use_legacy_opengl
1084
+
1085
+ if device is None:
1086
+ self._device = wp.get_preferred_device()
1087
+ else:
1088
+ self._device = wp.get_device(device)
1089
+
1090
+ self._title = title
1091
+
1092
+ self.window = pyglet.window.Window(
1093
+ width=screen_width, height=screen_height, caption=title, resizable=True, vsync=vsync, visible=not headless
1094
+ )
1095
+ if headless is None:
1096
+ self.headless = pyglet.options.get("headless", False)
1097
+ else:
1098
+ self.headless = headless
1099
+ self.app = pyglet.app
1100
+
1101
+ # making window current opengl rendering context
1102
+ self._switch_context()
1103
+
1104
+ self.screen_width, self.screen_height = self.window.get_framebuffer_size()
1105
+
1106
+ self.enable_mouse_interaction = enable_mouse_interaction
1107
+ self.enable_keyboard_interaction = enable_keyboard_interaction
1108
+
1109
+ self._camera_speed = 0.04
1110
+ if isinstance(up_axis, int):
1111
+ self._camera_axis = up_axis
1112
+ else:
1113
+ self._camera_axis = "XYZ".index(up_axis.upper())
1114
+ self._last_x, self._last_y = self.screen_width // 2, self.screen_height // 2
1115
+ self._first_mouse = True
1116
+ self._left_mouse_pressed = False
1117
+ self._keys_pressed = defaultdict(bool)
1118
+ self._input_processors = []
1119
+ self._key_callbacks = []
1120
+
1121
+ self.render_2d_callbacks = []
1122
+ self.render_3d_callbacks = []
1123
+
1124
+ self._camera_pos = PyVec3(0.0, 0.0, 0.0)
1125
+ self._camera_front = PyVec3(0.0, 0.0, -1.0)
1126
+ self._camera_up = PyVec3(0.0, 1.0, 0.0)
1127
+ self._scaling = scaling
1128
+
1129
+ self._model_matrix = self.compute_model_matrix(self._camera_axis, scaling)
1130
+ self._inv_model_matrix = np.linalg.inv(self._model_matrix.reshape(4, 4)).flatten()
1131
+ self.update_view_matrix(cam_pos=camera_pos, cam_front=camera_front, cam_up=camera_up)
1132
+ self.update_projection_matrix()
1133
+
1134
+ self._camera_front = self._camera_front.normalize()
1135
+ self._pitch = np.rad2deg(np.arcsin(self._camera_front.y))
1136
+ self._yaw = -np.rad2deg(np.arccos(self._camera_front.x / np.cos(np.deg2rad(self._pitch))))
1137
+
1138
+ self._frame_dt = 1.0 / fps
1139
+ self.time = 0.0
1140
+ self._start_time = time.time()
1141
+ self.clock_time = 0.0
1142
+ self._paused = False
1143
+ self._frame_speed = 0.0
1144
+ self.skip_rendering = False
1145
+ self._skip_frame_counter = 0
1146
+ self._fps_update = 0.0
1147
+ self._fps_render = 0.0
1148
+ self._fps_alpha = 0.1 # low pass filter rate to update FPS stats
1149
+
1150
+ self._body_name = {}
1151
+ self._shapes = []
1152
+ self._shape_geo_hash = {}
1153
+ self._shape_gl_buffers = {}
1154
+ self._shape_instances = defaultdict(list)
1155
+ self._instances = {}
1156
+ self._instance_custom_ids = {}
1157
+ self._instance_shape = {}
1158
+ self._instance_gl_buffers = {}
1159
+ self._instance_transform_gl_buffer = None
1160
+ self._instance_transform_cuda_buffer = None
1161
+ self._instance_color1_buffer = None
1162
+ self._instance_color2_buffer = None
1163
+ self._instance_count = 0
1164
+ self._wp_instance_ids = None
1165
+ self._wp_instance_custom_ids = None
1166
+ self._np_instance_visible = None
1167
+ self._instance_ids = None
1168
+ self._inverse_instance_ids = None
1169
+ self._wp_instance_transforms = None
1170
+ self._wp_instance_scalings = None
1171
+ self._wp_instance_bodies = None
1172
+ self._update_shape_instances = False
1173
+ self._add_shape_instances = False
1174
+ self.instance_matrix_size = 0
1175
+ self.instance_color_size = 0
1176
+
1177
+ # additional shape instancer used for points and line rendering
1178
+ self._shape_instancers = {}
1179
+
1180
+ # instancer for the arrow shapes sof the coordinate system axes
1181
+ self._axis_instancer = None
1182
+
1183
+ # toggle tiled rendering
1184
+ self._tiled_rendering = False
1185
+ self._tile_instances = None
1186
+ self._tile_ncols = 0
1187
+ self._tile_nrows = 0
1188
+ self._tile_width = 0
1189
+ self._tile_height = 0
1190
+ self._tile_viewports = None
1191
+ self._tile_view_matrices = None
1192
+ self._tile_projection_matrices = None
1193
+
1194
+ self._frame_texture = None
1195
+ self._frame_depth_texture = None
1196
+ self._frame_fbo = None
1197
+ self._frame_pbo = None
1198
+
1199
+ if not headless:
1200
+ self.window.push_handlers(on_draw=self._draw)
1201
+ self.window.push_handlers(on_resize=self._window_resize_callback)
1202
+ self.window.push_handlers(on_key_press=self._key_press_callback)
1203
+ self.window.push_handlers(on_close=self._close_callback)
1204
+
1205
+ self._key_handler = pyglet.window.key.KeyStateHandler()
1206
+ self.window.push_handlers(self._key_handler)
1207
+
1208
+ self.window.on_mouse_scroll = self._scroll_callback
1209
+ self.window.on_mouse_drag = self._mouse_drag_callback
1210
+
1211
+ gl.glClearColor(*self.background_color, 1)
1212
+ gl.glEnable(gl.GL_DEPTH_TEST)
1213
+ gl.glDepthMask(True)
1214
+ gl.glDepthRange(0.0, 1.0)
1215
+
1216
+ self._shape_shader = ShaderProgram(
1217
+ Shader(shape_vertex_shader, "vertex"), Shader(shape_fragment_shader, "fragment")
1218
+ )
1219
+ self._grid_shader = ShaderProgram(
1220
+ Shader(grid_vertex_shader, "vertex"), Shader(grid_fragment_shader, "fragment")
1221
+ )
1222
+
1223
+ self._sun_direction = np.array((-0.2, 0.8, 0.3))
1224
+ self._sun_direction /= np.linalg.norm(self._sun_direction)
1225
+ with self._shape_shader:
1226
+ gl.glUniform3f(
1227
+ gl.glGetUniformLocation(self._shape_shader.id, str_buffer("sunDirection")), *self._sun_direction
1228
+ )
1229
+ gl.glUniform3f(gl.glGetUniformLocation(self._shape_shader.id, str_buffer("lightColor")), 1, 1, 1)
1230
+ self._loc_shape_model = gl.glGetUniformLocation(self._shape_shader.id, str_buffer("model"))
1231
+ self._loc_shape_view = gl.glGetUniformLocation(self._shape_shader.id, str_buffer("view"))
1232
+ self._loc_shape_projection = gl.glGetUniformLocation(self._shape_shader.id, str_buffer("projection"))
1233
+ self._loc_shape_view_pos = gl.glGetUniformLocation(self._shape_shader.id, str_buffer("viewPos"))
1234
+ gl.glUniform3f(self._loc_shape_view_pos, 0, 0, 10)
1235
+
1236
+ # create grid data
1237
+ limit = 10.0
1238
+ ticks = np.linspace(-limit, limit, 21)
1239
+ grid_vertices = []
1240
+ for i in ticks:
1241
+ if self._camera_axis == 0:
1242
+ grid_vertices.extend([0, -limit, i, 0, limit, i])
1243
+ grid_vertices.extend([0, i, -limit, 0, i, limit])
1244
+ elif self._camera_axis == 1:
1245
+ grid_vertices.extend([-limit, 0, i, limit, 0, i])
1246
+ grid_vertices.extend([i, 0, -limit, i, 0, limit])
1247
+ elif self._camera_axis == 2:
1248
+ grid_vertices.extend([-limit, i, 0, limit, i, 0])
1249
+ grid_vertices.extend([i, -limit, 0, i, limit, 0])
1250
+ grid_vertices = np.array(grid_vertices, dtype=np.float32)
1251
+ self._grid_vertex_count = len(grid_vertices) // 3
1252
+
1253
+ with self._grid_shader:
1254
+ self._grid_vao = gl.GLuint()
1255
+ gl.glGenVertexArrays(1, self._grid_vao)
1256
+ gl.glBindVertexArray(self._grid_vao)
1257
+
1258
+ self._grid_vbo = gl.GLuint()
1259
+ gl.glGenBuffers(1, self._grid_vbo)
1260
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._grid_vbo)
1261
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, grid_vertices.nbytes, grid_vertices.ctypes.data, gl.GL_STATIC_DRAW)
1262
+
1263
+ self._loc_grid_view = gl.glGetUniformLocation(self._grid_shader.id, str_buffer("view"))
1264
+ self._loc_grid_model = gl.glGetUniformLocation(self._grid_shader.id, str_buffer("model"))
1265
+ self._loc_grid_projection = gl.glGetUniformLocation(self._grid_shader.id, str_buffer("projection"))
1266
+
1267
+ self._loc_grid_pos_attribute = gl.glGetAttribLocation(self._grid_shader.id, str_buffer("position"))
1268
+ gl.glVertexAttribPointer(self._loc_grid_pos_attribute, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
1269
+ gl.glEnableVertexAttribArray(self._loc_grid_pos_attribute)
1270
+
1271
+ # create sky data
1272
+ self._sky_shader = ShaderProgram(Shader(sky_vertex_shader, "vertex"), Shader(sky_fragment_shader, "fragment"))
1273
+
1274
+ with self._sky_shader:
1275
+ self._loc_sky_view = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("view"))
1276
+ self._loc_sky_inv_model = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("inv_model"))
1277
+ self._loc_sky_projection = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("projection"))
1278
+
1279
+ self._loc_sky_color1 = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("color1"))
1280
+ self._loc_sky_color2 = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("color2"))
1281
+ self._loc_sky_far_plane = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("farPlane"))
1282
+ gl.glUniform3f(self._loc_sky_color1, *background_color)
1283
+ # glUniform3f(self._loc_sky_color2, *np.clip(np.array(background_color)+0.5, 0.0, 1.0))
1284
+ gl.glUniform3f(self._loc_sky_color2, 0.8, 0.4, 0.05)
1285
+ gl.glUniform1f(self._loc_sky_far_plane, self.camera_far_plane)
1286
+ self._loc_sky_view_pos = gl.glGetUniformLocation(self._sky_shader.id, str_buffer("viewPos"))
1287
+ gl.glUniform3f(
1288
+ gl.glGetUniformLocation(self._sky_shader.id, str_buffer("sunDirection")), *self._sun_direction
1289
+ )
1290
+
1291
+ # create VAO, VBO, and EBO
1292
+ self._sky_vao = gl.GLuint()
1293
+ gl.glGenVertexArrays(1, self._sky_vao)
1294
+ gl.glBindVertexArray(self._sky_vao)
1295
+
1296
+ vertices, indices = self._create_sphere_mesh(self.camera_far_plane * 0.9, 32, 32, reverse_winding=True)
1297
+ self._sky_tri_count = len(indices)
1298
+
1299
+ self._sky_vbo = gl.GLuint()
1300
+ gl.glGenBuffers(1, self._sky_vbo)
1301
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._sky_vbo)
1302
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices.ctypes.data, gl.GL_STATIC_DRAW)
1303
+
1304
+ self._sky_ebo = gl.GLuint()
1305
+ gl.glGenBuffers(1, self._sky_ebo)
1306
+ gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self._sky_ebo)
1307
+ gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices.ctypes.data, gl.GL_STATIC_DRAW)
1308
+
1309
+ # set up vertex attributes
1310
+ vertex_stride = vertices.shape[1] * vertices.itemsize
1311
+ # positions
1312
+ gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(0))
1313
+ gl.glEnableVertexAttribArray(0)
1314
+ # normals
1315
+ gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(3 * vertices.itemsize))
1316
+ gl.glEnableVertexAttribArray(1)
1317
+ # uv coordinates
1318
+ gl.glVertexAttribPointer(2, 2, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(6 * vertices.itemsize))
1319
+ gl.glEnableVertexAttribArray(2)
1320
+
1321
+ gl.glBindVertexArray(0)
1322
+
1323
+ self._last_time = time.time()
1324
+ self._last_begin_frame_time = self._last_time
1325
+ self._last_end_frame_time = self._last_time
1326
+
1327
+ # create arrow shapes for the coordinate system axes
1328
+ vertices, indices = self._create_arrow_mesh(
1329
+ base_radius=0.02 * axis_scale, base_height=0.85 * axis_scale, cap_height=0.15 * axis_scale
1330
+ )
1331
+ self._axis_instancer = ShapeInstancer(self._shape_shader, self._device)
1332
+ self._axis_instancer.register_shape(vertices, indices)
1333
+ sqh = np.sqrt(0.5)
1334
+ self._axis_instancer.allocate_instances(
1335
+ positions=[(0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)],
1336
+ rotations=[(0.0, 0.0, 0.0, 1.0), (0.0, 0.0, -sqh, sqh), (sqh, 0.0, 0.0, sqh)],
1337
+ colors1=[(0.0, 1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)],
1338
+ colors2=[(0.0, 1.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0)],
1339
+ )
1340
+
1341
+ # create frame buffer for rendering to a texture
1342
+ self._frame_texture = None
1343
+ self._frame_depth_texture = None
1344
+ self._frame_fbo = None
1345
+ self._setup_framebuffer()
1346
+
1347
+ # fmt: off
1348
+ # set up VBO for the quad that is rendered to the user window with the texture
1349
+ self._frame_vertices = np.array([
1350
+ # Positions TexCoords
1351
+ -1.0, -1.0, 0.0, 0.0,
1352
+ 1.0, -1.0, 1.0, 0.0,
1353
+ 1.0, 1.0, 1.0, 1.0,
1354
+ -1.0, 1.0, 0.0, 1.0
1355
+ ], dtype=np.float32)
1356
+ # fmt: on
1357
+
1358
+ self._frame_indices = np.array([0, 1, 2, 2, 3, 0], dtype=np.uint32)
1359
+
1360
+ self._frame_vao = gl.GLuint()
1361
+ gl.glGenVertexArrays(1, self._frame_vao)
1362
+ gl.glBindVertexArray(self._frame_vao)
1363
+
1364
+ self._frame_vbo = gl.GLuint()
1365
+ gl.glGenBuffers(1, self._frame_vbo)
1366
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._frame_vbo)
1367
+ gl.glBufferData(
1368
+ gl.GL_ARRAY_BUFFER, self._frame_vertices.nbytes, self._frame_vertices.ctypes.data, gl.GL_STATIC_DRAW
1369
+ )
1370
+
1371
+ self._frame_ebo = gl.GLuint()
1372
+ gl.glGenBuffers(1, self._frame_ebo)
1373
+ gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self._frame_ebo)
1374
+ gl.glBufferData(
1375
+ gl.GL_ELEMENT_ARRAY_BUFFER, self._frame_indices.nbytes, self._frame_indices.ctypes.data, gl.GL_STATIC_DRAW
1376
+ )
1377
+
1378
+ gl.glVertexAttribPointer(0, 2, gl.GL_FLOAT, gl.GL_FALSE, 4 * self._frame_vertices.itemsize, ctypes.c_void_p(0))
1379
+ gl.glEnableVertexAttribArray(0)
1380
+ gl.glVertexAttribPointer(
1381
+ 1, 2, gl.GL_FLOAT, gl.GL_FALSE, 4 * self._frame_vertices.itemsize, ctypes.c_void_p(2 * vertices.itemsize)
1382
+ )
1383
+ gl.glEnableVertexAttribArray(1)
1384
+
1385
+ self._frame_shader = ShaderProgram(
1386
+ Shader(frame_vertex_shader, "vertex"), Shader(frame_fragment_shader, "fragment")
1387
+ )
1388
+ gl.glUseProgram(self._frame_shader.id)
1389
+ self._frame_loc_texture = gl.glGetUniformLocation(self._frame_shader.id, str_buffer("textureSampler"))
1390
+
1391
+ self._frame_depth_shader = ShaderProgram(
1392
+ Shader(frame_vertex_shader, "vertex"), Shader(frame_depth_fragment_shader, "fragment")
1393
+ )
1394
+ gl.glUseProgram(self._frame_depth_shader.id)
1395
+ self._frame_loc_depth_texture = gl.glGetUniformLocation(
1396
+ self._frame_depth_shader.id, str_buffer("textureSampler")
1397
+ )
1398
+
1399
+ # unbind the VBO and VAO
1400
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0)
1401
+ gl.glBindVertexArray(0)
1402
+
1403
+ # update model matrix
1404
+ self.scaling = scaling
1405
+
1406
+ check_gl_error()
1407
+
1408
+ # create text to render stats on the screen
1409
+ self._info_label = pyglet.text.Label(
1410
+ "",
1411
+ font_name="Arial",
1412
+ font_size=12,
1413
+ color=(255, 255, 255, 255),
1414
+ x=10,
1415
+ y=10,
1416
+ anchor_x="left",
1417
+ anchor_y="top",
1418
+ multiline=True,
1419
+ width=400,
1420
+ )
1421
+
1422
+ if not headless:
1423
+ # set up our own event handling so we can synchronously render frames
1424
+ # by calling update() in a loop
1425
+ from pyglet.window import Window
1426
+
1427
+ Window._enable_event_queue = False
1428
+
1429
+ self.window.dispatch_pending_events()
1430
+
1431
+ platform_event_loop = self.app.platform_event_loop
1432
+ platform_event_loop.start()
1433
+
1434
+ # start event loop
1435
+ self.app.event_loop.dispatch_event("on_enter")
1436
+
1437
+ @property
1438
+ def paused(self):
1439
+ return self._paused
1440
+
1441
+ @paused.setter
1442
+ def paused(self, value):
1443
+ self._paused = value
1444
+ if value:
1445
+ self.window.set_caption(f"{self._title} (paused)")
1446
+ else:
1447
+ self.window.set_caption(self._title)
1448
+
1449
+ @property
1450
+ def has_exit(self):
1451
+ return self.app.event_loop.has_exit
1452
+
1453
+ def clear(self):
1454
+ gl = OpenGLRenderer.gl
1455
+
1456
+ self._switch_context()
1457
+
1458
+ if not self.headless:
1459
+ self.app.event_loop.dispatch_event("on_exit")
1460
+ self.app.platform_event_loop.stop()
1461
+
1462
+ if self._instance_transform_gl_buffer is not None:
1463
+ try:
1464
+ gl.glDeleteBuffers(1, self._instance_transform_gl_buffer)
1465
+ gl.glDeleteBuffers(1, self._instance_color1_buffer)
1466
+ gl.glDeleteBuffers(1, self._instance_color2_buffer)
1467
+ except gl.GLException:
1468
+ pass
1469
+ for vao, vbo, ebo, _, _vertex_cuda_buffer in self._shape_gl_buffers.values():
1470
+ try:
1471
+ gl.glDeleteVertexArrays(1, vao)
1472
+ gl.glDeleteBuffers(1, vbo)
1473
+ gl.glDeleteBuffers(1, ebo)
1474
+ except gl.GLException:
1475
+ pass
1476
+
1477
+ self._body_name.clear()
1478
+ self._shapes.clear()
1479
+ self._shape_geo_hash.clear()
1480
+ self._shape_gl_buffers.clear()
1481
+ self._shape_instances.clear()
1482
+ self._instances.clear()
1483
+ self._instance_shape.clear()
1484
+ self._instance_gl_buffers.clear()
1485
+ self._instance_transform_gl_buffer = None
1486
+ self._instance_transform_cuda_buffer = None
1487
+ self._instance_color1_buffer = None
1488
+ self._instance_color2_buffer = None
1489
+ self._wp_instance_ids = None
1490
+ self._wp_instance_custom_ids = None
1491
+ self._wp_instance_transforms = None
1492
+ self._wp_instance_scalings = None
1493
+ self._wp_instance_bodies = None
1494
+ self._np_instance_visible = None
1495
+ self._update_shape_instances = False
1496
+
1497
+ def close(self):
1498
+ self.clear()
1499
+ self.window.close()
1500
+
1501
+ @property
1502
+ def tiled_rendering(self):
1503
+ return self._tiled_rendering
1504
+
1505
+ @tiled_rendering.setter
1506
+ def tiled_rendering(self, value):
1507
+ if value:
1508
+ assert self._tile_instances is not None, "Tiled rendering is not set up. Call setup_tiled_rendering first."
1509
+ self._tiled_rendering = value
1510
+
1511
+ def setup_tiled_rendering(
1512
+ self,
1513
+ instances: list[list[int]],
1514
+ rescale_window: bool = False,
1515
+ tile_width: int | None = None,
1516
+ tile_height: int | None = None,
1517
+ tile_ncols: int | None = None,
1518
+ tile_nrows: int | None = None,
1519
+ tile_positions: list[tuple[int]] | None = None,
1520
+ tile_sizes: list[tuple[int]] | None = None,
1521
+ projection_matrices: list[Mat44] | None = None,
1522
+ view_matrices: list[Mat44] | None = None,
1523
+ ):
1524
+ """
1525
+ Set up tiled rendering where the render buffer is split into multiple tiles that can visualize
1526
+ different shape instances of the scene with different view and projection matrices.
1527
+ See :meth:`get_pixels` which allows to retrieve the pixels of for each tile.
1528
+ See :meth:`update_tile` which allows to update the shape instances, projection matrix, view matrix, tile size, or tile position for a given tile.
1529
+
1530
+ :param instances: A list of lists of shape instance ids. Each list of shape instance ids
1531
+ will be rendered into a separate tile.
1532
+ :param rescale_window: If True, the window will be resized to fit the tiles.
1533
+ :param tile_width: The width of each tile in pixels (optional).
1534
+ :param tile_height: The height of each tile in pixels (optional).
1535
+ :param tile_ncols: The number of tiles rendered horizontally (optional). Will be considered
1536
+ if `tile_width` is set to compute the tile positions, unless `tile_positions` is defined.
1537
+ :param tile_positions: A list of (x, y) tuples specifying the position of each tile in pixels.
1538
+ If None, the tiles will be arranged in a square grid, or, if `tile_ncols` and `tile_nrows`
1539
+ is set, in a grid with the specified number of columns and rows.
1540
+ :param tile_sizes: A list of (width, height) tuples specifying the size of each tile in pixels.
1541
+ If None, the tiles will have the same size as specified by `tile_width` and `tile_height`.
1542
+ :param projection_matrices: A list of projection matrices for each tile (each view matrix is
1543
+ either a flattened 16-dimensional array or a 4x4 matrix).
1544
+ If the entire array is None, or only a view instances, the projection matrices for all, or these
1545
+ instances, respectively, will be derived from the current render settings.
1546
+ :param view_matrices: A list of view matrices for each tile (each view matrix is either a flattened
1547
+ 16-dimensional array or a 4x4 matrix).
1548
+ If the entire array is None, or only a view instances, the view matrices for all, or these
1549
+ instances, respectively, will be derived from the current camera settings and be
1550
+ updated when the camera is moved.
1551
+ """
1552
+
1553
+ assert len(instances) > 0 and all(isinstance(i, list) for i in instances), "Invalid tile instances."
1554
+
1555
+ self._tile_instances = instances
1556
+ n = len(self._tile_instances)
1557
+
1558
+ if tile_positions is None or tile_sizes is None:
1559
+ if tile_ncols is None or tile_nrows is None:
1560
+ # try to fit the tiles into a square
1561
+ self._tile_ncols = int(np.ceil(np.sqrt(n)))
1562
+ self._tile_nrows = int(np.ceil(n / float(self._tile_ncols)))
1563
+ else:
1564
+ self._tile_ncols = tile_ncols
1565
+ self._tile_nrows = tile_nrows
1566
+ self._tile_width = tile_width or max(32, self.screen_width // self._tile_ncols)
1567
+ self._tile_height = tile_height or max(32, self.screen_height // self._tile_nrows)
1568
+ self._tile_viewports = [
1569
+ (i * self._tile_width, j * self._tile_height, self._tile_width, self._tile_height)
1570
+ for i in range(self._tile_ncols)
1571
+ for j in range(self._tile_nrows)
1572
+ ]
1573
+ if rescale_window:
1574
+ self.window.set_size(self._tile_width * self._tile_ncols, self._tile_height * self._tile_nrows)
1575
+ else:
1576
+ assert len(tile_positions) == n and len(tile_sizes) == n, (
1577
+ "Number of tiles does not match number of instances."
1578
+ )
1579
+ self._tile_ncols = None
1580
+ self._tile_nrows = None
1581
+ self._tile_width = None
1582
+ self._tile_height = None
1583
+ if all(tile_sizes[i][0] == tile_sizes[0][0] for i in range(n)):
1584
+ # tiles all have the same width
1585
+ self._tile_width = tile_sizes[0][0]
1586
+ if all(tile_sizes[i][1] == tile_sizes[0][1] for i in range(n)):
1587
+ # tiles all have the same height
1588
+ self._tile_height = tile_sizes[0][1]
1589
+ self._tile_viewports = [(x, y, w, h) for (x, y), (w, h) in zip(tile_positions, tile_sizes)]
1590
+
1591
+ if projection_matrices is None:
1592
+ projection_matrices = [None] * n
1593
+ self._tile_projection_matrices = []
1594
+ for i, p in enumerate(projection_matrices):
1595
+ if p is None:
1596
+ w, h = self._tile_viewports[i][2:]
1597
+ self._tile_projection_matrices.append(
1598
+ self.compute_projection_matrix(
1599
+ self.camera_fov, w / h, self.camera_near_plane, self.camera_far_plane
1600
+ )
1601
+ )
1602
+ else:
1603
+ self._tile_projection_matrices.append(np.array(p).flatten())
1604
+
1605
+ if view_matrices is None:
1606
+ self._tile_view_matrices = [None] * n
1607
+ else:
1608
+ self._tile_view_matrices = [np.array(m).flatten() for m in view_matrices]
1609
+
1610
+ self._tiled_rendering = True
1611
+
1612
+ def update_tile(
1613
+ self,
1614
+ tile_id,
1615
+ instances: list[int] | None = None,
1616
+ projection_matrix: Mat44 | None = None,
1617
+ view_matrix: Mat44 | None = None,
1618
+ tile_size: tuple[int] | None = None,
1619
+ tile_position: tuple[int] | None = None,
1620
+ ):
1621
+ """
1622
+ Update the shape instances, projection matrix, view matrix, tile size, or tile position
1623
+ for a given tile given its index.
1624
+
1625
+ :param tile_id: The index of the tile to update.
1626
+ :param instances: A list of shape instance ids (optional).
1627
+ :param projection_matrix: A projection matrix (optional).
1628
+ :param view_matrix: A view matrix (optional).
1629
+ :param tile_size: A (width, height) tuple specifying the size of the tile in pixels (optional).
1630
+ :param tile_position: A (x, y) tuple specifying the position of the tile in pixels (optional).
1631
+ """
1632
+
1633
+ assert self._tile_instances is not None, "Tiled rendering is not set up. Call setup_tiled_rendering first."
1634
+ assert tile_id < len(self._tile_instances), "Invalid tile id."
1635
+
1636
+ if instances is not None:
1637
+ self._tile_instances[tile_id] = instances
1638
+ if projection_matrix is not None:
1639
+ self._tile_projection_matrices[tile_id] = np.array(projection_matrix).flatten()
1640
+ if view_matrix is not None:
1641
+ self._tile_view_matrices[tile_id] = np.array(view_matrix).flatten()
1642
+ (x, y, w, h) = self._tile_viewports[tile_id]
1643
+ if tile_size is not None:
1644
+ w, h = tile_size
1645
+ if tile_position is not None:
1646
+ x, y = tile_position
1647
+ self._tile_viewports[tile_id] = (x, y, w, h)
1648
+
1649
+ def _setup_framebuffer(self):
1650
+ gl = OpenGLRenderer.gl
1651
+
1652
+ self._switch_context()
1653
+
1654
+ if self._frame_texture is None:
1655
+ self._frame_texture = gl.GLuint()
1656
+ gl.glGenTextures(1, self._frame_texture)
1657
+ if self._frame_depth_texture is None:
1658
+ self._frame_depth_texture = gl.GLuint()
1659
+ gl.glGenTextures(1, self._frame_depth_texture)
1660
+
1661
+ # set up RGB texture
1662
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
1663
+ gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0)
1664
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_texture)
1665
+ gl.glTexImage2D(
1666
+ gl.GL_TEXTURE_2D,
1667
+ 0,
1668
+ gl.GL_RGB,
1669
+ self.screen_width,
1670
+ self.screen_height,
1671
+ 0,
1672
+ gl.GL_RGB,
1673
+ gl.GL_UNSIGNED_BYTE,
1674
+ None,
1675
+ )
1676
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
1677
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
1678
+
1679
+ # set up depth texture
1680
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_depth_texture)
1681
+ gl.glTexImage2D(
1682
+ gl.GL_TEXTURE_2D,
1683
+ 0,
1684
+ gl.GL_DEPTH_COMPONENT32,
1685
+ self.screen_width,
1686
+ self.screen_height,
1687
+ 0,
1688
+ gl.GL_DEPTH_COMPONENT,
1689
+ gl.GL_FLOAT,
1690
+ None,
1691
+ )
1692
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
1693
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
1694
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
1695
+
1696
+ # create a framebuffer object (FBO)
1697
+ if self._frame_fbo is None:
1698
+ self._frame_fbo = gl.GLuint()
1699
+ gl.glGenFramebuffers(1, self._frame_fbo)
1700
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._frame_fbo)
1701
+
1702
+ # attach the texture to the FBO as its color attachment
1703
+ gl.glFramebufferTexture2D(
1704
+ gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self._frame_texture, 0
1705
+ )
1706
+ # attach the depth texture to the FBO as its depth attachment
1707
+ gl.glFramebufferTexture2D(
1708
+ gl.GL_FRAMEBUFFER, gl.GL_DEPTH_ATTACHMENT, gl.GL_TEXTURE_2D, self._frame_depth_texture, 0
1709
+ )
1710
+
1711
+ if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
1712
+ print("Framebuffer is not complete!", flush=True)
1713
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
1714
+ sys.exit(1)
1715
+
1716
+ # unbind the FBO (switch back to the default framebuffer)
1717
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
1718
+
1719
+ if self._frame_pbo is None:
1720
+ self._frame_pbo = gl.GLuint()
1721
+ gl.glGenBuffers(1, self._frame_pbo) # generate 1 buffer reference
1722
+ gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, self._frame_pbo) # binding to this buffer
1723
+
1724
+ # allocate memory for PBO
1725
+ rgb_bytes_per_pixel = 3
1726
+ depth_bytes_per_pixel = 4
1727
+ pixels = np.zeros(
1728
+ (self.screen_height, self.screen_width, rgb_bytes_per_pixel + depth_bytes_per_pixel), dtype=np.uint8
1729
+ )
1730
+ gl.glBufferData(gl.GL_PIXEL_PACK_BUFFER, pixels.nbytes, pixels.ctypes.data, gl.GL_DYNAMIC_DRAW)
1731
+ gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, 0)
1732
+
1733
+ @staticmethod
1734
+ def compute_projection_matrix(
1735
+ fov: float,
1736
+ aspect_ratio: float,
1737
+ near_plane: float,
1738
+ far_plane: float,
1739
+ ) -> Mat44:
1740
+ """
1741
+ Compute a projection matrix given the field of view, aspect ratio, near plane, and far plane.
1742
+
1743
+ :param fov: The field of view in degrees.
1744
+ :param aspect_ratio: The aspect ratio (width / height).
1745
+ :param near_plane: The near plane.
1746
+ :param far_plane: The far plane.
1747
+ :return: A projection matrix.
1748
+ """
1749
+
1750
+ from pyglet.math import Mat4 as PyMat4
1751
+
1752
+ return np.array(PyMat4.perspective_projection(aspect_ratio, near_plane, far_plane, fov))
1753
+
1754
+ def update_projection_matrix(self):
1755
+ if self.screen_height == 0:
1756
+ return
1757
+ aspect_ratio = self.screen_width / self.screen_height
1758
+ self._projection_matrix = self.compute_projection_matrix(
1759
+ self.camera_fov, aspect_ratio, self.camera_near_plane, self.camera_far_plane
1760
+ )
1761
+
1762
+ @property
1763
+ def camera_pos(self):
1764
+ return self._camera_pos
1765
+
1766
+ @camera_pos.setter
1767
+ def camera_pos(self, value):
1768
+ self.update_view_matrix(cam_pos=value)
1769
+
1770
+ @property
1771
+ def camera_front(self):
1772
+ return self._camera_front
1773
+
1774
+ @camera_front.setter
1775
+ def camera_front(self, value):
1776
+ self.update_view_matrix(cam_front=value)
1777
+
1778
+ @property
1779
+ def camera_up(self):
1780
+ return self._camera_up
1781
+
1782
+ @camera_up.setter
1783
+ def camera_up(self, value):
1784
+ self.update_view_matrix(cam_up=value)
1785
+
1786
+ def compute_view_matrix(self, cam_pos, cam_front, cam_up):
1787
+ from pyglet.math import Mat4, Vec3
1788
+
1789
+ model = np.array(self._model_matrix).reshape((4, 4))
1790
+ cp = model @ np.array([*cam_pos / self._scaling, 1.0])
1791
+ cf = model @ np.array([*cam_front / self._scaling, 1.0])
1792
+ up = model @ np.array([*cam_up / self._scaling, 0.0])
1793
+ cp = Vec3(*cp[:3])
1794
+ cf = Vec3(*cf[:3])
1795
+ up = Vec3(*up[:3])
1796
+ return np.array(Mat4.look_at(cp, cp + cf, up), dtype=np.float32)
1797
+
1798
+ def update_view_matrix(self, cam_pos=None, cam_front=None, cam_up=None, stiffness=1.0):
1799
+ from pyglet.math import Vec3
1800
+
1801
+ if cam_pos is not None:
1802
+ self._camera_pos = self._camera_pos * (1.0 - stiffness) + Vec3(*cam_pos) * stiffness
1803
+ if cam_front is not None:
1804
+ self._camera_front = self._camera_front * (1.0 - stiffness) + Vec3(*cam_front) * stiffness
1805
+ if cam_up is not None:
1806
+ self._camera_up = self._camera_up * (1.0 - stiffness) + Vec3(*cam_up) * stiffness
1807
+
1808
+ self._view_matrix = self.compute_view_matrix(self._camera_pos, self._camera_front, self._camera_up)
1809
+
1810
+ @staticmethod
1811
+ def compute_model_matrix(camera_axis: int, scaling: float):
1812
+ if camera_axis == 0:
1813
+ return np.array((0, 0, scaling, 0, scaling, 0, 0, 0, 0, scaling, 0, 0, 0, 0, 0, 1), dtype=np.float32)
1814
+ elif camera_axis == 2:
1815
+ return np.array((0, scaling, 0, 0, 0, 0, scaling, 0, scaling, 0, 0, 0, 0, 0, 0, 1), dtype=np.float32)
1816
+
1817
+ return np.array((scaling, 0, 0, 0, 0, scaling, 0, 0, 0, 0, scaling, 0, 0, 0, 0, 1), dtype=np.float32)
1818
+
1819
+ def update_model_matrix(self, model_matrix: Mat44 | None = None):
1820
+ gl = OpenGLRenderer.gl
1821
+
1822
+ self._switch_context()
1823
+
1824
+ if model_matrix is None:
1825
+ self._model_matrix = self.compute_model_matrix(self._camera_axis, self._scaling)
1826
+ else:
1827
+ self._model_matrix = np.array(model_matrix).flatten()
1828
+ self._inv_model_matrix = np.linalg.inv(self._model_matrix.reshape((4, 4))).flatten()
1829
+ # update model view matrix in shaders
1830
+ ptr = arr_pointer(self._model_matrix)
1831
+ gl.glUseProgram(self._shape_shader.id)
1832
+ gl.glUniformMatrix4fv(self._loc_shape_model, 1, gl.GL_FALSE, ptr)
1833
+ gl.glUseProgram(self._grid_shader.id)
1834
+ gl.glUniformMatrix4fv(self._loc_grid_model, 1, gl.GL_FALSE, ptr)
1835
+ # sky shader needs inverted model view matrix
1836
+ gl.glUseProgram(self._sky_shader.id)
1837
+ inv_ptr = arr_pointer(self._inv_model_matrix)
1838
+ gl.glUniformMatrix4fv(self._loc_sky_inv_model, 1, gl.GL_FALSE, inv_ptr)
1839
+
1840
+ @property
1841
+ def num_tiles(self):
1842
+ return len(self._tile_instances)
1843
+
1844
+ @property
1845
+ def tile_width(self):
1846
+ return self._tile_width
1847
+
1848
+ @property
1849
+ def tile_height(self):
1850
+ return self._tile_height
1851
+
1852
+ @property
1853
+ def num_shapes(self):
1854
+ return len(self._shapes)
1855
+
1856
+ @property
1857
+ def num_instances(self):
1858
+ return self._instance_count
1859
+
1860
+ @property
1861
+ def scaling(self):
1862
+ return self._scaling
1863
+
1864
+ @scaling.setter
1865
+ def scaling(self, scaling):
1866
+ self._scaling = scaling
1867
+ self.update_model_matrix()
1868
+
1869
+ def begin_frame(self, t: float | None = None):
1870
+ self._last_begin_frame_time = time.time()
1871
+ self.time = t or self.clock_time
1872
+
1873
+ def end_frame(self):
1874
+ self._last_end_frame_time = time.time()
1875
+ if self._add_shape_instances:
1876
+ self.allocate_shape_instances()
1877
+ if self._update_shape_instances:
1878
+ self.update_shape_instances()
1879
+ self.update()
1880
+ while self.paused and self.is_running():
1881
+ self.update()
1882
+
1883
+ def update(self):
1884
+ self.clock_time = time.time() - self._start_time
1885
+ update_duration = self.clock_time - self._last_time
1886
+ frame_duration = self._last_end_frame_time - self._last_begin_frame_time
1887
+ self._last_time = self.clock_time
1888
+ self._frame_speed = update_duration * 100.0
1889
+
1890
+ if not self.headless:
1891
+ self.app.platform_event_loop.step(self._frame_dt * 1e-3)
1892
+
1893
+ if not self.skip_rendering:
1894
+ self._skip_frame_counter += 1
1895
+ if self._skip_frame_counter > 100:
1896
+ self._skip_frame_counter = 0
1897
+
1898
+ if frame_duration > 0.0:
1899
+ if self._fps_update is None:
1900
+ self._fps_update = 1.0 / frame_duration
1901
+ else:
1902
+ update = 1.0 / frame_duration
1903
+ self._fps_update = (1.0 - self._fps_alpha) * self._fps_update + self._fps_alpha * update
1904
+ if update_duration > 0.0:
1905
+ if self._fps_render is None:
1906
+ self._fps_render = 1.0 / update_duration
1907
+ else:
1908
+ update = 1.0 / update_duration
1909
+ self._fps_render = (1.0 - self._fps_alpha) * self._fps_render + self._fps_alpha * update
1910
+
1911
+ if not self.headless:
1912
+ self.app.event_loop._redraw_windows(self._frame_dt * 1e-3)
1913
+ else:
1914
+ self._draw()
1915
+
1916
+ def _draw(self):
1917
+ gl = OpenGLRenderer.gl
1918
+
1919
+ self._switch_context()
1920
+
1921
+ if not self.headless:
1922
+ # catch key hold events
1923
+ self._process_inputs()
1924
+
1925
+ if self.enable_backface_culling:
1926
+ gl.glEnable(gl.GL_CULL_FACE)
1927
+ else:
1928
+ gl.glDisable(gl.GL_CULL_FACE)
1929
+
1930
+ if self._frame_fbo is not None:
1931
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._frame_fbo)
1932
+
1933
+ gl.glClearColor(*self.background_color, 1)
1934
+ gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
1935
+ gl.glBindVertexArray(0)
1936
+
1937
+ if not self._tiled_rendering:
1938
+ if self.draw_grid:
1939
+ self._draw_grid()
1940
+
1941
+ if self.draw_sky:
1942
+ self._draw_sky()
1943
+
1944
+ view_mat_ptr = arr_pointer(self._view_matrix)
1945
+ projection_mat_ptr = arr_pointer(self._projection_matrix)
1946
+ gl.glUseProgram(self._shape_shader.id)
1947
+ gl.glUniformMatrix4fv(self._loc_shape_view, 1, gl.GL_FALSE, view_mat_ptr)
1948
+ gl.glUniform3f(self._loc_shape_view_pos, *self._camera_pos)
1949
+ gl.glUniformMatrix4fv(self._loc_shape_view, 1, gl.GL_FALSE, view_mat_ptr)
1950
+ gl.glUniformMatrix4fv(self._loc_shape_projection, 1, gl.GL_FALSE, projection_mat_ptr)
1951
+
1952
+ if self.render_wireframe:
1953
+ gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
1954
+
1955
+ if self._tiled_rendering:
1956
+ self._render_scene_tiled()
1957
+ else:
1958
+ self._render_scene()
1959
+
1960
+ for cb in self.render_3d_callbacks:
1961
+ cb()
1962
+
1963
+ gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
1964
+
1965
+ gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0)
1966
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
1967
+ gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
1968
+ gl.glViewport(0, 0, self.screen_width, self.screen_height)
1969
+
1970
+ # render frame buffer texture to screen
1971
+ if self._frame_fbo is not None:
1972
+ if self.render_depth:
1973
+ with self._frame_depth_shader:
1974
+ gl.glActiveTexture(gl.GL_TEXTURE0)
1975
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_depth_texture)
1976
+ gl.glUniform1i(self._frame_loc_depth_texture, 0)
1977
+
1978
+ gl.glBindVertexArray(self._frame_vao)
1979
+ gl.glDrawElements(gl.GL_TRIANGLES, len(self._frame_indices), gl.GL_UNSIGNED_INT, None)
1980
+ gl.glBindVertexArray(0)
1981
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
1982
+ else:
1983
+ with self._frame_shader:
1984
+ gl.glActiveTexture(gl.GL_TEXTURE0)
1985
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_texture)
1986
+ gl.glUniform1i(self._frame_loc_texture, 0)
1987
+
1988
+ gl.glBindVertexArray(self._frame_vao)
1989
+ gl.glDrawElements(gl.GL_TRIANGLES, len(self._frame_indices), gl.GL_UNSIGNED_INT, None)
1990
+ gl.glBindVertexArray(0)
1991
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
1992
+
1993
+ # check for OpenGL errors
1994
+ # check_gl_error()
1995
+
1996
+ if self.show_info:
1997
+ gl.glClear(gl.GL_DEPTH_BUFFER_BIT)
1998
+ gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
1999
+ gl.glEnable(gl.GL_BLEND)
2000
+
2001
+ # disable depth test to fix text rendering
2002
+ # https://github.com/pyglet/pyglet/issues/1302
2003
+ gl.glDisable(gl.GL_DEPTH_TEST)
2004
+
2005
+ text = f"""Sim Time: {self.time:.1f}
2006
+ Update FPS: {self._fps_update:.1f}
2007
+ Render FPS: {self._fps_render:.1f}
2008
+
2009
+ Shapes: {len(self._shapes)}
2010
+ Instances: {len(self._instances)}"""
2011
+ if self.paused:
2012
+ text += "\nPaused (press space to resume)"
2013
+
2014
+ self._info_label.text = text
2015
+ self._info_label.y = self.screen_height - 5
2016
+ self._info_label.draw()
2017
+
2018
+ gl.glEnable(gl.GL_DEPTH_TEST)
2019
+
2020
+ for cb in self.render_2d_callbacks:
2021
+ cb()
2022
+
2023
+ def _draw_grid(self, is_tiled=False):
2024
+ gl = OpenGLRenderer.gl
2025
+
2026
+ self._switch_context()
2027
+
2028
+ if not is_tiled:
2029
+ gl.glUseProgram(self._grid_shader.id)
2030
+
2031
+ gl.glUniformMatrix4fv(self._loc_grid_view, 1, gl.GL_FALSE, arr_pointer(self._view_matrix))
2032
+ gl.glUniformMatrix4fv(self._loc_grid_projection, 1, gl.GL_FALSE, arr_pointer(self._projection_matrix))
2033
+
2034
+ gl.glBindVertexArray(self._grid_vao)
2035
+ gl.glDrawArrays(gl.GL_LINES, 0, self._grid_vertex_count)
2036
+ gl.glBindVertexArray(0)
2037
+
2038
+ def _draw_sky(self, is_tiled=False):
2039
+ gl = OpenGLRenderer.gl
2040
+
2041
+ self._switch_context()
2042
+
2043
+ if not is_tiled:
2044
+ gl.glUseProgram(self._sky_shader.id)
2045
+
2046
+ gl.glUniformMatrix4fv(self._loc_sky_view, 1, gl.GL_FALSE, arr_pointer(self._view_matrix))
2047
+ gl.glUniformMatrix4fv(self._loc_sky_projection, 1, gl.GL_FALSE, arr_pointer(self._projection_matrix))
2048
+ gl.glUniform3f(self._loc_sky_view_pos, *self._camera_pos)
2049
+
2050
+ gl.glBindVertexArray(self._sky_vao)
2051
+ gl.glDrawElements(gl.GL_TRIANGLES, self._sky_tri_count, gl.GL_UNSIGNED_INT, None)
2052
+ gl.glBindVertexArray(0)
2053
+
2054
+ def _render_scene(self):
2055
+ gl = OpenGLRenderer.gl
2056
+
2057
+ self._switch_context()
2058
+
2059
+ start_instance_idx = 0
2060
+
2061
+ for shape, (vao, _, _, tri_count, _) in self._shape_gl_buffers.items():
2062
+ num_instances = len(self._shape_instances[shape])
2063
+
2064
+ gl.glBindVertexArray(vao)
2065
+ if self.use_legacy_opengl:
2066
+ if self.instance_matrix_size > 0 and self.instance_color_size:
2067
+ # update attribute pointers
2068
+ matrix_size = self.instance_matrix_size
2069
+ color_size = self.instance_color_size
2070
+
2071
+ # update transforms
2072
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_transform_gl_buffer)
2073
+ for i in range(4):
2074
+ gl.glVertexAttribPointer(
2075
+ 3 + i,
2076
+ 4,
2077
+ gl.GL_FLOAT,
2078
+ gl.GL_FALSE,
2079
+ matrix_size,
2080
+ ctypes.c_void_p(start_instance_idx * matrix_size + i * matrix_size // 4),
2081
+ )
2082
+ gl.glVertexAttribDivisor(3 + i, 1)
2083
+
2084
+ # update colors
2085
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color1_buffer)
2086
+ gl.glVertexAttribPointer(
2087
+ 7, 3, gl.GL_FLOAT, gl.GL_FALSE, color_size, ctypes.c_void_p(start_instance_idx * color_size)
2088
+ )
2089
+ gl.glVertexAttribDivisor(7, 1)
2090
+
2091
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color2_buffer)
2092
+ gl.glVertexAttribPointer(
2093
+ 8, 3, gl.GL_FLOAT, gl.GL_FALSE, color_size, ctypes.c_void_p(start_instance_idx * color_size)
2094
+ )
2095
+ gl.glVertexAttribDivisor(8, 1)
2096
+
2097
+ gl.glDrawElementsInstanced(gl.GL_TRIANGLES, tri_count, gl.GL_UNSIGNED_INT, None, num_instances)
2098
+ else:
2099
+ gl.glDrawElementsInstancedBaseInstance(
2100
+ gl.GL_TRIANGLES, tri_count, gl.GL_UNSIGNED_INT, None, num_instances, start_instance_idx
2101
+ )
2102
+
2103
+ start_instance_idx += num_instances
2104
+
2105
+ if self.draw_axis:
2106
+ self._axis_instancer.render()
2107
+
2108
+ for instancer in self._shape_instancers.values():
2109
+ instancer.render()
2110
+
2111
+ gl.glBindVertexArray(0)
2112
+
2113
+ def _render_scene_tiled(self):
2114
+ gl = OpenGLRenderer.gl
2115
+
2116
+ self._switch_context()
2117
+
2118
+ for i, viewport in enumerate(self._tile_viewports):
2119
+ projection_matrix_ptr = arr_pointer(self._tile_projection_matrices[i])
2120
+ view_matrix_ptr = arr_pointer(
2121
+ self._tile_view_matrices[i] if self._tile_view_matrices[i] is not None else self._view_matrix
2122
+ )
2123
+
2124
+ gl.glViewport(*viewport)
2125
+ if self.draw_grid:
2126
+ gl.glUseProgram(self._grid_shader.id)
2127
+ gl.glUniformMatrix4fv(self._loc_grid_projection, 1, gl.GL_FALSE, projection_matrix_ptr)
2128
+ gl.glUniformMatrix4fv(self._loc_grid_view, 1, gl.GL_FALSE, view_matrix_ptr)
2129
+ self._draw_grid(is_tiled=True)
2130
+
2131
+ if self.draw_sky:
2132
+ gl.glUseProgram(self._sky_shader.id)
2133
+ gl.glUniformMatrix4fv(self._loc_sky_projection, 1, gl.GL_FALSE, projection_matrix_ptr)
2134
+ gl.glUniformMatrix4fv(self._loc_sky_view, 1, gl.GL_FALSE, view_matrix_ptr)
2135
+ self._draw_sky(is_tiled=True)
2136
+
2137
+ gl.glUseProgram(self._shape_shader.id)
2138
+ gl.glUniformMatrix4fv(self._loc_shape_projection, 1, gl.GL_FALSE, projection_matrix_ptr)
2139
+ gl.glUniformMatrix4fv(self._loc_shape_view, 1, gl.GL_FALSE, view_matrix_ptr)
2140
+
2141
+ instances = self._tile_instances[i]
2142
+
2143
+ for instance in instances:
2144
+ shape = self._instance_shape[instance]
2145
+
2146
+ vao, _, _, tri_count, _ = self._shape_gl_buffers[shape]
2147
+
2148
+ start_instance_idx = self._inverse_instance_ids[instance]
2149
+
2150
+ gl.glBindVertexArray(vao)
2151
+ if self.use_legacy_opengl:
2152
+ if self.instance_matrix_size > 0 and self.instance_color_size:
2153
+ # update attribute pointers
2154
+ matrix_size = self.instance_matrix_size
2155
+ color_size = self.instance_color_size
2156
+
2157
+ # update transforms
2158
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_transform_gl_buffer)
2159
+ for j in range(4):
2160
+ gl.glVertexAttribPointer(
2161
+ 3 + j,
2162
+ 4,
2163
+ gl.GL_FLOAT,
2164
+ gl.GL_FALSE,
2165
+ matrix_size,
2166
+ ctypes.c_void_p(start_instance_idx * matrix_size + j * matrix_size // 4),
2167
+ )
2168
+ gl.glVertexAttribDivisor(3 + j, 1)
2169
+
2170
+ # update colors
2171
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color1_buffer)
2172
+ gl.glVertexAttribPointer(
2173
+ 7, 3, gl.GL_FLOAT, gl.GL_FALSE, color_size, ctypes.c_void_p(start_instance_idx * color_size)
2174
+ )
2175
+ gl.glVertexAttribDivisor(7, 1)
2176
+
2177
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color2_buffer)
2178
+ gl.glVertexAttribPointer(
2179
+ 8, 3, gl.GL_FLOAT, gl.GL_FALSE, color_size, ctypes.c_void_p(start_instance_idx * color_size)
2180
+ )
2181
+ gl.glVertexAttribDivisor(8, 1)
2182
+
2183
+ gl.glDrawElementsInstanced(gl.GL_TRIANGLES, tri_count, gl.GL_UNSIGNED_INT, None, 1)
2184
+ else:
2185
+ gl.glDrawElementsInstancedBaseInstance(
2186
+ gl.GL_TRIANGLES, tri_count, gl.GL_UNSIGNED_INT, None, 1, start_instance_idx
2187
+ )
2188
+
2189
+ if self.draw_axis:
2190
+ self._axis_instancer.render()
2191
+
2192
+ for instancer in self._shape_instancers.values():
2193
+ instancer.render()
2194
+
2195
+ gl.glBindVertexArray(0)
2196
+
2197
+ def _close_callback(self):
2198
+ self.close()
2199
+
2200
+ def _mouse_drag_callback(self, x, y, dx, dy, buttons, modifiers):
2201
+ if not self.enable_mouse_interaction:
2202
+ return
2203
+
2204
+ import pyglet
2205
+ from pyglet.math import Vec3 as PyVec3
2206
+
2207
+ if buttons & pyglet.window.mouse.LEFT:
2208
+ sensitivity = 0.1
2209
+ dx *= sensitivity
2210
+ dy *= sensitivity
2211
+
2212
+ self._yaw += dx
2213
+ self._pitch += dy
2214
+
2215
+ self._pitch = max(min(self._pitch, 89.0), -89.0)
2216
+
2217
+ self._camera_front = PyVec3(
2218
+ np.cos(np.deg2rad(self._yaw)) * np.cos(np.deg2rad(self._pitch)),
2219
+ np.sin(np.deg2rad(self._pitch)),
2220
+ np.sin(np.deg2rad(self._yaw)) * np.cos(np.deg2rad(self._pitch)),
2221
+ ).normalize()
2222
+
2223
+ self.update_view_matrix()
2224
+
2225
+ def _scroll_callback(self, x, y, scroll_x, scroll_y):
2226
+ if not self.enable_mouse_interaction:
2227
+ return
2228
+
2229
+ self.camera_fov -= scroll_y
2230
+ self.camera_fov = max(min(self.camera_fov, 90.0), 15.0)
2231
+ self.update_projection_matrix()
2232
+
2233
+ def _process_inputs(self):
2234
+ import pyglet
2235
+ from pyglet.math import Vec3 as PyVec3
2236
+
2237
+ for cb in self._input_processors:
2238
+ if cb(self._key_handler) == pyglet.event.EVENT_HANDLED:
2239
+ return
2240
+
2241
+ if self._key_handler[pyglet.window.key.W] or self._key_handler[pyglet.window.key.UP]:
2242
+ self._camera_pos += self._camera_front * (self._camera_speed * self._frame_speed)
2243
+ self.update_view_matrix()
2244
+ if self._key_handler[pyglet.window.key.S] or self._key_handler[pyglet.window.key.DOWN]:
2245
+ self._camera_pos -= self._camera_front * (self._camera_speed * self._frame_speed)
2246
+ self.update_view_matrix()
2247
+ if self._key_handler[pyglet.window.key.A] or self._key_handler[pyglet.window.key.LEFT]:
2248
+ camera_side = PyVec3.cross(self._camera_front, self._camera_up).normalize()
2249
+ self._camera_pos -= camera_side * (self._camera_speed * self._frame_speed)
2250
+ self.update_view_matrix()
2251
+ if self._key_handler[pyglet.window.key.D] or self._key_handler[pyglet.window.key.RIGHT]:
2252
+ camera_side = PyVec3.cross(self._camera_front, self._camera_up).normalize()
2253
+ self._camera_pos += camera_side * (self._camera_speed * self._frame_speed)
2254
+ self.update_view_matrix()
2255
+
2256
+ def register_input_processor(self, callback):
2257
+ self._input_processors.append(callback)
2258
+
2259
+ def _key_press_callback(self, symbol, modifiers):
2260
+ import pyglet
2261
+
2262
+ if not self.enable_keyboard_interaction:
2263
+ return
2264
+
2265
+ for cb in self._key_callbacks:
2266
+ if cb(symbol, modifiers) == pyglet.event.EVENT_HANDLED:
2267
+ return pyglet.event.EVENT_HANDLED
2268
+
2269
+ if symbol == pyglet.window.key.ESCAPE:
2270
+ self.close()
2271
+ if symbol == pyglet.window.key.SPACE:
2272
+ self.paused = not self.paused
2273
+ if symbol == pyglet.window.key.TAB:
2274
+ self.skip_rendering = not self.skip_rendering
2275
+ if symbol == pyglet.window.key.C:
2276
+ self.draw_axis = not self.draw_axis
2277
+ if symbol == pyglet.window.key.G:
2278
+ self.draw_grid = not self.draw_grid
2279
+ if symbol == pyglet.window.key.I:
2280
+ self.show_info = not self.show_info
2281
+ if symbol == pyglet.window.key.X:
2282
+ self.render_wireframe = not self.render_wireframe
2283
+ if symbol == pyglet.window.key.T:
2284
+ self.render_depth = not self.render_depth
2285
+ if symbol == pyglet.window.key.B:
2286
+ self.enable_backface_culling = not self.enable_backface_culling
2287
+
2288
+ def register_key_press_callback(self, callback):
2289
+ self._key_callbacks.append(callback)
2290
+
2291
+ def _window_resize_callback(self, width, height):
2292
+ self._first_mouse = True
2293
+ self.screen_width, self.screen_height = self.window.get_framebuffer_size()
2294
+ self.update_projection_matrix()
2295
+ self._setup_framebuffer()
2296
+
2297
+ def register_shape(self, geo_hash, vertices, indices, color1=None, color2=None):
2298
+ gl = OpenGLRenderer.gl
2299
+
2300
+ self._switch_context()
2301
+
2302
+ shape = len(self._shapes)
2303
+ if color1 is None:
2304
+ color1 = tab10_color_map(len(self._shape_geo_hash))
2305
+ if color2 is None:
2306
+ color2 = np.clip(np.array(color1) + 0.25, 0.0, 1.0)
2307
+ # TODO check if we actually need to store the shape data
2308
+ self._shapes.append((vertices, indices, color1, color2, geo_hash))
2309
+ self._shape_geo_hash[geo_hash] = shape
2310
+
2311
+ gl.glUseProgram(self._shape_shader.id)
2312
+
2313
+ # Create VAO, VBO, and EBO
2314
+ vao = gl.GLuint()
2315
+ gl.glGenVertexArrays(1, vao)
2316
+ gl.glBindVertexArray(vao)
2317
+
2318
+ vbo = gl.GLuint()
2319
+ gl.glGenBuffers(1, vbo)
2320
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, vbo)
2321
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, vertices.nbytes, vertices.ctypes.data, gl.GL_STATIC_DRAW)
2322
+
2323
+ vertex_cuda_buffer = wp.RegisteredGLBuffer(int(vbo.value), self._device)
2324
+
2325
+ ebo = gl.GLuint()
2326
+ gl.glGenBuffers(1, ebo)
2327
+ gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, ebo)
2328
+ gl.glBufferData(gl.GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices.ctypes.data, gl.GL_STATIC_DRAW)
2329
+
2330
+ # Set up vertex attributes
2331
+ vertex_stride = vertices.shape[1] * vertices.itemsize
2332
+ # positions
2333
+ gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(0))
2334
+ gl.glEnableVertexAttribArray(0)
2335
+ # normals
2336
+ gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(3 * vertices.itemsize))
2337
+ gl.glEnableVertexAttribArray(1)
2338
+ # uv coordinates
2339
+ gl.glVertexAttribPointer(2, 2, gl.GL_FLOAT, gl.GL_FALSE, vertex_stride, ctypes.c_void_p(6 * vertices.itemsize))
2340
+ gl.glEnableVertexAttribArray(2)
2341
+
2342
+ gl.glBindVertexArray(0)
2343
+
2344
+ self._shape_gl_buffers[shape] = (vao, vbo, ebo, len(indices), vertex_cuda_buffer)
2345
+
2346
+ return shape
2347
+
2348
+ def deregister_shape(self, shape):
2349
+ gl = OpenGLRenderer.gl
2350
+
2351
+ self._switch_context()
2352
+
2353
+ if shape not in self._shape_gl_buffers:
2354
+ return
2355
+
2356
+ vao, vbo, ebo, _, vertex_cuda_buffer = self._shape_gl_buffers[shape]
2357
+ try:
2358
+ gl.glDeleteVertexArrays(1, vao)
2359
+ gl.glDeleteBuffers(1, vbo)
2360
+ gl.glDeleteBuffers(1, ebo)
2361
+ except gl.GLException:
2362
+ pass
2363
+
2364
+ _, _, _, _, geo_hash = self._shapes[shape]
2365
+ del self._shape_geo_hash[geo_hash]
2366
+ del self._shape_gl_buffers[shape]
2367
+ self._shapes.pop(shape)
2368
+
2369
+ def add_shape_instance(
2370
+ self,
2371
+ name: str,
2372
+ shape: int,
2373
+ body,
2374
+ pos: tuple,
2375
+ rot: tuple,
2376
+ scale: tuple = (1.0, 1.0, 1.0),
2377
+ color1=None,
2378
+ color2=None,
2379
+ custom_index: int = -1,
2380
+ visible: bool = True,
2381
+ ):
2382
+ if color1 is None:
2383
+ color1 = self._shapes[shape][2]
2384
+ if color2 is None:
2385
+ color2 = self._shapes[shape][3]
2386
+ instance = len(self._instances)
2387
+ self._shape_instances[shape].append(instance)
2388
+ body = self._resolve_body_id(body)
2389
+ self._instances[name] = (instance, body, shape, [*pos, *rot], scale, color1, color2, visible)
2390
+ self._instance_shape[instance] = shape
2391
+ self._instance_custom_ids[instance] = custom_index
2392
+ self._add_shape_instances = True
2393
+ self._instance_count = len(self._instances)
2394
+ return instance
2395
+
2396
+ def remove_shape_instance(self, name: str):
2397
+ if name not in self._instances:
2398
+ return
2399
+
2400
+ instance, _, shape, _, _, _, _, _ = self._instances[name]
2401
+
2402
+ self._shape_instances[shape].remove(instance)
2403
+ self._instance_count = len(self._instances)
2404
+ self._add_shape_instances = self._instance_count > 0
2405
+ del self._instance_shape[instance]
2406
+ del self._instance_custom_ids[instance]
2407
+ del self._instances[name]
2408
+
2409
+ def update_instance_colors(self):
2410
+ gl = OpenGLRenderer.gl
2411
+
2412
+ self._switch_context()
2413
+
2414
+ colors1, colors2 = [], []
2415
+ all_instances = list(self._instances.values())
2416
+ for _shape, instances in self._shape_instances.items():
2417
+ for i in instances:
2418
+ if i >= len(all_instances):
2419
+ continue
2420
+ instance = all_instances[i]
2421
+ colors1.append(instance[5])
2422
+ colors2.append(instance[6])
2423
+ colors1 = np.array(colors1, dtype=np.float32)
2424
+ colors2 = np.array(colors2, dtype=np.float32)
2425
+
2426
+ # create color buffers
2427
+ if self._instance_color1_buffer is None:
2428
+ self._instance_color1_buffer = gl.GLuint()
2429
+ gl.glGenBuffers(1, self._instance_color1_buffer)
2430
+ if self._instance_color2_buffer is None:
2431
+ self._instance_color2_buffer = gl.GLuint()
2432
+ gl.glGenBuffers(1, self._instance_color2_buffer)
2433
+
2434
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color1_buffer)
2435
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, colors1.nbytes, colors1.ctypes.data, gl.GL_STATIC_DRAW)
2436
+
2437
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color2_buffer)
2438
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, colors2.nbytes, colors2.ctypes.data, gl.GL_STATIC_DRAW)
2439
+
2440
+ def allocate_shape_instances(self):
2441
+ gl = OpenGLRenderer.gl
2442
+
2443
+ self._switch_context()
2444
+
2445
+ self._add_shape_instances = False
2446
+ self._wp_instance_transforms = wp.array(
2447
+ [instance[3] for instance in self._instances.values()], dtype=wp.transform, device=self._device
2448
+ )
2449
+ self._wp_instance_scalings = wp.array(
2450
+ [instance[4] for instance in self._instances.values()], dtype=wp.vec3, device=self._device
2451
+ )
2452
+ self._wp_instance_bodies = wp.array(
2453
+ [instance[1] for instance in self._instances.values()], dtype=wp.int32, device=self._device
2454
+ )
2455
+
2456
+ gl.glUseProgram(self._shape_shader.id)
2457
+ if self._instance_transform_gl_buffer is None:
2458
+ # create instance buffer and bind it as an instanced array
2459
+ self._instance_transform_gl_buffer = gl.GLuint()
2460
+ gl.glGenBuffers(1, self._instance_transform_gl_buffer)
2461
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_transform_gl_buffer)
2462
+
2463
+ transforms = np.tile(np.diag(np.ones(4, dtype=np.float32)), (len(self._instances), 1, 1))
2464
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, transforms.nbytes, transforms.ctypes.data, gl.GL_DYNAMIC_DRAW)
2465
+
2466
+ # create CUDA buffer for instance transforms
2467
+ self._instance_transform_cuda_buffer = wp.RegisteredGLBuffer(
2468
+ int(self._instance_transform_gl_buffer.value), self._device
2469
+ )
2470
+
2471
+ self.update_instance_colors()
2472
+
2473
+ # set up instance attribute pointers
2474
+ matrix_size = transforms[0].nbytes
2475
+ self.instance_matrix_size = matrix_size
2476
+
2477
+ instance_ids = []
2478
+ instance_custom_ids = []
2479
+ instance_visible = []
2480
+ instances = list(self._instances.values())
2481
+ inverse_instance_ids = {}
2482
+ instance_count = 0
2483
+ colors_size = np.zeros(3, dtype=np.float32).nbytes
2484
+ self.instance_color_size = colors_size
2485
+ for shape, (vao, _vbo, _ebo, _tri_count, _vertex_cuda_buffer) in self._shape_gl_buffers.items():
2486
+ gl.glBindVertexArray(vao)
2487
+
2488
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_transform_gl_buffer)
2489
+
2490
+ # we can only send vec4s to the shader, so we need to split the instance transforms matrix into its column vectors
2491
+ for i in range(4):
2492
+ gl.glVertexAttribPointer(
2493
+ 3 + i, 4, gl.GL_FLOAT, gl.GL_FALSE, matrix_size, ctypes.c_void_p(i * matrix_size // 4)
2494
+ )
2495
+ gl.glEnableVertexAttribArray(3 + i)
2496
+ gl.glVertexAttribDivisor(3 + i, 1)
2497
+
2498
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color1_buffer)
2499
+ gl.glVertexAttribPointer(7, 3, gl.GL_FLOAT, gl.GL_FALSE, colors_size, ctypes.c_void_p(0))
2500
+ gl.glEnableVertexAttribArray(7)
2501
+ gl.glVertexAttribDivisor(7, 1)
2502
+
2503
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._instance_color2_buffer)
2504
+ gl.glVertexAttribPointer(8, 3, gl.GL_FLOAT, gl.GL_FALSE, colors_size, ctypes.c_void_p(0))
2505
+ gl.glEnableVertexAttribArray(8)
2506
+ gl.glVertexAttribDivisor(8, 1)
2507
+
2508
+ instance_ids.extend(self._shape_instances[shape])
2509
+ for i in self._shape_instances[shape]:
2510
+ inverse_instance_ids[i] = instance_count
2511
+ instance_count += 1
2512
+ instance_custom_ids.append(self._instance_custom_ids[i])
2513
+ instance_visible.append(instances[i][7])
2514
+
2515
+ # trigger update to the instance transforms
2516
+ self._update_shape_instances = True
2517
+
2518
+ self._wp_instance_ids = wp.array(instance_ids, dtype=wp.int32, device=self._device)
2519
+ self._wp_instance_custom_ids = wp.array(instance_custom_ids, dtype=wp.int32, device=self._device)
2520
+ self._np_instance_visible = np.array(instance_visible)
2521
+ self._instance_ids = instance_ids
2522
+ self._inverse_instance_ids = inverse_instance_ids
2523
+
2524
+ gl.glBindVertexArray(0)
2525
+
2526
+ def update_shape_instance(self, name, pos=None, rot=None, color1=None, color2=None, scale=None, visible=None):
2527
+ """Update the instance properties of the shape
2528
+
2529
+ Args:
2530
+ name: The name of the shape
2531
+ pos: The position of the shape
2532
+ rot: The rotation of the shape
2533
+ color1: The first color of the checker pattern
2534
+ color2: The second color of the checker pattern
2535
+ visible: Whether the shape is visible
2536
+ """
2537
+ gl = OpenGLRenderer.gl
2538
+
2539
+ self._switch_context()
2540
+
2541
+ if name in self._instances:
2542
+ i, body, shape, tf, old_scale, old_color1, old_color2, v = self._instances[name]
2543
+ if visible is None:
2544
+ visible = v
2545
+ new_tf = np.copy(tf)
2546
+ if pos is not None:
2547
+ new_tf[:3] = pos
2548
+ if rot is not None:
2549
+ new_tf[3:] = rot
2550
+ self._instances[name] = (
2551
+ i,
2552
+ body,
2553
+ shape,
2554
+ new_tf,
2555
+ old_scale if scale is None else scale,
2556
+ old_color1 if color1 is None else color1,
2557
+ old_color2 if color2 is None else color2,
2558
+ visible,
2559
+ )
2560
+ self._update_shape_instances = True
2561
+ if color1 is not None or color2 is not None:
2562
+ vao, vbo, ebo, tri_count, vertex_cuda_buffer = self._shape_gl_buffers[shape]
2563
+ gl.glBindVertexArray(vao)
2564
+ self.update_instance_colors()
2565
+ gl.glBindVertexArray(0)
2566
+ return True
2567
+ return False
2568
+
2569
+ def update_shape_instances(self):
2570
+ with self._shape_shader:
2571
+ self._update_shape_instances = False
2572
+ self._wp_instance_transforms = wp.array(
2573
+ [instance[3] for instance in self._instances.values()], dtype=wp.transform, device=self._device
2574
+ )
2575
+ self.update_body_transforms(None)
2576
+
2577
+ def update_body_transforms(self, body_tf: wp.array):
2578
+ if self._instance_transform_cuda_buffer is None:
2579
+ return
2580
+
2581
+ body_q = None
2582
+ if body_tf is not None:
2583
+ if body_tf.device.is_cuda:
2584
+ body_q = body_tf
2585
+ else:
2586
+ body_q = body_tf.to(self._device)
2587
+
2588
+ vbo_transforms = self._instance_transform_cuda_buffer.map(dtype=wp.mat44, shape=(self._instance_count,))
2589
+
2590
+ wp.launch(
2591
+ update_vbo_transforms,
2592
+ dim=self._instance_count,
2593
+ inputs=[
2594
+ self._wp_instance_ids,
2595
+ self._wp_instance_bodies,
2596
+ self._wp_instance_transforms,
2597
+ self._wp_instance_scalings,
2598
+ body_q,
2599
+ ],
2600
+ outputs=[
2601
+ vbo_transforms,
2602
+ ],
2603
+ device=self._device,
2604
+ record_tape=False,
2605
+ )
2606
+
2607
+ self._instance_transform_cuda_buffer.unmap()
2608
+
2609
+ def register_body(self, name):
2610
+ # register body name and return its ID
2611
+ if name not in self._body_name:
2612
+ self._body_name[name] = len(self._body_name)
2613
+ return self._body_name[name]
2614
+
2615
+ def _resolve_body_id(self, body):
2616
+ if body is None:
2617
+ return -1
2618
+ if isinstance(body, int):
2619
+ return body
2620
+ return self._body_name[body]
2621
+
2622
+ def is_running(self):
2623
+ return not self.app.event_loop.has_exit
2624
+
2625
+ def save(self):
2626
+ # save just keeps the window open to allow the user to interact with the scene
2627
+ while not self.app.event_loop.has_exit:
2628
+ self.update()
2629
+ if self.app.event_loop.has_exit:
2630
+ self.clear()
2631
+ self.app.event_loop.exit()
2632
+
2633
+ def get_pixels(self, target_image: wp.array, split_up_tiles=True, mode="rgb", use_uint8=False):
2634
+ """
2635
+ Read the pixels from the frame buffer (RGB or depth are supported) into the given array.
2636
+
2637
+ If `split_up_tiles` is False, array must be of shape (screen_height, screen_width, 3) for RGB mode or
2638
+ (screen_height, screen_width, 1) for depth mode.
2639
+ If `split_up_tiles` is True, the pixels will be split up into tiles (see :attr:`tile_width` and :attr:`tile_height` for dimensions):
2640
+ 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.
2641
+
2642
+ Args:
2643
+ target_image (array): The array to read the pixels into. Must have float32 as dtype and be on a CUDA device.
2644
+ split_up_tiles (bool): Whether to split up the viewport into tiles, see :meth:`setup_tiled_rendering`.
2645
+ mode (str): can be either "rgb" or "depth"
2646
+ use_uint8 (bool): Whether to use uint8 as dtype in RGB mode for the target_image array and return values in the range [0, 255]. Otherwise, float32 is assumed as dtype with values in the range [0, 1].
2647
+
2648
+ Returns:
2649
+ bool: Whether the pixels were successfully read.
2650
+ """
2651
+ gl = OpenGLRenderer.gl
2652
+
2653
+ self._switch_context()
2654
+
2655
+ channels = 3 if mode == "rgb" else 1
2656
+
2657
+ if split_up_tiles:
2658
+ assert self._tile_width is not None and self._tile_height is not None, (
2659
+ "Tile width and height are not set, tiles must all have the same size"
2660
+ )
2661
+ assert all(vp[2] == self._tile_width for vp in self._tile_viewports), (
2662
+ "Tile widths do not all equal global tile_width, use `get_tile_pixels` instead to retrieve pixels for a single tile"
2663
+ )
2664
+ assert all(vp[3] == self._tile_height for vp in self._tile_viewports), (
2665
+ "Tile heights do not all equal global tile_height, use `get_tile_pixels` instead to retrieve pixels for a single tile"
2666
+ )
2667
+ assert target_image.shape == (
2668
+ self.num_tiles,
2669
+ self._tile_height,
2670
+ self._tile_width,
2671
+ channels,
2672
+ ), (
2673
+ f"Shape of `target_image` array does not match {self.num_tiles} x {self._tile_height} x {self._tile_width} x {channels}"
2674
+ )
2675
+ else:
2676
+ assert target_image.shape == (
2677
+ self.screen_height,
2678
+ self.screen_width,
2679
+ channels,
2680
+ ), f"Shape of `target_image` array does not match {self.screen_height} x {self.screen_width} x {channels}"
2681
+
2682
+ gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, self._frame_pbo)
2683
+ if mode == "rgb":
2684
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_texture)
2685
+ if mode == "depth":
2686
+ gl.glBindTexture(gl.GL_TEXTURE_2D, self._frame_depth_texture)
2687
+ try:
2688
+ # read screen texture into PBO
2689
+ if mode == "rgb":
2690
+ gl.glGetTexImage(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, ctypes.c_void_p(0))
2691
+ elif mode == "depth":
2692
+ gl.glGetTexImage(gl.GL_TEXTURE_2D, 0, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT, ctypes.c_void_p(0))
2693
+ except gl.GLException:
2694
+ # this can happen if the window is closed/being moved to a different display
2695
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
2696
+ gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, 0)
2697
+ return False
2698
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
2699
+ gl.glBindBuffer(gl.GL_PIXEL_PACK_BUFFER, 0)
2700
+
2701
+ pbo_buffer = wp.RegisteredGLBuffer(int(self._frame_pbo.value), self._device, wp.RegisteredGLBuffer.READ_ONLY)
2702
+ screen_size = self.screen_height * self.screen_width
2703
+ if mode == "rgb":
2704
+ img = pbo_buffer.map(dtype=wp.uint8, shape=(screen_size * channels))
2705
+ elif mode == "depth":
2706
+ img = pbo_buffer.map(dtype=wp.float32, shape=(screen_size * channels))
2707
+ img = img.to(target_image.device)
2708
+ if split_up_tiles:
2709
+ positions = wp.array(self._tile_viewports, ndim=2, dtype=wp.int32, device=target_image.device)
2710
+ if mode == "rgb":
2711
+ wp.launch(
2712
+ copy_rgb_frame_tiles_uint8 if use_uint8 else copy_rgb_frame_tiles,
2713
+ dim=(self.num_tiles, self._tile_width, self._tile_height),
2714
+ inputs=[img, positions, self.screen_width, self.screen_height, self._tile_height],
2715
+ outputs=[target_image],
2716
+ device=target_image.device,
2717
+ )
2718
+ elif mode == "depth":
2719
+ wp.launch(
2720
+ copy_depth_frame_tiles,
2721
+ dim=(self.num_tiles, self._tile_width, self._tile_height),
2722
+ inputs=[
2723
+ img,
2724
+ positions,
2725
+ self.screen_width,
2726
+ self.screen_height,
2727
+ self._tile_height,
2728
+ self.camera_near_plane,
2729
+ self.camera_far_plane,
2730
+ ],
2731
+ outputs=[target_image],
2732
+ device=target_image.device,
2733
+ )
2734
+ else:
2735
+ if mode == "rgb":
2736
+ wp.launch(
2737
+ copy_rgb_frame_uint8 if use_uint8 else copy_rgb_frame,
2738
+ dim=(self.screen_width, self.screen_height),
2739
+ inputs=[img, self.screen_width, self.screen_height],
2740
+ outputs=[target_image],
2741
+ device=target_image.device,
2742
+ )
2743
+ elif mode == "depth":
2744
+ wp.launch(
2745
+ copy_depth_frame,
2746
+ dim=(self.screen_width, self.screen_height),
2747
+ inputs=[img, self.screen_width, self.screen_height, self.camera_near_plane, self.camera_far_plane],
2748
+ outputs=[target_image],
2749
+ device=target_image.device,
2750
+ )
2751
+ pbo_buffer.unmap()
2752
+ return True
2753
+
2754
+ # def create_image_texture(self, file_path):
2755
+ # from PIL import Image
2756
+ # img = Image.open(file_path)
2757
+ # img_data = np.array(list(img.getdata()), np.uint8)
2758
+ # texture = glGenTextures(1)
2759
+ # glBindTexture(GL_TEXTURE_2D, texture)
2760
+ # glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img.width, img.height, 0, GL_RGB, GL_UNSIGNED_BYTE, img_data)
2761
+ # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
2762
+ # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
2763
+ # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
2764
+ # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
2765
+ # return texture
2766
+
2767
+ # def create_check_texture(self, color1=(0, 0.5, 1.0), color2=None, width=default_texture_size, height=default_texture_size):
2768
+ # if width == 1 and height == 1:
2769
+ # pixels = np.array([np.array(color1)*255], dtype=np.uint8)
2770
+ # else:
2771
+ # pixels = np.zeros((width, height, 3), dtype=np.uint8)
2772
+ # half_w = width // 2
2773
+ # half_h = height // 2
2774
+ # color1 = np.array(np.array(color1)*255, dtype=np.uint8)
2775
+ # pixels[0:half_w, 0:half_h] = color1
2776
+ # pixels[half_w:width, half_h:height] = color1
2777
+ # if color2 is None:
2778
+ # color2 = np.array(np.clip(np.array(color1, dtype=np.float32) + 50, 0, 255), dtype=np.uint8)
2779
+ # else:
2780
+ # color2 = np.array(np.array(color2)*255, dtype=np.uint8)
2781
+ # pixels[half_w:width, 0:half_h] = color2
2782
+ # pixels[0:half_w, half_h:height] = color2
2783
+ # texture = glGenTextures(1)
2784
+ # glBindTexture(GL_TEXTURE_2D, texture)
2785
+ # glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels.flatten())
2786
+ # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
2787
+ # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
2788
+ # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
2789
+ # glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
2790
+ # return texture
2791
+
2792
+ def render_plane(
2793
+ self,
2794
+ name: str,
2795
+ pos: tuple,
2796
+ rot: tuple,
2797
+ width: float,
2798
+ length: float,
2799
+ color: tuple = (1.0, 1.0, 1.0),
2800
+ color2=None,
2801
+ parent_body: str | None = None,
2802
+ is_template: bool = False,
2803
+ u_scaling=1.0,
2804
+ v_scaling=1.0,
2805
+ visible: bool = True,
2806
+ ):
2807
+ """Add a plane for visualization
2808
+
2809
+ Args:
2810
+ name: The name of the plane
2811
+ pos: The position of the plane
2812
+ rot: The rotation of the plane
2813
+ width: The width of the plane
2814
+ length: The length of the plane
2815
+ color: The color of the plane
2816
+ texture: The texture of the plane (optional)
2817
+ """
2818
+ geo_hash = hash(("plane", width, length))
2819
+ if geo_hash in self._shape_geo_hash:
2820
+ shape = self._shape_geo_hash[geo_hash]
2821
+ if self.update_shape_instance(name, pos, rot):
2822
+ return shape
2823
+ else:
2824
+ faces = np.array([0, 1, 2, 2, 3, 0], dtype=np.uint32)
2825
+ normal = (0.0, 1.0, 0.0)
2826
+ width = width if width > 0.0 else 100.0
2827
+ length = length if length > 0.0 else 100.0
2828
+ aspect = width / length
2829
+ u = width * aspect * u_scaling
2830
+ v = length * v_scaling
2831
+ gfx_vertices = np.array(
2832
+ [
2833
+ [-width, 0.0, -length, *normal, 0.0, 0.0],
2834
+ [-width, 0.0, length, *normal, 0.0, v],
2835
+ [width, 0.0, length, *normal, u, v],
2836
+ [width, 0.0, -length, *normal, u, 0.0],
2837
+ ],
2838
+ dtype=np.float32,
2839
+ )
2840
+ shape = self.register_shape(geo_hash, gfx_vertices, faces, color1=color, color2=color2)
2841
+ if not is_template:
2842
+ body = self._resolve_body_id(parent_body)
2843
+ self.add_shape_instance(name, shape, body, pos, rot)
2844
+ return shape
2845
+
2846
+ def render_ground(self, size: float = 1000.0, plane=None):
2847
+ """Add a ground plane for visualization
2848
+
2849
+ Args:
2850
+ size: The size of the ground plane
2851
+ """
2852
+ color1 = (200 / 255, 200 / 255, 200 / 255)
2853
+ color2 = (150 / 255, 150 / 255, 150 / 255)
2854
+ sqh = np.sqrt(0.5)
2855
+ if self._camera_axis == 0:
2856
+ q = (0.0, 0.0, -sqh, sqh)
2857
+ elif self._camera_axis == 1:
2858
+ q = (0.0, 0.0, 0.0, 1.0)
2859
+ elif self._camera_axis == 2:
2860
+ q = (sqh, 0.0, 0.0, sqh)
2861
+ pos = (0.0, 0.0, 0.0)
2862
+ if plane is not None:
2863
+ normal = np.array(plane[:3])
2864
+ normal /= np.linalg.norm(normal)
2865
+ pos = plane[3] * normal
2866
+ if np.allclose(normal, (0.0, 1.0, 0.0)):
2867
+ # no rotation necessary
2868
+ q = (0.0, 0.0, 0.0, 1.0)
2869
+ else:
2870
+ c = np.cross(normal, (0.0, 1.0, 0.0))
2871
+ angle = wp.float32(np.arcsin(np.linalg.norm(c)))
2872
+ axis = wp.vec3(np.abs(c))
2873
+ axis = wp.normalize(axis)
2874
+ q = wp.quat_from_axis_angle(axis, angle)
2875
+ return self.render_plane(
2876
+ "ground",
2877
+ pos,
2878
+ q,
2879
+ size,
2880
+ size,
2881
+ color1,
2882
+ color2=color2,
2883
+ u_scaling=1.0,
2884
+ v_scaling=1.0,
2885
+ )
2886
+
2887
+ def render_sphere(
2888
+ self,
2889
+ name: str,
2890
+ pos: tuple,
2891
+ rot: tuple,
2892
+ radius: float,
2893
+ parent_body: str | None = None,
2894
+ is_template: bool = False,
2895
+ color: tuple[float, float, float] | None = None,
2896
+ visible: bool = True,
2897
+ ):
2898
+ """Add a sphere for visualization
2899
+
2900
+ Args:
2901
+ pos: The position of the sphere
2902
+ radius: The radius of the sphere
2903
+ name: A name for the USD prim on the stage
2904
+ color: The color of the sphere
2905
+ """
2906
+ geo_hash = hash(("sphere", radius))
2907
+ if geo_hash in self._shape_geo_hash:
2908
+ shape = self._shape_geo_hash[geo_hash]
2909
+ if self.update_shape_instance(name, pos, rot, color1=color, color2=color):
2910
+ return shape
2911
+ else:
2912
+ vertices, indices = self._create_sphere_mesh(radius)
2913
+ shape = self.register_shape(geo_hash, vertices, indices, color1=color, color2=color)
2914
+ if not is_template:
2915
+ body = self._resolve_body_id(parent_body)
2916
+ self.add_shape_instance(name, shape, body, pos, rot, color1=color, color2=color)
2917
+ return shape
2918
+
2919
+ def render_capsule(
2920
+ self,
2921
+ name: str,
2922
+ pos: tuple,
2923
+ rot: tuple,
2924
+ radius: float,
2925
+ half_height: float,
2926
+ parent_body: str | None = None,
2927
+ is_template: bool = False,
2928
+ up_axis: int = 1,
2929
+ color: tuple[float, float, float] | None = None,
2930
+ visible: bool = True,
2931
+ ):
2932
+ """Add a capsule for visualization
2933
+
2934
+ Args:
2935
+ pos: The position of the capsule
2936
+ radius: The radius of the capsule
2937
+ half_height: The half height of the capsule
2938
+ name: A name for the USD prim on the stage
2939
+ up_axis: The axis of the capsule that points up (0: x, 1: y, 2: z)
2940
+ color: The color of the capsule
2941
+ """
2942
+ geo_hash = hash(("capsule", radius, half_height, up_axis))
2943
+ if geo_hash in self._shape_geo_hash:
2944
+ shape = self._shape_geo_hash[geo_hash]
2945
+ if self.update_shape_instance(name, pos, rot):
2946
+ return shape
2947
+ else:
2948
+ vertices, indices = self._create_capsule_mesh(radius, half_height, up_axis=up_axis)
2949
+ shape = self.register_shape(geo_hash, vertices, indices, color1=color, color2=color)
2950
+ if not is_template:
2951
+ body = self._resolve_body_id(parent_body)
2952
+ self.add_shape_instance(name, shape, body, pos, rot)
2953
+ return shape
2954
+
2955
+ def render_cylinder(
2956
+ self,
2957
+ name: str,
2958
+ pos: tuple,
2959
+ rot: tuple,
2960
+ radius: float,
2961
+ half_height: float,
2962
+ parent_body: str | None = None,
2963
+ is_template: bool = False,
2964
+ up_axis: int = 1,
2965
+ color: tuple[float, float, float] | None = None,
2966
+ visible: bool = True,
2967
+ ):
2968
+ """Add a cylinder for visualization
2969
+
2970
+ Args:
2971
+ pos: The position of the cylinder
2972
+ radius: The radius of the cylinder
2973
+ half_height: The half height of the cylinder
2974
+ name: A name for the USD prim on the stage
2975
+ up_axis: The axis of the cylinder that points up (0: x, 1: y, 2: z)
2976
+ color: The color of the capsule
2977
+ """
2978
+ geo_hash = hash(("cylinder", radius, half_height, up_axis))
2979
+ if geo_hash in self._shape_geo_hash:
2980
+ shape = self._shape_geo_hash[geo_hash]
2981
+ if self.update_shape_instance(name, pos, rot):
2982
+ return shape
2983
+ else:
2984
+ vertices, indices = self._create_cylinder_mesh(radius, half_height, up_axis=up_axis)
2985
+ shape = self.register_shape(geo_hash, vertices, indices, color1=color, color2=color)
2986
+ if not is_template:
2987
+ body = self._resolve_body_id(parent_body)
2988
+ self.add_shape_instance(name, shape, body, pos, rot)
2989
+ return shape
2990
+
2991
+ def render_cone(
2992
+ self,
2993
+ name: str,
2994
+ pos: tuple,
2995
+ rot: tuple,
2996
+ radius: float,
2997
+ half_height: float,
2998
+ parent_body: str | None = None,
2999
+ is_template: bool = False,
3000
+ up_axis: int = 1,
3001
+ color: tuple[float, float, float] | None = None,
3002
+ visible: bool = True,
3003
+ ):
3004
+ """Add a cone for visualization
3005
+
3006
+ Args:
3007
+ pos: The position of the cone
3008
+ radius: The radius of the cone
3009
+ half_height: The half height of the cone
3010
+ name: A name for the USD prim on the stage
3011
+ up_axis: The axis of the cone that points up (0: x, 1: y, 2: z)
3012
+ color: The color of the cone
3013
+ """
3014
+ geo_hash = hash(("cone", radius, half_height, up_axis))
3015
+ if geo_hash in self._shape_geo_hash:
3016
+ shape = self._shape_geo_hash[geo_hash]
3017
+ if self.update_shape_instance(name, pos, rot):
3018
+ return shape
3019
+ else:
3020
+ vertices, indices = self._create_cone_mesh(radius, half_height, up_axis=up_axis)
3021
+ shape = self.register_shape(geo_hash, vertices, indices, color1=color, color2=color)
3022
+ if not is_template:
3023
+ body = self._resolve_body_id(parent_body)
3024
+ self.add_shape_instance(name, shape, body, pos, rot)
3025
+ return shape
3026
+
3027
+ def render_box(
3028
+ self,
3029
+ name: str,
3030
+ pos: tuple,
3031
+ rot: tuple,
3032
+ extents: tuple,
3033
+ parent_body: str | None = None,
3034
+ is_template: bool = False,
3035
+ color: tuple[float, float, float] | None = None,
3036
+ visible: bool = True,
3037
+ ):
3038
+ """Add a box for visualization
3039
+
3040
+ Args:
3041
+ pos: The position of the box
3042
+ extents: The extents of the box
3043
+ name: A name for the USD prim on the stage
3044
+ color: The color of the box
3045
+ """
3046
+ geo_hash = hash(("box", tuple(extents)))
3047
+ if geo_hash in self._shape_geo_hash:
3048
+ shape = self._shape_geo_hash[geo_hash]
3049
+ if self.update_shape_instance(name, pos, rot, color1=color, color2=color):
3050
+ return shape
3051
+ else:
3052
+ vertices, indices = self._create_box_mesh(extents)
3053
+ shape = self.register_shape(geo_hash, vertices, indices, color1=color, color2=color)
3054
+ if not is_template:
3055
+ body = self._resolve_body_id(parent_body)
3056
+ self.add_shape_instance(name, shape, body, pos, rot)
3057
+ return shape
3058
+
3059
+ def render_mesh(
3060
+ self,
3061
+ name: str,
3062
+ points,
3063
+ indices,
3064
+ colors=None,
3065
+ pos=(0.0, 0.0, 0.0),
3066
+ rot=(0.0, 0.0, 0.0, 1.0),
3067
+ scale=(1.0, 1.0, 1.0),
3068
+ update_topology=False,
3069
+ parent_body: str | None = None,
3070
+ is_template: bool = False,
3071
+ smooth_shading: bool = True,
3072
+ visible: bool = True,
3073
+ ):
3074
+ """Add a mesh for visualization
3075
+
3076
+ Args:
3077
+ points: The points of the mesh
3078
+ indices: The indices of the mesh
3079
+ colors: The colors of the mesh
3080
+ pos: The position of the mesh
3081
+ rot: The rotation of the mesh
3082
+ scale: The scale of the mesh
3083
+ name: A name for the USD prim on the stage
3084
+ smooth_shading: Whether to average face normals at each vertex or introduce additional vertices for each face
3085
+ """
3086
+ if colors is not None:
3087
+ colors = np.array(colors, dtype=np.float32)
3088
+
3089
+ points = np.array(points, dtype=np.float32)
3090
+ point_count = len(points)
3091
+
3092
+ indices = np.array(indices, dtype=np.int32).reshape((-1, 3))
3093
+ idx_count = len(indices)
3094
+
3095
+ geo_hash = hash((points.tobytes(), indices.tobytes()))
3096
+
3097
+ if name in self._instances:
3098
+ # We've already registered this mesh instance and its associated shape.
3099
+ shape = self._instances[name][2]
3100
+ else:
3101
+ if geo_hash in self._shape_geo_hash:
3102
+ # We've only registered the shape, which can happen when `is_template` is `True`.
3103
+ shape = self._shape_geo_hash[geo_hash]
3104
+ else:
3105
+ shape = None
3106
+
3107
+ # Check if we already have that shape registered and can perform
3108
+ # minimal updates since the topology is not changing, before exiting.
3109
+ if not update_topology:
3110
+ if name in self._instances:
3111
+ # Update the instance's transform.
3112
+ self.update_shape_instance(name, pos, rot, color1=colors, color2=colors, scale=scale, visible=visible)
3113
+
3114
+ if shape is not None:
3115
+ # Update the shape's point positions.
3116
+ self.update_shape_vertices(shape, points)
3117
+
3118
+ if not is_template and name not in self._instances:
3119
+ # Create a new instance.
3120
+ body = self._resolve_body_id(parent_body)
3121
+ self.add_shape_instance(name, shape, body, pos, rot, color1=colors, scale=scale)
3122
+
3123
+ return shape
3124
+
3125
+ # No existing shape for the given mesh was found, or its topology may have changed,
3126
+ # so we need to define a new one either way.
3127
+ with wp.ScopedDevice(self._device):
3128
+ if smooth_shading:
3129
+ normals = wp.zeros(point_count, dtype=wp.vec3)
3130
+ vertices = wp.array(points, dtype=wp.vec3)
3131
+ faces_per_vertex = wp.zeros(point_count, dtype=int)
3132
+ wp.launch(
3133
+ compute_average_normals,
3134
+ dim=idx_count,
3135
+ inputs=[wp.array(indices, dtype=int), vertices, scale],
3136
+ outputs=[normals, faces_per_vertex],
3137
+ record_tape=False,
3138
+ )
3139
+ gfx_vertices = wp.zeros((point_count, 8), dtype=float)
3140
+ wp.launch(
3141
+ assemble_gfx_vertices,
3142
+ dim=point_count,
3143
+ inputs=[vertices, normals, faces_per_vertex, scale],
3144
+ outputs=[gfx_vertices],
3145
+ record_tape=False,
3146
+ )
3147
+ gfx_vertices = gfx_vertices.numpy()
3148
+ gfx_indices = indices.flatten()
3149
+ else:
3150
+ gfx_vertices = wp.zeros((idx_count * 3, 8), dtype=float)
3151
+ wp.launch(
3152
+ compute_gfx_vertices,
3153
+ dim=idx_count,
3154
+ inputs=[wp.array(indices, dtype=int), wp.array(points, dtype=wp.vec3), scale],
3155
+ outputs=[gfx_vertices],
3156
+ record_tape=False,
3157
+ )
3158
+ gfx_vertices = gfx_vertices.numpy()
3159
+ gfx_indices = np.arange(idx_count * 3)
3160
+
3161
+ # If there was a shape for the given mesh, clean it up.
3162
+ if shape is not None:
3163
+ self.deregister_shape(shape)
3164
+
3165
+ # If there was an instance for the given mesh, clean it up.
3166
+ if name in self._instances:
3167
+ self.remove_shape_instance(name)
3168
+
3169
+ # Register the new shape.
3170
+ shape = self.register_shape(geo_hash, gfx_vertices, gfx_indices)
3171
+
3172
+ if not is_template:
3173
+ # Create a new instance if necessary.
3174
+ body = self._resolve_body_id(parent_body)
3175
+ self.add_shape_instance(name, shape, body, pos, rot, color1=colors, scale=scale)
3176
+
3177
+ return shape
3178
+
3179
+ def render_arrow(
3180
+ self,
3181
+ name: str,
3182
+ pos: tuple,
3183
+ rot: tuple,
3184
+ base_radius: float,
3185
+ base_height: float,
3186
+ cap_radius: float | None = None,
3187
+ cap_height: float | None = None,
3188
+ parent_body: str | None = None,
3189
+ is_template: bool = False,
3190
+ up_axis: int = 1,
3191
+ color: tuple[float, float, float] | None = None,
3192
+ visible: bool = True,
3193
+ ):
3194
+ """Add a arrow for visualization
3195
+
3196
+ Args:
3197
+ pos: The position of the arrow
3198
+ base_radius: The radius of the cylindrical base of the arrow
3199
+ base_height: The height of the cylindrical base of the arrow
3200
+ cap_radius: The radius of the conical cap of the arrow
3201
+ cap_height: The height of the conical cap of the arrow
3202
+ name: A name for the USD prim on the stage
3203
+ up_axis: The axis of the arrow that points up (0: x, 1: y, 2: z)
3204
+ """
3205
+ geo_hash = hash(("arrow", base_radius, base_height, cap_radius, cap_height, up_axis))
3206
+ if geo_hash in self._shape_geo_hash:
3207
+ shape = self._shape_geo_hash[geo_hash]
3208
+ if self.update_shape_instance(name, pos, rot, color1=color, color2=color):
3209
+ return shape
3210
+ else:
3211
+ vertices, indices = self._create_arrow_mesh(
3212
+ base_radius, base_height, cap_radius, cap_height, up_axis=up_axis
3213
+ )
3214
+ shape = self.register_shape(geo_hash, vertices, indices, color1=color, color2=color)
3215
+ if not is_template:
3216
+ body = self._resolve_body_id(parent_body)
3217
+ self.add_shape_instance(name, shape, body, pos, rot, color1=color, color2=color)
3218
+ return shape
3219
+
3220
+ def render_ref(
3221
+ self,
3222
+ name: str,
3223
+ path: str,
3224
+ pos: tuple,
3225
+ rot: tuple,
3226
+ scale: tuple,
3227
+ color: tuple[float, float, float] | None = None,
3228
+ ):
3229
+ """Create a reference (instance) with the given name to the given path."""
3230
+
3231
+ if path in self._instances:
3232
+ _, body, shape, _, original_scale, color1, color2 = self._instances[path]
3233
+ if color is not None:
3234
+ color1 = color2 = color
3235
+ self.add_shape_instance(name, shape, body, pos, rot, scale or original_scale, color1, color2)
3236
+ return
3237
+
3238
+ raise Exception("Cannot create reference to path: " + path)
3239
+
3240
+ def render_points(self, name: str, points, radius, colors=None, as_spheres: bool = True, visible: bool = True):
3241
+ """Add a set of points
3242
+
3243
+ Args:
3244
+ points: The points to render
3245
+ radius: The radius of the points (scalar or list)
3246
+ colors: The colors of the points
3247
+ name: A name for the USD prim on the stage
3248
+ """
3249
+
3250
+ if len(points) == 0:
3251
+ return
3252
+
3253
+ if isinstance(points, wp.array):
3254
+ wp_points = points
3255
+ else:
3256
+ wp_points = wp.array(points, dtype=wp.vec3, device=self._device)
3257
+
3258
+ if name not in self._shape_instancers:
3259
+ np_points = points.numpy() if isinstance(points, wp.array) else points
3260
+ instancer = ShapeInstancer(self._shape_shader, self._device)
3261
+ radius_is_scalar = np.isscalar(radius)
3262
+ if radius_is_scalar:
3263
+ vertices, indices = self._create_sphere_mesh(radius)
3264
+ else:
3265
+ vertices, indices = self._create_sphere_mesh(1.0)
3266
+ if colors is None:
3267
+ color = tab10_color_map(len(self._shape_geo_hash))
3268
+ elif len(colors) == 3:
3269
+ color = colors
3270
+ else:
3271
+ color = colors[0]
3272
+ instancer.register_shape(vertices, indices, color, color)
3273
+ scalings = None if radius_is_scalar else np.tile(radius, (3, 1)).T
3274
+ instancer.allocate_instances(np_points, colors1=colors, colors2=colors, scalings=scalings)
3275
+ self._shape_instancers[name] = instancer
3276
+ else:
3277
+ instancer = self._shape_instancers[name]
3278
+ if len(points) != instancer.num_instances:
3279
+ np_points = points.numpy() if isinstance(points, wp.array) else points
3280
+ instancer.allocate_instances(np_points)
3281
+
3282
+ with instancer:
3283
+ wp.launch(
3284
+ update_points_positions,
3285
+ dim=len(points),
3286
+ inputs=[wp_points, instancer.instance_scalings],
3287
+ outputs=[instancer.vbo_transforms],
3288
+ device=self._device,
3289
+ )
3290
+
3291
+ def _render_lines(self, name: str, lines, color: tuple, radius: float = 0.01):
3292
+ if len(lines) == 0:
3293
+ return
3294
+
3295
+ if name not in self._shape_instancers:
3296
+ instancer = ShapeInstancer(self._shape_shader, self._device)
3297
+ vertices, indices = self._create_capsule_mesh(radius, 0.5)
3298
+ if color is None or (isinstance(color, list) and len(color) > 0 and isinstance(color[0], list)):
3299
+ color = tab10_color_map(len(self._shape_geo_hash))
3300
+ instancer.register_shape(vertices, indices, color, color)
3301
+ instancer.allocate_instances(np.zeros((len(lines), 3)))
3302
+ self._shape_instancers[name] = instancer
3303
+ else:
3304
+ instancer = self._shape_instancers[name]
3305
+ if len(lines) != instancer.num_instances:
3306
+ instancer.allocate_instances(np.zeros((len(lines), 3)))
3307
+ instancer.update_colors(color, color)
3308
+
3309
+ lines_wp = wp.array(lines, dtype=wp.vec3, ndim=2, device=self._device)
3310
+ with instancer:
3311
+ wp.launch(
3312
+ update_line_transforms,
3313
+ dim=len(lines),
3314
+ inputs=[lines_wp],
3315
+ outputs=[instancer.vbo_transforms],
3316
+ device=self._device,
3317
+ )
3318
+
3319
+ def render_line_list(
3320
+ self,
3321
+ name: str,
3322
+ vertices,
3323
+ indices,
3324
+ color: tuple[float, float, float] | None = None,
3325
+ radius: float = 0.01,
3326
+ visible: bool = True,
3327
+ ):
3328
+ """Add a line list as a set of capsules
3329
+
3330
+ Args:
3331
+ vertices: The vertices of the line-list
3332
+ indices: The indices of the line-list
3333
+ color: The color of the line
3334
+ radius: The radius of the line
3335
+ """
3336
+ lines = []
3337
+ for i in range(len(indices) // 2):
3338
+ lines.append((vertices[indices[2 * i]], vertices[indices[2 * i + 1]]))
3339
+ lines = np.array(lines)
3340
+ self._render_lines(name, lines, color, radius)
3341
+
3342
+ def render_line_strip(
3343
+ self,
3344
+ name: str,
3345
+ vertices,
3346
+ color: tuple[float, float, float] | None = None,
3347
+ radius: float = 0.01,
3348
+ visible: bool = True,
3349
+ ):
3350
+ """Add a line strip as a set of capsules
3351
+
3352
+ Args:
3353
+ vertices: The vertices of the line-strip
3354
+ color: The color of the line
3355
+ radius: The radius of the line
3356
+ """
3357
+ lines = []
3358
+ for i in range(len(vertices) - 1):
3359
+ lines.append((vertices[i], vertices[i + 1]))
3360
+ lines = np.array(lines)
3361
+ self._render_lines(name, lines, color, radius)
3362
+
3363
+ def update_shape_vertices(self, shape, points):
3364
+ if isinstance(points, wp.array):
3365
+ wp_points = points.to(self._device)
3366
+ else:
3367
+ wp_points = wp.array(points, dtype=wp.vec3, device=self._device)
3368
+
3369
+ cuda_buffer = self._shape_gl_buffers[shape][4]
3370
+ vertices_shape = self._shapes[shape][0].shape
3371
+ vbo_vertices = cuda_buffer.map(dtype=wp.float32, shape=vertices_shape)
3372
+
3373
+ wp.launch(
3374
+ update_vbo_vertices,
3375
+ dim=vertices_shape[0],
3376
+ inputs=[wp_points],
3377
+ outputs=[vbo_vertices],
3378
+ device=self._device,
3379
+ )
3380
+
3381
+ cuda_buffer.unmap()
3382
+
3383
+ @staticmethod
3384
+ def _create_sphere_mesh(
3385
+ radius=1.0,
3386
+ num_latitudes=default_num_segments,
3387
+ num_longitudes=default_num_segments,
3388
+ reverse_winding=False,
3389
+ ):
3390
+ vertices = []
3391
+ indices = []
3392
+
3393
+ for i in range(num_latitudes + 1):
3394
+ theta = i * np.pi / num_latitudes
3395
+ sin_theta = np.sin(theta)
3396
+ cos_theta = np.cos(theta)
3397
+
3398
+ for j in range(num_longitudes + 1):
3399
+ phi = j * 2 * np.pi / num_longitudes
3400
+ sin_phi = np.sin(phi)
3401
+ cos_phi = np.cos(phi)
3402
+
3403
+ x = cos_phi * sin_theta
3404
+ y = cos_theta
3405
+ z = sin_phi * sin_theta
3406
+
3407
+ u = float(j) / num_longitudes
3408
+ v = float(i) / num_latitudes
3409
+
3410
+ vertices.append([x * radius, y * radius, z * radius, x, y, z, u, v])
3411
+
3412
+ for i in range(num_latitudes):
3413
+ for j in range(num_longitudes):
3414
+ first = i * (num_longitudes + 1) + j
3415
+ second = first + num_longitudes + 1
3416
+
3417
+ if reverse_winding:
3418
+ indices.extend([first, second, first + 1, second, second + 1, first + 1])
3419
+ else:
3420
+ indices.extend([first, first + 1, second, second, first + 1, second + 1])
3421
+
3422
+ return np.array(vertices, dtype=np.float32), np.array(indices, dtype=np.uint32)
3423
+
3424
+ @staticmethod
3425
+ def _create_capsule_mesh(radius, half_height, up_axis=1, segments=default_num_segments):
3426
+ vertices = []
3427
+ indices = []
3428
+
3429
+ x_dir, y_dir, z_dir = ((1, 2, 0), (2, 0, 1), (0, 1, 2))[up_axis]
3430
+ up_vector = np.zeros(3)
3431
+ up_vector[up_axis] = half_height
3432
+
3433
+ for i in range(segments + 1):
3434
+ theta = i * np.pi / segments
3435
+ sin_theta = np.sin(theta)
3436
+ cos_theta = np.cos(theta)
3437
+
3438
+ for j in range(segments + 1):
3439
+ phi = j * 2 * np.pi / segments
3440
+ sin_phi = np.sin(phi)
3441
+ cos_phi = np.cos(phi)
3442
+
3443
+ z = cos_phi * sin_theta
3444
+ y = cos_theta
3445
+ x = sin_phi * sin_theta
3446
+
3447
+ u = cos_theta * 0.5 + 0.5
3448
+ v = cos_phi * sin_theta * 0.5 + 0.5
3449
+
3450
+ xyz = x, y, z
3451
+ x, y, z = xyz[x_dir], xyz[y_dir], xyz[z_dir]
3452
+ xyz = np.array((x, y, z), dtype=np.float32) * radius
3453
+ if j < segments // 2:
3454
+ xyz += up_vector
3455
+ else:
3456
+ xyz -= up_vector
3457
+
3458
+ vertices.append([*xyz, x, y, z, u, v])
3459
+
3460
+ nv = len(vertices)
3461
+ for i in range(segments + 1):
3462
+ for j in range(segments + 1):
3463
+ first = (i * (segments + 1) + j) % nv
3464
+ second = (first + segments + 1) % nv
3465
+ indices.extend([first, second, (first + 1) % nv, second, (second + 1) % nv, (first + 1) % nv])
3466
+
3467
+ vertex_data = np.array(vertices, dtype=np.float32)
3468
+ index_data = np.array(indices, dtype=np.uint32)
3469
+
3470
+ return vertex_data, index_data
3471
+
3472
+ @staticmethod
3473
+ def _create_cone_mesh(radius, half_height, up_axis=1, segments=default_num_segments):
3474
+ # render it as a cylinder with zero top radius so we get correct normals on the sides
3475
+ return OpenGLRenderer._create_cylinder_mesh(radius, half_height, up_axis, segments, 0.0)
3476
+
3477
+ @staticmethod
3478
+ def _create_cylinder_mesh(radius, half_height, up_axis=1, segments=default_num_segments, top_radius=None):
3479
+ if up_axis not in (0, 1, 2):
3480
+ raise ValueError("up_axis must be between 0 and 2")
3481
+
3482
+ x_dir, y_dir, z_dir = (
3483
+ (1, 2, 0),
3484
+ (0, 1, 2),
3485
+ (2, 0, 1),
3486
+ )[up_axis]
3487
+
3488
+ indices = []
3489
+
3490
+ cap_vertices = []
3491
+ side_vertices = []
3492
+
3493
+ # create center cap vertices
3494
+ position = np.array([0, -half_height, 0])[[x_dir, y_dir, z_dir]]
3495
+ normal = np.array([0, -1, 0])[[x_dir, y_dir, z_dir]]
3496
+ cap_vertices.append([*position, *normal, 0.5, 0.5])
3497
+ cap_vertices.append([*-position, *-normal, 0.5, 0.5])
3498
+
3499
+ if top_radius is None:
3500
+ top_radius = radius
3501
+ side_slope = -np.arctan2(top_radius - radius, 2 * half_height)
3502
+
3503
+ # create the cylinder base and top vertices
3504
+ for j in (-1, 1):
3505
+ center_index = max(j, 0)
3506
+ if j == 1:
3507
+ radius = top_radius
3508
+ for i in range(segments):
3509
+ theta = 2 * np.pi * i / segments
3510
+
3511
+ cos_theta = np.cos(theta)
3512
+ sin_theta = np.sin(theta)
3513
+
3514
+ x = cos_theta
3515
+ y = j * half_height
3516
+ z = sin_theta
3517
+
3518
+ position = np.array([radius * x, y, radius * z])
3519
+
3520
+ normal = np.array([x, side_slope, z])
3521
+ normal = normal / np.linalg.norm(normal)
3522
+ uv = (i / (segments - 1), (j + 1) / 2)
3523
+ vertex = np.hstack([position[[x_dir, y_dir, z_dir]], normal[[x_dir, y_dir, z_dir]], uv])
3524
+ side_vertices.append(vertex)
3525
+
3526
+ normal = np.array([0, j, 0])
3527
+ uv = (cos_theta * 0.5 + 0.5, sin_theta * 0.5 + 0.5)
3528
+ vertex = np.hstack([position[[x_dir, y_dir, z_dir]], normal[[x_dir, y_dir, z_dir]], uv])
3529
+ cap_vertices.append(vertex)
3530
+
3531
+ cs = center_index * segments
3532
+ indices.extend([center_index, i + cs + 2, (i + 1) % segments + cs + 2][::-j])
3533
+
3534
+ # create the cylinder side indices
3535
+ for i in range(segments):
3536
+ index1 = len(cap_vertices) + i + segments
3537
+ index2 = len(cap_vertices) + ((i + 1) % segments) + segments
3538
+ index3 = len(cap_vertices) + i
3539
+ index4 = len(cap_vertices) + ((i + 1) % segments)
3540
+
3541
+ indices.extend([index1, index2, index3, index2, index4, index3])
3542
+
3543
+ vertex_data = np.array(np.vstack((cap_vertices, side_vertices)), dtype=np.float32)
3544
+ index_data = np.array(indices, dtype=np.uint32)
3545
+
3546
+ return vertex_data, index_data
3547
+
3548
+ @staticmethod
3549
+ def _create_arrow_mesh(
3550
+ base_radius, base_height, cap_radius=None, cap_height=None, up_axis=1, segments=default_num_segments
3551
+ ):
3552
+ if up_axis not in (0, 1, 2):
3553
+ raise ValueError("up_axis must be between 0 and 2")
3554
+ if cap_radius is None:
3555
+ cap_radius = base_radius * 1.8
3556
+ if cap_height is None:
3557
+ cap_height = base_height * 0.18
3558
+
3559
+ up_vector = np.array([0, 0, 0])
3560
+ up_vector[up_axis] = 1
3561
+
3562
+ base_vertices, base_indices = OpenGLRenderer._create_cylinder_mesh(
3563
+ base_radius, base_height / 2, up_axis, segments
3564
+ )
3565
+ cap_vertices, cap_indices = OpenGLRenderer._create_cone_mesh(cap_radius, cap_height / 2, up_axis, segments)
3566
+
3567
+ base_vertices[:, :3] += base_height / 2 * up_vector
3568
+ # move cap slightly lower to avoid z-fighting
3569
+ cap_vertices[:, :3] += (base_height + cap_height / 2 - 1e-3 * base_height) * up_vector
3570
+
3571
+ vertex_data = np.vstack((base_vertices, cap_vertices))
3572
+ index_data = np.hstack((base_indices, cap_indices + len(base_vertices)))
3573
+
3574
+ return vertex_data, index_data
3575
+
3576
+ @staticmethod
3577
+ def _create_box_mesh(extents):
3578
+ x_extent, y_extent, z_extent = extents
3579
+
3580
+ vertices = [
3581
+ # Position Normal UV
3582
+ [-x_extent, -y_extent, -z_extent, -1, 0, 0, 0, 0],
3583
+ [-x_extent, -y_extent, z_extent, -1, 0, 0, 1, 0],
3584
+ [-x_extent, y_extent, z_extent, -1, 0, 0, 1, 1],
3585
+ [-x_extent, y_extent, -z_extent, -1, 0, 0, 0, 1],
3586
+ [x_extent, -y_extent, -z_extent, 1, 0, 0, 0, 0],
3587
+ [x_extent, -y_extent, z_extent, 1, 0, 0, 1, 0],
3588
+ [x_extent, y_extent, z_extent, 1, 0, 0, 1, 1],
3589
+ [x_extent, y_extent, -z_extent, 1, 0, 0, 0, 1],
3590
+ [-x_extent, -y_extent, -z_extent, 0, -1, 0, 0, 0],
3591
+ [-x_extent, -y_extent, z_extent, 0, -1, 0, 1, 0],
3592
+ [x_extent, -y_extent, z_extent, 0, -1, 0, 1, 1],
3593
+ [x_extent, -y_extent, -z_extent, 0, -1, 0, 0, 1],
3594
+ [-x_extent, y_extent, -z_extent, 0, 1, 0, 0, 0],
3595
+ [-x_extent, y_extent, z_extent, 0, 1, 0, 1, 0],
3596
+ [x_extent, y_extent, z_extent, 0, 1, 0, 1, 1],
3597
+ [x_extent, y_extent, -z_extent, 0, 1, 0, 0, 1],
3598
+ [-x_extent, -y_extent, -z_extent, 0, 0, -1, 0, 0],
3599
+ [-x_extent, y_extent, -z_extent, 0, 0, -1, 1, 0],
3600
+ [x_extent, y_extent, -z_extent, 0, 0, -1, 1, 1],
3601
+ [x_extent, -y_extent, -z_extent, 0, 0, -1, 0, 1],
3602
+ [-x_extent, -y_extent, z_extent, 0, 0, 1, 0, 0],
3603
+ [-x_extent, y_extent, z_extent, 0, 0, 1, 1, 0],
3604
+ [x_extent, y_extent, z_extent, 0, 0, 1, 1, 1],
3605
+ [x_extent, -y_extent, z_extent, 0, 0, 1, 0, 1],
3606
+ ]
3607
+
3608
+ # fmt: off
3609
+ indices = [
3610
+ 0, 1, 2,
3611
+ 0, 2, 3,
3612
+ 4, 6, 5,
3613
+ 4, 7, 6,
3614
+ 8, 10, 9,
3615
+ 8, 11, 10,
3616
+ 12, 13, 14,
3617
+ 12, 14, 15,
3618
+ 16, 17, 18,
3619
+ 16, 18, 19,
3620
+ 20, 22, 21,
3621
+ 20, 23, 22,
3622
+ ]
3623
+ # fmt: on
3624
+ return np.array(vertices, dtype=np.float32), np.array(indices, dtype=np.uint32)
3625
+
3626
+ def _switch_context(self):
3627
+ try:
3628
+ self.window.switch_to()
3629
+ except AttributeError:
3630
+ # The window could be in the process of being closed, in which case
3631
+ # its corresponding context might have been destroyed and set to `None`.
3632
+ pass
3633
+
3634
+
3635
+ if __name__ == "__main__":
3636
+ renderer = OpenGLRenderer()