一个基于 OpenGL 的基本 X3DOM 编辑器, 运行在 ReactOS 上( 因此也运行在 Windows XP 及更高版本上)  





5.00/5 (4投票s)
创建一个基于OpenGL、代码量尽可能少、运行在ReactOS和Windows上的基本X3DOM编辑器,以检查X3DOM的功能。
目录
引言
X3DOM 是一个(截至2021年4月 - 尚未标准化)旨在为Web上的3D图形建立一个开源框架和运行时的新尝试,同时展示 HTML5 与(也是本文重点) 声明式3D内容 的集成可能是什么样的。
我在这里要介绍的这个简单的ReactOS X3DOM编辑器,能够解析符合HTML和X3DOM规范的XML,并使用OpenGL进行显示。长远来看,还应该能够编辑声明式3D元素并将其保存为XML(符合HTML和X3DOM规范)。
由于我想顺便推广ReactOS,所以源代码以Windows(Win32)的Visual Studio项目以及ReactOS(Win32)的Code::Blocks项目集合的形式提供。
ReactOS 是Windows操作系统的开源替代方案。即使ReactOS的第一个版本可以追溯到1998年,但至今仍没有“稳定”的ReactOS版本。也许,最重要的原因是缺乏关注。
受到OpenGL的几何图元和OpenGL中的光源特性及相关材质属性提示,以及在ReactOS上使用C/C++进行OpenGL入门和在ReactOS上运行的基本图标编辑器(及其在Windows XP及更高版本上的延续)等文章的启发,我想深入研究声明式3D内容的主题,并-长远来看-创建一个直观的应用程序,无需长时间研究文档或多年的3D建模经验即可使用。
在本文中,我基于其他文章的一些发现,例如:
- 《在ReactOS上使用C/C++进行OpenGL入门》中的ReactOS上的OpenGL支持、为ReaktOS选择开发环境以及添加文档生成器。
- 《在ReactOS上运行的基本图标编辑器(及其在Windows XP及更高版本上的延续)》中的OGWW C++类库和图标创建。
由于我简单的ReactOS X3DOM编辑器旨在运行在ReactOS和Windows XP及更高版本上,我需要使用纯Win32 API或一个在ReactOS上支持的C++类库。
尽管Code::Blocks附带了出色的C++类库wxWidgets,但我多年来使用MFC的经验让我不愿意再次熟悉一个如此复杂的类库,在该类库中,仅能通过深入研究源代码来修复bug,最终还是不得不在Win32 API层面找到问题的解决方法。因此,我决定使用文章《在ReactOS上运行的基本图标编辑器(及其在Windows XP及更高版本上的延续)》中的OGWW库,尽管我在此期间发现David Nash开发的令人印象深刻的库Win32++几乎与我之前发明的效果相同。目前,OGWW在某些可比方面比Win32++更依赖STL,并提供了我在此应用程序中所需的额外窗口控件。例如,简单的ReactOS X3DOM编辑器需要一个TreeView和一个PropertyGrid,这些在OGWW 0.7版本之后可用。
并且该项目中使用的所有图标也都是使用文章《在ReactOS上运行的基本图标编辑器(及其在Windows XP及更高版本上的延续)》中的图标编辑器创建的。
背景
应用程序
我简单的ReactOS X3DOM编辑器的源代码在World类中结合了显示3D内容所需的基本全局设置(X3DOM不认识)。3D内容本身驻留在Scene类中,该类实现了Scene节点(X3DOM Scene Author API)。Scene的所有子节点或多或少都是X3DOM Scene Author API中定义的节点的完整实现。
我简单的ReactOS X3DOM编辑器的UI通过应用程序窗口左侧的TreeView控件来访问World、Scene以及所有子节点。选定节点的属性可以通过应用程序窗口右侧的PropertyGrid控件来访问。

