【个人学习】Isaac Sim 零基础学习(上)
【个人学习】Isaac Sim 零基础学习(上)
引言
NVIDIA Isaac Sim 是一个基于 Omniverse 平台的强大机器人仿真工具。凭借其高逼真的图像渲染能力和精准的 PhysX 物理引擎,它在机器人算法验证、具身智能训练以及合成数据生成等领域有着非常广泛的应用。
这篇博客是我个人在 Windows 操作系统下从零开始学习 Isaac Sim 的一份记录与总结,主要是为了梳理和巩固自己的学习内容。由于初次接触,内容叙述上可能有所不足,但可以确保代码确实能跑通。若有错误虚心接受批评指正。
相关工具
- Isaac Sim 6.0:NVIDIA 对 Isaac Sim 的底层架构进行过重大重构,早版本的一些代码在 6.0 环境中大概率是无法运行的(根据后续一些学习,当前不建议用 6.0 版本,比如 Isaac Lab 很难安装好。但当时本文的脚本已经写完了,本文就当是 6.0 版本的经验了)。
- VS Code:作为编写 Python 脚本的主力 IDE。在 Isaac Sim 开发中配置 VS Code,按下
Ctrl + Shift + P,选择Python: Select Interpreter,然后手动浏览找到 Isaac Sim 安装目录下的自带 Python 环境(Windows 下通常在使用python.bat拉起的隐藏虚拟环境中)。只有指定了 Isaac Sim 自带的解释器,VS Code 的代码自动补全(Autocomplete)和语法高亮才能正确识别isaacsim和pxr等专有核心库,否则代码全篇都会报红色的波浪线。
一:初识 Isaac Sim 与生命周期
1. 验证 Python 运行环境(01_hello_world.py)
在正式开启仿真之前,我们需要先了解如何正确地运行 Isaac Sim 的 Python 脚本。
首先需要注意,由于 Isaac Sim 庞大的底层库(如 omni、pxr 等)都配置在它自带的 Python 环境中。在 Windows 操作系统下,NVIDIA 提供了一个专门的批处理文件 python.bat 来临时挂载这些环境变量并执行脚本。所以我们要调用 python.bat 运行命令而不是用系统的 python (python xxx.py)去跑 Isaac Sim 的代码。
创建一个简单的测试文件:
【代码】 01_hello_world.py:
print("Hello Isaac Sim!")
'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\01_hello_world.py"
'''
要运行这个脚本,我们需要先进入 Isaac Sim 安装目录,然后调用 python.bat,并传入脚本的绝对路径:
# 1. 切换到 Isaac Sim 的根目录
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
# 2. 使用自带的 python.bat 运行你的脚本
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\01_hello_world.py"
运行后在终端看到:

如果终端成功打印出 “Hello Isaac Sim!”,那么说明 Isaac Sim Python 编程环境已经准备就绪。
2. 启动仿真引擎(02_simulation_app.py)
在确认环境正常后,就可以真正“唤醒” Isaac Sim 的底层引擎了。在任何 Isaac Sim 脚本中,都有一个标准的生命周期:启动 -> 运行循环(步进) -> 关闭。
【代码】 02_simulation_app.py:
引入 SimulationApp 模块并拉起带界面的仿真器。
【注】 早期版本大量使用 omni.isaac.* 开头的库。例如:启动仿真器用 from omni.isaac.kit import SimulationApp,导入物体用 from omni.isaac.core.objects import DynamicCuboid。而 6.0 版全面启用 pip 标准化模块,命名空间变为了 isaacsim.*。例如:启动器变成了 from isaacsim import SimulationApp;底层 API 大量迁移至 isaacsim.core.api 和 isaacsim.core.experimental。
from isaacsim import SimulationApp
# 1. 启动 Isaac Sim
# {"headless": False} 表示开启图形界面(GUI)。设为 True 则在后台静默运行(适合服务器训练)
simulation_app = SimulationApp({"headless": False})
print("Isaac Sim 启动成功!")
# 2. 保持程序运行 300 帧
# 每次调用 update(),物理引擎就会推演一步,渲染器就会刷新一帧
for i in range(300):
simulation_app.update()
# 3. 关闭 Isaac Sim,释放显存与资源
simulation_app.close()
r'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\02_simulation_app.py"
'''
运行上述代码后,会看到 Isaac Sim 软件窗口被成功唤起,终端打印出“启动成功!”,随后界面保持几秒钟(大约运行 300 帧)后自动关闭。


二:物理世界构建与基础几何体
上一章我们仅仅是启动了 UI 界面,接下来我们要在这个界面中创建一个拥有重力、光照等物理法则的真实“世界”,并向其中添加实体。
1. 创建物理世界 World(03_create_world.py)
在 Isaac Sim 中,World 是一个非常核心的顶层类。它不仅帮我们管理场景中的所有物体(机器人、几何体、灯光),还负责控制物理引擎的时间线(Timeline)和物理步进(Physics Stepping)。
【代码】 03_create_world.py:
将“纯粹的图形界面”升级为“拥有物理法则的世界”。
from isaacsim import SimulationApp
# 1. 启动 Isaac Sim
simulation_app = SimulationApp({"headless": False})
# ⚠️ 官方强制推荐:所有与 Isaac Sim 相关的底层模块,必须在 SimulationApp 创建后再导入!
from isaacsim.core.api import World
# 2. 创建物理世界
world = World()
print("World 创建成功!")
# 3. 运行循环:使用 world.step() 替代 simulation_app.update()
for _ in range(300):
# render=True 保证在进行物理推演的同时更新画面
world.step(render=True)
simulation_app.close()
r'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\03_create_world.py"
'''
【注】 from isaacsim.core.api import World 需要写在 SimulationApp 实例化之后。
运行代码后,界面看起来和之前区别不大:


实际上,在代码逻辑上有本质区别:
- 当调用 World() 时,它会在后台自动为你创建一个默认的物理场景(Stage),并配置好默认的重力方向(Z轴向下)和基础的环境光照。
- 在上一节我们用的是 simulation_app.update(),它仅仅是刷新了 UI 界面,物理时间并没有流逝。这次换成了 world.step(render=True)。它不仅会刷新画面,还会让 PhysX 物理引擎往前推演一个时间步(默认通常是 1/60 秒)。后续我们控制机器人移动,全靠 world.step() 来驱动时间流逝。
2. 添加几何体与物理属性(04_add_cube.py)
有了空旷的“世界”之后,就可以往里面添加具体的物体了。这部分涉及 Isaac Sim 极其关键的几个概念:USD 路径(Paths)、刚体属性(Rigid Body)和碰撞体(Collision)。只有配置好这些,物体才具备真实的物理表现,比如受重力下落并砸在地面上。
如果只是单纯渲染一个方块,它就像是全息投影,会悬停在空中,或者直接穿模掉出屏幕。要让它变成“真实存在”的物体,我们需要给它赋予物理规则。
【代码】 04_add_cube.py:
从“渲染几何体”到“物理刚体”的完整创建流程
from isaacsim import SimulationApp
# 1. 启动 Isaac Sim
simulation_app = SimulationApp({"headless": False})
# 注意:SimulationApp 创建之后才能导入 Omniverse / Isaac API
import isaacsim.core.experimental.utils.app as app_utils
import isaacsim.core.experimental.utils.stage as stage_utils
from isaacsim.core.experimental.objects import Cube, GroundPlane, DistantLight
from isaacsim.core.experimental.prims import RigidPrim, GeomPrim
print("创建新的 Stage...")
# 初始化一个新的 USD 舞台
stage_utils.create_new_stage()
print("添加 GroundPlane...")
# 创建地面,作为物体的支撑面
GroundPlane("/World/GroundPlane")
print("添加光源...")
# 创建平行光,照亮场景
light = DistantLight("/World/Light")
light.set_intensities(300)
print("创建 Cube...")
# 在 Z 轴高度 1.0 的位置生成一个立方体
cube = Cube(
paths="/World/MyCube",
positions=[0.0, 0.0, 1.0],
sizes=1.0,
scales=[0.3, 0.3, 0.3],
)
# 赋予刚体属性(使其受重力等物理法则影响)
RigidPrim(paths=cube.paths)
# 赋予碰撞属性(使其可以与其他物体发生物理碰撞,不至于穿模)
GeomPrim(paths=cube.paths, apply_collision_apis=True)
print("开始仿真...")
# 启动物理时间线
app_utils.play()
# 运行 600 帧,你会看到方块受重力下落并停在地面上
for _ in range(600):
simulation_app.update()
print("结束。")
simulation_app.close()
r'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\04_add_cube.py"
'''
运行代码后,你将看到一个经典的物理仿真画面:一个方块从半空中自由落体,稳稳地砸在地面上。


