4. 材质与外观:让物体“看起来”真实

好,我们进入第四讲。前几章我们把物体放进了场景,让它们动了起来。但说实话,光秃秃的灰色模型,看着就像没上漆的石膏像。这一章,我们就来给它们“上色”——赋予材质、调整属性、贴上纹理。

在 Isaac Sim 里,材质控制着物体如何与光线交互。你想想看,一个金属球和一个塑料球,在同样的光照下,反射出来的感觉完全不同。这就是材质在起作用。

4.1 材质的核心属性

我个人习惯把材质属性分成三类:颜色、粗糙度、金属度。这三者基本决定了物体80%的外观。

属性 含义 取值范围 典型效果
颜色 (Color) 物体表面的基础色调 RGB 0.0 ~ 1.0 红色塑料、蓝色金属
粗糙度 (Roughness) 表面微观不平整程度 0.0 ~ 1.0 0.0 镜面,1.0 磨砂
金属度 (Metallic) 表面金属化程度 0.0 ~ 1.0 0.0 非金属,1.0 纯金属

这里有个小陷阱。我曾经在项目里把金属度设成0.5,结果物体看起来既不像金属也不像塑料,非常奇怪。后来我明白了,金属度最好只用0或1这两个极端值,中间值容易产生不自然的“塑料金属”感。

4.2 用 Python 创建和赋予材质

在 Isaac Sim 里操作材质,核心就两步:先创建材质对象,再把它赋给物体的视觉属性。我们直接看代码。

import omni.usd
from pxr import Usd, UsdShade, Gf, Sdf

# 获取当前 stage
stage = omni.usd.get_context().get_stage()

# 1. 创建一个材质 prim
material_path = "/World/Materials/MyRedPlastic"
material_prim = stage.DefinePrim(material_path, "Material")
material_prim.CreateAttribute("inputs:diffuse_color", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(1.0, 0.2, 0.2))
material_prim.CreateAttribute("inputs:roughness", Sdf.ValueTypeNames.Float).Set(0.6)
material_prim.CreateAttribute("inputs:metallic", Sdf.ValueTypeNames.Float).Set(0.0)

# 2. 创建材质输出
outputs = UsdShade.Material(material_prim).CreateSurfaceOutput()
outputs.SetConnectedSource(
    UsdShade.Shader(material_prim),
    "surface"
)

# 3. 获取目标物体并绑定材质
cube_prim = stage.GetPrimAtPath("/World/Cube")
if cube_prim:
    binding_api = UsdShade.MaterialBindingAPI(cube_prim)
    binding_api.Bind(UsdShade.Material(material_prim))
小提示: 材质路径最好统一放在 /World/Materials/ 下,方便管理。我习惯把材质和物体分开存放,这样多个物体可以共享同一个材质实例。

4.3 修改已有材质的属性

有时候我们不想创建新材质,只想改改现有物体的颜色。比如,让一个红色球变成蓝色。怎么做?直接修改材质属性就行。

# 获取已有材质
material_prim = stage.GetPrimAtPath("/World/Materials/MyRedPlastic")
if material_prim:
    # 修改颜色属性
    color_attr = material_prim.GetAttribute("inputs:diffuse_color")
    if color_attr:
        color_attr.Set(Gf.Vec3f(0.2, 0.5, 1.0))  # 改成蓝色
    
    # 修改粗糙度
    rough_attr = material_prim.GetAttribute("inputs:roughness")
    if rough_attr:
        rough_attr.Set(0.2)  # 变得更光滑

嗯,这里要注意。修改属性前一定要检查属性是否存在。我曾经因为拼写错误,把 diffuse_color 写成了 diffuse_colour,结果脚本跑完物体还是老样子,排查了半天才发现是拼写问题。

4.4 使用纹理贴图

纯色材质太单调了。真实世界里,木头有木纹,大理石有花纹,这些都需要纹理贴图来实现。在 Isaac Sim 里,纹理贴图本质上就是一张图片文件,我们把它作为材质的一个输入。

我个人建议纹理图片用 PNG 或 JPEG 格式,分辨率控制在 1024x1024 以内,太大反而影响加载速度。

import omni.usd
from pxr import Usd, UsdShade, Sdf

stage = omni.usd.get_context().get_stage()

