65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2021年5月16日

CPOL

12分钟阅读

viewsIcon

7794

downloadIcon

440

创建一个基于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控件来访问WorldScene以及所有子节点。选定节点的属性可以通过应用程序窗口右侧的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大致兼容的功能进行本地化和国际化。POTPO文件也与gettext的语法兼容。大致兼容意味着:一方面支持大多数gettext实现中的别名_(...),另一方面使用wchar_t*而不是char*,并且本地化/国际化的文本只能从PO文件中读取 - 目前不支持MO文件。OGWW DLL提供了现成的功能 - 最终可以实现更智能的本地化和国际化,因为应用程序无需重新编译即可实现。

XML解析器

XML解析器基于在他文章《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中。有两个子文件夹:

  • X3D包含X3DOM类。
  • XML包含XML解析器。

此项目部分完全编译到解决方案目标。

此外,项目文件夹Others还有两个文件夹:

  • Images包含应用程序的图标和纹理的位图。
  • Resources包含定义3D场景的HTML文件。

此项目部分通过构建前步骤复制到解决方案目标。

可以通过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 作为所有可在childrenaddChildrenremoveChildren字段中使用的节点的抽象基类型。
  • 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解析器基于在他文章《C++中的流式XML解析器》中的出色工作。为了将其转换为DOM解析器,能够从HTML文件中读取X3D场景,只需创建一个非常简单的文档类XmlDocument和四个辅助类XmlAttributeXmlAttributeCollectionXmlNodeListXmlNode。所有这些都有很好的文档记录,并位于文件XmlDocument.hppXmlDocument.cpp中。

另一个类,Parser类,负责将XML节点和属性转换为X3D对象。这个类也有很好的文档记录,并位于文件X3DParser.hppX3DParser.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编辑器的第一个版本将所有必要的拼图块组合在一起:带有TreeViewPropertyGrid的用户界面、X3DOM解析器、OpenGL以及PropertyGrid的自动填充。

历史

  • 2021年5月16日:初始版本
© . All rights reserved.