ReactOS X3DOM编辑器的源代码基于OpenGL Windows Wrapper (Ogww) DLL,该DLL在文章《在ReactOS上使用C/C++进行OpenGL入门》中被介绍。现在,该DLL已经发展以满足更专业的UI需求,但距离发布状态仍很遥远。然而,它仍然设计用于支持C/C++和C#的应用程序开发。
如果读者想知道:为什么是Ogww - 又一个Win32 API的包装器?为了回答这个问题,我参考了文章《在ReactOS上运行的基本图标编辑器(及其在Windows XP及更高版本上的延续)》的“应用程序”章节。
资源
由于gcc/g++不提供资源编译器,我改变了整个资源处理方式,使其不再需要资源编译器。
- 图标:所有图标都从CPP文件中本地编译和链接(我的基本图标编辑器自0.7版本起支持CPP文件导出),或者直接从ICO文件中动态加载。OGWW DLL提供了大量功能来方便地处理图标,并弥补了缺少资源文件的不足。
- I10n/L18n:所有UI文本都在源代码中预定义,并且可以使用与gettext大致兼容的功能进行本地化和国际化。POT和PO文件也与gettext的语法兼容。大致兼容意味着:一方面支持大多数gettext实现中的别名_(...),另一方面使用wchar_t*而不是char*,并且本地化/国际化的文本只能从PO文件中读取 - 目前不支持MO文件。OGWW DLL提供了现成的功能 - 最终可以实现更智能的本地化和国际化,因为应用程序无需重新编译即可实现。
XML解析器
XML解析器基于Przemek Mazurkiewicz在他文章《C++中的流式XML解析器》中的出色工作。唯一的缺点是:
- 流API基于char*而不是wchar_t*。
- 不支持DTD(<!DOCTYPE doc...>节点)。
- 自动变量Xml::Inspector<Xml::Encoding::...>(char*)的创建会在文件不存在时产生丑陋的崩溃。如果创建一个动态变量new Xml::Inspector<Xml::Encoding::...>(char*),则可以在之前捕获问题if (File::Exists(strFullPath))。
然而,对于XML文件的解析,支持多种数据类型和编码。该库结构良好,易于理解,并且有出色的文档。
使用代码
项目
Code::Blocks的初始工作空间结构如下(Visual Studio的解决方案结构非常相似):
|  | X3DomLight项目基于OpenGL Windows Wrapper (Ogww) DLL。该项目的编译结果(DLL)通过构建前步骤复制到解决方案目标。 用于寻址OpenGL Windows Wrapper (Ogww) DLL的面向对象外观位于项目文件夹OGWW_Wrapper中。此项目部分完全编译到解决方案目标。 应用程序本身位于项目文件夹X3DomLight中。有两个子文件夹: 
 此项目部分完全编译到解决方案目标。 此外,项目文件夹Others还有两个文件夹: 
 此项目部分通过构建前步骤复制到解决方案目标。 | 
可以通过Project | Properties...菜单访问的Project/targets options对话框来配置构建前步骤。Project/targets options对话框提供了Project's build options...按钮,可用于打开Project build options对话框。在Project build options对话框中,Pre/post build steps选项卡允许配置所需的构建前步骤。

