可视化#
MuJoCo 拥有原生的 3D 可视化工具。其使用方法在 simulate.cc 代码示例以及更简单的 basic.cc 代码示例中有所展示。虽然它不是一个功能齐全的渲染引擎,但它是一个方便、高效且外观相当不错可视化工具,有助于研发工作。它不仅能渲染仿真状态,还能渲染装饰性元素,如接触点和力、等效惯性框、凸包、运动树、约束违规、空间坐标系和文本标签;这些元素可以提供对物理仿真的深入洞察,并有助于微调模型。
该可视化工具与仿真器紧密集成,并支持屏幕内和屏幕外渲染,如 record.cc 代码示例所示。这使其适用于合成计算机视觉和机器学习应用,尤其是在云环境中。此外还提供 VR 集成,方便使用头戴式显示器的应用。
MuJoCo 中的可视化是一个两阶段过程
- 抽象可视化与交互
此阶段将 mjvScene 数据结构填充为 3D 渲染所需的几何对象、光源、摄像机及所有其他元素列表。它还提供了用于用户交互的抽象键盘和鼠标钩子。相关数据结构和函数名带有前缀
mjv。- OpenGL 渲染
此阶段接收抽象可视化阶段填充的 mjvScene 数据结构并进行渲染。它还提供基础的 2D 绘图和帧缓冲区访问,因此大多数应用程序无需直接调用 OpenGL。相关数据结构和函数名带有前缀
mjr。
这种分离有几个原因。首先,这两个阶段在概念上是不同的,将它们分离是良好的软件设计。其次,它们具有不同的依赖关系,无论是在内部还是在附加库方面;特别是抽象可视化不需要任何图形库。第三,希望将其他渲染引擎与 MuJoCo 集成的用户可以绕过原生 OpenGL 渲染器,但仍能利用抽象可视化工具。
以下是 C 代码和注释中伪代码的混合,说明了执行仿真和渲染的 MuJoCo 应用程序结构。这是 basic.cc 代码示例的简短版本。为了具体起见,我们假设使用了 GLFW,尽管它可以用其他窗口库(如 GLUT 或其衍生库)代替。
// MuJoCo data structures
mjModel* m = NULL; // MuJoCo model
mjData* d = NULL; // MuJoCo data
mjvCamera cam; // abstract camera
mjvOption opt; // visualization options
mjvScene scn; // abstract scene
mjrContext con; // custom GPU context
// ... load model and data
// init GLFW, create window, make OpenGL context current, request v-sync
glfwInit();
GLFWwindow* window = glfwCreateWindow(1200, 900, "Demo", NULL, NULL);
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
// initialize visualization data structures
mjv_defaultCamera(&cam);
mjv_defaultPerturb(&pert);
mjv_defaultOption(&opt);
mjr_defaultContext(&con);
// create scene and context
mjv_makeScene(m, &scn, 1000);
mjr_makeContext(m, &con, mjFONTSCALE_100);
// ... install GLFW keyboard and mouse callbacks
// run main loop, target real-time simulation and 60 fps rendering
while( !glfwWindowShouldClose(window) ) {
// advance interactive simulation for 1/60 sec
// Assuming MuJoCo can simulate faster than real-time, which it usually can,
// this loop will finish on time for the next frame to be rendered at 60 fps.
// Otherwise add a cpu timer and exit this loop when it is time to render.
mjtNum simstart = d->time;
while( d->time - simstart < 1.0/60.0 )
mj_step(m, d);
// get framebuffer viewport
mjrRect viewport = {0, 0, 0, 0};
glfwGetFramebufferSize(window, &viewport.width, &viewport.height);
// update scene and render
mjv_updateScene(m, d, &opt, NULL, &cam, mjCAT_ALL, &scn);
mjr_render(viewport, &scn, &con);
// swap OpenGL buffers (blocking call due to v-sync)
glfwSwapBuffers(window);
// process pending GUI events, call GLFW callbacks
glfwPollEvents();
}
// close GLFW, free visualization storage
glfwTerminate();
mjv_freeScene(&scn);
mjr_freeContext(&con);
// ... free MuJoCo model and data
抽象可视化与交互#
此阶段将 mjvScene 数据结构填充为 3D 渲染所需的几何对象、光源、摄像机及所有其他元素列表。它还提供了用于用户交互的抽象键盘和鼠标钩子。
摄像机#
有两种类型的摄像机对象:由独立数据结构 mjvCamera 表示的抽象摄像机,以及由嵌入在 mjvScene 中的数据结构 mjvGLCamera 表示的底层 OpenGL 摄像机。如果存在,抽象摄像机将在场景更新期间用于自动计算 OpenGL 摄像机参数,然后由 OpenGL 渲染器使用这些参数。或者,用户可以绕过抽象摄像机机制直接设置 OpenGL 摄像机参数,如下文“虚拟现实”部分所述。
抽象摄像机可以代表由 mjvCamera.type 决定的三种不同摄像机类型。可能的设置由枚举 mjtCamera 定义
- mjCAMERA_FREE
这是最常用的抽象摄像机。它可以用鼠标自由移动。它有一个观察点(lookat point)、到观察点的距离、方位角和仰角;不允许绕视线扭转。函数 mjv_moveCamera 是一个鼠标钩子,用于通过鼠标交互式地控制所有这些摄像机属性。simulate.cc 首次启动时,使用的是自由摄像机。
- mjCAMERA_TRACKING
这与自由摄像机类似,区别在于观察点不再是一个自由参数,而是与 mjvCamera.trackbodyid 指定的 MuJoCo 物体绑定。在每次更新时,观察点被设置为以指定物体为根的运动学子树的质心。此外还有一些产生平滑摄像机运动的过滤机制。距离、方位角和仰角由用户控制,不会自动修改。这对于在物体移动时跟踪它而不转动摄像机非常有用。要在 simulate.cc 中从自由摄像机切换到跟踪摄像机,请按住 Ctrl 并对感兴趣的物体进行右键双击。按 Esc 键可返回自由摄像机。
- mjCAMERA_FIXED
这指的是模型中明确定义的摄像机,不同于仅存在于可视化工具中且未在模型中定义的自由和跟踪摄像机。模型摄像机的 ID 由 mjvCamera.fixedcamid 给出。此摄像机是固定的,意味着可视化工具无法改变其位姿或任何其他参数。但是,仿真器会在每个时间步计算摄像机位姿,如果摄像机连接到移动物体或者处于跟踪或目标模式,它将会移动。
- mjCAMERA_USER
这意味着抽象摄像机在更新期间被忽略,底层 OpenGL 摄像机不会改变。这等同于根本不指定抽象摄像机,即在下述更新函数中向 mjvCamera 传递 NULL 指针。
底层的 mjvGLCamera 决定了实际的渲染效果。mjvScene 中嵌入了两个这样的摄像机,每只眼睛一个。每个都有位置、前向和上方向。前向对应于摄像机坐标系的负 Z 轴,而上方向对应于正 Y 轴。此外还有 OpenGL 意义上的平截头体(frustum),只是我们存储了左、右平截头体边缘的平均值,然后在渲染期间根据视口长宽比(假设像素长宽比为 1:1)计算实际边缘。两个摄像机位置之间的距离对应于瞳孔间距(ipd)。当底层摄像机参数从抽象摄像机自动计算时,自由和跟踪摄像机的 ipd 以及垂直视场角(fovy)取自 mjModel.vis.global.ipd/fovy,而对于模型中定义的摄像机,则取自特定于摄像机的 mjModel.cam_ipd/fovy。当未启用立体模式(由 mjvScene.stereo 决定)时,两只眼睛的摄像机数据在渲染过程中会在内部取平均值。
选择#
在许多应用中,我们需要点击一个点并确定该点/像素所属的 3D 对象。这是通过 mjv_select 函数完成的,它使用 射线碰撞。射线碰撞功能属于引擎级别,不依赖于可视化工具(事实上,它也被独立于可视化地用于模拟 测距仪 传感器),但选择函数是在可视化工具中实现的,因为它需要有关摄像机和视口的信息。
函数 mjv_select 返回指定窗口坐标处的几何体索引,如果该坐标处没有几何体,则返回 -1。还会返回 3D 位置。有关如何使用此函数的示例,请参阅代码示例 simulate.cc。在内部,mjv_select 调用引擎级函数 mj_ray,后者依次调用每个几何体的函数 mj_rayMesh、mj_rayHfield 和 mju_rayGeom。用户可以通过直接调用这些函数来实现自定义的选择机制。例如,在 VR 应用中,将手持控制器用作可以选中对象的“激光指示器”是合理的。
扰动#
交互式扰动在探索模型动力学以及探测闭环控制系统方面非常有用。用户可以通过将 mjData.qfrc_applied 或 mjData.xfrc_applied 设置为合适的力(分别在广义坐标和笛卡尔坐标中)来自由实现他们选择的任何扰动机制。
实现交互式扰动所需的所有对象都被归类到数据结构 mjvPerturb 中。其使用方法在 simulate.cc 中有说明。其核心思想是选择感兴趣的 MuJoCo 物体,并为该物体提供参考位姿(即 3D 位置和四元数方向)。这些信息存储在 mjPerturb.refpos/quat 中。函数 mjv_movePerturb 是一个用于通过鼠标控制参考位姿的鼠标钩子。函数 mjv_initPerturb 用于在扰动开始时将参考位姿设置为与所选物体的位姿相等,以避免跳变。
此扰动对象随后可用于直接移动所选物体(当仿真暂停或所选物体是运动捕捉物体时),或者对物体施加力和力矩。这分别通过函数 mjv_applyPerturbPose 和 mjv_applyPerturbForce 完成。后者将外部扰动力写入所选物体的 mjData.xfrc_applied。但是,它不会清除剩余物体的 mjData.xfrc_applied,因此建议在用户代码中清除它,以防所选物体发生更改且之前的扰动力残留在先前的时间步中。如果有多个设备可以施加扰动,或者用户代码需要添加其他来源的扰动,用户必须实现必要的逻辑,确保只有所需的扰动存在于 mjData.xfrc_applied 中,并清除任何旧的扰动。
除了影响仿真外,扰动对象还能被抽象可视化工具识别并渲染。这是通过添加一个表示位置差异的视觉线条,以及一个表示所选物体参考方向的旋转立方体来完成的。当 mjvOption 中启用了相应的可视化标志时,也可以渲染扰动力本身。
场景更新#
最后,我们将上述所有元素结合起来,说明在传递到 OpenGL 渲染阶段之前如何更新 mjvScene。这可以通过在每一帧调用一次 mjv_updateScene 函数来完成。mjvCamera 和 mjvPerturb 是此函数的参数,它们也可以是 NULL 指针,这种情况下相应的功能将被禁用。在 VR 应用中,mjvScene.camera[n] (n=0,1) 的参数也必须在每一帧进行设置;这是由 mjv_updateScene 之外的用户代码完成的。函数 mjv_updateScene 检查 mjModel 和 mjData,构建所有需要渲染的几何体(根据指定的可视化选项),并使用 mjvGeom 对象填充 mjvScene.geom 数组。请注意,mjvGeom 是抽象几何体,与 mjModel 和 mjData 中的仿真几何体并非一一对应。特别是,mjvGeom 包含几何体位姿、缩放、形状(mjModel 中的基元或网格索引)、材料属性、纹理(mjModel 中的索引)、标签以及指定如何进行渲染所需的其他一切。mjvScene 还包含从模型中复制的最多八个 OpenGL 光源,以及一个前照灯(存在时位于光源位置 0)。
上述过程是最常用的方法,它在每一帧更新整个场景。此外,我们还提供了两个用于更精细控制的函数。mjv_updateCamera 仅更新摄像机(即将抽象的 mjvCamera 映射到底层的 mjvGLCamera),但不触及几何体或光源。当用户快速移动摄像机但仿真状态未改变时,这非常有用——在这种情况下,重新创建几何体和光源列表毫无意义。
通过操作抽象几何体列表可以实现更高级的渲染效果。例如,用户可以在列表末尾添加自定义几何体。有时需要渲染一系列仿真状态(即轨迹)而不仅仅是当前状态。为此,我们提供了函数 mjv_addGeoms,它将对应于当前仿真状态的几何体添加到 mjvScene 的现有列表中。它不会更改光源列表,因为光照是累加的,光源过多会使场景过于明亮。重要的是,用户可以通过枚举类型 mjtCatBit 的位掩码选择要添加的几何体类别
- mjCAT_STATIC
这会选择属于世界物体(物体 ID 为 0)的 MuJoCo 几何体和位点。
- mjCAT_DYNAMIC
这会选择属于非世界物体的其他物体的 MuJoCo 几何体和位点。
- mjCAT_DECOR
这会选择装饰性元素,如力箭头、自动生成的骨架、等效惯性框以及由抽象可视化工具添加的、不对应于模型中定义的 MuJoCo 几何体和位点的任何其他元素。
- mjCAT_ALL
这会选择上述所有类别。
主更新函数 mjv_updateScene 通常会以 mjCAT_ALL 调用。它会清除几何体列表并调用 mjv_addGeom 仅添加当前模型状态的几何体。如果我们想渲染轨迹,必须小心避免视觉混乱。因此,使用 mjCAT_ALL 渲染其中一帧(通常是第一帧或最后一帧,具体取决于用例),而所有其他帧使用 mjCAT_DYNAMIC 是合理的。由于静态/世界对象不会移动,在每一帧中渲染它们只会降低 GPU 速度并产生视觉混叠。至于装饰元素,在某些情况下我们可能希望渲染所有这些元素——例如,为了可视化接触力随时间的演变。总之,构建 mjvScene 的方式非常灵活。我们为主要用例提供了自动化功能,但用户也可以根据需要进行编程更改。
虚拟现实#
在桌面应用程序中,使用允许直观鼠标控制的抽象 mjvCamera,然后将其自动映射到用于渲染的 mjvGLCamera 是很方便的。在 VR 应用中,情况截然不同。在这种情况下,用户的头部/眼睛以及投影表面都在被追踪,因此在房间内具有物理存在感。如果用户(使用鼠标或其他输入设备)可以移动任何东西,那就是模型相对于房间的位置、方向和比例。这称为模型变换(model transformation),并在 mjvScene 中表示。函数 mjv_moveModel 是一个用于控制此变换的鼠标钩子。在更新期间使用抽象 mjvCamera 时,通过将标志 mjvScene.enabletransform 设置为 0(而不是清除实际参数)来自动禁用模型变换。这样,用户可以在 VR 和桌面摄像机模式之间切换,而不会丢失模型变换参数。
由于引入了两个空间,即模型空间(model space)和房间空间(room space),我们需要在它们之间进行映射,并弄清楚哪些空间量是相对于哪个空间坐标系定义的。仿真器可访问的所有内容都存在于模型空间中。房间空间仅由可视化工具访问。唯一在房间空间中定义的量是 mjvGLCamera 参数。函数 mjv_room2model、mjv_model2room、mjv_cameraInModel 和 mjv_cameraInRoom 执行必要的转换,这些对于 VR 应用是必需的。
虽然 MuJoCo 没有提供内置的 VR 应用程序,但它提供了支持用户代码中 VR 集成的数据结构和函数。
- 头部跟踪和摄像机
在典型的 VR 应用中,跟踪设备实时提供用户眼睛的位置和方向。这些数据可以直接复制到
mjvScene.camera[n]中的两个mjvGLCamera结构中(其中n=0是左眼,n=1是右眼)。还必须根据跟踪显示器的物理特性设置mjvGLCamera的平截头体参数。- 控制器和运动捕捉物体
手持空间控制器也在房间空间中进行跟踪。函数 mjv_room2model 可以将这些位姿映射到模型空间。一旦进入模型空间,控制器位姿就可以用于更新 MuJoCo 运动捕捉物体(mocap bodies)的位置。从物理的角度来看,运动捕捉物体被视为固定的,但用户需要在每个仿真步骤中以编程方式移动它们。它们可以通过接触与仿真交互,或者更好的是,通过对刚体的软等式约束进行交互,进而产生接触。这提供了有效的动态过滤,避免了涉及表现得好像极其沉重的物体的接触。运动捕捉物体随时间变化的位置和方向存储在
mjData.mocap_pos和mjData.mocap_quat中。
OpenGL 渲染#
此阶段接收在抽象可视化阶段填充的 mjvScene 数据结构并进行渲染。它还提供基础的 2D 绘图和帧缓冲区访问,因此大多数应用程序无需直接调用 OpenGL。
上下文和 GPU 资源#
渲染过程的第一步是创建模型特定的 GPU 上下文 mjrContext。这通过首先使用函数 mjr_defaultContext 清除数据结构,然后调用函数 mjr_makeContext 来完成。这在前面已经说明过;相关代码是
mjModel* m;
mjrContext con;
// clear mjrContext only once before first use
mjr_defaultContext(&con);
// create window with OpenGL context, make it current
GLFWwindow* window = glfwCreateWindow(1200, 900, "Demo", NULL, NULL);
glfwMakeContextCurrent(window);
// ... load MuJoCo model
// make model-specific mjrContext
mjr_makeContext(m, &con, mjFONTSCALE_100);
// ... load another MuJoCo model
// make mjrContext for new model (old context freed automatically)
mjr_makeContext(m, &con, mjFONTSCALE_100);
// free context when done
mjr_freeContext(&con);
mjrContext 与 OpenGL 上下文有何关系?OpenGL 上下文是使应用程序能够与视频驱动程序通信并发送渲染命令的内容。在调用 mjr_makeContext 之前,它必须存在并且必须是调用线程中的当前上下文。如上所示,GLFW 和相关库提供了必要的函数。
mjrContext 是 MuJoCo 特有的。创建后,它包含对所有通过 mjr_makeContext 上传到 GPU 的资源的引用(在 OpenGL 中称为“名称”)。这些资源包括模型特定的资源(如网格和纹理),以及通用资源(如指定字体缩放的字体位图、用于阴影映射和屏幕外渲染的帧缓冲区对象以及相关的渲染缓冲区)。它还包含从 mjModel.vis 复制的 OpenGL 相关选项、自动发现的默认窗口帧缓冲区的功能,以及当前处于激活状态的渲染缓冲区;参见下方的 缓冲区。请注意,尽管 MuJoCo 使用固定功能的 OpenGL,但它避免了立即模式渲染,而是预先将所有资源上传到 GPU。这使其像现代着色器一样高效,甚至可能更高效,因为固定功能的 OpenGL 现在通过视频驱动程序开发人员编写并经过深度优化的内部着色器来实现。
mjrContext 的大多数字段在调用 mjr_makeContext 后保持不变。唯一的例外是 mjrContext.currentBuffer,它会随着活动缓冲区的更改而更改。一些 GPU 资源也可能发生变化,因为用户可以使用函数 mjr_uploadTexture、mjr_uploadMesh、mjr_uploadHField 上传修改后的资源。这可用于实现动态效果,例如将视频流插入渲染中,或调节地形图。此类修改会影响驻留在 GPU 上的资源,但它们的 OpenGL 名称会被重用;因此,这种变化在 mjrContext 中实际上不可见。
用户绝不应该直接更改 mjrContext。MuJoCo 的渲染器假设只有它能管理 mjrContext。事实上,这种类型的对象通常是不透明的,其内部结构不会暴露给用户。我们将其暴露是因为 MuJoCo 采用开放式设计,也因为用户可能希望将他们自己的 OpenGL 代码与 MuJoCo 的渲染器交错使用,在这种情况下,他们可能需要对 mjrContext 的某些字段进行读取访问。例如,在 VR 应用中,用户需要将 MuJoCo 的屏幕外缓冲区位块传输(blit)到 VR SDK 提供的纹理中。
当加载不同的 MuJoCo 模型时,必须再次调用 mjr_makeContext。还有一个函数 mjr_freeContext,它可以在保留初始化和功能标志的同时释放 GPU 资源。当应用程序即将退出时应调用此函数。它在 mjr_makeContext 内部被自动调用,因此当加载不同的模型时,您不需要直接调用它,尽管这样做也不会出错。在渲染开始前必须调用一次函数 mjr_defaultContext,以清除为数据结构 mjrContext 分配的内存。如果您在调用 mjr_makeContext 之后调用它,它会抹掉已分配 GPU 资源的任何记录而不会释放这些资源,所以不要这样做。
用于渲染的缓冲区#
除了默认窗口帧缓冲区外,OpenGL 还支持用于自定义渲染的无限帧缓冲区对象(FBO)。在 MuJoCo 中,我们为两个帧缓冲区提供了系统支持:默认窗口帧缓冲区和一个屏幕外帧缓冲区。它们由枚举类型 mjtFramebuffer 中的常量引用,即 mjFB_WINDOW 和 mjFB_OFFSCREEN。在任何时候,这两个缓冲区中的一个都会处于活动状态,用于 MuJoCo 渲染,这意味着所有后续命令都会指向它。mjrContext 中引用了另外两个帧缓冲区对象,用于阴影映射和解析多重采样缓冲区,但这些是内部使用的,用户不应尝试直接访问它们。
活动缓冲区由函数 mjr_setBuffer 设置。这会设置 mjrContext.activeBuffer 的值并相应地配置 OpenGL 状态。当调用 mjr_makeContext 时,它内部会使用参数 mjFB_WINDOW 调用 mjr_setBuffer,以便默认在窗口缓冲区中开始渲染。如果指定的缓冲区不存在,mjr_setBuffer 会自动默认为另一个缓冲区(请注意,在 Linux 上使用无头渲染时,可能没有窗口帧缓冲区)。
从 OpenGL 的角度来看,窗口帧缓冲区和屏幕外帧缓冲区之间存在重要差异,这些差异影响了 MuJoCo 用户与渲染器的交互方式。窗口帧缓冲区由操作系统创建和管理,而不是由 OpenGL 管理。因此,分辨率、双缓冲、四缓冲立体声、多采样、垂直同步等属性是在 OpenGL 之外设置的;这些在我们代码示例中是通过 GLFW 调用完成的。OpenGL 能做的只是检测这些属性;我们在 mjr_makeContext 中执行此操作,并将结果记录在 mjrContext 的各种窗口功能字段中。这就是为什么此类属性不是 MuJoCo 模型的一部分;它们是会话/软件特定的,而不是模型特定的。相比之下,屏幕外帧缓冲区完全由 OpenGL 管理,因此我们可以使用我们想要的任何属性(即 mjModel.vis 中指定的分辨率和多采样属性)创建该缓冲区。
用户可以直接访问这两个缓冲区中的像素。这通过函数 mjr_readPixels、mjr_drawPixels 和 mjr_blitBuffer 来完成。读取/绘制操作在活动缓冲区和 CPU 之间传输像素。位块传输(Blit)在 GPU 上的两个缓冲区之间传输像素,因此速度要快得多。传输方向是从活动缓冲区到非活动缓冲区。请注意,mjr_blitBuffer 具有可以有不同大小的源和目标视口,从而允许在过程中缩放图像。
绘制像素#
主要的渲染函数是 mjr_render。其参数是用于渲染的矩形视口、由抽象可视化工具填充的 mjvScene,以及由 mjr_makeContext 创建的 mjrContext。视口可以是整个活动缓冲区,也可以是其一部分以实现自定义效果。对应于整个缓冲区的视口可以通过函数 mjr_maxViewport 获得。请注意,虽然屏幕外缓冲区大小不会改变,但窗口缓冲区大小会在用户调整或最大化窗口时改变。因此用户代码不应假设固定的视口大小。在代码示例 simulate.cc 中,我们使用在窗口大小改变时触发的回调,而在 basic.cc 中,我们只是在每次渲染时检查窗口大小。在某些缩放显示器上(特别是 MacOS 上),窗口大小和帧缓冲区大小可能不同。因此,如果您使用 GLFW 函数获取大小,请使用 glfwGetFramebufferSize 而不是 glfwGetWindowSize。另一方面,鼠标坐标是由操作系统以窗口单位而非帧缓冲区单位返回的;因此,前面讨论的鼠标交互函数应使用 glfwGetWindowSize 来获取规范化鼠标位移数据所需的窗口高度。
mjr_render 渲染来自列表 mjvScene.geom 的所有 mjvGeom。抽象可视化选项 mjvOption 在此处不再相关;它们由 mjv_updateScene 用于确定要添加哪些几何体,就 mjr_render 而言,这些选项已经内置好了。但是,还有另一组渲染选项嵌入在 mjvScene 中,它们会影响 OpenGL 渲染过程。数组 mjvScene.flags 包含由枚举类型 mjtRndFlag 索引的标志,包括启用和禁用线框模式、阴影、反射、天空盒和雾的选项。阴影和反射涉及额外的渲染通道。MuJoCo 的渲染器非常高效,但根据模型的复杂性和可用的 GPU,在某些情况下可能需要禁用其中一个或两个效果。
参数 mjvScene.stereo 决定立体模式。可能的值由枚举类型 mjtStereo 给出,如下所示
- mjSTEREO_NONE
立体渲染已禁用。使用 mjvScene 中两个 OpenGL 摄像机的平均值。请注意,即使不使用立体声,渲染器也始终期望两个摄像机都被正确定义。
- mjSTEREO_QUADBUFFERED
此模式仅在活动缓冲区为窗口且窗口支持四缓冲 OpenGL 时有效。这需要专业的显卡。代码示例 simulate.cc 尝试打开这样的窗口。在此模式下,当窗口是双缓冲时,MuJoCo 的渲染器使用 GL_BACK_LEFT 和 GL_BACK_RIGHT 缓冲区来渲染两个视图(由 mjvScene 中的两个 OpenGL 摄像机确定),否则使用 GL_FRONT_LEFT 和 GL_FRONT_RIGHT。如果窗口不支持四缓冲 OpenGL 或活动缓冲区是屏幕外缓冲区,渲染器将恢复为接下来描述的并排模式。
- mjSTEREO_SIDEBYSIDE
这种立体模式不需要特殊硬件,并且始终可用。提供给 mjr_render 的视口被分成两个等大的矩形,左右并排。左视图渲染在左侧,右视图渲染在右侧。原则上用户可以斜视并在普通显示器上看到立体效果,但这里的目标是在立体设备中显示它。大多数头戴式显示器都支持此立体模式。
除了主要的 mjr_render 函数外,我们还提供了几个用于“装饰”图像的函数。这些是 2D 渲染函数,包括 mjr_overlay、mjr_text、mjr_rectangle 和 mjr_figure。用户可以用自己的 OpenGL 代码绘制额外的装饰。这应该在 mjr_render 之后完成,因为 mjr_render 会清除视口。
我们还提供了用于与 GPU 显式同步以及 OpenGL 错误检查的函数 mjr_finish 和 mjr_getError。它们在内部只需调用 glFinish 和 glGetError。这与上述基础 2D 绘图函数一起,旨在提供足够的功能,以便大多数用户无需编写 OpenGL 代码。当然,我们无法在所有情况下都实现这一点,除非为所有 OpenGL 提供封装器。