这里需要注意以下内容:
- 代码里的 /World/MyCube 和 /World/GroundPlane即为 USD 路径。Isaac Sim 底层使用的是 Pixar 研发的 USD(通用场景描述)格式,可以理解为电脑里的文件夹目录树,所有的物体都必须挂载在这个树状结构的某个节点下。
- 穿模掉落情况:如果不加 RigidPrim,方块会一直悬空,因为系统认为它只是个“画上去的风景”。如果你加了 RigidPrim 但忘记加 GeomPrim,方块会受重力下落,但会直接穿透地面。因为没有碰撞体积(Collision),系统无法计算它与地面的接触。
- 在上一节我们用了 world.step() 来推进时间。而在这个使用了 experimental API 的脚本中,我们通过 app_utils.play() 点下了仿真器的“播放键”,随后 simulation_app.update() 就会自动按照物理规则不断推演画面。
三:导入机器人的 USD 资产(05_load_robot.py)
在这一章,我们将导入机器人界非常经典的 7 自由度机械臂 —— Franka Panda。同时,我们将学习一个在 Isaac Sim 机器人控制中极其重要的核心概念:Articulation(关节系统)。
【代码】 05_load_robot.py:
# -----------------------------------------
# 1. Launch Isaac Sim
# -----------------------------------------
from isaacsim import SimulationApp
simulation_app = SimulationApp({"headless": False})
# -----------------------------------------
# 2. Import libraries
# -----------------------------------------
import sys
import carb
import isaacsim.core.experimental.utils.stage as stage_utils
import isaacsim.core.experimental.utils.app as app_utils
from isaacsim.core.experimental.objects import GroundPlane, DistantLight
from isaacsim.core.experimental.prims import Articulation
from isaacsim.storage.native import get_assets_root_path
# -----------------------------------------
# 3. Create Stage
# -----------------------------------------
stage_utils.create_new_stage()
GroundPlane("/World/GroundPlane")
light = DistantLight("/World/DistantLight")
light.set_intensities(300)
# -----------------------------------------
# 4. Load Robot
# -----------------------------------------
# 获取 Isaac Sim 官方资产库的根目录(通常指向 Nucleus 服务器)
assets_root_path = get_assets_root_path()
if assets_root_path is None:
carb.log_error("Could not find Isaac Sim assets folder")
simulation_app.close()
sys.exit()
# 拼接 Franka 机器人的 USD 路径
asset_path = assets_root_path + "/Isaac/Robots/FrankaRobotics/FrankaPanda/franka.usd"
# 将机器人 USD 作为引用(Reference)添加到舞台的 /World/Franka 节点下
stage_utils.add_reference_to_stage(
usd_path=asset_path,
path="/World/Franka",
)
print("Franka Panda loaded successfully.")
# -----------------------------------------
# 5. Run Simulation
# -----------------------------------------
# 将导入的 USD 节点包装为一个 Articulation 对象
robot = Articulation("/World/Franka")
# 启动物理时间线
app_utils.play()
simulation_app.update()
# -----------------------------------------
# 6. 获取机器人信息
# -----------------------------------------
print("=" * 60)
print("Robot Information")
print("=" * 60)
print("\nNumber of DOFs (自由度数量):")
print(robot.num_dofs)
print("\nJoint Names (所有关节名称):")
for i, name in enumerate(robot.dof_names):
print(f"{i}: {name}")
print("\nCurrent Joint Positions (当前关节角):")
print(robot.get_dof_positions())
print("\nRobot Pose (机器人根节点的世界坐标):")
print(robot.get_world_poses())
# 使用 while 循环保持窗口开启,直到用户手动关闭
while simulation_app.is_running():
simulation_app.update()
print("Closing Isaac Sim...")
simulation_app.close()
r'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\05_load_robot.py"
'''
运行脚本后,会看到一台白色的 Franka 机械臂安静地停在界面中央,同时终端会打印出这台机器人的详细硬件信息。


这里有几个关键的知识点:
- 官方资产路径 (get_assets_root_path):Isaac Sim 的大型素材通常存放在本地或云端的 Nucleus 服务器上。这个函数能自动帮你找到服务器的根目录,避免了硬编码绝对路径。
- Articulation:在上一章,我们将方块变成了 RigidPrim(刚体)。但机器人是由多个刚体(连杆)通过各种关节(铰链、滑轨等)连接起来的复杂树状结构。在底层物理引擎中,这种结构被称为 Articulation。通过将 /World/Franka 包装成 Articulation 类,我们就获得了一系列强大的 API,可以轻松读取机器人的自由度(num_dofs)、关节名(dof_names)以及当前姿态(get_dof_positions())。
- 更优雅的运行循环:注意到最后的循环从 for _ in range(600): 变成了 while simulation_app.is_running():。这意味着仿真会一直持续运行,直到你手动点击软件右上角的红叉关闭窗口。
四:关节驱动与键盘控制
要让机器人动起来,最基础的方法就是位置控制(Position Control),即直接给机器人的某个关节发送一个目标角度(Target Position),底层的物理驱动器就会自动计算力矩,将关节推到指定位置。
1. 静态关节目标控制 (06_control_robot (1).py)
首先,我们尝试让机器人的第一个关节(底座)转动到一个固定的角度。
【代码】 06_control_robot (1).py:
【注】 环境搭建和导入机器人的前置代码与上一章完全相同
# ... [前置导入代码省略,见上一章] ...
# -----------------------------------------
# 5. Run Simulation
# -----------------------------------------
robot = Articulation("/World/Franka")
# 启动物理时间线
app_utils.play()
# ⚠️ 避坑:刚启动物理引擎时,状态可能未完全准备好,先空跑 10 帧让其稳定
for _ in range(10):
simulation_app.update()
# -----------------------------------------
# 6. 关节控制
# -----------------------------------------
import numpy as np
# 获取当前所有关节的角度状态
joint_positions = robot.get_dof_positions()
print("\nCurrent Joint Positions:")
print(joint_positions)
# 将元组转换为 Numpy 数组,方便修改
target_positions = np.array(joint_positions)
# 控制第 1 个关节(索引 0)转动到 0.8 弧度
target_positions[0][0] = 0.8
print("\nTarget Joint Positions:")
print(target_positions)
# 持续运行仿真
while simulation_app.is_running():
# 在每一帧中,持续向机器人发送目标指令
robot.set_dof_position_targets(target_positions)
simulation_app.update()
print("Closing Isaac Sim...")
simulation_app.close()
r'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\06_control_robot (1).py"
'''
运行代码后,我们会看到 Franka 机械臂的底座平滑地旋转了一段距离然后停下。


