如何使用 Morphologica 在 C++ 中创建高性能(出版级)二维图形





5.00/5 (4投票s)
介绍如何使用这个纯头文件库 morphologica,在 C++ 程序中绘制高质量的二维图形。
引言
本文将指导您完成使用名为 morphologica 的仅头文件库,从 C++ 程序绘制精美的二维图表的步骤。这些图表使用 OpenGL 绘制,达到了出版质量。由于 morphologica 图表渲染速度快,非常适合用于实时可视化的模拟,或为数据可视化电影制作帧。
完成本文后,您将能够绘制出如下所示的图表
背景
Morphologica 由谢菲尔德大学的计算神经科学家开发,是一个用于三个任务的仅头文件代码库:以 JSON 格式读取程序配置;以通用数据格式保存模拟输出;以及执行 OpenGL 可视化。在本文中,我们只对最后一个任务感兴趣。由于该代码是仅头文件的,您只会将可视化代码编译到您的程序中(这样就不会有其他代码的干扰)。要使用本文中的代码,您需要一份 morphologica 头文件,并且需要链接代码所依赖的库。本文的第一部分将展示如何在 Linux 或 Mac 上使用 CMake 进行设置(代码在 Windows 上也同样适用)。我将假设您已经安装并准备好使用 CMake 和 C++ 编译器,如 gcc 或 clang。
创建基本的二维图表
编译图表需要四个步骤:首先,我们安装程序所需的一些库;其次,我们下载并安装 morphologica 头文件;第三,我们创建一个 CMake 编译文件;最后,我们创建程序文件,然后编译它。
安装依赖项
程序需要一些库。您需要一个支持 OpenGL 的图形驱动程序、OpenGL 库、一个名为 GLFW3 的库以及 Freetype 字体渲染库。如果您使用的是 Debian 类的 Linux,如 Ubuntu,您可以使用以下命令安装它们:
sudo apt install build-essential cmake \
freeglut3-dev libglu1-mesa-dev libxmu-dev libxi-dev \
libglfw3-dev libfreetype-dev
在 Mac 上,如果您安装了 XCode,您只需要安装 CMake 和 GLFW3。请使用您喜欢的方法进行安装(我使用 Mac Ports 安装 cmake 并从源代码编译/安装 GLFW3)。
安装 Morphologica 头文件
第一项任务是下载 morphologica。从以下链接下载 morphologica "The unique_ptr release":
您可以下载 zip 文件或 tar.gz 文件;根据您的偏好选择,但我下载了 .tar.gz。现在创建一个名为 'codeproject' 的文件夹来存放工作文件,然后解压 morphologica-2.0。
[seb@cube 12:54:06 ~]$ mkdir codeproject
[seb@cube 12:54:38 ~]$ mv ~/Downloads/morphologica-2.0.tar.gz codeproject/
[seb@cube 12:54:54 ~]$ cd codeproject/
[seb@cube 12:54:58 codeproject]$ tar xf morphologica-2.0.tar.gz
[seb@cube 12:55:05 codeproject]$ ls
morphologica-2.0 morphologica-2.0.tar.gz
就是这样——您已经完成了“安装”morphologica 所需的所有工作——您拥有了所有的头文件。
设置 CMake 构建文件
使用 CMake 是定义编译器如何查找库和编译程序的绝佳方式。我们在这里将这样做,以指导 morphologica 图表程序的编译。
CMake 使用一个名为 CMakeLists.txt 的配置文件。在您的 CodeProject 文件夹中,编辑一个 CMakeLists.txt 文件,并将这个初步的、样板化的部分放入其中——对于任何包含 morphologica 的程序,这都可以几乎保持不变。
#
# This is an example CMakeLists.txt file to compile a program with morphologica headers
#
# cmake version 3.1 provides the set(CMAKE_CXX_STANDARD 17) feature
cmake_minimum_required(VERSION 3.1)
# Give your project a name
project(codeproject)
# From CMAKE_SYSTEM work out which of __OSX__, __GLN__, __NIX__ are required
if(CMAKE_SYSTEM MATCHES Linux.*)
set(OS_FLAG "-D__GLN__")
elseif(CMAKE_SYSTEM MATCHES BSD.*)
set(OS_FLAG "-D__NIX__")
elseif(APPLE)
set(OS_FLAG "-D__OSX__")
else()
message(ERROR "Operating system not supported: " ${CMAKE_SYSTEM})
endif()
# morphologica uses c++-17 language features
set(CMAKE_CXX_STANDARD 17)
# Add the host definition to CXXFLAGS along with other switches, depending on OS/Compiler
if (APPLE)
set(CMAKE_CXX_FLAGS "${OS_FLAG} -Wall -Wfatal-errors -Wno-missing-braces -g -O3")
else()
# This assumes a gcc compiler (or a gcc mimic like Clang)
set(CMAKE_CXX_FLAGS "${OS_FLAG} -Wall -Wfatal-errors -Wno-missing-braces -g -O3 -Wno-unused-result -Wno-unknown-pragmas -march=native")
endif()
# Additional GL compiler flags.
set(OpenGL_GL_PREFERENCE "GLVND") # Following `cmake --help-policy CMP0072`
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGL3_PROTOTYPES -DGL_GLEXT_PROTOTYPES")
if(APPLE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGL_SILENCE_DEPRECATION")
endif()
# Tell the program where the morph fonts are
# (in morphologica-2.0 that you just downloaded):
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMORPH_FONTS_DIR=\"\\\"${PROJECT_SOURCE_DIR}/morphologica-2.0/fonts\\\"\"")
# Find the 3 dependency libraries which are needed to draw graphs
find_package(OpenGL REQUIRED)
find_package(glfw3 3.3 REQUIRED)
find_package(Freetype REQUIRED)
# Tell compiler where to find the included dependencies
include_directories(${OpenCV_INCLUDE_DIRS} ${OPENGL_INCLUDE_DIR}
${GLFW3_INCLUDE_DIR} ${FREETYPE_INCLUDE_DIRS})
# Tell compiler where to find the morphologica headers
set(MORPH_INCLUDE_PATH "${PROJECT_SOURCE_DIR}/morphologica-2.0" CACHE PATH "The path to morphologica")
include_directories(BEFORE ${MORPH_INCLUDE_PATH}/include) # Allows GL3/gl3.h to be found
include_directories(BEFORE ${MORPH_INCLUDE_PATH}) # Allows morph/Header.h
# to be found
现在我们将向 CMakeLists.txt 添加更多行,以说明我们的程序将如何编译。添加这些行:
# Our program will be written in a single file called graph2d.cpp and
# compiled into the exe graph2d:
add_executable(graph2d graph2d.cpp)
# We need to tell the compiler to link the OpenGL
# and Freetype libraries to the graph2d executable
target_link_libraries(graph2d OpenGL::GL Freetype::Freetype glfw)
创建绘制图表的代码
现在我们准备编辑我们的程序了。仍然在 codeproject 文件夹中,使用您喜欢的文本编辑器创建 graph2d.cpp,并将以下代码行放入文件中以 #include
我们需要的库:
#include <vector>
#include <morph/Visual.h>
#include <morph/GraphVisual.h>
我们将使用标准库中的 std::vector
来存储我们将要绘制的数据,并包含 morph/Visual.h 来引入 morph::Visual
类,该类提供了图形“场景”。morph::Visual
提供了一个非常简单的 3D 世界,其中可以渲染一个或多个 morph::VisualModel
对象。morph/GraphVisual.h 引入了 morph::GraphVisual
类,它是 morph::VisualModel
的一个专门化,专门用于渲染二维图表。因此,场景中的每个图表都是一个由三角形图元组成的“图形模型”,就像大多数其他 OpenGL 模型一样。
现在我们将编写 main()
函数;以以下行开始:
int main() {
第一个任务是创建 morph::Visual
场景。在构造函数中,我们指定窗口大小为 1024 x 768 像素,并为其指定一个窗口标题:
morph::Visual v(1024, 768, "Made with morph::GraphVisual");
现在我们创建一些数据容器并填充它们。这是普通的 C++,不使用任何 morphologica 功能(尽管 morphologica 有一个不错的类叫做 morph::vVector
,它类似于具有内置数学运算的 std::vector
)。我们创建一个 'x' 和一个 'y',其中 y=x3:
// x holds data for the x axis
std::vector<double> x(14, 0.0);
// y holds data for the y axis.
std::vector<double> y(14, 0.0);
// Populate x and y (we'll raise x to the power 3)
double val = -0.5;
for (unsigned int i = 0; i < 14; ++i) {
x[i] = val;
y[i] = val * val * val;
val += 0.1;
}
现在我们有了要绘制的数据,我们需要创建一个 morph::GraphVisual
对象并传入数据。我们调用 GraphVisual
构造函数,必须传递三个参数。前两个参数 v.shaderprog
和 v.tshaderprog
是 OpenGL 子系统使用的两个着色器程序的整数引用(一个用于渲染 3D 对象,一个用于渲染文本对象)。morph::Visual
的设计意味着这是必不可少的样板代码,但您只需要知道这些。第三个参数,这里以字面值 ({0,0,0}) 给出,是一个定义 GraphVisual
模型在场景中位置的三维坐标。在单个 morph::Visual
场景中放置多个模型时很重要。GraphVisual::setdata()
函数将要绘制的数据点复制到 GraphVisual
对象中,然后 GraphVisual::finalize()
调用 morph::GraphVisual
和 morph::VisualModel
中的函数,这些函数定义了构成 3D 模型的所有三角形图元,并使 OpenGL 能够实现渲染魔法。请注意,GraphVisual
是一个模板类,其模板参数用于将被绘制的数据类型。在这里,我们为数据使用双精度数字。
// Create a unique_ptr to a GraphVisual with offset within the scene of 0,0,0
auto gv = std::make_unique<morph::GraphVisual<double>> (v.shaders, morph::vec<float>({0,0,0}));
// The setdata function passes our data into the GraphVisual object
gv->setdata (x, y);
// finalize() makes GraphVisual compute the vertices of the OpenGL model
gv->finalize();
一旦 morph::GraphVisual
被创建并 finalized,它就可以被添加到 morph::Visual
场景中,然后进行渲染:
// Add the GraphVisual OpenGL model to the Visual scene, which takes ownership of the unique_ptr
v.addVisualModel (gv);
// Render the scene on the screen until user quits with 'x'
v.keepOpen();
最后,返回 0
并关闭 main()
函数:
return 0;
}
编译程序
现在您可以使用 CMake
进行程序的定向编译了。确保您在 codeproject 文件夹中,然后运行:
cmake .
make graph2d
./graph2d
您应该会看到一个带有图表的窗口出现。您可以右键单击图表并移动鼠标来在场景中移动图表。您可以使用鼠标滚轮进行缩放。按“c”键可以看到坐标轴对象出现。按“h”键,并在 stdout 中查看 morphologica 提供的其他功能。完成后,按“x”键退出(或使用窗口关闭按钮)。
修改图表的功能
好的,这是一个默认图表。让我们探索一下 morph::GraphVisual
的功能。要更改图例、轴标签等功能,我们在 gv->finalize();
行之前进行修改。
轴标签
假设我们想将 x 轴标签设置为“z”,而不是“x”,并将 y 轴标签设置为 phi(z)(即希腊字母 phi)。我们在 setdata()
和 finalize()
之间插入对 GraphVisual::xlabel
和 GraphVisual::ylabel
的调用:
gv->setdata (x, y);
gv->xlabel = "z";
std::string y_label = morph::unicode::toUtf8 (morph::unicode::phi) + "(z)";
gv->ylabel = y_label;
gv->finalize();
xlabel 很简单,我们只使用了字符串常量“z
”。要将 y 标签设置为包含希腊字母符号,我们使用了 morphologica 内置的功能,可以将 Unicode 字符(作为 UTF-8)写入字符串,并使用 DejaVu 字体(该字体内置于可执行文件中)进行渲染。
图例
GraphVisual
允许您为数据设置图例。为此,请使用 setdata()
的重载。用以下代码替换 setdata
行:
gv->setdata (x, y, "Cubic function");
图例显示在图表框的上方。
线条和标记样式
要格式化数据线的样式——使用什么标记、颜色以及线条是填充的还是虚线的——您可以定义一个 morph::DatasetStyle
。然后将 DatasetStyle
传递给 setdata()
函数的另一个重载。
这些代码行应放在调用 gv->setdata(x, y)
之前:
// The 'stylepolicy' can be 'lines', 'markers' or (the default) 'both'.
morph::DatasetStyle ds(morph::stylepolicy::lines);
// Colours can be specified as an RGB colour triplet.
// Note British spelling of colour to confuse you. Sorry.
ds.linecolour = {0.4, 0.0, 0.1};
ds.linewidth = 0.005f;
// You can set the legend text for this DatasetStyle, too:
ds.datalabel = "Cubic function";
// Now pass in ds to your call to setdata():
gv->setdata (x, y, ds);
上面的代码应该会给您一个只有线条、没有标记、并且线条较细的红色图表。要只显示标记,请尝试以下方法:
morph::DatasetStyle ds(morph::stylepolicy::markers);
// Set the shape of the markers:
ds.markerstyle = morph::markerstyle::triangle;
// You can also use named colours from morph/colour.h:
ds.markercolour = morph::colour::crimson;
// You can use unicode in the legend:
ds.datalabel = morph::unicode::toUtf8 (morph::unicode::phi);
gv->setdata (x, y, ds);
我在这里使用了命名的颜色。您可以在此网页上找到颜色及其名称:http://www.cloford.com/resources/colours/500col.htm
要同时设置标记样式和线条样式,只需在 DatasetStyle
中同时设置它们即可:
morph::DatasetStyle ds(morph::stylepolicy::both);
ds.markerstyle = morph::markerstyle::diamond;
ds.markercolour = morph::colour::crimson;
ds.linecolour = morph::colour::dodgerblue3;
ds.datalabel = morph::unicode::toUtf8 (morph::unicode::phi);
gv->setdata (x, y, ds);
最终的图表,带有轴标签、图例和修改后的线条样式,看起来是这样的:
供参考,标记样式的选项可以在 morph/GraphVisual.h 中找到。
//! What shape for the graph markers?
enum class markerstyle
{
none,
triangle,
uptriangle,
downtriangle,
square,
diamond,
pentagon, // A polygon has a flat top edge, the 'uppolygon'
// has a vertex pointing up
uppentagon,
hexagon,
uphexagon,
heptagon,
upheptagon,
octagon,
upoctagon,
circle,
bar, // Special. For a bar graph.
numstyles
};
关注点
我希望这能让您了解 morph::GraphVisual
如何让您在 C++ 程序中绘制二维图表。更多示例,请参阅 morphologica 中的 examples/ 文件夹(找到 graph1.cpp、graph2.cpp 等)。本文未涵盖的一些主题包括:在一个 morph::Visual
场景中添加多个图表;动态更新图表;双轴图表(具有两个 y 轴);以及特殊的条形图。这些是未来文章的主题。
注意 morph::GraphVisual
的内存管理是如何工作的。您使用 std::make_unique<>
创建一个新实例(它会创建一个 std::unique_ptr
),然后将该实例的所有权通过 v.addVisualModel (gv)
传递给 morph::Visual
。程序在程序完成时会释放 GraphVisual
,因为 std::unique_ptr
超出了其作用域。
这里还有一个有趣的尝试:构成图表线条和形状的 OpenGL 模型实际上是一个 3D 模型。将以下行放在您的程序中 gv->finalize()
之后,然后重新编译:
gv->twodimensional = false;
现在运行程序,用您的主鼠标按钮单击窗口并拖动。
历史
- 2022年8月1日:初版
- 2022年8月2日:改进了代码描述
- 2023年3月8日:更新以反映 morphologica 的重要更改(使用 unique_ptrs)