我为调试版本使用的构建前步骤是:
cmd /c copy "$(PROJECT_DIR)\..\OGWW\bin\Debug\ogww32.dll" "$(PROJECT_DIR)\bin\Debug" /Y
cmd /c if not exist "$(PROJECT_DIR)\bin\Debug\Images" mkdir "$(PROJECT_DIR)\bin\Debug\Images"
cmd /c copy "$(PROJECT_DIR)\Images\ReactOS_X3D_Explorer.ico" "$(PROJECT_DIR)\bin\Debug\Images" /Y
cmd /c copy "$(PROJECT_DIR)\Images\WoodParquet_24bpp.bmp" "$(PROJECT_DIR)\bin\Debug\Images" /Y
cmd /c if not exist "$(PROJECT_DIR)\bin\Debug\Resources" mkdir "$(PROJECT_DIR)\bin\Debug\Resources"
cmd /c copy "$(PROJECT_DIR)\Resources\sample_01.htm" "$(PROJECT_DIR)\bin\Debug\Resources" /Y
我为发布版本使用的构建前步骤是:
cmd /c copy "$(PROJECT_DIR)\..\OGWW\bin\Release\ogww32.dll" "$(PROJECT_DIR)\bin\Release" /Y
cmd /c if not exist "$(PROJECT_DIR)\bin\Release\Images" mkdir "$(PROJECT_DIR)\bin\Release\Images"
cmd /c copy "$(PROJECT_DIR)\Images\ReactOS_X3D_Explorer.ico" "$(PROJECT_DIR)\bin\Release\Images" /Y
cmd /c copy "$(PROJECT_DIR)\Images\WoodParquet_24bpp.bmp" "$(PROJECT_DIR)\bin\Release\Images" /Y
cmd /c if not exist "$(PROJECT_DIR)\bin\Release\Resources" mkdir "$(PROJECT_DIR)\bin\Release\Resources"
cmd /c copy "$(PROJECT_DIR)\Resources\sample_01.htm" "$(PROJECT_DIR)\bin\Release\Resources" /Y
如果我使用Code::Blocks的项目依赖项功能,那么每个构建前步骤的第一行将是不必要的 - 我决定不使用它,但每个人都可以自由使用项目依赖项功能。
X3DOM场景作者API
X3DOM Scene Author API目前定义了240多个节点类型(类)。我简单的ReactOS X3DOM编辑器目前实现了其中的25个 - 而且并非全部实现完整。
- X3DNodePropertyNames 用于提供X3D节点属性的属性名称。
- CSSColors 用于提供命名的CSS颜色(目前147种)。
- X3DNode 作为X3D系统中所有节点的抽象基类型。
- X3DChildNode 作为所有可在children、addChildren和removeChildren字段中使用的节点的抽象基类型。
- X3DBoundedObject 作为所有在其定义中包含边界的节点的抽象基类型。
- X3DGroupingNode 作为所有包含子节点并是所有聚合基础的节点的抽象基类型。
- X3DTransformNode 作为所有对子节点进行分组和变换的节点的抽象基类型。
- X3DAppearanceNode 作为X3D中所有外观节点的抽象基类型。
- X3DGeometryNode 作为X3D中所有几何节点的抽象基类型。
- X3DSpatialGeometryNode 作为X3D中所有空间几何节点的抽象基类型。
- X3DShapeNode 作为X3D中所有形状节点的抽象基类型。
- X3DAppearanceChildNode 作为X3DShapeNode所有子节点的抽象基类型。
- X3DTextureNode 作为所有指定纹理图像源的节点的抽象基类型。
- X3DMaterialNode 作为X3D中所有材质节点的抽象基类型。
- Scene*用于表示一个场景,由形状及其外观组成。
- Appearance*用于表示形状的材质和纹理。
- Material*用于表示形状与光的关系。
- Texture 用于表示形状的表面一致性。
- ImageTexture*用于通过图像表示形状的表面一致性。
- Transform*用于表示形状的平移、旋转和缩放。
- Shape*用于表示场景中的2D或3D对象。
- Box*用于表示3D立方体。
- Cylinder*用于表示3D圆柱体。
- Cone*用于表示3D圆锥体。
- Sphere*用于表示3D球体。
其中*表示XML解析器从HTML文件中读取相应的节点类型。
XML解析器
如前所述 - XML解析器基于Przemek Mazurkiewicz在他文章《C++中的流式XML解析器》中的出色工作。为了将其转换为DOM解析器,能够从HTML文件中读取X3D场景,只需创建一个非常简单的文档类XmlDocument和四个辅助类XmlAttribute、XmlAttributeCollection、XmlNodeList和XmlNode。所有这些都有很好的文档记录,并位于文件XmlDocument.hpp和XmlDocument.cpp中。
另一个类,Parser类,负责将XML节点和属性转换为X3D对象。这个类也有很好的文档记录,并位于文件X3DParser.hpp和X3DParser.cpp中。
MinGW工具链的局限性
GCC/MinGW工具链的局限性影响到:
- Microsoft随Windows提供的一些额外的(新的)C运行时函数,例如:
#if defined(__GNUC__) || defined(__MINGW32__)
    wcscat(buf, L" ");