其中,set_dof_position_targets() 是控制关节的核心函数。注意它设置的是目标值(Targets),而不是瞬间把机器人传送到那个位置。机器人底层的 PD 控制器会模拟真实的电机,施加力矩平滑地将其驱动到目标角度。
2. 动态关节摆动控制 (06_control_robot_(2).py)
只动一下显然不够有意思。接下来我们在 while 循环中加入时间(帧)的概念,通过不断修改目标值,让机器人的关节左右来回摆动。
【代码】 06_control_robot (2).py:
# ... [前置导入代码省略] ...
robot = Articulation("/World/Franka")
app_utils.play()
for _ in range(10):
simulation_app.update()
# -----------------------------------------
# 6. 动态关节摆动控制
# -----------------------------------------
import numpy as np
target_positions = np.array(robot.get_dof_positions())
target_positions[0][0] = 0.8
# 设置初始运动方向和帧计数器
direction = 1
frame = 0
while simulation_app.is_running():
frame += 1
# 每 200 帧(约 3.3 秒)改变一次运动方向
if frame % 200 == 0:
direction *= -1 # 反转方向
target_positions[0][0] = 0.8 * direction # 目标在 0.8 和 -0.8 之间切换
print("--------------------------------")
print(f"New Target for Joint 0: {target_positions[0][0]}")
# 将新的目标值发送给机器人
robot.set_dof_position_targets(target_positions)
simulation_app.update()
simulation_app.close()
'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\06_control_robot (2).py"
'''
运行这段代码,会看到机械臂像雷达一样,底座在 0.8 弧度和 -0.8 弧度之间来回扫动。
通过 frame 帧数计数器结合取余 frame % 200 == 0,我们构建了一个简单的时间触发器,实现了机器人的连续自动化运动逻辑。


3. 键盘交互与遥操作 (07_keyboard_control.py)
要实现键盘控制,核心逻辑是:监听键盘按键 → 修改全局的目标关节角 (target_positions) → 在每一帧的主循环中把新的目标角度发送给机器人。
我们需要引入 omni.appwindow 和 carb.input 这两个 Isaac Sim 底层 UI 与输入接口模块。
【代码】 07_keyboard_control.py:
【注】 前置的创世与加载机器人的代码与前文相同。
# ... [前置导入库、创建 World、加载 Franka 机器人的代码省略] ...
# 引入必要的底层库
import omni.appwindow
import carb
robot = Articulation("/World/Franka")
app_utils.play()
for _ in range(10):
simulation_app.update()
# -----------------------------------------
# 6. 键盘事件注册与控制逻辑
# -----------------------------------------
# 获取当前的初始关节角
target_positions = np.array(robot.get_dof_positions())
# 每次按键关节转动的步长(弧度)
STEP = 0.10
# 获取应用窗口和键盘设备接口
app_window = omni.appwindow.get_default_app_window()
keyboard = app_window.get_keyboard()
input_interface = carb.input.acquire_input_interface()
# 定义键盘回调函数
def keyboard_event(event, *args, **kwargs):
global target_positions
# 仅响应按键按下事件 (忽略松开等事件)
if event.type != carb.input.KeyboardEventType.KEY_PRESS:
return True
key = event.input.name
# -----------------------------
# 关节 1 (底座旋转)
# -----------------------------
if key == "A":
target_positions[0][0] -= STEP
elif key == "D":
target_positions[0][0] += STEP
# -----------------------------
# 关节 2 (大臂俯仰)
# -----------------------------
elif key == "W":
target_positions[0][1] += STEP
elif key == "S":
target_positions[0][1] -= STEP
# -----------------------------
# 关节 3 (小臂旋转)
# -----------------------------
elif key == "Q":
target_positions[0][2] += STEP
elif key == "E":
target_positions[0][2] -= STEP
# -----------------------------
# 夹爪控制 (Gripper: 关节 7 和 8)
# -----------------------------
elif key == "J": # 张开夹爪
target_positions[0][7] += 0.01
target_positions[0][8] += 0.01
elif key == "L": # 闭合夹爪
target_positions[0][7] -= 0.01
target_positions[0][8] -= 0.01
print("--------------------------------")
print("Pressed Key :", key)
return True
# 订阅键盘事件,将键盘动作与回调函数绑定
sub = input_interface.subscribe_to_keyboard_events(
keyboard,
keyboard_event,
)
# -----------------------------------------
# 7. Main Loop (主循环)
# -----------------------------------------
while simulation_app.is_running():
# 实时将可能已经被键盘修改过的目标角度发送给机器人
robot.set_dof_position_targets(target_positions)
simulation_app.update()
print("Closing Isaac Sim...")
simulation_app.close()
运行代码后,点击一下 Isaac Sim 的仿真窗口确保其处于激活状态(焦点在画面内),然后按下 W、S、A、D、Q、E,你会发现你可以直接控制机械臂的前三个轴了!按下 J 和 L 还可以控制末端夹爪的开合。
(1)控制键 W / S


(2)控制键 A / D


(3)控制键 Q / E


(4)控制键 J / L


这里涉及到几个关键技术点:
- 回调函数 (Callback):keyboard_event 是一个回调函数。我们并不需要在 while 循环里去写 if 键盘被按下,而是通过 subscribe_to_keyboard_events 告诉系统:“如果有人按键盘,就自动去执行这个 keyboard_event 函数”。
- global 声明:在回调函数中修改外部变量 target_positions 时,必须使用 global 关键字,否则 Python 会认为你在函数内部新建了一个同名局部变量。
- Franka 的夹爪由两个对称的指头组成,所以开合动作需要同时控制两个关节发生同步的相对位移。
五:监听机器人的实时状态(08_robot_state.py)
在 Isaac Sim 中,通过将机器人的 USD 包装为 Articulation 类,我们可以非常方便地读取它底层的各种物理状态。
【代码】 08_robot_state.py:
【注】 前置的创世与加载机器人的代码与前文相同。
# ... [前置导入库、创建 World、加载 Franka 机器人的代码省略] ...
robot = Articulation("/World/Franka")
# 初始化物理引擎并预热
app_utils.play()
for _ in range(10):
simulation_app.update()
# -----------------------------------------
# 7. Print Robot Information
# -----------------------------------------
print("=" * 60)
print("Robot Basic Information")
print("=" * 60)
print(f"Number of DOFs : {robot.num_dofs}")
print("\nJoint Names:")
for i, name in enumerate(robot.dof_names):
print(f"{i}: {name}")
# -----------------------------------------
# 8. Read Robot State (监听实时状态)
# -----------------------------------------
frame = 0
while simulation_app.is_running():
simulation_app.update()
frame += 1
# 为了防止终端被数据刷屏,我们设置每 60 帧(约 1 秒)打印一次状态
if frame % 60 == 0:
print("\n" + "=" * 60)
print("Robot State")
print("=" * 60)
# -------------------------------
# 1. 关节位置 (Joint Positions)
# -------------------------------
# 返回一个 Numpy 数组,包含所有自由度的当前角度(弧度)或位移(米)
joint_positions = robot.get_dof_positions()
print("\nJoint Positions:")
print(joint_positions)
# -------------------------------
# 2. 关节速度 (Joint Velocities)
# -------------------------------
# 返回所有自由度的当前速度(rad/s 或 m/s)
joint_velocities = robot.get_dof_velocities()
print("\nJoint Velocities:")
print(joint_velocities)
# -------------------------------
# 3. 机器人基座世界坐标 (Robot World Pose)
# -------------------------------
# get_world_poses() 返回两个数组:位置(Position)和姿态(Orientation, 四元数)
position, orientation = robot.get_world_poses()
print("\nRobot Position [x, y, z]:")
print(position)
print("\nRobot Orientation [w, x, y, z] (四元数):")
print(orientation)
print("\nClosing Isaac Sim...")
simulation_app.close()
运行代码后,会在终端中看到每隔大约 1 秒钟,系统就会播报一次机器人的全面状态。


这里有几个核心知识点:
get_dof_positions()与get_dof_velocities()这两个 API 是后续做强化学习环境封装时最常用的函数。它们返回的是 Numpy 数组。对于旋转关节(Revolute Joint,如机械臂的手臂部分),返回值单位是弧度(rad)和弧度/秒(rad/s);对于平移关节(Prismatic Joint,如两指夹爪的开合滑动部分),单位则是米(m)和米/秒(m/s)。get_world_poses()这个函数返回的是该 Articulation 根节点(对于 Franka 来说就是底座 Base)在整个世界坐标系下的绝对位置。Position 是一维数组 [x, y, z],表示三维空间坐标。Orientation 是一维数组 [w, x, y, z](或类似顺序),代表四元数(Quaternion)。在机器人学和 3D 引擎中,为了避免欧拉角带来的“万向节死锁”问题,姿态通常默认使用四元数来表示。- 频率控制:代码中使用 frame % 60 == 0 来控制打印频率。在实际的控制算法中,我们也经常用这种方式来模拟真实传感器的数据刷新率(例如摄像头通常是 30fps,而底层关节编码器可能是 1000Hz)。
六:运动学结算:正向与逆向 (FK & IK)
1. 正向运动学 (FK):实时获取末端执行器位姿(09_forward_kinematics.py)
如果说在上一章我们获取了机器人的“内部感受”(关节角),那么在这一节,我们要获取的是机器人在现实世界中的“外部表现”——它的手到底在哪儿。
为了精准追踪机械臂末端(手部)的位置,我们引入了一个新的类 RigidPrim,用它来单独提取 Franka 机器人的 panda_hand 连杆。
【代码】 09_forward_kinematics.py:
# -----------------------------------------
# 1-4. 初始化与导入机器人的代码与前文一致
# -----------------------------------------
from isaacsim import SimulationApp
simulation_app = SimulationApp({"headless": False})
import sys
import carb
import isaacsim.core.experimental.utils.stage as stage_utils
import isaacsim.core.experimental.utils.app as app_utils
from isaacsim.core.experimental.objects import GroundPlane, DistantLight
from isaacsim.core.experimental.prims import Articulation, RigidPrim
from isaacsim.storage.native import get_assets_root_path
stage_utils.create_new_stage()
GroundPlane("/World/GroundPlane")
light = DistantLight("/World/DistantLight")
light.set_intensities(300)
assets_root_path = get_assets_root_path()
asset_path = assets_root_path + "/Isaac/Robots/FrankaRobotics/FrankaPanda/franka.usd"
stage_utils.add_reference_to_stage(usd_path=asset_path, path="/World/Franka")
# -----------------------------------------
# 5. Create Robot & End Effector Objects
# -----------------------------------------
robot = Articulation("/World/Franka")
# FK 的核心:获取末端执行器(End Effector)
# 引入 RigidPrim 去独立追踪 "panda_hand" 这个连杆的位姿
end_effector = RigidPrim("/World/Franka/panda_hand")
# -----------------------------------------
# 6. Initialize Physics
# -----------------------------------------
app_utils.play()
for _ in range(10):
simulation_app.update()
# -----------------------------------------
# 7. FK Read Loop
# -----------------------------------------
frame = 0
while simulation_app.is_running():
simulation_app.update()
frame += 1
# 每 60 帧读取并打印一次
if frame % 60 == 0:
# 【输入】当前关节角度
joint_positions = robot.get_dof_positions()
# 【输出】正向运动学结果:末端执行器 (panda_hand) 的空间坐标与姿态
# get_world_poses 会返回 (位置, 四元数姿态)
ee_position, ee_orientation = end_effector.get_world_poses()
print("\n" + "=" * 60)
print("Forward Kinematics (FK) - Realtime Output")
print("=" * 60)
# ⚠️ 避坑:Isaac Sim 支持多机器人(Batch)并行运算,所以返回的是二维张量
# 我们使用 .numpy() 转换为 CPU 数组,并取 [0] 获取第一个机器人的数据
pos = ee_position.numpy()[0]
ori = ee_orientation.numpy()[0]
print("Output [End Effector Position]:")
print(f"X: {float(pos[0]):.4f} m")
print(f"Y: {float(pos[1]):.4f} m")
print(f"Z: {float(pos[2]):.4f} m")
print("\nOutput [End Effector Orientation (w, x, y, z)]:")
print(f"W: {float(ori[0]):.4f}")
print(f"X: {float(ori[1]):.4f}")
print(f"Y: {float(ori[2]):.4f}")
print(f"Z: {float(ori[3]):.4f}")
print("\nClosing Isaac Sim...")
simulation_app.close()
运行代码,会发现终端精准输出了 panda_hand 的 X、Y、Z 坐标和姿态。

有如下内容需要注意:
-
在上一章读取状态时,
robot.get_world_poses()返回的是机器人底座(Base)的坐标(通常固定在0,0,0)。而通过实例化RigidPrim("/World/Franka/panda_hand"),我们将监视器精确地挂载到了机械臂末端的那个零件上。 -
张量与批处理机制 (
.numpy()[0]):因为 Isaac Sim 的核心优势之一是大规模并行强化学习训练(比如同时模拟 1000 个机械臂)。因此,它的底层 API 默认返回的是支持 GPU 计算的二维张量(Tensor/Array),维度格式为[环境数量, 状态数据]。由于我们目前只创建了 1 个机器人,所以必须加上[0]将属于我们这个机器人的具体数据提取出来。
2. 逆向运动学 (IK):让机械臂指哪打哪 (10_inverse_kinematics.py)
在上一节的正向运动学(FK)中,物理引擎充当了“测量员”的角色,告诉我们机器人的手目前在哪。而在这一节的逆向运动学(Inverse Kinematics, IK)中,我们将引入一个强大的“数学大脑”——Lula 求解器。
我们要对大脑下达指令:“我想要把手放到空间坐标 X, Y, Z 的位置,并且姿态是 W, X, Y, Z。请帮我算出来,我的这 7 个马达分别该转到多少度?”
这一部分我们将利用底层 USD API 在空间中画一个“红色的虚拟小球”作为目标点。只要程序运行无误,机械臂的末端就会自动去追踪并到达这个小球正上方。
【代码】 10_inverse_kinematics.py:
# ... [1-3. 初始化与导入库代码省略,与前文一致] ...
from isaacsim import SimulationApp
simulation_app = SimulationApp({"headless": False})
import sys, carb
import numpy as np
import isaacsim.core.experimental.utils.stage as stage_utils
import isaacsim.core.experimental.utils.app as app_utils
from isaacsim.core.experimental.objects import GroundPlane, DistantLight
from isaacsim.core.experimental.prims import Articulation
from isaacsim.storage.native import get_assets_root_path
from pxr import UsdGeom, Gf
from isaacsim.robot_motion.motion_generation import LulaKinematicsSolver, interface_config_loader
stage_utils.create_new_stage()
GroundPlane("/World/GroundPlane")
light = DistantLight("/World/DistantLight")
light.set_intensities(300)
# -----------------------------------------
# 4. Define Target (定义红色目标点与抓取偏移量)
# -----------------------------------------
SPHERE_POSITION = np.array([0.5, 0.2, 0.25])
# 核心技巧:机械臂目标点设在小球正上方 15cm 处,防止夹爪撞击地面
TARGET_POSITION = SPHERE_POSITION + np.array([0.0, 0.0, 0.15])
stage = stage_utils.get_current_stage()
sphere = UsdGeom.Sphere.Define(stage, "/World/TargetMarker")
sphere.GetRadiusAttr().Set(0.03)
sphere.GetDisplayColorAttr().Set([(1.0, 0.0, 0.0)])
xform = UsdGeom.Xformable(sphere)
xform.AddTranslateOp().Set(Gf.Vec3d(float(SPHERE_POSITION[0]), float(SPHERE_POSITION[1]), float(SPHERE_POSITION[2])))
# -----------------------------------------
# 5-7. 导入机器人与预热物理引擎 [代码省略]
# -----------------------------------------
# ...加载 Franka,调用 app_utils.play(),并 update 10 帧...
# -----------------------------------------
# 7.5 Setup Raw Lula IK Solver (加载纯数学解算器)
# -----------------------------------------
print("Loading Raw Lula Kinematics Solver...")
kinematics_config = interface_config_loader.load_supported_lula_kinematics_solver_config("Franka")
lula_solver = LulaKinematicsSolver(**kinematics_config)
# -----------------------------------------
# 8. IK Main Loop
# -----------------------------------------
# 设定目标姿态 (四元数:让夹爪垂直朝下)
TARGET_ORIENTATION = np.array([0.0, 1.0, 0.0, 0.0])
print("\n" + "=" * 60)
print("Starting Inverse Kinematics (IK) Control...")
print("=" * 60)
while simulation_app.is_running():
simulation_app.update()
# 如果物理张量失效,优雅跳出循环
if not robot.is_physics_tensor_entity_valid():
print("Physics stopped. Exiting loop safely...")
break
# 1. 获取当前所有的 9 个关节角度 (7手臂 + 2夹爪)
current_joints_2d = robot.get_dof_positions()
current_joints_1d = current_joints_2d.numpy()[0]
# 2. Lula 求解器只需要前 7 个“手臂关节”的数据作为计算起点 (Warm Start)
arm_joints_warm_start = current_joints_1d[:7]
# 3. 直接调用 Lula 引擎进行纯数学求解
target_arm_array, success = lula_solver.compute_inverse_kinematics(
frame_name="panda_hand",
target_position=TARGET_POSITION,
target_orientation=TARGET_ORIENTATION,
warm_start=arm_joints_warm_start
)
if success:
# 4. 如果求解成功,target_arm_array 里面直接就是算好的 7 个关节角度!
# 5. 我们把这 7 个新角度拼回到原来 9 个关节的数组里 (保证夹爪状态不被破坏)
target_joints_1d = current_joints_1d.copy()
target_joints_1d[:7] = target_arm_array
# 6. 转回 Isaac Sim 物理引擎需要的 2D 矩阵格式并下发执行
target_joints_2d = np.expand_dims(target_joints_1d, axis=0)
robot.set_dof_position_targets(target_joints_2d)
print("\nClosing Isaac Sim...")
simulation_app.close()
运行代码,我们会看到 Franka 机械臂迅速且精准地将它的夹爪移动到了红色小球的正上方,并且夹爪垂直向下对准。


在 while 循环开头,我们加入了 if not robot.is_physics_tensor_entity_valid(): break,这是为了避免关闭窗口时候的报错(纯报错难受,实际没什么影响)。在 Isaac Sim 中,如果用户点击了关闭或者后台重置了状态,物理引擎的底层张量会瞬间断开。如果没有这个判断直接去 get_dof_positions(),程序会立刻抛出 AssertionError 崩溃。
七:场景搭建与夹爪控制
这一章我们添加一个工作台和几个测试物体。与第二章使用 isaacsim.core.experimental.objects 中的高级封装类不同,这次我们将接触更底层的 USD 原生 API。
1. 搭建真实的交互场景 (11_create_scene.py)
我们将在这个脚本中生成一张桌子,并在桌面上放置一个蓝色的方块和一个绿色的球体。
【代码】 11_create_scene.py:
【注】 在新版的 experimental 架构中,官方更加鼓励开发者直接调用 pxr.UsdGeom 和 pxr.UsdPhysics 等原生接口来构建场景,这让仿真环境的搭建更加严谨和贴近底层逻辑。
from isaacsim import SimulationApp
simulation_app = SimulationApp({"headless": False})
import sys, carb
import isaacsim.core.experimental.utils.stage as stage_utils
import isaacsim.core.experimental.utils.app as app_utils
from isaacsim.core.experimental.objects import GroundPlane, DistantLight
from isaacsim.storage.native import get_assets_root_path
# 引入底层 USD 几何与物理模块
from pxr import UsdGeom, Gf, UsdPhysics
# -----------------------------------------
# 1. Create Basic Environment
# -----------------------------------------
stage_utils.create_new_stage()
stage = stage_utils.get_current_stage()
GroundPlane("/World/GroundPlane")
light = DistantLight("/World/DistantLight")
light.set_intensities(300)
# -----------------------------------------
# 2. Create Scene Objects (搭建实验台)
# -----------------------------------------
# --- 2.1 创建桌子 (Table) ---
print("Creating Table...")
table = UsdGeom.Cube.Define(stage, "/World/Table")
table.GetSizeAttr().Set(1.0) # 初始规格
table.GetDisplayColorAttr().Set([(0.4, 0.25, 0.1)]) # 棕色
table_xform = UsdGeom.Xformable(table)
table_xform.ClearXformOpOrder()
table_xform.AddTranslateOp().Set(Gf.Vec3d(0.6, 0.0, 0.15)) # 放置在机器人正前方
table_xform.AddScaleOp().Set(Gf.Vec3f(0.3, 0.6, 0.3)) # 压扁拉长,变成桌子的形状
# ⚠️ 注意:桌子只添加了碰撞体,没有添加刚体!
UsdPhysics.CollisionAPI.Apply(table.GetPrim())
# --- 2.2 创建可抓取的方块 (Cube) ---
print("Creating Graspable Cube...")
cube = UsdGeom.Cube.Define(stage, "/World/Cube")
cube.GetSizeAttr().Set(0.04) # 边长 4cm
cube.GetDisplayColorAttr().Set([(0.0, 0.4, 0.8)]) # 蓝色
cube_xform = UsdGeom.Xformable(cube)
cube_xform.ClearXformOpOrder()
cube_xform.AddTranslateOp().Set(Gf.Vec3d(0.6, -0.1, 0.45)) # 悬空放置在桌子上方
UsdPhysics.CollisionAPI.Apply(cube.GetPrim())
# 方块需要受重力落下并被抓取,所以必须添加刚体
UsdPhysics.RigidBodyAPI.Apply(cube.GetPrim())
# --- 2.3 创建可抓取的球体 (Sphere) ---
print("Creating Graspable Sphere...")
sphere = UsdGeom.Sphere.Define(stage, "/World/Sphere")
sphere.GetRadiusAttr().Set(0.02) # 半径 2cm
sphere.GetDisplayColorAttr().Set([(0.2, 0.8, 0.2)]) # 绿色
sphere_xform = UsdGeom.Xformable(sphere)
sphere_xform.ClearXformOpOrder()
sphere_xform.AddTranslateOp().Set(Gf.Vec3d(0.6, 0.1, 0.45))
UsdPhysics.CollisionAPI.Apply(sphere.GetPrim())
UsdPhysics.RigidBodyAPI.Apply(sphere.GetPrim())
# -----------------------------------------
# 3. Load Franka
# -----------------------------------------
assets_root_path = get_assets_root_path()
asset_path = assets_root_path + "/Isaac/Robots/FrankaRobotics/FrankaPanda/franka.usd"
stage_utils.add_reference_to_stage(usd_path=asset_path, path="/World/Franka")
# -----------------------------------------
# 4. Initialize Physics & Loop
# -----------------------------------------
app_utils.play()
print("\nObserve the scene. The Cube and Sphere should drop exactly onto the Table.")
while simulation_app.is_running():
simulation_app.update()
simulation_app.close()
r'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\11_create_scene.py"
'''
运行代码后,我们会看到 Franka 机器人的前方出现了一张棕色的长方形桌子,随后半空中掉下了一蓝一绿两个物体,稳稳地砸在了桌面上。


