[3D数据存储] 对象 | OObject | IObject | 属性 | O<类型>Property | I<类型>Property
第二章:对象(场景节点)
在第一章:归档(文件容器)中,我们了解到归档类似于专用.zip
文件,保存全部3D场景数据。但这些数据如何组织?
这正是对象(又称场景节点)概念的用武之地。
何为对象?为何需要它?
想象Maya
、Blender
或3ds Max
中的3D场景,场景并非无序三角面堆砌,而是按层级组织:
- 存在"根"组
- 其下可能有"角色"组和"环境"组
- “角色"组内包含"主角”,后者可能包含"身体"、“头部”、"左臂"等
- 每个部分通常有自己的位置、旋转和缩放(“变换”)
这种结构化组织方式称为场景图或层级结构,其中的每个元素(组、几何体、灯光或摄像机)本质上都是节点。
在Alembic中,这些节点称为对象。
对象
承担两大功能:
- 层级结构:构建树状结构,类似文件系统目录或DCC软件中的组。对象可包含其他对象作为"子级"
- 数据容器:对象本身定义场景结构,同时持有实际3D数据(存储在属性中,详见第三章:属性(数据容器))
示例场景结构:
code:https://github.com/lvy010/Cpp-Lib-test/tree/main/3D/Alembic
每个Alembic归档自动包含特殊"顶层"对象,作为文件内场景图的根节点,类似文件系统根目录(/
)。
所有其他对象都是该顶层对象的直接或间接子级。
写入对象(OObject
)
使用OObject
类(“O"代表"输出/写入”)在归档中创建对象及层级结构:
#include <Alembic/AbcCoreHDF5/All.h> // HDF5写入支持
#include <Alembic/Abc/All.h> // OArchive, OObject// ... 函数内 ...
Alembic::Abc::OArchive outArchive = Alembic::Abc::OArchive
(Alembic::AbcCoreHDF5::WriteArchive(),"my_scene.abc",Alembic::Abc::MetaData(),Alembic::Abc::ErrorHandler::kThrowPolicy
);// 获取归档顶层对象
Alembic::Abc::OObject topObject = outArchive.getTop();
printf("顶层对象名称: %s\n", topObject.getName().c_str()); // 输出: 顶层对象名称: AbcExport// 创建"my_scene_group"作为顶层对象的子级
Alembic::Abc::OObject mySceneGroup(topObject, "my_scene_group");
printf("创建对象: %s\n", mySceneGroup.getFullName().c_str()); // 输出: 创建对象: /my_scene_group// 创建"Desk_Group"作为my_scene_group的子级
Alembic::Abc::OObject deskGroup(mySceneGroup, "Desk_Group");
printf("创建对象: %s\n", deskGroup.getFullName().c_str()); // 输出: 创建对象: /my_scene_group/Desk_Group// 创建"Chair_Group"作为my_scene_group的子级
Alembic::Abc::OObject chairGroup(mySceneGroup, "Chair_Group");
printf("创建对象: %s\n", chairGroup.getFullName().c_str()); // 输出: 创建对象: /my_scene_group/Chair_Group
关键点解析:
outArchive.getTop()
:获取归档内初始OObject
,默认名称常为"AbcExport",完整路径为/
OObject
构造函数:通过父对象和名称创建子对象,建立父子关系getFullName()
:获取对象在归档内的完整路径
读取对象(IObject
)
使用IObject
类(“I"代表"输入/读取”)从现有归档读取对象:
#include <Alembic/AbcCoreFactory/All.h> // IFactory
#include <Alembic/Abc/All.h> // IArchive, IObject// ... 函数内 ...
Alembic::AbcCoreFactory::IFactory factory;
Alembic::AbcCoreFactory::IFactory::CoreType coreType;Alembic::Abc::IArchive inArchive = factory.getArchive("my_scene.abc", coreType);if (!inArchive.valid()) {printf("错误: 无法打开my_scene.abc\n");return 1;
}// 获取归档顶层对象
Alembic::Abc::IObject topObject = inArchive.getTop();
printf("根对象名称: %s\n", topObject.getName().c_str());// 获取顶层对象的直接子级数量
size_t numTopChildren = topObject.getNumChildren();
printf("根对象有 %zu 个子级\n", numTopChildren);// 访问第一个子级(应为"my_scene_group")
if (numTopChildren > 0) {// 通过索引获取子级Alembic::Abc::IObject mySceneGroup = topObject.getChild(0);printf("第一个子级: %s (完整路径: %s)\n",mySceneGroup.getName().c_str(), mySceneGroup.getFullName().c_str());// 通过名称获取子级Alembic::Abc::IObject deskGroup = mySceneGroup.getChild("Desk_Group");printf(" my_scene_group的子级: %s (完整路径: %s)\n",deskGroup.getName().c_str(), deskGroup.getFullName().c_str());// 获取对象父级Alembic::Abc::IObject parentOfDesk = deskGroup.getParent();printf(" Desk_Group的父级: %s\n", parentOfDesk.getName().c_str());
}
关键读取方法:
getNumChildren()
:获取直接子级数量getChild(index/name)
:通过索引或名称获取子对象getParent()
:获取父对象
底层原理:对象工作机制
创建对象(OObject
)
读取对象(IObject
)
总结
本章核心要点:
- 对象是组织3D场景数据为**层级树结构**的基础单元
- 每个归档包含特殊顶层对象作为场景图根节点
OObject
用于写入时创建对象层级IObject
用于读取时遍历层级结构
对象定义场景图中的位置,实际数据存储于属性中。下章将深入探讨:第三章:属性(数据容器)
第三章:属性(数据容器)
code: https://github.com/lvy010/Cpp-Lib-test/tree/main/3D/Alembic
在第二章:对象(场景节点)中,我们了解到对象(或场景节点)帮助我们将3D场景组织成层级结构,就像计算机中的文件夹。一个对象
定义了场景中某物的位置,但它并不保存实际数据,比如桌子的形状或椅子的颜色。
那么实际数据存储在哪里呢?这就是属性的用武之地!
什么是属性?为什么需要它?
我们可以把对象
想象成文件夹。文件夹对组织管理很有用,但没有内部文件时它是空的。属性就像对象文件夹内存储具体数据的文档或电子表格。
例如,如果我们有一个"桌子"对象
:
- 它的高度可以是数值(如0.75米)
- 它的材质颜色可以是三个数值(红、绿、蓝)
- 实际的3D形状(所有顶点坐标)可以是长数值列表
这些数据片段都存储在Alembic属性中。属性让我们能够以结构化方式将特定数据附加到特定对象
。
Alembic定义了三种主要属性类型:
- 标量属性(ScalarProperty):存储单个数据(如单个数、单词或简单3D向量)
- 数组属性(ArrayProperty):存储多个相同数据元素的列表(如网格的所有顶点位置)
- 复合属性(CompoundProperty):作为其他属性的容器(类似子文件夹),用于组织复杂数据
Alembic中的每个对象
都自动拥有特殊的"顶层"复合属性
。这是存储该对象
所有标量
、数组
和其他复合
属性的地方,可以视为该对象的主数据目录。
让我们继续使用上一章的"Desk_Group"示例。我们将为其添加现实属性:高度(标量)、材质属性(组织在复合属性中)和顶点位置(数组属性)。
写入属性(O<类型>Property
)
为了将数据写入Alembic归档文件中的对象
,我们使用第二章中的OObject
,然后创建O<类型>Property
实例。
首先为桌子设置OArchive
和OObject
:
#include <Alembic/AbcCoreHDF5/All.h> // HDF5写入支持
#include <Alembic/Abc/All.h> // OArchive、OObject和属性// 用于V3f(3D浮点向量)示例
#include <vector>// ... 在main等函数内部 ...Alembic::Abc::OArchive outArchive = Alembic::Abc::OArchive(Alembic::AbcCoreHDF5::WriteArchive(),"my_scene_with_props.abc",Alembic::Abc::MetaData(),Alembic::Abc::ErrorHandler::kThrowPolicy
);Alembic::Abc::OObject topObject = outArchive.getTop();// 从第二章重建"Desk_Group"
Alembic::Abc::OObject deskGroup(topObject, "Desk_Group");// 获取deskGroup的顶层复合属性
Alembic::Abc::OCompoundProperty deskProps = deskGroup.getProperties();printf("准备向/Desk_Group添加属性\n");
设置完成后,deskProps
成为Desk_Group
对象的"数据文件夹"。
写入标量属性(OScalarProperty
)
标量属性保存单个值,可以是float
、int
、string
、bool
,甚至是固定大小的类型如Vec3f
(3浮点向量)或Matrix44d
(4x4双精度矩阵)。
添加desk_height
作为double
类型标量属性:
// 假设已有前文中的'deskProps'// 创建名为"desk_height"的双精度标量属性
Alembic::Abc::ODoubleProperty deskHeightProp(deskProps, // 父属性容器"desk_height" // 属性名称
);// 设置高度值
double height_value = 0.75; // 0.75米
deskHeightProp.set(height_value);
printf(" 设置desk_height为: %.2f\n", height_value); // 输出:设置desk_height为: 0.75
写入数组属性(OArrayProperty
)
数组属性存储同类型值的列表,非常适合网格顶点、UV坐标或颜色列表等数据。
添加desk_vertices
作为V3f
数组:
// 假设已有前文中的'deskProps'// 创建存储3D浮点向量(V3f)的数组属性
Alembic::Abc::OV3fArrayProperty deskVerticesProp(deskProps, // 父属性容器"desk_vertices" // 属性名称
);// 准备示例数据:两个3D点
std::vector<Alembic::Abc::V3f> points = {{0.0f, 0.0f, 0.0f}, // 第一个顶点{1.0f, 2.0f, 3.0f} // 第二个顶点
};// 创建数组样本
Alembic::Abc::V3fArraySample pointsSample( points.data(), points.size() );// 将样本写入属性
deskVerticesProp.set(pointsSample);
printf(" 写入%zu个顶点位置\n", points.size()); // 输出:写入2个顶点位置
写入复合属性(OCompoundProperty
)
复合属性本身不存储数据,而是作为其他属性的容器,用于组织复杂数据结构(如包含多个标量属性的材质定义)。
创建material
复合属性并添加color
和roughness
:
// 假设已有前文中的'deskProps'// 在deskProps中创建"material"复合属性
Alembic::Abc::OCompoundProperty materialProps(deskProps, "material");// 在material中添加颜色标量属性
Alembic::Abc::OV3fProperty materialColor(materialProps, // 父属性现在是materialProps"color"
);
Alembic::Abc::V3f color_value = {0.8f, 0.6f, 0.4f}; // RGB颜色
materialColor.set(color_value);
printf(" 设置材质颜色为(%.1f, %.1f, %.1f)\n", color_value.x, color_value.y, color_value.z);// 添加粗糙度标量属性
Alembic::Abc::OFloatProperty materialRoughness(materialProps, "roughness"
);
float roughness_value = 0.5f;
materialRoughness.set(roughness_value);
printf(" 设置材质粗糙度为: %.1f\n", roughness_value);
读取属性(I<类型>Property
)
从现有Alembic归档文件读取数据时,使用IObject
并创建I<类型>Property
实例。
设置读取环境:
#include <Alembic/AbcCoreFactory/All.h> // IFactory
#include <Alembic/Abc/All.h> // IArchive、IObject和属性// 用于V3f示例
#include <vector>// ... 在main等函数内部 ...Alembic::AbcCoreFactory::IFactory factory;
Alembic::AbcCoreFactory::IFactory::CoreType coreType;Alembic::Abc::IArchive inArchive = factory.getArchive("my_scene_with_props.abc", coreType);// 验证并获取顶层对象
Alembic::Abc::IObject deskGroup = inArchive.getTop().getChild("Desk_Group");
Alembic::Abc::ICompoundProperty deskProps = deskGroup.getProperties();
读取标量属性
Alembic::Abc::IDoubleProperty deskHeightProp(deskProps, "desk_height");
double height_value;
deskHeightProp.get(&height_value);
printf(" 读取desk_height: %.2f\n", height_value);
读取数组属性
Alembic::Abc::IV3fArrayProperty deskVerticesProp(deskProps, "desk_vertices");
Alembic::Abc::V3fArraySample sample;
deskVerticesProp.get(sample);
printf(" 读取%zu个顶点位置\n", sample.size());
读取复合属性
Alembic::Abc::ICompoundProperty materialProps(deskProps, "material");Alembic::Abc::IV3fProperty materialColor(materialProps, "color");
Alembic::Abc::V3f color_value;
materialColor.get(&color_value);Alembic::Abc::IFloatProperty materialRoughness(materialProps, "roughness");
float roughness_value;
materialRoughness.get(&roughness_value);
底层原理
当创建或读取属性时,Alembic管理其在归档文件中的存储和检索。
每个属性都需要知道其数据类型
(如单精度浮点或3浮点向量)和元素数量。
属性创建流程
实现了属性和对象的解耦,方便复用
属性读取流程
总结
本章我们学习了Alembic对象中的数据容器——属性:
- 标量属性:存储
单个数值
- 数组属性:存储
同类型数据序列
- 复合属性:作为属性容器实现嵌套结构
- 使用
O<类型>Property
类进行数据写入 - 使用
I<类型>Property
类进行数据读取
属性存储着定义3D场景的几何体、动画等核心数据。
当涉及随时间变化的动画数据时,Alembic如何管理时间维度
?这正是我们接下来要探讨的内容
第四章:时间采样(动画时间线)