#else
    wcscat_s(buf, L" ");
#endif
- Windows头文件中不存在的一些额外的(新的)Windows API函数,例如:
#if !defined(__GNUC__) && !defined(__MINGW32__)
    /// <summary>
    /// Checks whether the indicated major version is equal to current OS major version.
    /// </summary>
    /// <param name="dwMajorVersion">The major version to check for equality.</param>
    /// <returns>Returns <c>TRUE</c> on equality, or <c>FALSE</c> otherwise.</returns>
    /// <returns>Starting with Windows 8.1 (version 6.3) the manifested version,
    /// not the runtime version, is tested.</returns>
    BOOL EqualsMajorVersion(DWORD dwMajorVersion)
    {
        OSVERSIONINFOEX osVersionInfo;
        ::ZeroMemory(&osVersionInfo, sizeof(OSVERSIONINFOEX));
        osVersionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
        osVersionInfo.dwMajorVersion = dwMajorVersion;
        ULONGLONG maskCondition = ::VerSetConditionMask(0, VER_MAJORVERSION, VER_EQUAL);
        return ::VerifyVersionInfo(&osVersionInfo, VER_MAJORVERSION, maskCondition);
    }
#endif
- 以及ReactOS中的间接限制(它基本上对应于Windows XP的功能级别,并且与GCC/MinGW工具链相关,因为GCC/MinGW仅用于在ReactOS上进行编译),例如:
#if defined(__GNUC__) || defined(__MINGW32__)
    bAvoidDebuggerInterference = ::IsDebuggerPresent();
#endif
    if (bAvoidDebuggerInterference)
    {
        Console::WriteError(L"OpenGL enabling failed!\n");
        result = MainFrame::Run((IDLEPROCCB)NULL);
    }
    else
    {
        ...
    }