在代码中,对于桌子(Table)我们仅仅给它施加了 CollisionAPI(碰撞属性)。这就把它变成了一个静态碰撞体(Static Collider)。它就像是长在地上的一块石头,不受重力影响,机器人的手撞不烂它,上面的方块也掉不下去。如果手贱给桌子加上了 RigidBodyAPI,它就会变成一块由于重心不稳而倒塌的积木;而抓取物 (Cube / Sphere)不仅有 CollisionAPI,还有了 RigidBodyAPI(刚体属性)。因此它们拥有质量,受重力影响,并且在未来可以被机械臂施加力矩强行“夹”起来。
2. 末端执行器:夹爪 (Gripper) 控制 (12_gripper_control.py)
有了场景和物体,接下来我们就要赋予机器人“抓取”的能力。
在这节中,我们将聚焦于 Franka 机械臂的末端执行器(End Effector)—— 也就是那两根负责夹取物体的手指。在 Isaac Sim 的物理底层中,控制夹爪与控制手臂关节的逻辑是一脉相承的。
Franka Panda 机械臂共有 9 个自由度(DOF)。前 7 个是旋转关节(Revolute Joint),控制手臂的扭动;最后 2 个(索引为 7 和 8)是平移关节(Prismatic Joint),分别控制左右两根手指的直线滑动。
在这个脚本中,我们将写一个简单的逻辑:保持手臂不动,让夹爪每隔 2 秒钟自动张开、闭合一次。
【代码】 12_gripper_control.py:
# ... [前置环境搭建与机器人导入代码省略,见前文] ...
robot = Articulation("/World/Franka")
app_utils.play()
for _ in range(10):
simulation_app.update()
# -----------------------------------------
# 7. Gripper Control Loop
# -----------------------------------------
print("\n" + "=" * 60)
print("Starting Gripper Control: Open <-> Close")
print("=" * 60)
frame = 0
is_open = True
# Franka 夹爪的物理极限:
# 张开极限: 约 0.04 米 (每个手指分别向外移动 4 厘米,总开口 8 厘米)
# 闭合极限: 0.00 米
GRIPPER_OPEN = 0.04
GRIPPER_CLOSED = 0.00
while simulation_app.is_running():
simulation_app.update()
frame += 1
# 每 120 帧 (由于默认 60fps,大约 2 秒) 切换一次夹爪状态
if frame % 120 == 0:
is_open = not is_open
# 1. 获取当前所有 9 个自由度(DOF)的位置
current_joints = robot.get_dof_positions().numpy()[0]
# 2. 我们不想动手臂,所以把当前的手臂角度作为目标保留
target_joints = current_joints.copy()
# 3. 仅修改最后两个值(即两个手指的目标位置)
if is_open:
print(f"[Frame {frame}] Action: OPENING Gripper...")
target_joints[7] = GRIPPER_OPEN
target_joints[8] = GRIPPER_OPEN
else:
print(f"[Frame {frame}] Action: CLOSING Gripper...")
target_joints[7] = GRIPPER_CLOSED
target_joints[8] = GRIPPER_CLOSED
# 4. 转换为二维张量并下发执行
target_joints_2d = np.expand_dims(target_joints, axis=0)
robot.set_dof_position_targets(target_joints_2d)
print("\nClosing Isaac Sim...")
simulation_app.close()
r'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\12_gripper_control.py"
'''
运行代码,我们会看到机械臂稳稳地停在原地,末端的夹爪有节奏地张开和闭合。


八:Pick & Place 任务
前面七章,我们分别学习了如何搭建世界、导入机器人、读取状态、逆向运动学(IK)解算以及夹爪的开合控制。现在,我们将把所有积木拼装在一起,通过一个简易状态机(State Machine)和航点规划(Waypoints),完成机器人仿真中最经典的抓取与放置任务
【注】 真实的物理引擎是非常残酷的。如果按照默认的物理材质直接抓取,方块极易从夹爪中滑落。所以在这段代码中,我们还需要过底层 API 调整物体的质量与摩擦系数。
在这个脚本中,机器人的任务是:移动到蓝色方块上方 -> 下降套住方块 -> 闭合夹爪 -> 提起方块 -> 移动到绿色球体旁边 -> 松开夹爪完成放置。
【代码】 13_pick_place.py:
# -----------------------------------------
# 1-2. 初始化与导入库 [代码省略,同前文]
# -----------------------------------------
from isaacsim import SimulationApp
simulation_app = SimulationApp({"headless": False})
import sys, carb
import numpy as np
import isaacsim.core.experimental.utils.stage as stage_utils
import isaacsim.core.experimental.utils.app as app_utils
from isaacsim.core.experimental.objects import GroundPlane, DistantLight
from isaacsim.core.experimental.prims import Articulation
from isaacsim.storage.native import get_assets_root_path
from pxr import UsdGeom, Gf, UsdPhysics, UsdShade
from isaacsim.robot_motion.motion_generation import LulaKinematicsSolver, interface_config_loader
# -----------------------------------------
# 3. Create Basic Environment & Scene
# -----------------------------------------
stage_utils.create_new_stage()
stage = stage_utils.get_current_stage()
GroundPlane("/World/GroundPlane")
light = DistantLight("/World/DistantLight")
light.set_intensities(300)
print("Creating Table and Objects...")
# 桌子 (高度 0.25, 中心在 X=0.6)
table = UsdGeom.Cube.Define(stage, "/World/Table")
table_xform = UsdGeom.Xformable(table)
table_xform.ClearXformOpOrder()
table_xform.AddTranslateOp().Set(Gf.Vec3d(0.6, 0.0, 0.125))
table_xform.AddScaleOp().Set(Gf.Vec3f(0.3, 0.6, 0.25))
table.GetDisplayColorAttr().Set([(0.4, 0.25, 0.1)])
UsdPhysics.CollisionAPI.Apply(table.GetPrim())
# 方块 (我们要抓的目标,放在 Y=-0.1 处)
cube = UsdGeom.Cube.Define(stage, "/World/Cube")
cube_xform = UsdGeom.Xformable(cube)
cube_xform.ClearXformOpOrder()
cube_xform.AddTranslateOp().Set(Gf.Vec3d(0.6, -0.1, 0.45))
cube_xform.AddScaleOp().Set(Gf.Vec3f(0.03, 0.03, 0.03))
cube.GetDisplayColorAttr().Set([(0.0, 0.4, 0.8)])
UsdPhysics.CollisionAPI.Apply(cube.GetPrim())
UsdPhysics.RigidBodyAPI.Apply(cube.GetPrim())
# 👉 【抓取成功的核心秘密:摩擦力与质量设置】
# 1. 显式设置方块的质量(比如 50克,防止默认质量过大导致夹爪托不住)
mass_api = UsdPhysics.MassAPI.Apply(cube.GetPrim())
mass_api.CreateMassAttr(0.05)
# 2. 创建高摩擦物理材质
material_path = "/World/HighFrictionMaterial"
material = UsdShade.Material.Define(stage, material_path)
physics_material = UsdPhysics.MaterialAPI.Apply(material.GetPrim())
physics_material.CreateStaticFrictionAttr(2.0) # 提高静摩擦 (默认通常是0.5左右)
physics_material.CreateDynamicFrictionAttr(2.0) # 提高动摩擦
physics_material.CreateRestitutionAttr(0.0) # 恢复系数(弹性)设为0,防止触碰时弹开
# 3. 将物理材质绑定到方块上
binding_api = UsdShade.MaterialBindingAPI.Apply(cube.GetPrim())
binding_api.Bind(material, UsdShade.Tokens.weakerThanDescendants, "physics")
# 球体 (参考物,放在 Y=0.1 处)
sphere = UsdGeom.Sphere.Define(stage, "/World/Sphere")
sphere_xform = UsdGeom.Xformable(sphere)
sphere_xform.ClearXformOpOrder()
sphere_xform.AddTranslateOp().Set(Gf.Vec3d(0.6, 0.1, 0.45))
sphere_xform.AddScaleOp().Set(Gf.Vec3f(0.02, 0.02, 0.02))
sphere.GetDisplayColorAttr().Set([(0.2, 0.8, 0.2)])
UsdPhysics.CollisionAPI.Apply(sphere.GetPrim())
UsdPhysics.RigidBodyAPI.Apply(sphere.GetPrim())
# -----------------------------------------
# 4 & 5. 导入机器人与加载 Lula IK 求解器 [同前文]
# -----------------------------------------
assets_root_path = get_assets_root_path()
asset_path = assets_root_path + "/Isaac/Robots/FrankaRobotics/FrankaPanda/franka.usd"
stage_utils.add_reference_to_stage(usd_path=asset_path, path="/World/Franka")
robot = Articulation("/World/Franka")
print("Loading Lula Kinematics Solver...")
kinematics_config = interface_config_loader.load_supported_lula_kinematics_solver_config("Franka")
lula_solver = LulaKinematicsSolver(**kinematics_config)
app_utils.play()
for _ in range(10):
simulation_app.update()
# -----------------------------------------
# 6. Pick & Place State Machine (终极融合)
# -----------------------------------------
print("\n" + "=" * 60)
print("Starting Pick & Place Task...")
print("=" * 60)
# 定义关键航点 (Waypoints)
PRE_GRASP_POS = np.array([0.6, -0.1, 0.60]) # 悬停在方块上方
GRASP_POS = np.array([0.6, -0.1, 0.52]) # 下降套住方块
LIFT_POS = np.array([0.6, -0.1, 0.60]) # 提起方块
PLACE_POS = np.array([0.6, 0.1, 0.60]) # 移动到球体上方
RETREAT_POS = np.array([0.5, 0.0, 0.65]) # 完工撤退
TARGET_ORI = np.array([0.0, 1.0, 0.0, 0.0]) # 夹爪永远垂直朝下
GRIPPER_OPEN = 0.04
GRIPPER_GRASP = 0.014 # 不完全闭合,卡住方块尺寸
GRIPPER_CLOSE = 0.00
# 初始指令
current_target_pos = PRE_GRASP_POS
current_gripper = GRIPPER_OPEN
frame = 0
while simulation_app.is_running():
simulation_app.update()
# 如果物理张量已经无效(比如用户关闭了窗口),就优雅地跳出循环
if not robot.is_physics_tensor_entity_valid():
print("Physics stopped. Exiting loop safely...")
break
frame += 1
# --- 简易状态机:根据帧数执行一连串动作 ---
if frame == 10:
print("[State 1] 移动到方块上方准备...")
current_target_pos = PRE_GRASP_POS
elif frame == 120:
print("[State 2] 下降夹爪...")
current_target_pos = GRASP_POS
elif frame == 240:
print("[State 3] 抓取!")
current_gripper = GRIPPER_GRASP
elif frame == 320:
print("[State 4] 提起方块...")
current_target_pos = LIFT_POS
elif frame == 420:
print("[State 5] 移动到球体旁边...")
current_target_pos = PLACE_POS
elif frame == 560:
print("[State 6] 释放方块!")
current_gripper = GRIPPER_OPEN
elif frame == 620:
print("[State 7] 任务完成,撤退...")
current_target_pos = RETREAT_POS
# --- 底层控制逻辑:IK 融合夹爪 ---
current_joints_1d = robot.get_dof_positions().numpy()[0]
arm_joints_warm_start = current_joints_1d[:7]
# 让 Lula 计算目标手臂角度
target_arm_array, success = lula_solver.compute_inverse_kinematics(
frame_name="panda_hand",
target_position=current_target_pos,
target_orientation=TARGET_ORI,
warm_start=arm_joints_warm_start
)
if success:
target_joints_1d = current_joints_1d.copy()
# 前7个给手臂 (来自 IK 解算)
target_joints_1d[:7] = target_arm_array
# 后2个给夹爪 (来自状态机指令)
target_joints_1d[7] = current_gripper
target_joints_1d[8] = current_gripper
target_joints_2d = np.expand_dims(target_joints_1d, axis=0)
robot.set_dof_position_targets(target_joints_2d)
print("\nClosing Isaac Sim...")
simulation_app.close()
r'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\13_pick_place.py"
'''
运行代码,我们会看到一个抓取与放置流程。



