一个基于 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日:初始版本