在所有情况下,这些限制都通过以下方式进行条件编译:
#if defined(__GNUC__) || defined(__MINGW32__) 或
#if !defined(__GNUC__) && !defined(__MINGW32__).
与属性网格的交互
X3DOM场景作者API节点类型的属性
在我目前实现的25个X3DOM Scene Author API节点类型中,有11个节点类型和World节点类型可以在应用程序的TreeView中显示。这些节点类型中的每一种都可以在其中被选中,然后其属性会显示在PropertyGrid中。为了实现这一点,选定节点应显示在PropertyGrid中的属性必须传递给PropertyGrid。一些节点类型的属性很少,其他节点类型的属性则多得多。
所有属性都可以通过特定的Set...(...)和Get...()方法以及通用的HasProperty(wszName)和GetProperty(wszName)方法访问。以下是一个示例,说明了这一点,以X3DBoundedObject节点类型为例:
void SetBBoxCenter(Vector3f vec3dCenter) noexcept
{ ... }
Vector3f GetBBoxCenter() noexcept
{ ... }
而(其中所有属性名wszName都取自字符串常量,如X3DNodePropertyNames::BBoxCenterN)
virtual bool HasProperty(LPCWSTR wszName) noexcept
{ ... }
inline LPPROPERTYDATA GetProperty(LPCWSTR wszName) noexcept
{ ... }
换句话说,这意味着属性也可以通过其名称使用通用函数来寻址。到目前为止,这对于读取访问效果很好,下一步将提供写入访问。
节点类型的属性显示
为了自动化将属性传输到PropertyGrid,我引入了一个名为DISCLOSEDPROPERTY的辅助结构。每个应该显示在PropertyGrid中的属性 - 通常是从类的完整属性集中选择的 - 都由一个DISCLOSEDPROPERTY条目表示。
我将以X3DBoundedObject节点类型为例,演示已公开属性的创建。
/// <summary>Initializes the <c>X3DBoundedObject</c> object's data holder.</summary>
/// <remarks>Provides the possibility to initialize members outside the constructor.</remarks>
void InitInstance()
{
    X3DChildNode::InitInstance();
    AddBooleanProperty(X3DNodePropertyNames::RenderN(), true);
    AddDisclosedProperty(X3DNode::DISCLOSEDPROPERTY{ X3DNodePropertyNames::RenderN(),
                         PropertyDataType::Boolean, X3DNode::PropertyValueLimit::None,
                         X3DNode::PropertyValueLimit::None, false });
    AddVec3dProperty(X3DNodePropertyNames::BBoxCenterN(), 0.0F, 0.0F, 0.0F);
    AddDisclosedProperty(X3DNode::DISCLOSEDPROPERTY{ X3DNodePropertyNames::BBoxCenterN(),
                         PropertyDataType::Vec3f, X3DNode::PropertyValueLimit::FloatMin,
                         X3DNode::PropertyValueLimit::FloatMax, false });
    AddVec3dProperty(X3DNodePropertyNames::BBoxSizeN(), -1.0F, -1.0F, -1.0F);
    AddDisclosedProperty(X3DNode::DISCLOSEDPROPERTY{ X3DNodePropertyNames::BBoxSizeN(),
                         PropertyDataType::Vec3f, X3DNode::PropertyValueLimit::FloatMin,
                         X3DNode::PropertyValueLimit::FloatMax, false });
}
虽然调用Add...Property(...)会注册该属性,但调用AddDisclosedProperty(...)会注册一个相关的DISCLOSEDPROPERTY条目。最终,每个节点类型都有N个属性和一个M个DISCLOSEDPROPERTY条目的集合,其中N >= M。
在DISCLOSEDPROPERTY结构包含属性的相应名称之后,可以通过通用函数并结合DISCLOSEDPROPERTY结构来访问该属性。
现在可以迭代DISCLOSEDPROPERTY条目,并以这种方式自动处理已公开的属性(从而将它们从所属的节点传递到PropertyGrid)。
    auto it = pNode->GetDisclosedPropertyBeginIterator();
    for (; it != pNode->GetDisclosedPropertyEndIterator(); it++)
    {
        X3DNode::DISCLOSEDPROPERTY    propertyData = *it;
        const X3DNode::LPPROPERTYDATA pData        = pNode->GetProperty(propertyData.Name);
    ...
    }
由于属性将在PropertyGrid中进行编辑,因此有时需要为编辑设置值限制。这些值限制也是DISCLOSEDPROPERTY结构的一部分。DISCLOSEDPROPERTY结构的完整声明如下:
/// <summary>This structure is designed to provide information about a disclosed 
///          property (that can be bound to a property grid dynamically).</summary>
typedef struct tagDISCLOSEDPROPERTY
{
    /// <summary>The name of the property, that is disclosed.</summary>
    LPCWSTR             Name;
    /// <summary>The data type of the property.</summary>
    PropertyDataType    DataType;
    /// <summary>The lower limit of the value of the property.</summary>
    PropertyValueLimit  LowerLimitAlias;
    /// <summary>The upper limit of the value of the property.</summary>
    PropertyValueLimit  UpperLimitAlias;
    /// <summary>The flag indicating whether the property is read-only.</summary>
    bool                ReadOnly;
} DISCLOSEDPROPERTY;
关注点
我简单的ReactOS X3DOM编辑器的第一个版本将所有必要的拼图块组合在一起:带有TreeView和PropertyGrid的用户界面、X3DOM解析器、OpenGL以及PropertyGrid的自动填充。
历史
- 2021年5月16日:初始版本