# 创建带纹理的材质
material_path = "/World/Materials/WoodFloor"
material_prim = stage.DefinePrim(material_path, "Material")

# 创建纹理着色器
shader_path = material_path + "/Shader"
shader_prim = stage.DefinePrim(shader_path, "Shader")
shader_prim.CreateAttribute("info:id", Sdf.ValueTypeNames.).Set("UsdPreviewSurface")

# 设置纹理文件路径(使用相对路径或路径)
texture_path = "./textures/wood_floor_diffuse.png"
shader_prim.CreateAttribute("inputs:diffuseColor", Sdf.ValueTypeNames.Asset).Set(texture_path)
shader_prim.CreateAttribute("inputs:roughness", Sdf.ValueTypeNames.Float).Set(0.8)
shader_prim.CreateAttribute("inputs:metallic", Sdf.ValueTypeNames.Float).Set(0.0)

# 连接材质输出到着色器
material = UsdShade.Material(material_prim)
shader = UsdShade.Shader(shader_prim)
material.CreateSurfaceOutput().SetConnectedSource(shader, "surface")

# 绑定到物体
floor_prim = stage.GetPrimAtPath("/World/Floor")
if floor_prim:
    UsdShade.MaterialBindingAPI(floor_prim).Bind(material)
注意: 纹理路径不要用中文,不要有空格。Isaac Sim 对路径中的特殊字符支持不太好。我踩过这个坑,路径里带了个中文“木”字,结果纹理死活加载不出来。

4.5 材质与外观的知识体系

为了让你更直观地理解材质系统的结构,我画了一张图。它展示了从材质创建到最终渲染的完整链路。

材质与外观系统结构图 创建材质 Prim 设置属性 颜色/粗糙度/金属度 纹理贴图 PNG/JPEG 图片 材质输出 (Surface) 绑定到物体 最终渲染效果 核心流程: 创建材质 → 设置属性/纹理 → 连接输出 → 绑定物体 → 渲染显示

4.6 实战:给机器人换皮肤

光说不练假把式。我们结合之前章节的机器人模型,给它换一套金属质感的外观。

def apply_metallic_finish(robot_prim_path):
    """给机器人应用金属质感材质"""
    stage = omni.usd.get_context().get_stage()
    
    # 创建金属材质
    mat_path = "/World/Materials/RobotMetal"
    mat_prim = stage.DefinePrim(mat_path, "Material")
    mat_prim.CreateAttribute("inputs:diffuse_color", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(0.8, 0.6, 0.2))
    mat_prim.CreateAttribute("inputs:roughness", Sdf.ValueTypeNames.Float).Set(0.3)
    mat_prim.CreateAttribute("inputs:metallic", Sdf.ValueTypeNames.Float).Set(1.0)
    
    # 获取机器人所有子部件
    robot_prim = stage.GetPrimAtPath(robot_prim_path)
    if not robot_prim:
        print("机器人不存在")
        return
    
    # 遍历所有子 prim 并绑定材质
    for child in robot_prim.GetChildren():
        if child.IsA(UsdGeom.Mesh):
            UsdShade.MaterialBindingAPI(child).Bind(
                UsdShade.Material(mat_prim)
            )
    
    print(f"已为 {robot_prim_path} 应用金属质感")

# 调用函数
apply_metallic_finish("/World/Robot")
关键点: 给复杂物体(如机器人)应用材质时,要遍历其所有子 Mesh。只给根 prim 绑定材质是没用的,子部件不会继承。

4.7 常见问题与避坑

  • 材质不显示? 检查材质路径是否正确,属性名称是否拼写正确。我经常犯的错误是把 diffuse_color 写成 diffuseColor。
  • 纹理加载失败? 确认图片路径是相对于当前工作目录的,或者使用路径。纹理文件不要放在中文目录下。
  • 物体看起来太亮或太暗? 调整粗糙度。粗糙度越低,反射越强,看起来越亮。金属度高的物体对光照更敏感。
  • 多个物体共享材质? 没问题。同一个材质 prim 可以绑定到多个物体上,修改材质属性时所有物体都会同步变化。

好了,这一章的内容就到这里。材质系统是 Isaac Sim 里比较直观的部分,但也是让仿真效果“以假乱真”的关键。多动手试试不同的参数组合,你会很快找到感觉。