默认的物理引擎设定里,夹爪是金属的,方块也是光滑的,而且方块的默认系统质量可能高达几千克。因此代码中,我们使用 UsdPhysics.MaterialAPI 创建了一个高摩擦力材质(静摩擦和动摩擦都拉到了 2.0,并且取消了弹性 Restitution),并使用 UsdPhysics.MassAPI 将方块质量限制在 0.05kg。
此外,变量 GRIPPER_GRASP = 0.014。我们没有让夹爪完全闭合(0.00),而是让它闭合到了比方块尺寸稍微小一点点的尺寸。这样物理引擎的穿透恢复力(Penetration Resolution)就会转化为持续的“夹紧力”,把物体牢牢锁住。
九:赋予视觉:RGB 与深度相机配置
如果说前面八章我们是在教机器人“怎么动”,那么这一章,我们要赋予机器人“看”的能力。我们将利用 Isaac Sim 的 Camera 传感器组件,在虚拟世界里架设一台单反相机,并拍下一张带有真实光影的照片保存到本地。
1. 配置与截取 RGB 图像 (14_camera_rgb.py)
为了让视觉效果足够明显,我们在场景正前方放置了一个巨大且极具对比度的纯蓝色方块,并在距离它 6 米远的地方架设了一台分辨率为 720P 的虚拟相机。
【代码】 14_camera_rgb.py:
from isaacsim import SimulationApp
simulation_app = SimulationApp({"headless": False})
import os
import numpy as np
from PIL import Image
import isaacsim.core.experimental.utils.stage as stage_utils
import isaacsim.core.experimental.utils.app as app_utils
from isaacsim.core.experimental.objects import GroundPlane
from pxr import UsdGeom, Gf, UsdLux, UsdPhysics
# 引入 Camera 传感器
from isaacsim.sensors.camera import Camera
# -----------------------------------------
# 1. Create Environment (修复曝光与隐身问题)
# -----------------------------------------
stage_utils.create_new_stage()
stage = stage_utils.get_current_stage()
GroundPlane("/World/GroundPlane")
# 💡 技巧 1:降低环境光曝光,把刺眼的默认值降到 500
print("Setting up Dome Light...")
dome_light = UsdLux.DomeLight.Define(stage, "/World/DomeLight")
dome_light.CreateIntensityAttr(500)
# 💡 技巧 2:造一个巨大无比、对比度极强的纯蓝色方块作为拍摄对象
print("Creating Massive Blue Cube...")
cube = UsdGeom.Cube.Define(stage, "/World/Cube")
cube_xform = UsdGeom.Xformable(cube)
cube_xform.ClearXformOpOrder()
cube_xform.AddTranslateOp().Set(Gf.Vec3d(1.0, 0.0, 0.5)) # 放在正前方 X=1.0
cube_xform.AddScaleOp().Set(Gf.Vec3f(0.5, 0.5, 0.5)) # 放大尺寸
cube.GetDisplayColorAttr().Set([(0.0, 0.4, 1.0)]) # 漆成显眼的蓝色
UsdPhysics.CollisionAPI.Apply(cube.GetPrim())
# -----------------------------------------
# 2. Setup Camera (架设机位)
# -----------------------------------------
print("Setting up Camera...")
# 方块在 X = 1.0。我们把相机放在 X = -5.0,两者在一条绝对笔直的线上
cam_position = np.array([-5.0, 0.0, 0.5])
# 绝对零旋转:[W, X, Y, Z] = [1.0, 0.0, 0.0, 0.0]
# 没有任何抬头、低头、左转、右转,死死盯住正前方的 X 轴
cam_orientation = np.array([1.0, 0.0, 0.0, 0.0])
camera = Camera(
prim_path="/World/RGB_Camera",
position=cam_position,
orientation=cam_orientation,
frequency=20,
resolution=(1280, 720) # 设置画面分辨率
)
camera.initialize()
# -----------------------------------------
# 3. Warm up and Capture (渲染器预热与截图)
# -----------------------------------------
print("Playing simulation timeline...")
app_utils.play()
# 💡 技巧 3:RTX 渲染器预热
print("📸 Warming up RTX Renderer (Waiting 60 frames)...")
for i in range(60):
simulation_app.update()
camera.get_rgba() # 强制相机在后台连续刷新缓存
print("📸 Taking the actual picture...")
rgba_image = camera.get_rgba()
# -----------------------------------------
# 4. Save Image (数据处理与保存)
# -----------------------------------------
if rgba_image is not None and rgba_image.shape[0] > 0:
# 剔除 Alpha 通道,只保留 RGB
rgb_image = rgba_image[:, :, :3]
# 格式转换:物理引擎输出的通常是浮点数,需要转为 0-255 的整数
if rgb_image.dtype != np.uint8:
if rgb_image.max() <= 1.0:
rgb_image = (rgb_image * 255).astype(np.uint8)
else:
rgb_image = rgb_image.astype(np.uint8)
# 保存图片
output_dir = r"D:\11_MATERIALSorPROJECTS\HelloIsaac\_output"
os.makedirs(output_dir, exist_ok=True)
output_name = os.path.join(output_dir, "14_captured_rgb.png")
img = Image.fromarray(rgb_image)
img.save(output_name)
print(f"🎉 Success! Open '{output_name}'.")
else:
print("❌ Failed to capture image.")
simulation_app.close()
r'''
cd "D:\02_ACADEMICorPRACTICE\Modeling_and_Simulation\IsaacSim"
.\python.bat "D:\11_MATERIALSorPROJECTS\HelloIsaac\scripts\14_camera_rgb.py"
'''
运行代码后,前往你设置的输出目录会看到一张 14_captured_rgb.png 照片。



2. 提取 Depth 深度图与 Replicator 架构 (15_camera_depth.py)
对于机器人的视觉感知来说,仅仅有彩色的 RGB 图像是不够的,它只能提供 2D 信息。如果我们要让机器人知道物体离自己有多远(比如进行 3D 抓取或避障),就必须获取 Depth 深度图。
为了稳定、高效地提取各种机器视觉数据(如深度、法线、语义分割掩码),我们必须引入 Isaac Sim 最强大的合成数据引擎:Omniverse Replicator (omni.replicator.core)。
【代码】 15_camera_depth.py:
from isaacsim import SimulationApp
simulation_app = SimulationApp({"headless": False})
import os
import numpy as np
from PIL import Image
import isaacsim.core.experimental.utils.stage as stage_utils
import isaacsim.core.experimental.utils.app as app_utils
from isaacsim.core.experimental.objects import GroundPlane
from pxr import UsdGeom, Gf, UsdLux, UsdPhysics
from isaacsim.sensors.camera import Camera
# 💡 核心:引入底层的合成数据生成器 (Replicator)
import omni.replicator.core as rep
# -----------------------------------------
# 1. Create Environment & Camera
# -----------------------------------------
stage_utils.create_new_stage()
stage = stage_utils.get_current_stage()
GroundPlane("/World/GroundPlane")
dome_light = UsdLux.DomeLight.Define(stage, "/World/DomeLight")
dome_light.CreateIntensityAttr(500)
# 创建一个蓝色方块作为测距参考物
cube = UsdGeom.Cube.Define(stage, "/World/Cube")
cube_xform = UsdGeom.Xformable(cube)
cube_xform.ClearXformOpOrder()
cube_xform.AddTranslateOp().Set(Gf.Vec3d(1.0, 0.0, 0.5))
cube_xform.AddScaleOp().Set(Gf.Vec3f(0.5, 0.5, 0.5))
cube.GetDisplayColorAttr().Set([(0.0, 0.4, 1.0)])
UsdPhysics.CollisionAPI.Apply(cube.GetPrim())
# 架设相机
cam_position = np.array([-5.0, 0.0, 0.5])
cam_orientation = np.array([1.0, 0.0, 0.0, 0.0])
camera = Camera(
prim_path="/World/Depth_Camera",
position=cam_position,
orientation=cam_orientation,
frequency=20,
resolution=(1280, 720)
)
camera.initialize()
# -----------------------------------------
# 2. Initialize Replicator Annotators (挂载数据节点)
# -----------------------------------------
print("Activating Replicator Annotators...")
# 手动创建一个 RenderProduct(渲染产品),绑定到我们的相机上
render_product = rep.create.render_product(camera.prim_path, (1280, 720))
# 强制挂载深度计算节点!
# "distance_to_image_plane" 返回的是物体到相机平面的绝对物理距离(米)
depth_annotator = rep.AnnotatorRegistry.get_annotator("distance_to_image_plane")
depth_annotator.attach(render_product)
# -----------------------------------------
# 3. Warm up and Capture DEPTH
# -----------------------------------------
print("Playing simulation timeline...")
app_utils.play()
print("📸 Warming up RTX Renderer (Waiting 60 frames)...")
for i in range(60):
simulation_app.update()
print("📸 Taking the DEPTH picture...")
# 直接从挂载的 annotator 里抽取出底层的深度张量
depth_data = depth_annotator.get_data()
# -----------------------------------------
# 4. Process and Save Depth Image
# -----------------------------------------
if depth_data is not None:
try:
depth_image = np.array(depth_data)
if depth_image.size > 0:
print(f"✅ Depth data extracted! Shape: {depth_image.shape}")
# 💡 深度图处理核心算法:
# 1. 截断天空(无穷远,np.inf)到 20.0 米,防止归一化失效
depth_image[depth_image == np.inf] = 20.0
depth_image[depth_image > 20.0] = 20.0
d_min = depth_image.min()
d_max = depth_image.max()
print(f"📏 Distance measured: from {d_min:.2f}m to {d_max:.2f}m")
if d_max > d_min:
# 2. 距离归一化,转为 0~255 的灰度图
depth_normalized = 255.0 * (depth_image - d_min) / (d_max - d_min)
else:
depth_normalized = np.zeros_like(depth_image)
depth_uint8 = depth_normalized.astype(np.uint8)
output_dir = r"D:\11_MATERIALSorPROJECTS\HelloIsaac\_output"
os.makedirs(output_dir, exist_ok=True)
output_name = os.path.join(output_dir, "15_captured_depth.png")
# 使用 PIL 的 'L' 模式保存单通道灰度图
img = Image.fromarray(depth_uint8, mode='L')
img.save(output_name)
print(f"🎉 Depth map saved! Open '{output_name}' to see the 3D distance gradient!")
except Exception as e:
print(f"❌ Error processing depth data: {e}")
else:
print("❌ Failed to capture depth image (Annotator returned None).")
simulation_app.close()
打开保存的 15_captured_depth.png,你会看到一张典型的黑白灰度图:离相机越近的物体越黑(或越白),越远的背景则颜色相反。


在底层的运算中,相机的视角被称为一个渲染产品 (RenderProduct)。我们可以往这个产品上“挂载”不同的注释器 (Annotator)。比如代码中挂载了 distance_to_image_plane,底层物理引擎就会逐个像素地去计算射线碰撞的距离。如果挂载 semantic_segmentation,它就会输出色彩斑斓的实例分割掩码。这就是生成 AI 合成数据集的核心原理。
深度数据本质上只是一个二维数组,表示距离,它没有 RGB 三种颜色。所以在用 PIL 保存时,必须显式指定 mode=‘L’,将其编码为 8-bit 的单通道灰度图像。
参考资料
代码下载
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)