OpenGL-数据可视化秘籍-全-
OpenGL 数据可视化秘籍(全)
原文:
zh.annas-archive.org/md5/ccc838953409725c719b3f1c2dc85441译者:飞龙
前言
OpenGL 是一个多平台、跨语言、硬件加速的应用程序编程接口,用于高性能渲染 2D 和 3D 图形。OpenGL 的一种新兴用途是在从医学成像、建筑和工程中的模拟或建模,到前沿的移动/可穿戴计算等领域的实时、高性能数据可视化应用开发。确实,随着数据集变得更大和更复杂,尤其是在大数据的演变中,使用传统的非图形硬件加速方法进行数据可视化变得越来越具有挑战性。从移动设备到复杂的高性能计算集群,OpenGL 库为开发者提供了一个易于使用的接口,以创建实时、3D 的惊人视觉效果,适用于广泛的交互式应用。
本书包含一系列针对经验不足的初学者和希望探索最先进技术的更高级用户定制的动手实践配方。我们从第一章到第三章通过演示如何在 Windows、Mac OS X 和 Linux 中设置环境,以及如何使用原语渲染基本的 2D 数据集和交互式地学习更复杂的 3D 体积数据集开始,对 OpenGL 进行基本介绍。这部分只需要 OpenGL 2.0 或更高版本,以便即使拥有较老图形硬件的读者也可以尝试代码。在第四章到第六章中,我们过渡到更高级的技术(需要 OpenGL 3.2 或更高版本),例如用于图像/视频处理的纹理映射、从 3D 范围感应相机渲染深度传感器数据的点云渲染,以及立体 3D 渲染。最后,在第七章到第九章中,我们通过介绍在日益强大的移动(基于 Android)计算平台上使用 OpenGL ES 3.0 以及在移动设备上开发高度交互式、增强现实应用来结束本书。
本书中的每个配方都为读者提供了一组可以导入现有项目中的标准函数,这些函数可以成为创建各种实时、交互式数据可视化应用的基础。本书还利用了一系列流行的开源库,如 GLFW、GLM、Assimp 和 OpenCV,以简化应用程序开发,并通过启用 OpenGL 上下文管理和 3D 模型加载以及使用最先进的计算机视觉算法进行图像/视频处理来扩展 OpenGL 的功能。
本书涵盖内容
第一章, 开始使用 OpenGL,介绍了创建基于 OpenGL 的数据可视化应用所需的必需开发工具,并提供了在 Windows、Mac OS X 和 Linux 中设置我们第一个 OpenGL 演示应用的逐步教程。
第二章, OpenGL 原语和 2D 数据可视化,专注于 OpenGL 2.0 原语的使用,如点、线和三角形,以实现数据的基本 2D 可视化,包括时间序列,如心电图(ECG)。
第三章, 交互式 3D 数据可视化,基于之前讨论的基本概念,并将演示扩展到包含更复杂的 OpenGL 功能以进行 3D 渲染。
第四章, 使用纹理映射渲染 2D 图像和视频,介绍了 OpenGL 技术来可视化另一类重要的数据集——涉及图像或视频的数据集。这类数据集在许多领域都很常见,包括医学成像应用。
第五章, 为 3D 范围感应相机渲染点云数据,介绍了用于可视化另一类有趣且新兴的数据类——来自 3D 范围感应相机的深度信息的技术。
第六章, 使用 OpenGL 渲染立体 3D 模型,展示了如何使用 OpenGL 这项令人惊叹的立体 3D 技术来可视化数据。OpenGL 本身并不提供加载、保存或操作 3D 模型的机制。因此,为了支持这一点,我们将集成一个名为 Assimp 的新库到我们的代码中。
第七章, 使用 OpenGL ES 3.0 在移动平台上进行实时图形渲染的介绍,通过展示如何设置 Android 开发环境并在最新的移动设备上创建第一个基于 Android 的应用程序(从智能手机到平板电脑),使用嵌入式系统 OpenGL(OpenGL ES)过渡到一个越来越强大且无处不在的计算平台。
第八章, 移动设备上的交互式实时数据可视化,展示了如何通过使用内置的运动传感器,称为惯性测量单元(IMUs),以及移动设备上发现的多点触控界面来交互式地可视化数据。
第九章, 基于增强现实在移动或可穿戴平台上的可视化,介绍了在基于 Android 的通用移动设备上创建第一个基于 AR 的应用程序所需的基本构建块:OpenCV 用于计算机视觉,OpenGL 用于图形渲染,以及 Android 的传感器框架用于交互。
你需要这本书的内容
本书支持广泛的平台和开源库,从基于 Windows、Mac OS X 或 Linux 的桌面应用程序到基于 Android 的便携式移动应用程序。您需要对 C/C++ 编程有基本的了解,并对基本线性代数和几何模型有背景知识。
第一章到第三章的要求如下:
-
OpenGL 版本: 2.0 或更高版本(易于在旧版图形硬件上进行测试)。
-
平台: Windows、Mac OS X 或 Linux。
-
库: GLFW 用于 OpenGL 窗口上下文管理和处理用户输入。不需要额外的库,这使得它很容易集成到现有项目中。
-
开发工具: Windows Visual Studio 或 Xcode、CMake 和 gcc。
第四章到第六章的要求如下:
-
OpenGL 版本: 3.2 或更高版本。
-
平台: Windows、Mac OS X 或 Linux。
-
库: Assimp 用于 3D 模型加载,SOIL 用于图像和纹理加载,GLEW 用于运行时 OpenGL 扩展支持,GLM 用于矩阵运算,以及 OpenCV 用于图像处理
-
开发工具: Windows Visual Studio 或 Xcode、CMake 和 gcc。
第七章到第九章的要求如下:
-
OpenGL 版本: OpenGL ES 3.0
-
平台: 开发时使用 Linux 或 Mac OS X,部署时使用 Android OS 4.3 及更高版本(API 18 及更高版本)
-
库: OpenCV for Android 和 GLM
-
开发工具: Mac OS X 或 Linux 中的 Android SDK、Android NDK 和 Apache Ant
更多信息,请注意,本书中的代码是在所有支持的平台上的以下库和开发工具中构建和测试的:
-
OpenCV 2.4.9 (
opencv.org/downloads.html) -
OpenCV 3.0.0 for Android (
opencv.org/downloads.html) -
SOIL (
www.lonesock.net/soil.html) -
GLEW 1.12.0 (
glew.sourceforge.net/) -
GLFW 3.0.4 (
www.glfw.org/download.html) -
GLM 0.9.5.4 (
glm.g-truc.net/0.9.5/index.html) -
Assimp 3.0 (
assimp.sourceforge.net/main_downloads.html) -
Android SDK r24.3.3 (
developer.android.com/sdk/index.html) -
Android NDK r10e (
developer.android.com/ndk/downloads/index.html) -
Windows Visual Studio 2013 (
www.visualstudio.com/en-us/downloads/download-visual-studio-vs.aspx) -
CMake 3.2.1 (
www.cmake.org/download/)
本书面向的对象
这本书的目标是面向任何对使用现代图形硬件创建令人印象深刻的可视化工具感兴趣的人。无论你是开发者、工程师还是科学家,如果你对探索 OpenGL 在数据可视化方面的强大功能感兴趣,这本书就是为你准备的。虽然推荐熟悉 C/C++,但不需要有 OpenGL 的先验经验。
部分
在这本书中,你会发现一些频繁出现的标题(准备就绪、如何操作、工作原理、更多内容、相关内容)。
为了清楚地说明如何完成食谱,我们使用以下这些部分:
准备就绪
本节告诉你在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。
如何操作...
本节包含遵循食谱所需的步骤。
工作原理...
本节通常包含对前一个章节发生情况的详细解释。
更多内容...
本节包含有关食谱的附加信息,以便使读者对食谱有更多的了解。
相关内容
本节提供了对其他有用信息的链接,这些信息对食谱很有帮助。
惯例
在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“我们假设所有文件都保存在名为code的顶级目录中,而main.cpp文件保存在/code/Tutorial1子目录中。”
代码块设置如下:
typedef struct
{
GLfloat x, y, z;
} Data;
任何命令行输入或输出都按如下方式编写:
sudo port install glfw
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“选择空项目选项,然后点击完成。”
注意
警告或重要注意事项以如下框中的方式出现。
小贴士
小技巧和技巧看起来像这样。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。
下载示例代码
您可以从您在www.packtpub.com的账户中下载示例代码文件,适用于您购买的所有 Packt Publishing 书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。这些彩色图像将帮助您更好地理解输出的变化。您可以从以下链接下载此文件:www.packtpub.com/sites/default/files/downloads/9727OS.pdf。
勘误
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下现有的勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上对版权材料的盗版是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。
第一章. 开始使用 OpenGL
在本章中,我们将涵盖以下主题:
-
设置基于 Windows 的开发平台
-
设置基于 Mac 的开发平台
-
设置基于 Linux 的开发平台
-
在 Windows 中安装 GLFW 库
-
在 Mac OS X 和 Linux 中安装 GLFW 库
-
使用 GLFW 创建您的第一个 OpenGL 应用程序
-
在 Windows 中编译和运行您的第一个 OpenGL 应用程序
-
在 Mac OS X 或 Linux 中编译和运行您的第一个 OpenGL 应用程序
简介
OpenGL 是一个理想的跨平台、跨语言和硬件加速的图形渲染接口,非常适合在许多领域中可视化大量的 2D 和 3D 数据集。实际上,OpenGL 已经成为创建令人惊叹的图形的行业标准,尤其是在游戏应用和众多 3D 建模的专业工具中。随着我们在从生物医学成像到可穿戴计算(特别是随着大数据的发展)等领域的数据收集越来越多,高性能的数据可视化平台正成为许多未来应用的一个基本组成部分。确实,大规模数据集的可视化正在成为许多领域中的开发者、科学家和工程师面临的一个越来越具有挑战性的问题。因此,OpenGL 可以提供许多实时应用中创建令人印象深刻、令人惊叹视觉的统一解决方案。
OpenGL 的 API 封装了硬件交互的复杂性,同时允许用户对过程进行低级控制。从复杂的多服务器设置到移动设备,OpenGL 库为开发者提供了一个易于使用的界面,用于高性能图形渲染。图形硬件和大量存储设备的可用性和能力的不断提高,以及它们成本的降低,进一步推动了基于交互式 OpenGL 的数据可视化工具的开发。
现代计算机配备了专门的图形处理单元(GPU),这是高度定制的硬件组件,旨在加速图形渲染。GPU 还可用于加速通用、高度可并行化的计算任务。通过利用硬件和 OpenGL,我们可以产生高度交互和美观的结果。
本章介绍了开发基于 OpenGL 的数据可视化应用程序的基本工具,并提供了设置第一个演示应用程序环境的逐步教程。此外,本章概述了设置一个名为 CMake 的流行工具的步骤,CMake 是一个跨平台软件,它通过简单的配置文件自动化生成标准构建文件(例如,Linux 中的 makefiles,定义编译参数和命令)的过程。在未来的开发中,我们将使用 CMake 工具编译额外的库,包括本章后面介绍的 GLFW(OpenGL 框架)库。简而言之,GLFW 库是一个开源的多平台库,允许用户使用 OpenGL 上下文创建和管理窗口,以及处理来自鼠标和键盘等外围设备的输入。默认情况下,OpenGL 本身不支持其他外围设备;因此,我们使用 GLFW 库来填补这一空白。我们希望这个详细的教程对那些对探索 OpenGL 进行数据可视化感兴趣但经验很少或没有经验的初学者特别有用。然而,我们将假设你已经熟悉 C/C++编程语言。
设置基于 Windows 的开发平台
在 Windows 环境中,有各种开发工具可用于创建应用程序。在本书中,我们将专注于使用微软 Visual Studio 2013 中的 Visual C++创建 OpenGL 应用程序,鉴于其广泛的文档和支持。
安装 Visual Studio 2013
在本节中,我们概述了安装 Visual Studio 2013 的步骤。
准备工作
我们假设你已经安装了 Windows 7.0 或更高版本。为了获得最佳性能,我们建议你获得一块专用显卡,例如 NVIDIA GeForce 显卡,并在你的计算机上至少有 10 GB 的空闲磁盘空间以及 4 GB 的 RAM。下载并安装你显卡的最新驱动程序。
如何操作...
要免费安装 Microsoft Visual Studio 2013,请从微软的官方网站下载 Windows 桌面 Express 2013 版本(参考www.visualstudio.com/en-us/downloads/)。一旦下载了安装程序可执行文件,我们就可以开始这个过程。默认情况下,我们将假设程序安装在以下路径:

为了验证安装,点击安装结束处的启动按钮,它将首次执行 VS Express 2013 for Desktop 应用程序。
在 Windows 中安装 CMake
在本节中,我们概述了安装 CMake 的步骤,CMake 是一个流行的工具,它自动化了为 Visual Studio(以及其他工具)创建标准构建文件的过程。
准备工作
要获取 CMake 工具(CMake 3.2.1),您可以从www.cmake.org/download/下载可执行文件(cmake-3.2.1-win32-x86.exe)。
如何操作…
安装向导将引导您完成过程(在提示安装选项时,选择将 CMake 添加到系统 PATH 以供所有用户使用)。要验证安装,请运行 CMake(cmake-gui)。

到目前为止,您应该在您的机器上成功安装了 Visual Studio 2013 和 CMake,并准备好编译/安装 GLFW 库以创建您的第一个 OpenGL 应用程序。
设置基于 Mac 的开发平台
使用 OpenGL 的一个重要优势是可以在不同的平台上交叉编译相同的源代码。如果您计划在 Mac 平台上开发应用程序,您可以使用以下步骤轻松设置开发环境。我们假设您已安装 Mac OS X 10.9 或更高版本。OpenGL 更新已集成到 Mac OS X 的系统更新中,通过图形驱动程序进行。
安装 Xcode 和命令行工具
苹果公司的 Xcode 开发软件为开发者提供了一套全面的工具,包括 IDE、OpenGL 头文件、编译器和调试工具,用于创建原生 Mac 应用程序。为了简化过程,我们将使用与 Linux 中共享大多数常见功能的命令行界面来编译我们的代码。
准备工作
如果您正在使用 Mac OS X 10.9 或更高版本,您可以通过随 Mac OS 一起提供的 App Store 下载 Xcode。完整的安装支持和说明可在苹果开发者网站上找到(developer.apple.com/xcode/)。
如何操作...
注意事项
-
在Spotlight中搜索关键字
Terminal并运行Terminal。![如何操作...]()
-
在终端中执行以下命令:
xcode-select --install如果您之前已安装命令行工具,将出现错误信息“命令行工具已安装”。在这种情况下,只需跳到步骤 4 以验证安装。
-
点击安装按钮直接安装命令行工具。这将安装基本编译工具,如gcc和make,用于应用程序开发(注意 CMake 需要单独安装)。
-
最后,输入
gcc --version以验证安装。![如何操作...]()
相关信息
如果遇到命令未找到错误或其他类似问题,请确保命令行工具已成功安装。苹果公司提供了一套广泛的文档,有关安装 Xcode 的更多信息,请参阅developer.apple.com/xcode。
安装 MacPorts 和 CMake
在本节中,我们概述了安装 MacPorts 的步骤,这大大简化了后续的设置步骤,以及 Mac 上的 CMake。
准备工作
与 Windows 安装类似,您可以从www.cmake.org/cmake/resources/software.html下载CMake的二进制发行版,并手动配置命令行选项。然而,为了简化安装并自动化配置过程,我们强烈建议您使用 MacPorts。
如何操作...
要安装 MacPorts,请按照以下步骤操作:
-
下载适用于相应版本 Mac OS X 的 MacPorts 包安装程序(
guide.macports.org/#installing.macports):-
Mac OS X 10.10 Yosemite:
distfiles.macports.org/MacPorts/MacPorts-2.3.3-10.10-Yosemite.pkg -
Mac OS X 10.9 Mavericks:
distfiles.macports.org/MacPorts/MacPorts-2.3.3-10.9-Mavericks.pkg
-
-
双击包安装程序,并按照屏幕上的说明操作。
![如何操作...]()
-
通过在终端中输入
port version来验证安装,它将返回当前安装的 MacPorts 版本(在前面的包中为Version: 2.3.3)。
要在 Mac 上安装CMake,请按照以下步骤操作:
-
打开终端应用程序。
-
执行以下命令:
sudo port install cmake +gui
要验证安装,请输入cmake –version以显示当前安装的版本,并输入cmake-gui以探索 GUI。

在这个阶段,您的 Mac 已配置好用于 OpenGL 开发,并准备好编译您的第一个 OpenGL 应用程序。对于那些更习惯于 GUI 的用户,使用 Mac 的命令行界面最初可能是一种令人不知所措的体验。然而,从长远来看,它是一种有回报的学习体验,因为其整体简单性。与不断演变的 GUI 相比,命令行工具和界面通常更具有时间不变性。最终,您只需复制并粘贴相同的命令行,从而节省了每次 GUI 更改时查阅新文档所需的大量时间。
设置基于 Linux 的开发平台
要在 Linux 平台上准备开发环境,我们可以利用强大的 Debian 包管理系统。apt-get或aptitude程序会自动从服务器检索预编译的包,并解决和安装所有所需的依赖包。如果您使用的是非 Debian 平台,如 Fedora,您可以通过搜索此配方中列出的每个包的关键词来找到等效程序。
准备工作
我们假设你已经成功安装了所有更新以及与你的图形硬件相关的最新图形驱动程序。Ubuntu 12.04 或更高版本支持第三方专有 NVIDIA 和 AMD 图形驱动程序,更多信息可以在help.ubuntu.com/community/BinaryDriverHowto找到。
如何操作...
使用以下步骤安装所有开发工具和相关依赖项:
-
打开一个终端。
-
输入更新命令:
sudo apt-get update -
输入安装命令,并在所有提示中输入
y:sudo apt-get install build-essential cmake-gui xorg-dev libglu1-mesa-dev mesa-utils -
验证结果:
gcc --version如果成功,此命令应返回已安装的
gcc当前版本。
工作原理...
总结来说,apt-get update命令自动更新 Debian 包管理系统中的本地数据库。这确保了在过程中检索和安装了最新的软件包。apt-get系统还提供其他包管理功能,如软件包移除(卸载)、依赖关系检索以及软件包升级。这些高级功能超出了本书的范围,但更多信息可以在wiki.debian.org/apt-get找到。
前面的命令安装了多个软件包到你的机器上。在这里,我们将简要解释每个软件包的目的。
如其名称所暗示的,build-essential软件包封装了必需的软件包,即 gcc 和 g++,这些软件包是编译 Linux 中的 C 和 C++源代码所必需的。此外,它还会在过程中下载头文件并解决所有依赖关系。
cmake-gui软件包是本章中较早描述的 CMake 程序。它不是直接从网站下载 CMake 并从源代码编译,而是检索由 Ubuntu 社区编译、测试和发布的最新支持的版本。使用 Debian 包管理系统的优点是稳定性和未来更新的便捷性。然而,对于寻找最新版本的用户,基于 apt-get 的系统可能会落后几个版本。
xorg-dev和libglu1-mesa-dev软件包是编译 GLFW 库所需的发展文件。这些软件包包括其他程序所需的头文件和库。如果你选择使用预编译的二进制版本 GLFW,你可能能够跳过一些软件包。然而,我们强烈建议你遵循本教程的步骤。
相关内容
更多信息,大多数步骤在本在线文档中有详细说明和解释:help.ubuntu.com/community/UsingTheTerminal。
在 Windows 中安装 GLFW 库
在 Windows 中安装 GLFW 库有两种方法,这两种方法将在本节中讨论。第一种方法涉及直接使用 CMake 编译 GLFW 源代码以实现完全控制。然而,为了简化过程,我们建议您下载预编译的二进制发行版。
准备工作
我们假设您已经按照前面章节所述成功安装了 Visual Studio 2013 和 CMake。为了完整性,我们将演示如何使用 CMake 安装 GLFW。
如何操作...
要使用预编译的二进制包安装 GLFW,请按照以下步骤操作:
-
创建
C:/Program Files (x86)/glfw-3.0.4目录。在提示时授予必要的权限。 -
从
sourceforge.net/projects/glfw/files/glfw/3.0.4/glfw-3.0.4.bin.WIN32.zip下载glfw-3.0.4.bin.WIN32.zip包,并解压该包。 -
将
glfw-3.0.4.bin.WIN32文件夹内所有提取的内容(例如,包括lib-msvc2012)复制到C:/Program Files (x86)/glfw-3.0.4目录中。在提示时授予权限。 -
将
lib-msvc2012文件夹重命名为lib,位于C:/Program Files (x86)/glfw-3.0.4目录中。在提示时授予权限。
或者,要直接编译源文件,请按照以下步骤操作:
-
从
sourceforge.net/projects/glfw/files/glfw/3.0.4/glfw-3.0.4.zip下载源代码包,并在桌面上解压该包。在解压的glfw-3.0.4文件夹内创建一个名为build的新文件夹以存储二进制文件,并打开cmake-gui。 -
将
glfw-3.0.4(从桌面)选为源目录,将glfw-3.0.4/build选为构建目录。截图如下所示:![如何操作...]()
-
点击生成,并在提示中选择Visual Studio 12 2013。
![如何操作...]()
-
再次点击生成。
![如何操作...]()
-
打开
build目录,双击GLFW.sln以打开 Visual Studio。 -
在 Visual Studio 中,点击构建解决方案(按F7)。
-
将build/src/Debug/glfw3.lib复制到C:/Program Files (x86)/glfw-3.0.4/lib。
-
将
include目录(位于glfw-3.0.4/include内部)复制到C:/Program Files (x86)/glfw-3.0.4/.
在此步骤之后,我们应该在C:/Program Files (x86)/glfw-3.0.4目录内拥有include(glfw3.h)和library(glfw3.lib)文件,如图所示使用预编译二进制文件的设置过程。
在 Mac OS X 和 Linux 中安装 GLFW 库
Mac 和 Linux 的安装过程使用命令行界面基本相同。为了简化过程,我们建议 Mac 用户使用 MacPorts。
准备工作
我们假设您已成功安装了基本开发工具,包括 CMake,如前文所述。为了最大灵活性,我们可以直接从源代码编译库(参考 www.glfw.org/docs/latest/compile.html 和 www.glfw.org/download.html)。
如何操作...
对于 Mac 用户,请在终端中输入以下命令以使用 MacPorts 安装 GLFW:
sudo port install glfw
对于 Linux 用户(或希望练习使用命令行工具的 Mac 用户),以下是在命令行界面中直接编译和安装 GLFW 源包的步骤:
-
创建一个名为
opengl_dev的新文件夹,并将当前目录更改为新路径:mkdir ~/opengl_dev cd ~/opengl_dev -
从官方仓库获取 GLFW 源包 (
glfw-3.0.4):sourceforge.net/projects/glfw/files/glfw/3.0.4/glfw-3.0.4.tar.gz。 -
解压缩包。
tar xzvf glfw-3.0.4.tar.gz -
执行编译和安装:
cd glfw-3.0.4 mkdir build cd build cmake ../ make && sudo make install
工作原理...
第一组命令创建一个新的工作目录以存储使用 wget 命令检索的新文件,该命令将 GLFW 库的副本下载到当前目录。tar xzvf 命令解压缩压缩包并创建一个包含所有内容的新的文件夹。
然后,cmake 命令自动在当前 build 目录中生成编译过程所需的必要构建文件。此过程还会检查缺失的依赖项并验证应用程序的版本。
make 命令随后从自动生成的 Makefile 脚本中获取所有指令,并将源代码编译成库。
sudo make install 命令将库头文件以及静态或共享库安装到您的机器上。由于此命令需要写入根目录,因此需要 sudo 命令来授予此类权限。默认情况下,文件将被复制到 /usr/local 目录。在本书的其余部分,我们将假设安装遵循这些默认路径。
对于高级用户,我们可以通过使用 CMake 图形用户界面 (cmake-gui) 来配置软件包以优化编译。

例如,如果您计划将 GLFW 库编译为共享库,可以启用 BUILD_SHARED_LIBS 选项。在本书中,我们不会探索 GLFW 库的全部功能,但这些选项对于寻求进一步定制的开发者可能很有用。此外,如果您希望将库文件安装到单独的位置,还可以自定义安装前缀 (CMAKE_INSTALL_PREFIX)。
使用 GLFW 创建您的第一个 OpenGL 应用程序
现在您已经成功配置了开发平台并安装了 GLFW 库,我们将提供如何创建您的第一个基于 OpenGL 的应用程序的教程。
准备工作
到目前为止,无论您使用的是哪种操作系统,您都应该已经准备好了所有预置工具,因此我们将立即开始使用这些工具构建您的第一个 OpenGL 应用程序。
如何操作...
以下代码概述了创建一个简单 OpenGL 程序的基本步骤,该程序利用 GLFW 库并绘制一个旋转的三角形:
-
创建一个空文件,然后包含 GLFW 库头文件和标准 C++库的头文件:
#include <GLFW/glfw3.h> #include <stdlib.h> #include <stdio.h> -
初始化 GLFW 并创建一个 GLFW 窗口对象(640 x 480):
int main(void) { GLFWwindow* window; if (!glfwInit()) exit(EXIT_FAILURE); window = glfwCreateWindow(640, 480, "Chapter 1: Simple GLFW Example", NULL, NULL); if (!window) { glfwTerminate(); exit(EXIT_FAILURE); } glfwMakeContextCurrent(window); -
定义一个循环,当窗口关闭时终止:
while (!glfwWindowShouldClose(window)) { -
设置视口(使用窗口的宽度和高度)并清除屏幕颜色缓冲区:
float ratio; int width, height; glfwGetFramebufferSize(window, &width, &height); ratio = (float) width / (float) height; glViewport(0, 0, width, height); glClear(GL_COLOR_BUFFER_BIT); -
设置相机矩阵。注意,关于相机模型的更多细节将在第三章 交互式 3D 数据可视化中讨论:
glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(-ratio, ratio, -1.f, 1.f, 1.f, -1.f); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); -
绘制一个旋转的三角形,并为三角形的每个顶点(x,y,和z)设置不同的颜色(红色、绿色和蓝色通道)。第一行代码使三角形随时间旋转:
glRotatef((float)glfwGetTime() * 50.f, 0.f, 0.f, 1.f); glBegin(GL_TRIANGLES); glColor3f(1.f, 0.f, 0.f); glVertex3f(-0.6f, -0.4f, 0.f); glColor3f(0.f, 1.f, 0.f); glVertex3f(0.6f, -0.4f, 0.f); glColor3f(0.f, 0.f, 1.f); glVertex3f(0.f, 0.6f, 0.f); glEnd(); -
交换前后缓冲区(GLFW 使用双缓冲),以更新屏幕并处理所有挂起的事件:
glfwSwapBuffers(window); glfwPollEvents(); } -
释放内存并终止 GLFW 库。然后,退出应用程序:
glfwDestroyWindow(window); glfwTerminate(); exit(EXIT_SUCCESS); } -
使用您选择的文本编辑器将文件保存为
main.cpp。
它是如何工作的...
通过包含 GLFW 库头文件glfw3.h,我们自动导入 OpenGL 库中所有必要的文件。最重要的是,GLFW 自动确定平台,从而允许您无缝地编写可移植的源代码。
在主函数中,我们必须首先使用glfwInit函数在主线程中初始化 GLFW 库。在使用任何 GLFW 函数之前,这是必需的。在程序退出之前,GLFW 应该被终止以释放任何分配的资源。
然后,glfwCreateWindow函数创建一个窗口及其相关上下文,并且它还返回一个指向GLFWwindow对象的指针。在这里,我们可以定义窗口的宽度、高度、标题和其他属性。在窗口创建后,我们接着调用glfwMakeContextCurrent函数来切换上下文,并确保指定窗口的上下文在调用线程上是当前的。
到目前为止,我们已经准备好在窗口上渲染我们的图形元素。while循环提供了一个机制,只要窗口保持打开状态,就会重新绘制我们的图形。OpenGL 需要在相机参数上进行显式设置;更多细节将在接下来的章节中讨论。将来,我们可以提供不同的参数来模拟透视,并处理更复杂的问题(如抗锯齿)。目前,我们已经设置了一个简单的场景来渲染一个基本的原始形状(即三角形),并固定了顶点的颜色。用户可以通过修改glColor3f和glVertex3f函数中的参数来改变颜色以及顶点的位置。
本例演示了使用 OpenGL 创建图形所需的基本知识。尽管示例代码很简单,但它提供了一个很好的入门框架,说明了如何使用 OpenGL 和 GLFW 通过图形硬件创建高性能的图形渲染应用程序。
在 Windows 中编译和运行你的第一个 OpenGL 应用程序
设置 OpenGL 项目有多种方法。在这里,我们使用 Visual Studio 2013 或更高版本创建一个示例项目,并提供 OpenGL 和 GLFW 库首次配置的完整指南。这些相同的步骤将来也可以应用到你的项目中。
准备工作
假设你已经成功在你的环境中安装了 Visual Studio 2013 和 GLFW(版本 3.0.4),我们将从头开始我们的项目。
如何操作...
在 Visual Studio 2013 中,按照以下步骤创建一个新项目并编译源代码:
-
打开 Visual Studio 2013(桌面版 VS Express 2013)。
-
创建一个新的 Win32 控制台应用程序,并将其命名为
Tutorial1。![如何操作...] -
选择空项目选项,然后点击完成。![如何操作...]
-
右键点击源文件,添加一个新的 C++源文件(添加 | 新建项),命名为main.cpp
![如何操作...]()
-
将上一节中的源代码复制并粘贴到main.cpp中,并保存。
-
打开项目属性(Alt + F7)。
-
通过导航到配置属性 | C/C++ | 通用 | 附加包含目录,添加 GLFW 库的
include路径,C:\Program Files (x86)\glfw-3.0.4\include。![如何操作...]小贴士
下载示例代码
您可以从您在
www.packtpub.com的账户下载示例代码文件,以获取您购买的所有 Packt Publishing 书籍的示例代码。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。 -
通过导航到 配置属性 | 链接器 | 常规 | 附加库目录,添加 GLFW 库路径,C:\Program Files (x86)\glfw-3.0.4\lib。
![如何操作...]()
-
通过导航到 配置属性 | 链接器 | 输入 | 附加依赖项,添加 GLFW 和 OpenGL 库(
glu32.lib、glfw3.lib和opengl32.lib)。![如何操作...]()
-
构建 解决方案(按 F7)。
-
运行程序(按 F5)。
这是您的第一个 OpenGL 应用程序,它显示了一个在您的图形硬件上运行的旋转三角形。尽管我们只定义了顶点的颜色为红色、绿色和蓝色,但图形引擎会插值中间结果,并且所有计算都是使用图形硬件完成的。截图如下所示:

在 Mac OS X 或 Linux 上编译和运行您的第一个 OpenGL 应用程序
使用命令行界面设置 Linux 或 Mac 机器变得简单得多。我们假设您已经准备好了之前讨论的所有组件,并且所有默认路径都使用推荐的方式。
准备工作
我们将首先编译之前描述的示例代码。您可以从 Packt Publishing 的官方网站 www.packtpub.com 下载完整的代码包。我们假设所有文件都保存在名为 code 的顶级目录中,而 main.cpp 文件则保存在 /code/Tutorial1 子目录中。
如何操作...
-
打开终端或等效的命令行界面。
-
将当前目录更改为工作目录:
cd ~/code -
输入以下命令以编译程序:
gcc -Wall `pkg-config --cflags glfw3` -o main Tutorial1/main.cpp `pkg-config --static --libs glfw3` -
运行程序:
./main
这是您的第一个 OpenGL 应用程序,它在本机图形硬件上运行并显示一个旋转的三角形。尽管我们只定义了三个顶点的颜色为红色、绿色和蓝色,但图形引擎会插值中间结果,并且所有计算都是使用图形硬件完成的。

为了进一步简化过程,我们在示例代码中提供了一个编译脚本。您可以通过在终端中简单地输入以下命令来执行脚本:
chmod +x compile.sh
./compile.sh
您可能会注意到 OpenGL 代码是平台无关的。GLFW 库最强大的功能之一是它在幕后处理窗口管理和其他平台相关函数。因此,相同的源代码(main.cpp)可以在多个平台上共享和编译,而无需任何更改。
第二章:OpenGL 原语和二维数据可视化
在本章中,我们将涵盖以下主题:
-
OpenGL 原语
-
使用原语创建二维图表
-
时间序列的实时可视化
-
3D/4D 数据集的二维可视化
简介
在上一章中,我们提供了一个示例代码,使用 OpenGL 和 GLFW 库在屏幕上渲染三角形。在本章中,我们将专注于使用 OpenGL 原语,如点、线和三角形,来实现数据的二维可视化,包括时间序列,如心电图(ECG)。我们将从介绍每个原语开始,并提供示例代码,以便读者可以以最小的学习曲线实验 OpenGL 原语。
可以将原语视为使用 OpenGL 创建图形的基本构建块。这些构建块可以轻松地在许多应用程序中重用,并且在不同的平台之间具有高度的便携性。通常,程序员会为以视觉上吸引人的方式显示他们的结果而挣扎,并且可能会花费大量时间在屏幕上执行简单的绘图任务。在本章中,我们将介绍一种使用 OpenGL 进行二维数据可视化的快速原型设计方法,以便以最小的努力创建令人印象深刻的图形。最重要的是,所提出的框架非常直观且可重用,可以扩展用于更复杂的应用。一旦你掌握了 OpenGL 语言的基础,你将具备创建利用现代图形硬件 OpenGL 数据可视化真正潜力的令人印象深刻的技能。
OpenGL 原语
简而言之,原语只是 OpenGL 中绘制的基本形状。在本节中,我们将简要概述 OpenGL 支持的主要几何原语,并特别关注三种常用原语(这些原语也将出现在我们的演示应用中):点、线和三角形。
绘制点
我们从许多可视化问题的简单但非常有用的构建块开始:一个点原语。一个点可以是二维中的有序对,也可以在三维空间中可视化。
准备工作
为了简化工作流程并提高代码的可读性,我们首先定义了一个名为Vertex的结构,它封装了基本元素,如顶点的位置和颜色。
typedef struct
{
GLfloat x, y, z; //position
GLfloat r, g, b, a; //color and alpha channels
} Vertex;
现在,我们可以将每个对象和形状视为空间中一组顶点(具有特定颜色)。在本章中,由于我们的重点是二维可视化,顶点的Z位置通常手动设置为0.0f。
例如,我们可以创建一个位于屏幕中心(0, 0, 0)的顶点,颜色为白色:
Vertex v = {0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f};
注意,颜色元素由红色、绿色、蓝色和 alpha 通道组成。这些值范围从 0.0 到 1.0。alpha 通道允许我们创建透明度(0:完全透明;1:完全不透明),以便对象可以混合在一起。
如何操作...
我们可以先定义一个名为drawPoint的函数来封装 OpenGL 原始函数的复杂性,如下所示:
-
创建一个名为
drawPoint的函数来绘制点,该函数接受两个参数(顶点和点的大小):void drawPoint(Vertex v1, GLfloat size){ -
指定点的大小:
glPointSize(size); -
设置要指定的顶点列表的起始位置,并指示与顶点关联的原始类型(在本例中为
GL_POINTS):glBegin(GL_POINTS); -
使用
Vertex结构中的字段设置颜色和顶点位置:glColor4f(v1.r, v1.g, v1.b, v1.a); glVertex3f(v1.x, v1.y, v1.z); -
设置列表的结束:
glEnd(); } -
此外,我们还可以定义一个名为
drawPointsDemo的函数来进一步封装复杂性。此函数绘制一系列大小递增的点:void drawPointsDemo(int width, int height){ GLfloat size=5.0f; for(GLfloat x = 0.0f; x<=1.0f; x+=0.2f, size+=5) { Vertex v1 = {x, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}; drawPoint(v1, size); } }
最后,让我们将这些函数集成到一个完整的 OpenGL 演示程序中(参考第一章中的相同步骤,开始使用 OpenGL):
-
创建一个名为
main_point.cpp的源文件,然后包含 GLFW 库和标准 C++库的头文件:#include <GLFW/glfw3.h> #include <stdlib.h> #include <stdio.h> -
定义显示的窗口大小:
const int WINDOWS_WIDTH = 640*2; const int WINDOWS_HEIGHT = 480; -
定义
Vertex结构和函数原型:typedef struct { GLfloat x, y, z; GLfloat r, g, b, a; } Vertex; void drawPoint(Vertex v1, GLfloat size); void drawPointsDemo(int width, int height); -
实现之前显示的
drawPoint和drawPointsDemo函数。 -
初始化 GLFW 并创建一个 GLFW 窗口对象:
int main(void) { GLFWwindow* window; if (!glfwInit()) exit(EXIT_FAILURE); window = glfwCreateWindow(WINDOWS_WIDTH, WINDOWS_HEIGHT, "Chapter 2: Primitive drawings", NULL, NULL); if (!window){ glfwTerminate(); exit(EXIT_FAILURE); } glfwMakeContextCurrent(window); -
启用抗锯齿和平滑处理:
glEnable(GL_POINT_SMOOTH); glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); -
定义一个循环,当窗口关闭时终止。在每个迭代开始时设置视口(使用窗口的大小)并清除颜色缓冲区以更新新内容:
while (!glfwWindowShouldClose(window)) { float ratio; int width, height; glfwGetFramebufferSize(window, &width, &height); ratio = (float) width / (float)height; glViewport(0, 0, width, height); glClear(GL_COLOR_BUFFER_BIT); -
设置用于正交投影的相机矩阵:
glMatrixMode(GL_PROJECTION); glLoadIdentity(); //Orthographic Projection glOrtho(-ratio, ratio, -1.f, 1.f, 1.f, -1.f); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); -
调用
drawPointsDemo函数:drawPointsDemo(width, height); -
交换窗口的前后缓冲区并处理事件队列(如键盘输入)以避免锁定:
glfwSwapBuffers(window); glfwPollEvents(); } -
释放内存并终止 GLFW 库。然后,退出应用程序:
glfwDestroyWindow(window); glfwTerminate(); exit(EXIT_SUCCESS); }
这里是结果(禁用抗锯齿)显示一系列大小递增的点(即,每个点的大小由glPointSize指定):

它是如何工作的...
glBegin和glEnd函数定义了与所需原始类型(在本演示中为GL_POINTS)对应的顶点列表。glBegin函数接受一组符号常量,代表不同的绘图方法,包括GL_POINTS、GL_LINES和GL_TRIANGLES,如本章所述。
有几种方法可以控制绘制点的过程。首先,我们可以使用glPointSize函数设置每个点的直径(以像素为单位)。默认情况下,一个点没有启用抗锯齿(一种平滑采样伪影的方法)时,直径为 1。此外,我们还可以使用glColor4f函数定义每个点的颜色以及 alpha 通道(透明度)。alpha 通道允许我们叠加点和混合图形元素。这是一种强大而简单的技术,在图形设计和用户界面设计中得到广泛应用。最后,我们使用glVertex3f函数定义点在空间中的位置。
在 OpenGL 中,我们可以以两种不同的方式定义投影变换:正交投影或透视投影。在 2D 绘图时,我们通常使用正交投影,它不涉及透视校正(例如,屏幕上的对象将保持相同的大小,无论其与摄像机的距离如何)。在 3D 绘图时,我们使用透视投影来创建更逼真的场景,类似于人眼看到的场景。在代码中,我们使用glOrtho函数设置正交投影。glOrtho函数接受以下参数:垂直裁剪平面的坐标、水平裁剪平面的坐标以及近裁剪面和远裁剪面的距离。这些参数确定投影矩阵,详细文档可以在developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/glOrtho.3.html找到。
抗锯齿和平滑处理是产生现代图形中看到的精致外观所必需的。大多数显卡支持原生平滑处理,在 OpenGL 中,可以通过以下方式启用:
glEnable(GL_POINT_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
这是启用抗锯齿后的最终结果,显示了一系列大小递增的圆形点:

注意,在前面的图中,由于启用了抗锯齿功能,点现在被渲染为圆形而不是正方形。鼓励读者禁用和启用前面图的特性,以查看操作的效果。
参见
在这个教程中,我们由于其简洁性而专注于 C 编程风格。在接下来的章节中,我们将迁移到使用 C++的面向对象编程风格。此外,在本章中,我们关注三个基本原语(并在适当的地方讨论这些原语的导数):GL_POINTS、GL_LINES和GL_TRIANGLES。以下是 OpenGL 支持的原语更详尽的列表(有关更多信息,请参阅www.opengl.org/wiki/Primitive):
GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_QUADS, GL_QUAD_STRIP, and GL_POLYGON
绘制线段
现在的一个自然扩展是连接数据点之间的线,然后将这些线连接起来形成一个用于绘图的网格。实际上,OpenGL 原生支持绘制线段,其过程与绘制点非常相似。
准备工作
在 OpenGL 中,我们可以通过一组两个顶点简单地定义一个线段,并通过在 glBegin 语句中选择 GL_LINES 作为符号常量来自动在这两个顶点之间形成一条线。
如何做…
在这里,我们定义了一个新的线段绘制函数,称为 drawLineSegment,用户可以通过简单地替换前一部分中的 drawPointsDemo 函数来测试它:
-
定义
drawLineSegment函数,它接受两个顶点和线的宽度作为输入:void drawLineSegment(Vertex v1, Vertex v2, GLfloat width) { -
设置线的宽度:
glLineWidth(width); -
设置线段绘制的原语类型:
glBegin(GL_LINES); -
设置线段的顶点和颜色:
glColor4f(v1.r, v1.g, v1.b, v1.a); glVertex3f(v1.x, v1.y, v1.z); glColor4f(v2.r, v2.g, v2.b, v2.a); glVertex3f(v2.x, v2.y, v2.z); glEnd(); }
此外,我们定义了一个新的网格绘制函数,称为 drawGrid,它基于 drawLineSegment 函数如下:
void drawGrid(GLfloat width, GLfloat height, GLfloat grid_width){
//horizontal lines
for(float i=-height; i<height; i+=grid_width){
Vertex v1 = {-width, i, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f};
Vertex v2 = {width, i, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f};
drawLineSegment(v1, v2);
}
//vertical lines
for(float i=-width; i<width; i+=grid_width){
Vertex v1 = {i, -height, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f};
Vertex v2 = {i, height, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f};
drawLineSegment(v1, v2);
}
}
最后,我们可以通过替换前一部分中对 drawPointsDemo 函数的调用,使用以下 drawLineDemo 函数来执行完整的演示:
void drawLineDemo(){
//draw a simple grid
drawGrid(5.0f, 1.0f, 0.1f);
//define the vertices and colors of the line segments
Vertex v1 = {-5.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.7f};
Vertex v2 = {5.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.7f};
Vertex v3 = {0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.7f};
Vertex v4 = {0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.7f};
//draw the line segments
drawLineSegment(v1, v2, 10.0f);
drawLineSegment(v3, v4, 10.0f);
}
这是一张演示截图,显示了具有等间距的网格以及使用线原语绘制的 x 和 y 轴:

工作原理…
在 OpenGL 中绘制线段有多种方式。我们已经展示了使用 GL_LINES 的用法,它将列表中的每个连续顶点对组合成独立的线段。另一方面,如果您想绘制没有间隙的线,可以使用 GL_LINE_STRIP 选项,它以连续的方式连接所有顶点。最后,为了形成一个闭合的循环序列,其中线的端点被连接,您将使用 GL_LINE_LOOP 选项。
此外,我们可以使用 glLineWidth 和 glColor4f 函数分别修改每条线的宽度和颜色。
绘制三角形
我们现在将转到另一个非常常用的原语,即三角形,它是绘制所有可能多边形的基础。
准备工作
与绘制线段类似,我们可以简单地通过一组三个顶点定义一个三角形,并通过在 glBegin 语句中选择 GL_TRIANGLES 作为符号常量来自动形成线段。
如何做…
最后,我们定义了一个新的函数,称为 drawTriangle,用户可以通过简单地替换 drawPointsDemo 函数来测试它。我们还将重用前一部分中的 drawGrid 函数:
-
定义
drawTriangle函数,它接受三个顶点作为输入:void drawTriangle(Vertex v1, Vertex v2, Vertex v3){ -
将绘图原语类型设置为绘制三角形:
glBegin(GL_TRIANGLES); -
设置三角形的顶点和颜色:
glColor4f(v1.r, v1.g, v1.b, v1.a); glVertex3f(v1.x, v1.y, v1.z); glColor4f(v2.r, v2.g, v2.b, v2.a); glVertex3f(v2.x, v2.y, v2.z); glColor4f(v3.r, v3.g, v3.b, v3.a); glVertex3f(v3.x, v3.y, v3.z); glEnd(), } -
通过用以下
drawTriangleDemo函数替换完整演示代码中对drawPointsDemo函数的调用来执行演示:void drawTriangleDemo(){ //Triangle Demo Vertex v1 = {0.0f, 0.8f, 0.0f, 1.0f, 0.0f, 0.0f, 0.6f}; Vertex v2 = {-1.0f, -0.8f, 0.0f, 0.0f, 1.0f, 0.0f, 0.6f}; Vertex v3 = {1.0f, -0.8f, 0.0f, 0.0f, 0.0f, 1.0f, 0.6f}; drawTriangle(v1, v2, v3); }
这里是最终结果,三角形以 60%的透明度叠加在网格线上渲染:

它是如何工作的…
虽然在 OpenGL 中绘制三角形的流程看起来与之前的示例相似,但其中存在一些细微的差异和更复杂的方面可以融入。这个原始形状有三种不同的模式(GL_TRIANGLES、GL_TRIANGLE_STRIP和GL_TRIANGLE_FAN),每种模式以不同的方式处理顶点。首先,GL_TRIANGLES从一个给定的列表中取出三个顶点来创建一个三角形。这些三角形独立地从每个顶点三元组形成(也就是说,每个三个顶点形成一个不同的三角形)。另一方面,GL_TRIANGLE_STRIP使用前三个顶点形成一个三角形,每个后续的顶点使用前两个顶点形成一个新的三角形。最后,GL_TRIANGLE_FAN通过创建具有中心顶点 v_1 的三角形来创建一个任意复杂的凸多边形,这个中心顶点由第一个顶点指定,形成一个由三角形组成的扇形结构。换句话说,三角形的生成顺序如下所示:
(v1, v2, v3), (v1, v3, v4),...,(v1, vn-1, vn)
for n vertices
尽管每个顶点都设置了不同的颜色,但 OpenGL 会自动处理颜色过渡(线性插值),如前一个示例中的三角形绘制所示。顶点被设置为红色、绿色和蓝色,但颜色的光谱可以清晰地看到。此外,可以使用 alpha 通道设置透明度,这使得我们可以清楚地看到三角形后面的网格。使用 OpenGL,我们还可以添加其他元素,例如高级的颜色和阴影处理,这些内容将在接下来的章节中讨论。
使用原始形状创建 2D 图
创建 2D 图是许多应用中可视化数据集中趋势的常见方式。与传统的做法(如基本的 MATLAB 绘图)相比,使用 OpenGL 可以以更动态的方式渲染此类图表,因为我们能够完全控制图形着色器进行颜色操作,我们还可以向系统提供实时反馈。这些独特的功能使用户能够创建高度交互的系统,例如,心电图等时间序列可以以最小的努力进行可视化。
在这里,我们首先演示了简单 2D 数据集的可视化,即离散时间上的正弦函数。
准备工作
这个演示需要实现之前提到的多个函数(包括drawPoint、drawLineSegment和drawGrid函数)。此外,我们将重用第一章中引入的代码结构,即使用 OpenGL 入门,以执行此演示。
如何做…
我们首先生成一个在时间间隔上正弦函数的模拟数据流。实际上,数据流可以是任何任意的信号或关系:
-
让我们定义一个额外的结构体
Data以简化接口:typedef struct { GLfloat x, y, z; } Data; -
定义一个名为
draw2DscatterPlot的通用 2D 数据绘图函数,输入数据流和点数作为输入:void draw2DscatterPlot (const Data *data, int num_points){ -
使用前面描述的
drawLineSegment函数绘制 x 和 y 轴:Vertex v1 = {-10.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}; Vertex v2 = {10.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f}; drawLineSegment(v1, v2, 2.0f); v1.x = 0.0f; v2.x = 0.0f; v1.y = -1.0f; v2.y = 1.0f; drawLineSegment(v1, v2, 2.0f); -
使用
drawPoint函数逐个绘制数据点:for(int i=0; i<num_points; i++){ GLfloat x=data[i].x; GLfloat y=data[i].y; Vertex v={x, y, 0.0f, 1.0f, 1.0f, 1.0f, 0.5f}; drawPoint(v, 8.0f); } } -
创建一个名为
draw2DlineSegments的类似函数,以便使用线段连接点,从而同时显示曲线和数据点:void draw2DlineSegments(const Data *data, int num_points){ for(int i=0; i<num_points-1; i++){ GLfloat x1=data[i].x; GLfloat y1=data[i].y; GLfloat x2=data[i+1].x; GLfloat y2=data[i+1].y; Vertex v1={x1, y1, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f}; Vertex v2={x2, y2, 0.0f, 0.0f, 1.0f, 0.0f, 0.5f}; drawLineSegment(v1, v2, 4.0f); } } -
通过创建网格、使用余弦函数生成模拟数据点并绘制数据点,将所有内容集成到一个完整的演示中:
void linePlotDemo(float phase_shift){ drawGrid(5.0f, 1.0f, 0.1f); GLfloat range = 10.0f; const int num_points = 200; Data *data=(Data*)malloc(sizeof(Data)*num_points); for(int i=0; i<num_points; i++){ data[i].x=((GLfloat)i/num_points)*range-range/2.0f; data[i].y= 0.8f*cosf(data[i].x*3.14f+phase_shift); } draw2DScatterPlot(data, num_points); draw2DLineSegments(data, num_points); free(data); } -
最后,在主程序中,包含
math.h头文件以使用余弦函数,并在循环外部添加一个名为phase_shift的新变量来执行此演示。您可以从 Packt Publishing 网站下载代码包以获取完整的演示代码:#include <math.h> ... int main(void){ ... float phase_shift=0.0f; while (!glfwWindowShouldClose(window)){ ... phase_shift+=0.02f; linePlotDemo(phase_shift); ... //finished all demo calls glfwSwapBuffers(window); glfwPollEvents(); } ... }
使用前几节讨论的基本原语组合,将模拟的实时输入数据流(具有正弦形状)绘制在网格线上:

它是如何工作的…
使用我们之前使用基本 OpenGL 原语创建的简单工具包,我们绘制了一个正弦函数,数据点(以恒定的时间间隔采样)叠加在曲线上。平滑的曲线由使用 draw2DlineSegments 函数绘制的许多单独线段组成,而样本则是使用 drawPoint 函数绘制的。这种直观的界面为下一节中更有趣的时序可视化提供了基础。
时间序列的实时可视化
在本节中,我们进一步展示了我们框架的通用性,以绘制生物医学应用中的通用时间序列数据。特别是,我们将实时显示 ECG。作为简要介绍,ECG 是一种非常常用的诊断和监测工具,用于检测心脏异常。ECG 表面记录本质上探测心脏的电活动。例如,最大的峰值(称为 QRS 复合波)通常对应于心脏的室壁去极化(心脏中泵血的强肌肉室腔)。对 ECG 的仔细分析可以是一种非常强大、非侵入性的方法,用于在临床上区分许多心脏病,包括许多形式的心律失常和心脏病发作。
准备工作
我们首先导入一个计算机生成的 ECG 数据流。ECG 数据流存储在 data_ecg.h 中(这里只提供了数据流的一小部分):
float data_ecg[]={0.396568808f, 0.372911844f, 0.311059085f, 0.220346775f, 0.113525529f, 0.002200333f, -0.103284775f, -0.194218528f, -0.266285973f, -0.318075979f, -0.349670132f, -0.362640042f, -0.360047348f, -0.346207663f, -0.325440887f, -0.302062532f, -0.279400804f, -0.259695686f … };
如何做…
-
使用以下代码通过绘制线段来绘制 ECG 数据:
void plotECGData(int offset, int size, float offset_y, float scale){ //space between samples const float space = 2.0f/size*ratio; //initial position of the first vertex to render float pos = -size*space/2.0f; //set the width of the line glLineWidth(5.0f); glBegin(GL_LINE_STRIP); //set the color of the line to green glColor4f(0.1f, 1.0f, 0.1f, 0.8f); for (int i=offset; i<size+offset; i++){ const float data = scale*data_ecg[i]+offset_y; glVertex3f(pos, data, 0.0f); pos += space; } glEnd(); } -
显示多个 ECG 数据流(模拟来自不同导联的记录):
void ecg_demo(int counter){ const int data_size=ECG_DATA_BUFFER_SIZE; //Emulate the presence of multiple ECG leads (just for demo/ display purposes) plotECGData(counter, data_size*0.5, -0.5f, 0.1f); plotECGData(counter+data_size, data_size*0.5, 0.0f, 0.5f); plotECGData(counter+data_size*2, data_size*0.5, 0.5f, -0.25f); } -
最后,在主程序中,包含
data_ecg.h头文件,并在循环中添加以下代码行。您可以从 Packt Publishing 网站下载完整的演示代码包:#include "data_ecg.h" ... int main(void){ ... while (!glfwWindowShouldClose(window)){ ... drawGrid(5.0f, 1.0f, 0.1f); //reset counter to 0 after reaching the end of the sample data if(counter>5000){ counter=0; } counter+=5; //run the demo visualizer ecg_demo(counter); ... } }
这里是两个在不同时间点模拟的多个 ECG 导联的实时显示快照。如果您运行演示,您将看到来自多个导联的 ECG 记录随着数据流处理显示而移动穿过屏幕。

这是稍后时间点的第二个快照:

工作原理…
这个演示展示了之前描述的GL_LINE_STRIP选项的使用,用于绘制 ECG 时间序列。我们不是使用GL_LINE选项绘制单个独立的线段,而是通过为每个数据点调用glVertex3f函数来绘制连续的数据流。此外,时间序列通过屏幕动画,并在交互式帧上提供动态更新,对 CPU 计算周期的影响最小。
2D visualization of 3D/4D datasets
我们现在已经学习了多种在屏幕上使用点和线生成图表的方法。在最后一节中,我们将演示如何使用 OpenGL 实时可视化 3D 数据集中的百万个数据点。可视化复杂 3D 数据集的常见策略是将第三维(例如,z 维度)编码为具有理想颜色方案的热图。作为一个例子,我们展示了一个 2D 高斯函数及其高度 z 的热图,使用简单的颜色方案编码。一般来说,一个二维高斯函数,
,定义如下:

在这里,A 是分布中心在
的振幅,
和
是分布沿 x 和 y 方向的标准差(分散度)。为了使这个演示更加有趣和更具视觉吸引力,我们随时间变化标准差或 sigma 项(在 x 和 y 方向上相等)。实际上,我们可以应用相同的方法来可视化非常复杂的 3D 数据集。
准备工作
到目前为止,你应该已经非常熟悉前几节中描述的基本原语了。在这里,我们使用GL_POINTS选项生成具有不同颜色编码 z 维度的密集数据点网格。
How to do it…
-
使用 2-D 高斯函数生成一百万个数据点(1,000 x 1,000 网格):
void gaussianDemo(float sigma){ //construct a 1000x1000 grid const int grid_x = 1000; const int grid_y = 1000; const int num_points = grid_x*grid_y; Data *data=(Data*)malloc(sizeof(Data)*num_points); int data_counter=0; for(int x = -grid_x/2; x<grid_x/2; x+=1){ for(int y = -grid_y/2; y<grid_y/2; y+=1){ float x_data = 2.0f*x/grid_x; float y_data = 2.0f*y/grid_y; //compute the height z based on a //2D Gaussian function. float z_data = exp(-0.5f*(x_data*x_data)/(sigma*sigma) -0.5f*(y_data*y_data)/(sigma*sigma))/(sigma*sigma*2.0f*M_PI); data[data_counter].x = x_data; data[data_counter].y = y_data; data[data_counter].z = z_data; data_counter++; } } //visualize the result using a 2D heat map draw2DHeatMap(data, num_points); free(data); } -
使用热图函数绘制数据点以进行颜色可视化:
void draw2DHeatMap(const Data *data, int num_points){ //locate the maximum and minimum values in the dataset float max_value=-999.9f; float min_value=999.9f; for(int i=0; i<num_points; i++){ const Data d = data[i]; if(d.z > max_value){ max_value = d.z; } if(d.z < min_value){ min_value = d.z; } } const float halfmax = (max_value + min_value) / 2; //display the result glPointSize(2.0f); glBegin(GL_POINTS); for(int i = 0; i<num_points; i++){ const Data d = data[i]; float value = d.z; float b = 1.0f - value/halfmax; float r = value/halfmax - 1.0f; if(b < 0){ b=0; } if(r < 0){ r=0; } float g = 1.0f - b - r; glColor4f(r, g, b, 0.5f); glVertex3f(d.x, d.y, 0.0f); } glEnd(); } -
最后,在主程序中,包含
math.h头文件,并在循环中添加以下代码行以随时间变化 sigma 项。您可以从 Packt Publishing 网站下载示例代码以获取完整的演示代码:#define _USE_MATH_DEFINES // M_PI constant #include <math.h> ... int main(void){ ... float sigma = 0.01f; while (!glfwWindowShouldClose(window)){ ... drawGrid(5.0f, 1.0f, 0.1f); sigma+=0.01f; if(sigma>1.0f) sigma=0.01; gaussianDemo(sigma); ... } }
这里展示了四个图表,说明了随时间变化(从 0.01 到 1)调整二维高斯函数的 sigma 项的效果:

它是如何工作的...
我们已经展示了如何使用简单的热图来可视化高斯函数,其中最大值用红色表示,而最小值用蓝色表示。总共绘制了 100 万个数据点(1,000 x 1,000),每个高斯函数使用具有特定 sigma 项的顶点进行绘制。这个 sigma 项从 0.01 变化到 1,以展示随时间变化的高斯分布。为了减少开销,未来可以实施顶点缓冲区(我们可以一次性执行内存复制操作并移除glVertex3f调用)。类似的技巧也可以应用于颜色通道。
还有更多...
我们在这里描述的热图提供了一种强大的可视化工具,用于展示许多科学和生物医学应用中看到的复杂 3D 数据集。实际上,我们已经将我们的演示扩展到了 4D 数据集的可视化,更确切地说,是一个随时间变化的 3D 函数;使用颜色图编码高度值被显示出来。这个演示展示了仅使用基于 OpenGL 原语的 2D 技术以有趣、动态的方式展示数据的多重可能性。在下一章中,我们将通过结合 3D 渲染并添加用户输入来进一步展示 OpenGL 的潜力,从而实现更复杂数据集的 3D 交互式可视化。
第三章. 交互式 3D 数据可视化
在本章中,我们将涵盖以下主题:
-
设置用于 3D 渲染的虚拟相机
-
使用透视渲染创建 3D 图表
-
使用 GLFW 创建交互式环境
-
渲染体数据集 – MCML 模拟
简介
OpenGL 是一个非常吸引人的平台,用于创建动态、高度交互的工具,以在 3D 中可视化数据。在本章中,我们将基于前一章讨论的基本概念,并扩展我们的演示,以包含更多复杂的 OpenGL 3D 渲染功能。为了实现 3D 可视化,我们首先将介绍在 OpenGL 中设置虚拟相机的基本步骤。此外,为了创建更多交互式演示,我们将介绍使用 GLFW 回调函数来处理用户输入。使用这些概念,我们将说明如何使用 OpenGL 创建具有透视渲染的交互式 3D 图表。最后,我们将演示如何渲染由生物组织中光传输的蒙特卡洛模拟生成的 3D 体数据集。到本章结束时,读者将能够使用透视渲染可视化数据,并通过用户输入动态地与环境交互,适用于广泛的用途。
设置用于 3D 渲染的虚拟相机
在现实世界中,渲染 3D 场景类似于使用数码相机拍照。创建照片所采取的步骤也可以应用于 OpenGL。
例如,你可以将相机从一个位置移动到另一个位置,并在空间中自由调整视点,这被称为视图变换。你还可以调整场景中感兴趣对象的位姿。然而,与现实世界不同,在虚拟世界中,你可以自由地以任何方向定位对象,没有任何物理约束,这被称为建模变换。最后,我们可以更换相机镜头来调整缩放并创建不同的视角,这个过程被称为投影变换。
当你应用视图和建模变换拍照时,数码相机获取信息并在你的屏幕上创建图像。这个过程称为光栅化。
这些矩阵集——包括视图变换、建模变换和投影变换——是我们可以在运行时调整的基本元素,这使我们能够创建场景的交互式和动态渲染。要开始,我们将首先探讨相机矩阵的设置,以及我们如何创建具有不同视角的场景。
准备工作
本章中的源代码基于上一章的最终演示。基本上,我们将通过使用透视矩阵设置摄像机模型来修改之前的实现。在接下来的章节中,我们将探讨使用 OpenGL 着色语言(GLSL)来启用更复杂的渲染技术和更高的性能。
如何做到这一点...
让我们开始处理 OpenGL 中透视变换的第一个新要求。由于摄像机参数依赖于窗口大小,我们首先需要实现一个处理窗口大小事件的回调函数,并相应地更新矩阵:
-
定义回调函数的函数原型:
void framebuffer_size_callback(GLFWwindow* window, int width, int height) { -
预设摄像机参数:垂直 视野角度(fovY),到 近裁剪面(前)的距离,到 远裁剪面(后)的距离,以及屏幕纵横比(宽度/高度)
![如何做到这一点...]()
const float fovY = 45.0f; const float front = 0.1f; const float back = 128.0f; float ratio = 1.0f; if (height > 0) ratio = (float) width / (float) height; -
设置虚拟摄像机的视口(使用窗口大小):
glViewport(0, 0, width, height); -
将矩阵模式指定为
GL_PROJECTION并允许后续的矩阵操作应用于投影矩阵栈:glMatrixMode(GL_PROJECTION); -
将单位矩阵加载到当前矩阵中(即重置矩阵到其默认状态):
glLoadIdentity(); -
为虚拟摄像机设置透视投影矩阵:
gluPerspective(fovY, ratio, front, back); }
它是如何工作的...
framebuffer_size_callback 函数的目的是处理来自 GLFW 库的回调事件。当窗口大小改变时,将捕获一个事件,回调函数提供了一个机制来相应地更新虚拟摄像机的参数。一个重要的问题是,如果我们不适当调整虚拟摄像机渲染参数,改变屏幕的纵横比可能会引入扭曲。因此,update 函数也会调用 glViewport 函数,以确保图形被渲染到新的可视区域。
此外,想象一下我们正在用真实世界中的物理摄像头拍摄一个场景。gluPerspective函数基本上控制着摄像头镜头的缩放(即视场角)以及摄像头传感器(即图像平面)的宽高比。虚拟摄像头和真实摄像头之间一个主要的不同之处在于近裁剪面和远裁剪面(前后变量)的概念,它限制了渲染图像的可视区域。这些限制与更高级的主题(深度缓冲区和深度测试)以及图形引擎如何与虚拟 3D 场景协同工作有关。一个经验法则是,我们永远不应该设置一个不必要的过大值,因为它会影响深度测试结果的精度,这可能导致 Z 冲突问题。Z 冲突是一种现象,当物体具有非常相似的深度值且深度值的精度不足以解决这种歧义(由于 3D 渲染过程中的浮点表示精度损失)时发生。设置更高分辨率的深度缓冲区或减少裁剪面之间的距离通常是减轻此类问题的最简单方法。
示例代码提供了场景的透视渲染,类似于人眼观察世界的方式。例如,如果一个物体离摄像头更近,它看起来会更大;如果它离摄像头更远,它看起来会更小。这允许我们更真实地观察场景。另一方面,通过控制视场角,我们可以夸大透视扭曲,类似于使用超广角镜头捕捉场景。
还有更多...
或者,我们可以通过用以下代码替换gluPerspective()函数来使用glFrustum()函数设置摄像头:
const double DEG2RAD = 3.14159265 / 180;
// tangent of half fovY
double tangent = tan(fovY/2 * DEG2RAD);
// half height of near plane
double height_f = front * tangent;
// half width of near plane
double width_f = height_f * ratio;
//Create the projection matrix based on the near clipping
//plane and the location of the corners
glFrustum(-width_f, width_f, -height_f, height_f, front, back);
}
glFrustum 函数接受近裁剪面和远裁剪面的角点来构建投影矩阵。从根本上讲,gluPerspective 和 glFrustum 函数之间没有区别,因此它们可以互换使用。
正如我们所见,OpenGL 中的虚拟摄像头可以在屏幕帧缓冲区(窗口大小)变化时更新,这些事件更新通过 GLFW 库的回调机制捕获。当然,我们也可以处理其他事件,例如键盘和鼠标输入。关于如何处理其他事件的更多细节将在稍后讨论。在下一节中,让我们实现演示的其余部分,以创建我们的第一个具有透视渲染的 3D 图形。
使用透视渲染创建 3D 图形
在上一章中,我们展示了随时间变化的二维高斯分布的标准差热图。现在,我们将继续使用相同的数据集在 3D 中进行更高级的渲染,并展示使用 OpenGL 可视化多维数据的有效性。上一章的代码库将被修改以启用 3D 渲染。
我们不是在平面上渲染二维高斯分布函数,而是将高斯函数
的输出作为 z(高度)值,如下所示:

在这里 A 是以
和
为中心的分布的振幅,而
是分布沿 x 和 y 方向的标准差(分散度)。在我们的例子中,我们将随时间改变分布的分散度以改变其在 3D 中的形状。此外,我们还将根据高度应用热图到每个顶点,以获得更好的可视化效果。
准备工作
使用投影模型设置好相机后,我们可以通过改变一些虚拟相机参数(如视场角以实现透视扭曲以及不同视角的旋转角度)来以期望的效果渲染我们的图。为了减少编码复杂性,我们将重新使用在 第二章,OpenGL 原语和 2D 数据可视化 中实现的 draw2DHeatMap 和 gaussianDemo 函数,并进行一些小的修改。渲染技术将基于前一章中描述的 OpenGL 原语。
如何做到这一点...
让我们修改 第二章,OpenGL 原语和 2D 数据可视化 中的最终演示(代码包中的 main_gaussian_demo.cpp),以启用 3D 中的透视渲染。首先提供整体代码结构以引导读者,主要更改将按顺序在较小的块中讨论:
#include <GLFW/glfw3.h>
...
// Window size
const int WINDOWS_WIDTH = 1280;
const int WINDOWS_HEIGHT = 720;
// NEW: Callback functions and helper functions for 3D plot
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void draw2DHeatMap(const Data *data, int num_points);
void gaussianDemo(float sigma);
...
int main(void)
{
GLFWwindow* window;
int width, height;
if (!glfwInit()){
exit(EXIT_FAILURE);
}
window = glfwCreateWindow(WINDOWS_WIDTH, WINDOWS_HEIGHT, "Chapter 3: 3D Data Plotting", NULL, NULL);
if (!window){
glfwTerminate();
exit(EXIT_FAILURE);
}
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
// NEW: Callback functions
...
//enable anti-aliasing
glEnable(GL_BLEND);
//smooth the points
glEnable(GL_LINE_SMOOTH);
//smooth the lines
glEnable(GL_POINT_SMOOTH);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
//needed for alpha blending
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_ALPHA_TEST) ;
// NEW: Initialize parameters for perspective rendering
...
while (!glfwWindowShouldClose(window))
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
// NEW: Perspective rendering
...
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
在心中牢记前面的框架,在 main 函数中,让我们添加之前章节中实现的窗口调整大小的 callback 函数:
glfwGetFramebufferSize(window, &width, &height);
framebuffer_size_callback(window, width, height);
让我们定义几个全局变量并初始化它们以进行透视渲染,包括缩放级别(zoom)以及围绕 x (beta)和 z (alpha)轴的旋转角度,分别:
GLfloat alpha=210.0f, beta=-70.0f, zoom=2.0f;
此外,在 main 循环之外,让我们初始化一些用于渲染高斯分布的参数,包括标准差(sigma)、符号和随时间动态改变函数的步长:
float sigma = 0.1f;
float sign = 1.0f;
float step_size = 0.01f;
在 while 循环中,我们执行以下变换以在 3D 中渲染高斯函数:
-
将矩阵模式指定为
GL_MODELVIEW以允许后续的矩阵操作应用于MODELVIEW矩阵栈:glMatrixMode(GL_MODELVIEW); -
执行对象的平移和旋转:
glLoadIdentity(); glTranslatef(0.0, 0.0, -2.0); // rotate by beta degrees around the x-axis glRotatef(beta, 1.0, 0.0, 0.0); // rotate by alpha degrees around the z-axis glRotatef(alpha, 0.0, 0.0, 1.0); -
在 3D 中绘制原点(带有 x、y 和 z 轴)和高斯函数。动态地绘制一系列具有不同 sigma 值的高斯函数,并在达到某个阈值时反转符号:
drawOrigin(); sigma=sigma+sign*step_size; if(sigma>1.0f){ sign = -1.0f; } if(sigma<0.1){ sign = 1.0f; } gaussianDemo(sigma);为了处理上述每个绘图任务,我们在单独的函数中实现了原点可视化器、高斯函数生成器和 3D 点可视化器。
为了可视化原点,实现以下绘图函数:
-
定义函数原型:
void drawOrigin(){ -
分别用红色、绿色和蓝色绘制x、y和z轴:
glLineWidth(4.0f); glBegin(GL_LINES); float transparency = 0.5f; //draw a red line for the x-axis glColor4f(1.0f, 0.0f, 0.0f, transparency); glVertex3f(0.0f, 0.0f, 0.0f); glColor4f(1.0f, 0.0f, 0.0f, transparency); glVertex3f(0.3f, 0.0f, 0.0f); //draw a green line for the y-axis glColor4f(0.0f, 1.0f, 0.0f, transparency); glVertex3f(0.0f, 0.0f, 0.0f); glColor4f(0.0f, 1.0f, 0.0f, transparency); glVertex3f(0.0f, 0.0f, 0.3f); //draw a blue line for the z-axis glColor4f(0.0f, 0.0f, 1.0f, transparency); glVertex3f(0.0f, 0.0f, 0.0f); glColor4f(0.0f, 0.0f, 1.0f, transparency); glVertex3f(0.0f, 0.3f, 0.0f); glEnd(); }
对于高斯函数演示的实现,我们将问题分解为两部分:一个高斯数据生成器和带有点绘制的热图可视化函数。结合 3D 渲染和热图,我们现在可以清楚地看到高斯分布的形状以及样本如何在空间中随时间动画和移动:
-
生成高斯分布:
void gaussianDemo(float sigma){ const int grid_x = 400; const int grid_y = 400; const int num_points = grid_x*grid_y; Data *data=(Data*)malloc(sizeof(Data)*num_points); int data_counter=0; //standard deviation const float sigma2=sigma*sigma; //amplitude const float sigma_const = 10.0f*(sigma2*2.0f*(float)M_PI); for(float x = -grid_x/2.0f; x<grid_x/2.0f; x+=1.0f){ for(float y = -grid_y/2.0f; y<grid_y/2.0f; y+=1.0f){ float x_data = 2.0f*x/grid_x; float y_data = 2.0f*y/grid_y; //Set the mean to 0 float z_data = exp(-0.5f*(x_data*x_data)/(sigma2) -0.5f*(y_data*y_data)/(sigma2)) /sigma_const; data[data_counter].x = x_data; data[data_counter].y = y_data; data[data_counter].z = z_data; data_counter++; } } draw2DHeatMap(data, num_points); free(data); } -
接下来,实现
draw2DHeatMap函数以可视化结果。注意,与第二章不同,OpenGL 原语和 2D 数据可视化,我们在glVertex3f函数内部使用 z 值:void draw2DHeatMap(const Data *data, int num_points){ glPointSize(3.0f); glBegin(GL_POINTS); float transparency = 0.25f; //locate the maximum and minimum values in the dataset float max_value=-999.9f; float min_value=999.9f; for(int i=0; i<num_points; i++){ Data d = data[i]; if(d.z > max_value) max_value = d.z; if(d.z < min_value) min_value = d.z; } float halfmax = (max_value + min_value) / 2; //display the result for(int i = 0; i<num_points; i++){ Data d = data[i]; float value = d.z; float b = 1.0f - value/halfmax; float r = value/halfmax - 1.0f; if(b < 0) b=0; if(r < 0) r=0; float g = 1.0f - b - r; glColor4f(r, g, b, transparency); glVertex3f(d.x, d.y, d.z); } glEnd(); }
渲染结果如下所示。我们可以看到透明度(alpha 混合)使我们能够看到数据点,并提供了视觉上吸引人的结果:

它是如何工作的...
这个简单的示例演示了透视渲染的使用以及 OpenGL 变换函数来在虚拟空间中旋转和移动渲染对象。正如你所见,整体代码结构与第二章相同,OpenGL 原语和 2D 数据可视化,主要变化包括设置透视渲染的相机参数(在framebuffer_size_callback函数内部)以及执行所需的变换以在 3D 中渲染高斯函数(在将矩阵模式设置为GL_MODELVIEW之后)。两个非常常用的变换函数来操纵虚拟对象包括glRotatef和glTranslatef,这些函数允许我们将对象定位在任何方向和位置。这些函数可以显著提高你自己的应用程序的动态性,因为它们在开发和计算时间上的成本非常低,因为它们经过了高度优化。
glRotatef函数接受四个参数:旋转角度和方向向量的三个分量 (x, y, z),这些分量定义了旋转轴。该函数还将当前矩阵替换为旋转矩阵和当前矩阵的乘积:

这里
和
。
更多内容...
可能有人会问,如果我们想将两个对象放置在不同的方向和位置怎么办?如果我们想将许多部分在空间中相对于彼此定位怎么办?对这些问题的答案是使用 glPushMatrix 和 glPopMatrix 函数来控制变换矩阵的堆栈。对于具有大量部件的模型,这个概念可能会相对复杂,并且保持具有许多组件的状态机的历史可能会很繁琐。为了解决这个问题,我们将探讨 GLSL 支持的新版本(OpenGL 3.x 及更高版本)。
使用 GLFW 创建交互式环境
在前两个部分中,我们专注于创建 3D 对象以及利用虚拟相机的基本 OpenGL 渲染技术。现在,我们准备将用户输入,如鼠标和键盘输入,结合到使用相机控制功能(如缩放和旋转)的更动态的交互中。这些功能将是即将到来的应用程序的基本构建块,代码将在后面的章节中重用。
准备中
GLFW 库提供了一个机制来处理来自不同环境的用户输入。事件处理程序以 C/C++ 中的回调函数的形式实现,在前面的教程中,我们为了简化而跳过了这些选项。要开始,我们首先需要启用这些回调函数并实现基本功能来控制渲染参数。
如何做到这一点...
要处理键盘输入,我们将自己的 callback 函数实现附加到 GLFW 的事件处理程序上。在 callback 函数中,我们将执行以下操作:
-
定义以下全局变量(包括一个名为
locked的新变量,用于跟踪鼠标按钮是否按下,以及旋转角度和缩放级别),这些变量将由callback函数更新:GLboolean locked = GL_FALSE; GLfloat alpha=210.0f, beta=-70.0f, zoom=2.0f; -
定义键盘
callback函数原型:void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { -
如果收到除按键事件之外的任何事件,则忽略它:
if (action != GLFW_PRESS) return; -
创建一个
switch语句来处理每个按键事件的案例:switch (key) { -
如果按下 Esc 键,则退出程序:
case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(window, GL_TRUE); break; -
如果按下空格键,通过切换变量来开始或停止动画:
case GLFW_KEY_SPACE: freeze=!freeze; break; -
如果按下方向键(上、下、左和右),则更新控制渲染对象旋转角度的变量:
case GLFW_KEY_LEFT: alpha += 5.0f; break; case GLFW_KEY_RIGHT: alpha -= 5.0f; break; case GLFW_KEY_UP: beta -= 5.0f; break; case GLFW_KEY_DOWN: beta += 5.0f; break; -
最后,如果按下 Page Up 或 Page Down 键,则通过更新
zoom变量来从对象中缩放或缩小:case GLFW_KEY_PAGE_UP: zoom -= 0.25f; if (zoom < 0.0f) zoom = 0.0f; break; case GLFW_KEY_PAGE_DOWN: zoom += 0.25f; break; default: break; } }
要处理鼠标点击事件,我们实现另一个类似于键盘的 callback 函数。鼠标点击事件相当简单,因为可用的按钮有限:
-
定义鼠标按键的
callback函数原型:void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) { -
为了简单起见,忽略除左键点击事件之外的所有输入:
if (button != GLFW_MOUSE_BUTTON_LEFT) return; -
切换
lock变量以存储鼠标按下事件。lock变量将用于确定鼠标移动是否用于旋转对象:if (action == GLFW_PRESS) { glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); locked = GL_TRUE; } else { locked = GL_FALSE; glfwSetInputMode(window, GLFW_CURSOR,GLFW_CURSOR_NORMAL); } }
对于处理鼠标移动事件,我们需要创建另一个callback函数。鼠标移动的callback函数从窗口中获取x和y坐标,而不是唯一的键输入:
-
定义一个接受鼠标坐标的
callback函数原型:void cursor_position_callback(GLFWwindow* window, double x, double y) { -
在鼠标按下和移动时,我们使用鼠标的x和y坐标更新对象的旋转角度:
//if the mouse button is pressed if (locked) { alpha += (GLfloat) (x - cursorX) / 10.0f; beta += (GLfloat) (y - cursorY) / 10.0f; } //update the cursor position cursorX = (int) x; cursorY = (int) y; }
最后,我们将实现鼠标滚动回调函数,允许用户通过滚动来放大和缩小对象。
-
定义一个捕获
x和y滚动变量的callback函数原型:void scroll_callback(GLFWwindow* window, double x, double y) { -
取 y 参数(上下滚动)并更新缩放变量:
zoom += (float) y / 4.0f; if (zoom < 0.0f) zoom = 0.0f; }
在所有callback函数实现之后,我们现在可以准备将这些函数链接到 GLFW 库的事件处理器。GLFW 库提供了一个平台无关的 API 来处理这些事件,因此相同的代码可以在 Windows、Linux 和 Mac OS X 上无缝运行。
要将回调函数与 GLFW 库集成,请在main函数中调用以下函数:
//keyboard input callback
glfwSetKeyCallback(window, key_callback);
//framebuffer size callback
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//mouse button callback
glfwSetMouseButtonCallback(window, mouse_button_callback);
//mouse movement callback
glfwSetCursorPosCallback(window, cursor_position_callback);
//mouse scroll callback
glfwSetScrollCallback(window, scroll_callback);
最终结果是用户可以自由控制空间中渲染对象的交互式界面。首先,当用户滚动鼠标(见以下截图)时,我们向前或向后移动对象。这产生了视觉上的感知,即对象被相机放大或缩小:

这里是另一个不同缩放级别的截图:

这些简单而强大的技术允许用户实时操纵虚拟对象,并且在可视化复杂数据集时可能非常有用。此外,我们可以通过按住鼠标按钮并在不同方向上拖动对象来以不同的角度旋转对象。下面的截图显示了我们可以如何以任意角度渲染图表以更好地理解数据分布。
这里是一张显示高斯函数侧视图的截图:

这里是一张从顶部显示高斯函数的截图:

最后,这是一张显示高斯函数底部的截图:

它是如何工作的...
这个示例代码展示了构建跨多个平台高度可移植的交互式应用程序所需的基本界面,这些应用程序使用 OpenGL 和 GLFW 库。在 GLFW 库中使用callback函数允许非阻塞调用,这些调用与渲染引擎并行运行。这个概念特别有用,因为鼠标、键盘和游戏手柄等输入设备都有不同的输入速率和延迟。这些callback函数允许异步执行而不阻塞主渲染循环。
glfwSetKeyCallback、glfwSetFramebufferSizeCallback、glfwSetScrollCallback、glfwSetMouseBcuttonCallback和glfwSetCursorPosCallback函数提供了对鼠标按钮和滚动轮、键盘输入以及窗口调整大小事件的控制。这些只是我们可以使用 GLFW 库支持实现的许多处理程序中的一部分。例如,我们可以通过添加额外的callback函数来进一步扩展错误处理能力。此外,我们可以处理窗口关闭和打开事件,从而实现与多个窗口的更复杂接口。到目前为止提供的示例中,我们介绍了如何通过相对简单的 API 调用创建交互式界面的基础知识。
参见
为了全面覆盖 GLFW 库函数调用,本网站提供了一套全面的示例和文档,包括所有回调函数以及输入和其他事件的处理:www.glfw.org/docs/latest/。
渲染体积数据集 – MCML 模拟
在本节中,我们将演示从生物组织中光传输的蒙特卡洛模拟生成的 3D 体积数据集的渲染,称为多层介质蒙特卡洛(MCML)。为了简化,本章代码包中包含了模拟输出文件,以便读者可以直接运行演示,而无需设置模拟代码。蒙特卡洛模拟的源代码在“参见”部分列出的系列出版物中有详细描述,并且对于感兴趣的读者,GPU 实现可在网上获得(code.google.com/p/gpumcml/))。
生物组织中的光传输可以用辐射传输方程(RTE)来模拟,该方程对于复杂几何形状来说,解析求解已被证明是困难的。时间相关的 RTE 可以表示为:

这里
是辐射亮度 [W m(−2)sr(−1)],定义为穿过位置 r 处垂直于方向 Ω 的单位立体角的无穷小面积上的辐射功率 [W],μ[s] 是散射系数,μ[a] 是吸收系数,ν 是光速,而
是源项。为了数值求解 RTE,威尔逊和亚当引入了蒙特卡洛(MC)方法,由于其准确性和多功能性(特别是对于复杂组织几何形状),该方法被广泛接受为光子迁移建模的金标准方法。
MC 方法是一种统计抽样技术,已被应用于许多不同领域的许多重要问题,从医学中的放射治疗计划到金融中的期权定价。蒙特卡洛这个名字来源于摩纳哥的度假胜地,该地以其赌场等景点而闻名。正如其名所示,MC 方法的关键特征涉及利用随机机会(通过生成具有特定概率分布的随机数)来模拟所讨论的物理过程。
在我们的案例中,我们感兴趣的是模拟生物组织中的光子传播。MCML 算法提供了多层介质中稳态光传输的蒙特卡洛模型。特别是,我们将模拟在组织表面具有圆形光源的均匀介质中的光子传播,以计算光剂量(吸收能量)分布。这类计算有广泛的应用范围,包括光疗(如光动力疗法)的治疗计划(这可以被视为一种针对癌症的光激活化疗)。
在这里,我们演示如何将我们的代码库与 OpenGL 渲染函数集成以显示体量数据。我们将利用诸如 alpha 混合、透视渲染和热图渲染等技术。结合 GLFW 接口捕获用户输入,我们可以创建一个交互式可视化器,可以实时显示大量体量数据,并使用几个简单的键输入控制放大体量数据集中数据点平面的切片器。
准备工作
模拟结果存储在一个包含 3D 矩阵的 ASCII 文本文件中。矩阵中的每个值代表在体素化几何结构中某个固定位置的吸收光子能量密度。在这里,我们将提供一个简单的解析器,从文件中提取模拟输出矩阵并将其存储在本地内存中。
如何实现...
让我们从实现 MCML 数据解析器、喷射颜色方案热图生成器以及 OpenGL 中的切片器开始:
-
从模拟输出的文本文件中获取数据并将其存储在浮点数组中:
#define MCML_SIZE_X 50 #define MCML_SIZE_Y 50 #define MCML_SIZE_Z 200 float mcml_data[MCML_SIZE_X][MCML_SIZE_Y][MCML_SIZE_Z]; Vertex mcml_vertices[MCML_SIZE_X][MCML_SIZE_Y][MCML_SIZE_Z]; float max_data, min_data; int slice_x = 0, slice_z = 0, slice_y = 0; float point_size=5.0f; //load the data from a text file void loadMCML(){ FILE *ifp; //open the file for reading ifp = fopen("MCML_output.txt", "r"); if (ifp == NULL) { fprintf(stderr, "ERROR: Can't open MCML Data file!\n"); exit(1); } float data; float max=0, min=9999999; for(int x=0; x<MCML_SIZE_X; x++){ for(int z=0; z<MCML_SIZE_Z; z++){ for(int y=0; y<MCML_SIZE_Y; y++){ if (fscanf(ifp, "%f\n", &data) == EOF){ fprintf(stderr, "ERROR: Missing MCML Data file!\n"); exit(1); } //store the log compressed data point data = log(data+1); mcml_data[x][y][z]=data; //find the max and min from the data set for heatmap if(data>max){ max=data; } if(data<min){ min=data; } //normalize the coordinates mcml_vertices[x][y][z].x=(float)(x-MCML_SIZE_X/2.0f)/MCML_SIZE_X; mcml_vertices[x][y][z].y=(float)(y-MCML_SIZE_Y/2.0f)/MCML_SIZE_Y; mcml_vertices[x][y][z].z=(float)(z-MCML_SIZE_Z/2.0f)/MCML_SIZE_Z*2.0f; } } } fclose(ifp); max_data = max; min_data = min; halfmax= (max+min)/2.0f; -
使用自定义颜色图对模拟输出值进行编码以进行显示:
//store the heat map representation of the data for(int z=0; z<MCML_SIZE_Z; z++){ for(int x=0; x<MCML_SIZE_X; x++){ for(int y=0; y<MCML_SIZE_Y; y++){ float value = mcml_data[x][y][z]; COLOUR c = GetColour(value, min_data,max_data); mcml_vertices[x][y][z].r=c.r; mcml_vertices[x][y][z].g=c.g; mcml_vertices[x][y][z].b=c.b; } } } } -
使用喷射颜色方案实现热图生成器:
Color getHeatMapColor(float value, float min, float max) { //remapping the value to the JET color scheme Color c = {1.0f, 1.0f, 1.0f}; // default value float dv; //clamp the data if (value < min) value = min; if (value > max) value = max; range = max - min; //the first region (0%-25%) if (value < (min + 0.25f * range)) { c.r = 0.0f; c.g = 4.0f * (value - min) / range; } //the second region of value (25%-50%) else if (value < (min + 0.5f * range)) { c.r = 0.0f; c.b = 1.0f + 4.0f * (min + 0.25f * range - value) / range; } //the third region of value (50%-75%) else if (value < (min + 0.75f * range)) { c.r = 4.0f * (value - min - 0.5f * range) / range; c.b = 0.0f; } //the fourth region (75%-100%) else { c.g = 1.0f + 4.0f * (min + 0.75f * range - value) / range; c.b = 0.0f; } return(c); } -
在屏幕上绘制所有数据点,并启用透明度:
void drawMCMLPoints(){ glPointSize(point_size); glBegin(GL_POINTS); for(int z=0; z<MCML_SIZE_Z; z++){ for(int x=0; x<MCML_SIZE_X; x++){ for(int y=0; y<MCML_SIZE_Y; y++){ glColor4f(mcml_vertices[x][y][z].r,mcml_vertices[x][y][z].g,mcml_vertices[x][y][z].b, 0.15f); glVertex3f(mcml_vertices[x][y][z].x,mcml_vertices[x][y][z].y,mcml_vertices[x][y][z].z); } } } glEnd(); } -
绘制三个数据点的切片以进行横截面可视化:
void drawMCMLSlices(){ glPointSize(10.0f); glBegin(GL_POINTS); //display data on xy plane for(int x=0; x<MCML_SIZE_X; x++){ for(int y=0; y<MCML_SIZE_Y; y++){ int z = slice_z; glColor4f(mcml_vertices[x][y][z].r,mcml_vertices[x][y][z].g,mcml_vertices[x][y][z].b, 0.9f); glVertex3f(mcml_vertices[x][y][z].x,mcml_vertices[x][y][z].y,mcml_vertices[x][y][z].z); } } //display data on yz plane for(int z=0; z<MCML_SIZE_Z; z++){ for(int y=0; y<MCML_SIZE_Y; y++){ int x = slice_x; glColor4f(mcml_vertices[x][y][z].r,mcml_vertices[x][y][z].g,mcml_vertices[x][y][z].b, 0.9f); glVertex3f(mcml_vertices[x][y][z].x,mcml_vertices[x][y][z].y,mcml_vertices[x][y][z].z); } } //display data on xz plane for(int z=0; z<MCML_SIZE_Z; z++){ for(int x=0; x<MCML_SIZE_X; x++){ int y = slice_y; glColor4f(mcml_vertices[x][y][z].r,mcml_vertices[x][y][z].g,mcml_vertices[x][y][z].b, 0.9f); glVertex3f(mcml_vertices[x][y][z].x,mcml_vertices[x][y][z].y,mcml_vertices[x][y][z].z); } } glEnd(); } -
此外,我们还需要更新
key_callback函数以移动切片:void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { if (action != GLFW_PRESS) return; switch (key) { case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(window, GL_TRUE); break; case GLFW_KEY_P: point_size+=0.5; break; case GLFW_KEY_O: point_size-=0.5; break; case GLFW_KEY_A: slice_y -=1; if(slice_y < 0) slice_y = 0; break; case GLFW_KEY_D: slice_y +=1; if(slice_y >= MCML_SIZE_Y-1) slice_y = MCML_SIZE_Y-1; break; case GLFW_KEY_W: slice_z +=1; if(slice_z >= MCML_SIZE_Z-1) slice_z = MCML_SIZE_Z-1; break; case GLFW_KEY_S: slice_z -= 1; if (slice_z < 0) slice_z = 0; break; case GLFW_KEY_E: slice_x -=1; if(slice_x < 0) slice_x = 0; break; case GLFW_KEY_Q: slice_x +=1; if(slice_x >= MCML_SIZE_X-1) slice_x = MCML_SIZE_X-1; break; case GLFW_KEY_PAGE_UP: zoom -= 0.25f; if (zoom < 0.f) zoom = 0.f; break; case GLFW_KEY_PAGE_DOWN: zoom += 0.25f; break; default: break; } } -
最后,为了完成演示,只需在
main循环中调用drawMCMLPoints和drawMCMLSlices函数,并使用之前演示中用于绘制高斯函数的透视渲染相同的代码结构即可:while (!glfwWindowShouldClose(window)) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0, 0.0, -zoom); glRotatef(beta, 1.0, 0.0, 0.0); glRotatef(alpha, 0.0, 0.0, 1.0); //disable depth test so we can render the points with blending glDisable(GL_DEPTH_TEST); drawMCMLPoints(); //must enable this to ensure the slides are rendered in the right order glEnable(GL_DEPTH_TEST); drawMCMLSlices(); //draw the origin with the x,y,z axes for visualization drawOrigin(); glfwSwapBuffers(window); glfwPollEvents(); }
在下面的屏幕截图中,以 3D 形式显示了模拟结果,表示在体素化几何形状中的光子吸收分布。光源照亮组织表面(底部z=0),并通过模拟为无限宽的均匀介质组织的组织传播(正z方向)。光子吸收分布遵循有限尺寸、平坦和圆形光束的预期形状:

如何工作...
这个演示说明了我们如何将蒙特卡洛模拟(以及更一般地,任何应用程序生成的体数据集)生成的体数据集通过一个高度交互式的界面使用 OpenGL 进行渲染。数据解析器以 ASCII 文本文件作为输入。然后,我们将浮点数据转换为可以适应我们的渲染管道的单独顶点。初始化时,变量mcml_vertices和mcml_data存储预先计算的热图数据以及每个数据点的位置。parser函数还计算数据集中的最大值和最小值,以便进行热图可视化。getHeatMapColor函数将模拟输出值映射到喷气色系中的颜色。该算法基本上定义了一个颜色光谱,并根据其范围重新映射值。
在下面的屏幕截图中,我们展示了模拟结果的一个俯视图,这使我们能够可视化光分布的对称性:

drawMCMLSlices函数接受一个数据切片(即一个平面),并以全不透明度和更大的点大小渲染数据点。这提供了一种有用且非常常见的可视化方法(特别是在医学成像中),允许用户通过移动横截面切片来详细检查体数据。正如以下屏幕截图所示,我们可以沿x、y和z方向移动切片器来可视化感兴趣的区域:

还有更多...
这个演示提供了一个在交互式 3D 环境中渲染模拟数据的实时体数据可视化的示例。当前的实现可以很容易地修改,以适应需要体数据集可视化的广泛应用程序。我们的方法提供了一个直观的方式来渲染复杂的 3D 数据集,包括热图生成器、切片器和使用 OpenGL 的 3D 透视渲染技术。
一个重要的观察是,这个演示需要大量的 glVertex3f 调用,这可能会成为性能瓶颈的主要因素。为了解决这个问题,在接下来的章节中,我们将探讨更复杂的方法来处理内存传输,并使用顶点缓冲对象(VBOs)绘制更复杂的模型,VBOs是显卡中的内存缓冲区,用于存储顶点信息。这将引导我们走向片段程序和自定义顶点着色器程序(即从 OpenGL 2.0 过渡到 OpenGL 3.2 或更高版本)。然而,如果我们目标是缩短开发周期、最小化开销以及与旧硬件的向后兼容性,那么使用经典 OpenGL 2.0 调用的简单性是一个重要的考虑因素。
参见
如需更多信息,请参考以下参考文献:
-
E. Alerstam & W. C. Y. Lo, T. Han, J. Rose, S. Andersson-Engels, 和 L. Lilge, "使用 GPU 对浑浊介质中光传输的下一代加速和代码优化," Biomed. Opt. Express 1, 658-675 (2010).
-
W. C. Y. Lo, K. Redmond, J. Luu, P. Chow, J. Rose, 和 L. Lilge, "用于光动力治疗计划的光动力治疗蒙特卡洛模拟的硬件加速," J. Biomed. Opt. 14, 014019 (2009).
-
W. Wang, S. Jacques, 和 L. Zheng, "MCML - 多层组织中光传输的蒙特卡洛建模," Comput. Meth. Prog. Biol. 47, 131–146 (1995).
-
B. Wilson 和 G. Adam, "组织中光吸收和通量分布的蒙特卡洛模型," Med. Phys. 10, 824 (1983).
第四章:使用纹理映射渲染 2D 图像和视频
在本章中,我们将涵盖以下主题:
-
开始使用现代 OpenGL(3.2 或更高版本)
-
在 Windows 中设置 GLEW、GLM、SOIL 和 OpenCV 库
-
在 Mac OS X/Linux 中设置 GLEW、GLM、SOIL 和 OpenCV 库
-
使用 GLSL 创建你的第一个顶点和片段着色器
-
使用纹理映射渲染 2D 图像
-
使用过滤器进行实时视频渲染
简介
在本章中,我们将介绍 OpenGL 技术来可视化另一类重要的数据集:涉及图像或视频的数据集。这类数据集在许多领域都很常见,包括医学成像应用。为了启用图像的渲染,我们将讨论纹理映射的基本 OpenGL 概念,并过渡到需要更新版本的 OpenGL(OpenGL 3.2 或更高版本)的更高级技术。为了简化我们的任务,我们还将使用几个额外的库,包括OpenGL 扩展包装库(GLEW)用于运行时 OpenGL 扩展支持,简单 OpenGL 图像加载器(SOIL)用于加载不同的图像格式,OpenGL 数学(GLM)用于向量和矩阵操作,以及OpenCV用于图像/视频处理。为了开始,我们将首先介绍现代 OpenGL 3.2 和更高版本的特性。
开始使用现代 OpenGL(3.2 或更高版本)
OpenGL API 的持续进化导致了现代标准的出现。其中最大的变化发生在 2008 年,OpenGL 3.0 版本中引入了新的上下文创建机制,并将大多数较老的功能,如 Begin/End 原语规范,标记为已弃用。移除这些较老的标准特性也意味着以更灵活且更强大的方式处理图形管道。在 OpenGL 3.2 或更高版本中,定义了核心和兼容配置文件来区分已弃用的 API 和当前功能。这些配置文件为各种功能提供了清晰的定义(核心配置文件),同时支持向后兼容(兼容配置文件)。在 OpenGL 4.x 版本中,提供了对运行 Direct3D 11 的最新图形硬件的支持,OpenGL 3.x 和 OpenGL 4.x 之间的详细比较可以在www.g-truc.net/post-0269.html找到。
准备工作
从本章开始,我们需要具有 OpenGL 3.2(或更高版本)支持的兼容图形卡。2008 年之前发布的绝大多数图形卡可能不会得到支持。例如,NVIDIA GeForce 100、200、300 系列及更高版本支持 OpenGL 3 标准。我们鼓励您查阅您图形卡的技术规格以确认兼容性(参考developer.nvidia.com/opengl-driver)。
如何做到这一点...
要启用 OpenGL 3.2 支持,我们需要在每个程序的初始化部分包含以下代码行:
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
它是如何工作的...
glfwWindowHint 函数定义了创建 GLFW 窗口上下文的约束集(参考 第一章,使用 OpenGL 入门)。这里的前两行代码定义了将要使用的 OpenGL 当前版本(在本例中为 3.2)。第三行启用了向前兼容性,而最后一行指定将使用核心配置文件。
参见
可以在 www.opengl.org/wiki/History_of_OpenGL 找到关于各种 OpenGL 版本之间差异的详细说明。
在 Windows 中设置 GLEW、GLM、SOIL 和 OpenCV 库
在本节中,我们将提供逐步说明来设置本章(以及随后的章节)中将广泛使用的几个流行库,包括 GLEW、GLM、SOIL 和 OpenCV 库:
-
GLEW 库是一个开源的 OpenGL 扩展库。
-
GLM 库是一个仅包含头文件的 C++ 库,它提供了一套易于使用的常用数学运算。它基于 GLSL 规范构建,并且作为一个仅包含头文件的库,它不需要繁琐的编译步骤。
-
SOIL 库是一个简单的 C 库,用于在 OpenGL 纹理中加载各种常见格式的图像(如 BMP、PNG、JPG、TGA、TIFF 和 HDR)。
-
OpenCV 库是一个非常强大的开源计算机视觉库,我们将使用它来简化本章中的图像和视频处理。
准备工作
我们首先需要从以下网站下载必需的库:
-
GLEW (glew-1.10.0):
sourceforge.net/projects/glew/files/glew/1.10.0/glew-1.10.0-win32.zip -
GLM (glm-0.9.5.4):
sourceforge.net/projects/ogl-math/files/glm-0.9.5.4/glm-0.9.5.4.zip -
OpenCV (opencv-2.4.9):
sourceforge.net/projects/opencvlibrary/files/opencv-win/2.4.9/opencv-2.4.9.exe
如何操作...
要使用 GLEW 的预编译包,请按照以下步骤操作:
-
解压该包。
-
将目录复制到
C:/Program Files (x86)。 -
确保在运行时可以找到
glew32.dll文件(C:\Program Files (x86)\glew-1.10.0\bin\Release\Win32),可以通过将其放置在可执行文件相同的文件夹中或在 Windows 系统的PATH环境变量中包含该目录来实现(导航到 控制面板 | 系统和安全 | 系统 | 高级系统设置 | 环境变量)。
要使用仅包含头文件的 GLM 库,请按照以下步骤操作:
-
解压该包。
-
将目录复制到
C:/Program Files (x86)。 -
在您的源代码中包含所需的头文件。以下是一个示例:
#include <glm/glm.hpp>
要使用 SOIL 库,请按照以下步骤操作:
-
解压包。
-
将目录复制到
C:/Program Files (x86)。 -
通过打开 Visual Studio 解决方案文件(
C:\Program Files (x86)\Simple OpenGL Image Library\projects\VC9\SOIL.sln)并编译项目文件来生成SOIL.lib文件。将此文件从C:\Program Files (x86)\Simple OpenGL Image Library\projects\VC9\Debug复制到C:\Program Files (x86)\Simple OpenGL Image Library\lib。 -
在您的源代码中包含头文件:
#include <SOIL.h>
最后,为了安装 OpenCV,我们建议您使用预构建的二进制文件以简化过程:
-
从
sourceforge.net/projects/opencvlibrary/files/opencv-win/2.4.9/opencv-2.4.9.exe下载预构建的二进制文件并提取包。 -
将目录(
opencv文件夹)复制到C:\Program Files (x86)。 -
将以下内容添加到系统
PATH环境变量中(导航到控制面板 | 系统和安全 | 系统 | 高级系统设置 | 环境变量) –C:\Program Files (x86)\opencv\build\x86\vc12\bin。 -
在您的源代码中包含所需的头文件:
#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp>
现在,我们使用CMake生成我们的 Microsoft Visual Studio 解决方案文件(构建环境)。在每个项目目录中创建CMakeList.txt文件,其中列出项目所需的所有库和依赖项。以下是我们第一个演示应用的示例CMakeList.txt文件:
cmake_minimum_required (VERSION 2.8)
set(CMAKE_CONFIGURATION_TYPES Debug Release)
set(PROGRAM_PATH "C:/Program Files \(x86\)")
set(OpenCV_DIR ${PROGRAM_PATH}/opencv/build)
project (code_simple)
#modify these path based on your configuration
#OpenCV
find_package(OpenCV REQUIRED )
INCLUDE_DIRECTORIES(${OpenCV_INCLUDE_DIRS})
INCLUDE_DIRECTORIES(${PROGRAM_PATH}/glm)
INCLUDE_DIRECTORIES(${PROGRAM_PATH}/glew-1.10.0/include)
LINK_DIRECTORIES(${PROGRAM_PATH}/glew-1.10.0/lib/Release)
INCLUDE_DIRECTORIES(${PROGRAM_PATH}/glfw-3.0.4/include)
LINK_DIRECTORIES(${PROGRAM_PATH}/glfw-3.0.4/lib)
INCLUDE_DIRECTORIES(${PROGRAM_PATH}/Simple\ OpenGL\ Image\ Library/src)
LINK_DIRECTORIES(${PROGRAM_PATH}/Simple\ OpenGL\ Image\ Library/lib)
add_subdirectory (../common common)
add_executable (main main.cpp)
target_link_libraries (main LINK_PUBLIC shader controls texture glew32s glfw3 opengl32 ${OpenCV_LIBS} SOIL)
如您在CMakeList.txt文件中所见,包括 OpenCV、SOIL、GLFW 和 GLEW 库在内的各种依赖项都被包含在内。
最后,我们运行CMake程序来生成项目的 Microsoft Visual Studio 解决方案(有关详细信息,请参阅第一章,使用 OpenGL 入门)。请注意,由于着色程序依赖项,二进制的输出路径必须与项目文件夹匹配。以下是生成名为code_simple的第一个示例项目后的CMake窗口截图:

我们将为每个创建的项目重复此步骤,并相应地生成相应的 Microsoft Visual Studio 解决方案文件(例如,本例中的code_simple.sln)。要编译代码,使用 Microsoft Visual Studio 2013 打开code_simple.sln,并使用常规的构建(按F7)功能构建项目。确保在运行程序之前将main设置为启动项目(通过在解决方案资源管理器中右键单击main项目并左键单击设置为启动项目选项),如下所示:

参见
关于安装的每个库的进一步文档可以在此处找到:
-
GLEW:
glew.sourceforge.net/ -
OpenCV:
opencv.org/
在 Mac OS X/Linux 中设置 GLEW、GLM、SOIL 和 OpenCV 库
在本节中,我们将概述在 Mac OS X 和 Linux 中设置相同库所需的步骤。
准备工作
我们首先需要从以下网站下载必备库:
-
GLEW (glew-1.10.0):
sourceforge.net/projects/glew/files/glew/1.10.0/glew-1.10.0.tgz -
GLM (glm-0.9.5.4):
sourceforge.net/projects/ogl-math/files/glm-0.9.5.4/glm-0.9.5.4.zip -
OpenCV (opencv-2.4.9):
sourceforge.net/projects/opencvlibrary/files/opencv-unix/2.4.9/opencv-2.4.9.zip
为了简化 Mac OS X 或 Ubuntu 用户的安装过程,强烈建议在 Mac OS X 中使用 MacPorts 或在 Linux 中使用apt-get命令(如第一章中所述,开始使用 OpenGL)。
以下部分假设下载目录为~/opengl_dev(参考第一章,开始使用 OpenGL)。
如何操作...
安装必备库有两种方法。第一种方法使用预编译的二进制文件。这些二进制文件是从远程仓库服务器获取的,库的版本更新由外部控制。这种方法的一个重要优点是简化了安装过程,特别是在解决依赖关系方面。然而,在发布环境中,建议您禁用自动更新,从而保护二进制文件不受版本偏差的影响。第二种方法要求用户直接下载并编译源代码,并进行各种自定义。这种方法适用于希望控制安装过程(如路径)的用户,同时也提供了更多跟踪和修复错误方面的灵活性。
对于初学者或寻找快速原型开发的开发者,我们建议使用第一种方法,因为它将简化工作流程并具有短期维护性。在 Ubuntu 或 Debian 系统上,我们可以使用apt-get命令安装各种库。要在 Ubuntu 上安装所有必备库及其依赖项,只需在终端中运行以下命令:
sudo apt-get install libglm-dev libglew1.6-dev libsoil-dev libopencv
类似地,在 Mac OS X 上,我们可以通过终端中的命令行使用 MacPorts 安装 GLEW、OpenCV 和 GLM。
sudo port install opencv glm glew
然而,SOIL 库目前不支持 MacPorts,因此,安装必须按照以下章节手动完成。
对于高级用户,我们可以通过直接从源代码编译来安装最新包,并且这些步骤在 Mac OS 以及其他 Linux OS 中都是通用的。
要编译 GLEW 包,请按照以下步骤操作:
-
解压
glew-1.10.0.tgz包:tar xzvf glew-1.10.0.tgz -
在
/usr/include/GL和/usr/lib中安装 GLEW:cd glew-1.10.0 make && sudo make install
要设置仅包含头文件的 GLM 库,请按照以下步骤操作:
-
解压
glm-0.9.5.4.zip包:unzip glm-0.9.5.4.zip -
将仅包含头文件的 GLM 库目录(
~/opengl_dev/glm/glm)复制到/usr/include/glm:sudo cp -r glm/glm/ /usr/include/glm
要设置 SOIL 库,请按照以下步骤操作:
-
解压
soil.zip包:unzip soil.zip -
编辑
makefile(位于projects/makefile目录中)并将-arch x86_64和-arch i386添加到CXXFLAGS以确保适当的支持:CXXFLAGS =-arch x86_64 –arch i386 -O2 -s -Wall -
编译源代码库:
cd Simple\ OpenGL\ Image\ Library/projects/makefile mkdir obj make && sudo make install
要设置 OpenCV 库,请按照以下步骤操作:
-
解压
opencv-2.4.9.zip包:unzip opencv-2.4.9.zip -
使用
CMake构建 OpenCV 库:cd opencv-2.4.9/ mkdir build cd build cmake ../ make && sudo make install -
配置库路径:
sudo sh -c 'echo "/usr/local/lib" > /etc/ld.so.conf.d/opencv.conf' sudo ldconfig –v -
在开发环境完全配置后,我们现在可以在每个项目文件夹中创建编译脚本(
Makefile):CFILES = ../common/shader.cpp ../common/texture.cpp ../common/controls.cpp main.cpp CFLAGS = -O3 -c -Wall INCLUDES = -I/usr/include -I/usr/include/SOIL -I../common `pkg-config --cflags glfw3` `pkg-config --cflags opencv` LIBS = -lm -L/usr/local/lib -lGLEW -lSOIL `pkg-config --static --libs glfw3` `pkg-config --libs opencv` CC = g++ OBJECTS=$(CFILES:.cpp=.o) EXECUTABLE=main all: $(CFILES) $(EXECUTABLE) $(EXECUTABLE): $(OBJECTS) $(CC) $(INCLUDES) $(OBJECTS) -o $@ $(LIBS) .cpp.o: $(CC) $(CFLAGS) $(INCLUDES) $< -o $@ clean: rm -v -f *~ ../common/*.o *.o *.obj $(EXECUTABLE)
要编译代码,我们只需在项目目录中运行make命令,它将自动生成可执行文件(main)。
参见
关于已安装的每个库的进一步文档可以在此找到:
-
GLEW:
glew.sourceforge.net/ -
OpenCV:
opencv.org/ -
MacPorts:
www.macports.org/
使用 GLSL 创建你的第一个顶点和片段着色器
在我们能够使用 OpenGL 渲染图像之前,我们首先需要了解 GLSL 的基本知识。特别是,着色程序的概念在 GLSL 中至关重要。着色器是简单地运行在图形处理器(GPU)上的程序,一组着色器被编译并链接形成一个程序。这一概念是由于现代图形硬件中各种常见处理任务的日益复杂而产生的,例如顶点和片段处理,这需要专用处理器的更大可编程性。因此,顶点着色器和片段着色器是我们在这里将涵盖的两种重要类型的着色器,它们分别运行在顶点处理器和片段处理器上。以下是一个简化的整体处理流程图:

顶点着色器的主要目的是处理流式传输的顶点数据。一个重要的处理任务是将每个顶点的位置从 3D 虚拟空间转换到屏幕上显示的 2D 坐标。顶点着色器还可以操纵颜色和纹理坐标。因此,顶点着色器是 OpenGL 管道中的一个重要组件,用于控制移动、光照和颜色。
片段着色器主要是为了计算单个像素(片段)的最终颜色。通常,我们在这个阶段实现各种图像后处理技术,如模糊或锐化;最终结果存储在帧缓冲区中,这将显示在屏幕上。
对于想要了解整个管道的读者,可以在 www.opengl.org/wiki/Rendering_Pipeline_Overview 找到这些阶段的详细总结,例如裁剪、光栅化和细分。此外,可以在 www.opengl.org/registry/doc/GLSLangSpec.4.40.pdf 找到 GLSL 的详细文档。
准备工作
在这个阶段,我们应该已经安装了所有必要的库,例如 GLEW、GLM 和 SOIL。配置好 GLFW 以支持 OpenGL 核心配置文件后,我们现在可以开始实现第一个简单的示例代码,该代码利用了现代 OpenGL 管道。
如何做到这一点...
为了使代码简单,我们将程序分为两个组件:主程序(main.cpp)和着色器程序(shader.cpp、shader.hpp、simple.vert 和 simple.frag)。主程序执行设置简单演示的基本任务,而着色器程序在现代 OpenGL 管道中执行专门的处理。完整的示例代码可以在 code_simple 文件夹中找到。
首先,让我们看一下着色器程序。我们将创建两个非常简单的顶点和片段着色器程序(在 simple.vert 和 simple.frag 文件中指定),这些程序将在程序运行时编译和加载。
对于 simple.vert 文件,输入以下代码行:
#version 150
in vec3 position;
in vec3 color_in;
out vec3 color;
void main() {
color = color_in;
gl_Position = vec4(position, 1.0);
}
对于 simple.frag 文件,输入以下代码行:
#version 150
in vec3 color;
out vec4 color_out;
void main() {
color_out = vec4(Color, 1.0);
}
首先,让我们在 shader.hpp 内定义一个名为 LoadShaders 的函数,用于编译和加载着色器程序(simple.frag 和 simple.vert):
#ifndef SHADER_HPP
#define SHADER_HPP
GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path);
#endif
接下来,我们将创建 shader.cpp 文件以实现 LoadShaders 函数和两个处理文件 I/O(readSourceFile)以及着色器编译(CompileShader)的辅助函数:
-
包含必要的库和
shader.hpp头文件:#include <iostream> #include <fstream> #include <algorithm> #include <vector> #include "shader.hpp" -
按如下方式实现
readSourceFile函数:std::string readSourceFile(const char *path){ std::string code; std::ifstream file_stream(path, std::ios::in); if(file_stream.is_open()){ std::string line = ""; while(getline(file_stream, line)) code += "\n" + line; file_stream.close(); return code; }else{ printf("Failed to open \"%s\".\n", path); return ""; } } -
按如下方式实现
CompileShader函数:void CompileShader(std::string program_code, GLuint shader_id){ GLint result = GL_FALSE; int infolog_length; char const * program_code_pointer = program_code.c_str(); glShaderSource(shader_id, 1, &program_code_pointer , NULL); glCompileShader(shader_id); //check the shader for successful compile glGetShaderiv(shader_id, GL_COMPILE_STATUS, &result); glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &infolog_length); if ( infolog_length > 0 ){ std::vector<char> error_msg(infolog_length+1); glGetShaderInfoLog(shader_id, infolog_length, NULL, &error_msg[0]); printf("%s\n", &error_msg[0]); } } -
现在,让我们实现
LoadShaders函数。首先,创建着色器 ID 并从由vertex_file_path和fragment_file_path指定的两个文件中读取着色器代码:GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path){ GLuint vertex_shader_id = glCreateShader(GL_VERTEX_SHADER); GLuint fragment_shader_id = glCreateShader(GL_FRAGMENT_SHADER); std::string vertex_shader_code = readSourceFile(vertex_file_path); if(vertex_shader_code == ""){ return 0; } std::string fragment_shader_code = readSourceFile(fragment_file_path); if(fragment_shader_code == ""){ return 0; } -
编译顶点着色器和片段着色器程序:
printf("Compiling Vertex shader : %s\n", vertex_file_path); CompileShader(vertex_shader_code, vertex_shader_id); printf("Compiling Fragment shader : %s\n",fragment_file_path); CompileShader(fragment_shader_code, fragment_shader_id); -
将程序链接在一起,检查错误,并清理:
GLint result = GL_FALSE; int infolog_length; printf("Linking program\n"); GLuint program_id = glCreateProgram(); glAttachShader(program_id, vertex_shader_id); glAttachShader(program_id, fragment_shader_id); glLinkProgram(program_id); //check the program and ensure that the program is linked properly glGetProgramiv(program_id, GL_LINK_STATUS, &result); glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &infolog_length); if ( infolog_length > 0 ){ std::vector<char> program_error_msg(infolog_length+1); glGetProgramInfoLog(program_id, infolog_length, NULL, &program_error_msg[0]); printf("%s\n", &program_error_msg[0]); }else{ printf("Linked Successfully\n"); } //flag for delete, and will free all memories //when the attached program is deleted glDeleteShader(vertex_shader_id); glDeleteShader(fragment_shader_id); return program_id; }
最后,让我们使用main.cpp文件将所有内容组合在一起:
-
在公共文件夹中包含先决库和着色器程序头文件:
#include <stdio.h> #include <stdlib.h> //GLFW and GLEW libraries #include <GL/glew.h> #include <GLFW/glfw3.h> #include "common/shader.hpp" -
创建一个用于 GLFW 窗口的全局变量:
//Global variables GLFWwindow* window; -
使用 GLFW 库的初始化启动主程序:
int main(int argc, char **argv) { //Initialize GLFW if(!glfwInit()){ fprintf( stderr, "Failed to initialize GLFW\n" ); exit(EXIT_FAILURE); } -
设置 GLFW 窗口:
//enable anti-aliasing 4x with GLFW glfwWindowHint(GLFW_SAMPLES, 4); /* specify the client API version that the created context must be compatible with. */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); //make the GLFW forward compatible glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); //use the OpenGL Core glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); -
创建 GLFW 窗口对象并在调用线程中将指定窗口的上下文设置为当前:
window = glfwCreateWindow(640, 480, "Chapter 4 - GLSL", NULL, NULL); if(!window){ fprintf( stderr, "Failed to open GLFW window. If you have an Intel GPU, they are not 3.3 compatible. Try the 2.1 version of the tutorials.\n" ); glfwTerminate(); exit(EXIT_FAILURE); } glfwMakeContextCurrent(window); glfwSwapInterval(1); -
初始化 GLEW 库并包含对实验性驱动程序的支持:
glewExperimental = true; if (glewInit() != GLEW_OK) { fprintf(stderr, "Final to Initialize GLEW\n"); glfwTerminate(); exit(EXIT_FAILURE); } -
设置着色器程序:
GLuint program = LoadShaders("simple.vert", "simple.frag"); glBindFragDataLocation(program, 0, "color_out"); glUseProgram(program); -
设置顶点缓冲对象(和颜色缓冲区)并将顶点数据复制到其中:
GLuint vertex_buffer; GLuint color_buffer; glGenBuffers(1, &vertex_buffer); glGenBuffers(1, &color_buffer); const GLfloat vertices[] = { -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f }; const GLfloat colors[]={ 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f }; glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, color_buffer); glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW); -
指定顶点数据的布局:
GLint position_attrib = glGetAttribLocation(program, "position"); glEnableVertexAttribArray(position_attrib); glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); glVertexAttribPointer(position_attrib, 3, GL_FLOAT, GL_FALSE, 0, (void*)0); GLint color_attrib = glGetAttribLocation(program, "color_in"); glEnableVertexAttribArray(color_attrib); glBindBuffer(GL_ARRAY_BUFFER, color_buffer); glVertexAttribPointer(color_attrib, 3, GL_FLOAT, GL_FALSE, 0, (void*)0); -
运行绘图程序:
while(!glfwWindowShouldClose(window)){ // Clear the screen to black glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Draw a rectangle from the 2 triangles using 6 vertices glDrawArrays(GL_TRIANGLES, 0, 6); glfwSwapBuffers(window); glfwPollEvents(); } -
清理并退出程序:
//clean up the memories glDisableVertexAttribArray(position_attrib); glDisableVertexAttribArray(color_attrib); glDeleteBuffers(1, &vertex_buffer); glDeleteBuffers(1, &color_buffer); glDeleteVertexArrays(1, &vertex_array); glDeleteProgram(program); // Close OpenGL window and terminate GLFW glfwDestroyWindow(window); glfwTerminate(); exit(EXIT_SUCCESS); }
现在我们通过定义自定义着色器创建了第一个 GLSL 程序:

它是如何工作的...
由于这个实现中有多个组件,我们将分别突出每个组件的关键特性,按照与上一节相同的顺序组织,为了简单起见,使用相同的文件名。
在simple.vert中,我们定义了一个简单的顶点着色器。在第一个简单实现中,顶点着色器只是将信息传递给渲染管道的其余部分。首先,我们需要定义与 OpenGL 3.2 支持相对应的 GLSL 版本,即 1.50 (#version 150)。顶点着色器接受两个参数:顶点的位置(in vec3 position)和颜色(in vec3 color_in)。请注意,只有颜色在输出变量中明确定义(out vec3 color),因为gl_Position是一个内置变量。通常,在 OpenGL 的着色器程序中不应使用以gl为前缀的变量名,因为这些是为内置变量保留的。注意,最终的位置gl_Position是以齐次坐标表示的。
在simple.frag中,我们定义了片段着色器,它再次将颜色信息传递到输出帧缓冲区。请注意,最终输出(color_out)是以 RGBA 格式表示的,其中 A 是 alpha 值(透明度)。
接下来,在shader.cpp中,我们创建了一个框架来编译和链接着色器程序。工作流程与 C/C++的传统代码编译有一些相似之处。简要来说,有六个主要步骤:
-
创建一个着色器对象(
glCreateShader)。 -
读取并设置着色器源代码(
glShaderSource)。 -
编译(
glCompileShader)。 -
创建最终的程序 ID(
glCreateProgram)。 -
将着色器附加到程序 ID(
glAttachShader)。 -
将所有内容链接在一起(
glLinkProgram)。
最后,在main.cpp中,我们设置了一个演示来展示编译后的着色器程序的使用。正如本章“现代 OpenGL 入门”部分所述,我们需要使用glfwWindowHint函数在 OpenGL 3.2 中正确创建 GLFW 窗口上下文。这个演示的一个有趣之处在于,尽管我们只定义了六个顶点(使用glDrawArrays函数绘制的两个三角形各三个顶点)及其相应的颜色,但最终结果是插值的颜色渐变。
使用纹理映射渲染 2D 图像
现在我们已经通过一个简单的示例介绍了 GLSL 的基础知识,我们将引入更多的复杂性,以提供一个完整的框架,使用户能够在未来修改渲染管道的任何部分。
本框架中的代码被划分为更小的模块,以处理着色器程序(shader.cpp和shader.hpp)、纹理映射(texture.cpp和texture.hpp)以及用户输入(controls.hpp和controls.hpp)。首先,我们将重用之前在 OpenGL 中引入的加载着色器程序的机制,并纳入新的着色器程序以供我们的目的使用。接下来,我们将介绍纹理映射所需的步骤。最后,我们将描述主程序,它整合了所有逻辑部分并准备最终的演示。在本节中,我们将展示如何加载图像并将其转换为 OpenGL 中渲染的纹理对象。考虑到这个框架,我们将在下一节进一步演示如何渲染视频。
准备工作
为了避免重复,我们将引导读者参考前文中的部分内容(特别是shader.cpp和shader.hpp)。
如何实现...
首先,我们将程序中使用的所有常用库聚合到common.h头文件中。然后,common.h文件被包含在shader.hpp、controls.hpp、texture.hpp和main.cpp中:
#ifndef _COMMON_h
#define _COMMON_h
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <string>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
using namespace std;
#endif
我们之前实现了一个从文件中加载片段和顶点着色器程序的机制,并将在此处重用代码(shader.cpp和shader.hpp)。然而,我们将修改实际的顶点和着色器程序如下。
对于顶点着色器(transform.vert),我们将实现以下内容:
#version 150
in vec2 UV;
out vec4 color;
uniform sampler2D textureSampler;
void main(){
color = texture(textureSampler, UV).rgba;
}
对于片段着色器(texture.frag),我们将实现以下内容:
#version 150
in vec3 vertexPosition_modelspace;
in vec2 vertexUV;
out vec2 UV;
uniform mat4 MVP;
void main(){
//position of the vertex in clip space
gl_Position = MVP * vec4(vertexPosition_modelspace,1);
UV = vertexUV;
}
对于纹理对象,在texture.cpp中,我们提供了一个机制来将图像或视频流加载到纹理内存中。我们还利用了 SOIL 库进行简单的图像加载,以及 OpenCV 库进行更高级的视频流处理和过滤(参考下一节)。
在texture.cpp中,我们将实现以下内容:
-
包含
texture.hpp头文件和 SOIL 库头文件以进行简单的图像加载:#include "texture.hpp" #include <SOIL.h> -
定义纹理对象的初始化并设置所有参数:
GLuint initializeTexture(const unsigned char *image_data, int width, int height, GLenum format){ GLuint textureID=0; //create and bind one texture element glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_2D, textureID); glPixelStorei(GL_UNPACK_ALIGNMENT,1); /* Specify target texture. The parameters describe the format and type of the image data */ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, image_data); /* Set the wrap parameter for texture coordinate s & t to GL_CLAMP, which clamps the coordinates within [0, 1] */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); /* Set the magnification method to linear and return weighted average of four texture elements closest to the center of the pixel */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); /* Choose the mipmap that most closely matches the size of the pixel being textured and use the GL_NEAREST criterion (the texture element nearest to the center of the pixel) to produce a texture value. */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glGenerateMipmap(GL_TEXTURE_2D); return textureID; } -
定义更新纹理内存的例程:
void updateTexture(const unsigned char *image_data, int width, int height, GLenum format){ // Update Texture glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, width, height, format, GL_UNSIGNED_BYTE, image_data); /* Sets the wrap parameter for texture coordinate s & t to GL_CLAMP, which clamps the coordinates within [0, 1]. */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); /* Set the magnification method to linear and return weighted average of four texture elements closest to the center of the pixel */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); /* Choose the mipmap that most closely matches the size of the pixel being textured and use the GL_NEAREST criterion (the texture element nearest to the center of the pixel) to produce a texture value. */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glGenerateMipmap(GL_TEXTURE_2D); } -
最后,实现图像的纹理加载机制。该函数接受图像路径并自动将图像转换为纹理对象兼容的各种格式:
GLuint loadImageToTexture(const char * imagepath){ int width, height, channels; GLuint textureID=0; //Load the images and convert them to RGBA format unsigned char* image = SOIL_load_image(imagepath, &width, &height, &channels, SOIL_LOAD_RGBA); if(!image){ printf("Failed to load image %s\n", imagepath); return textureID; } printf("Loaded Image: %d x %d - %d channels\n", width, height, channels); textureID=initializeTexture(image, width, height, GL_RGBA); SOIL_free_image_data(image); return textureID; }
在控制器方面,我们捕获箭头键并在实时中修改相机模型参数。这允许我们改变相机的位置和方向以及视场角度。在 controls.cpp 中,我们实现以下内容:
-
包含 GLM 库头文件和
controls.hpp头文件以进行投影矩阵和视图矩阵的计算:#define GLM_FORCE_RADIANS #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include "controls.hpp" -
定义全局变量(相机参数以及视图和投影矩阵)以在每一帧之后更新:
//initial position of the camera glm::vec3 g_position = glm::vec3( 0, 0, 2 ); const float speed = 3.0f; // 3 units / second float g_initial_fov = glm::pi<float>()*0.4f; //the view matrix and projection matrix glm::mat4 g_view_matrix; glm::mat4 g_projection_matrix; -
创建辅助函数以返回最新的视图矩阵和投影矩阵:
glm::mat4 getViewMatrix(){ return g_view_matrix; } glm::mat4 getProjectionMatrix(){ return g_projection_matrix; } -
根据用户输入计算视图矩阵和投影矩阵:
void computeViewProjectionMatrices(GLFWwindow* window){ static double last_time = glfwGetTime(); // Compute time difference between current and last frame double current_time = glfwGetTime(); float delta_time = float(current_time - last_time); int width, height; glfwGetWindowSize(window, &width, &height); //direction vector for movement glm::vec3 direction(0, 0, -1); //up vector glm::vec3 up = glm::vec3(0,-1,0); if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS){ g_position += direction * delta_time * speed; } else if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS){ g_position -= direction * delta_time * speed; } else if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS){ g_initial_fov -= 0.1 * delta_time * speed; } else if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS){ g_initial_fov += 0.1 * delta_time * speed; } /* update projection matrix: Field of View, aspect ratio, display range : 0.1 unit <-> 100 units */ g_projection_matrix = glm::perspective(g_initial_fov, (float)width/(float)height, 0.1f, 100.0f); // update the view matrix g_view_matrix = glm::lookAt( g_position, // camera position g_position+direction, // viewing direction up // up direction ); last_time = current_time; }
在 main.cpp 中,我们将使用之前定义的各种函数来完成实现:
-
将 GLFW 和 GLM 库以及我们存储在名为
common文件夹中的辅助函数包含在内:#define GLM_FORCE_RADIANS #include <stdio.h> #include <stdlib.h> #include <GL/glew.h> #include <GLFW/glfw3.h> #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> using namespace glm; #include <common/shader.hpp> #include <common/texture.hpp> #include <common/controls.hpp> #include <common/common.h> -
定义所有用于设置的全局变量:
GLFWwindow* g_window; const int WINDOWS_WIDTH = 1280; const int WINDOWS_HEIGHT = 720; float aspect_ratio = 3.0f/2.0f; float z_offset = 2.0f; float rotateY = 0.0f; float rotateX = 0.0f; //Our vertices static const GLfloat g_vertex_buffer_data[] = { -aspect_ratio,-1.0f,z_offset, aspect_ratio,-1.0f,z_offset, aspect_ratio,1.0f,z_offset, -aspect_ratio,-1.0f,z_offset, aspect_ratio,1.0f,z_offset, -aspect_ratio,1.0f,z_offset }; //UV map for the vertices static const GLfloat g_uv_buffer_data[] = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f }; -
定义键盘
callback函数:static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { if (action != GLFW_PRESS && action != GLFW_REPEAT) return; switch (key) { case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(window, GL_TRUE); break; case GLFW_KEY_SPACE: rotateX=0; rotateY=0; break; case GLFW_KEY_Z: rotateX+=0.01; break; case GLFW_KEY_X: rotateX-=0.01; break; case GLFW_KEY_A: rotateY+=0.01; break; case GLFW_KEY_S: rotateY-=0.01; break; default: break; } } -
使用启用 OpenGL 核心配置文件的 GLFW 库初始化:
int main(int argc, char **argv) { //Initialize the GLFW if(!glfwInit()){ fprintf( stderr, "Failed to initialize GLFW\n" ); exit(EXIT_FAILURE); } //enable anti-alising 4x with GLFW glfwWindowHint(GLFW_SAMPLES, 4); //specify the client API version glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); //make the GLFW forward compatible glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); //enable the OpenGL core profile for GLFW glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); -
设置 GLFW 窗口和键盘输入处理程序:
//create a GLFW windows object window = glfwCreateWindow(WINDOWS_WIDTH, WINDOWS_HEIGHT, "Chapter 4 - Texture Mapping", NULL, NULL); if(!window){ fprintf( stderr, "Failed to open GLFW window. If you have an Intel GPU, they are not 3.3 compatible. Try the 2.1 version of the tutorials.\n" ); glfwTerminate(); exit(EXIT_FAILURE); } /* make the context of the specified window current for the calling thread */ glfwMakeContextCurrent(window); glfwSwapInterval(1); glewExperimental = true; // Needed for core profile if (glewInit() != GLEW_OK) { fprintf(stderr, "Final to Initialize GLEW\n"); glfwTerminate(); exit(EXIT_FAILURE); } //keyboard input callback glfwSetInputMode(window,GLFW_STICKY_KEYS,GL_TRUE); glfwSetKeyCallback(window, key_callback); -
设置黑色背景并启用 alpha 混合以实现各种视觉效果:
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); -
加载顶点着色器和片段着色器:
GLuint program_id = LoadShaders( "transform.vert", "texture.frag" ); -
使用 SOIL 库将图像文件加载到纹理对象中:
char *filepath; //load the texture from image with SOIL if(argc<2){ filepath = (char*)malloc(sizeof(char)*512); sprintf(filepath, "texture.png"); } else{ filepath = argv[1]; } int width; int height; GLuint texture_id = loadImageToTexture(filepath, &width, &height); aspect_ratio = (float)width/(float)height; if(!texture_id){ //if we get 0 with no texture glfwTerminate(); exit(EXIT_FAILURE); } -
获取着色器程序中特定变量的位置:
//get the location for our "MVP" uniform variable GLuint matrix_id = glGetUniformLocation(program_id, "MVP"); //get a handler for our "myTextureSampler" uniform GLuint texture_sampler_id = glGetUniformLocation(program_id, "textureSampler"); //attribute ID for the variables GLint attribute_vertex, attribute_uv; attribute_vertex = glGetAttribLocation(program_id, "vertexPosition_modelspace"); attribute_uv = glGetAttribLocation(program_id, "vertexUV"); -
定义我们的 顶点数组对象(VAO):
GLuint vertex_array_id; glGenVertexArrays(1, &vertex_array_id); glBindVertexArray(vertex_array_id); -
定义我们的顶点数组对象(VAO)和 UV 映射:
//initialize the vertex buffer memory. GLuint vertex_buffer; glGenBuffers(1, &vertex_buffer); glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW); //initialize the UV buffer memory GLuint uv_buffer; glGenBuffers(1, &uv_buffer); glBindBuffer(GL_ARRAY_BUFFER, uv_buffer); glBufferData(GL_ARRAY_BUFFER, sizeof(g_uv_buffer_data), g_uv_buffer_data, GL_STATIC_DRAW); -
使用着色器程序并绑定所有纹理单元和属性缓冲区:
glUseProgram(program_id); //binds our texture in Texture Unit 0 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture_id); glUniform1i(texture_sampler_id, 0); //1st attribute buffer: vertices for position glEnableVertexAttribArray(attribute_vertex); glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); glVertexAttribPointer(attribute_vertex, 3, GL_FLOAT, GL_FALSE, 0, (void*)0); //2nd attribute buffer: UVs mapping glEnableVertexAttribArray(attribute_uv); glBindBuffer(GL_ARRAY_BUFFER, uv_buffer); glVertexAttribPointer(attribute_uv, 2, GL_FLOAT, GL_FALSE, 0, (void*)0); -
在主循环中,清除屏幕和深度缓冲区:
//time-stamping for performance measurement double previous_time = glfwGetTime(); do{ //clear the screen glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(1.0f, 1.0f, 1.0f, 0.0f); -
计算变换并将信息存储在着色器变量中:
//compute the MVP matrix from keyboard and mouse input computeMatricesFromInputs(g_window); //obtain the View and Model Matrix for rendering glm::mat4 projection_matrix = getProjectionMatrix(); glm::mat4 view_matrix = getViewMatrix(); glm::mat4 model_matrix = glm::mat4(1.0); model_matrix = glm::rotate(model_matrix, glm::pi<float>() * rotateY, glm::vec3(0.0f, 1.0f, 0.0f)); model_matrix = glm::rotate(model_matrix, glm::pi<float>() * rotateX, glm::vec3(1.0f, 0.0f, 0.0f)); glm::mat4 mvp = projection_matrix * view_matrix * model_matrix; //send our transformation to the currently bound shader //in the "MVP" uniform variable glUniformMatrix4fv(matrix_id, 1, GL_FALSE, &mvp[0][0]); -
绘制元素并刷新屏幕:
glDrawArrays(GL_TRIANGLES, 0, 6); //draw a square //swap buffers glfwSwapBuffers(window); glfwPollEvents(); -
最后,定义退出
main循环的条件并清除所有内存以优雅地退出程序:} // Check if the ESC key was pressed or the window was closed while(!glfwWindowShouldClose(window) && glfwGetKey(window, GLFW_KEY_ESCAPE )!=GLFW_PRESS); glDisableVertexAttribArray(attribute_vertex); glDisableVertexAttribArray(attribute_uv); // Clean up VBO and shader glDeleteBuffers(1, &vertex_buffer); glDeleteBuffers(1, &uv_buffer); glDeleteProgram(program_id); glDeleteTextures(1, &texture_id); glDeleteVertexArrays(1, &vertex_array_id); // Close OpenGL window and terminate GLFW glfwDestroyWindow(g_window); glfwTerminate(); exit(EXIT_SUCCESS); }
它是如何工作的...
为了展示框架在数据可视化中的应用,我们将将其应用于组织学切片(皮肤样本的 H&E 横切面)的可视化,如下面的截图所示:

与之前的演示相比,一个重要的区别是这里,我们实际上将图像加载到纹理内存中(texture.cpp)。为了完成这项任务,我们使用 SOIL 库调用(SOIL_load_image)以 RGBA 格式(GL_RGBA)加载组织学图像,并使用 glTexImage2D 函数调用生成可以由着色器读取的纹理图像。
另一个重要的区别是,我们现在可以动态地重新计算视图(g_view_matrix)和投影(g_projection_matrix)矩阵,以实现交互式且有趣的 3D 空间中图像的可视化。请注意,包含 GLM 库头文件是为了方便矩阵计算。使用在controls.cpp中定义的键盘输入(上、下、左和右)以及 GLFW 库调用,我们可以放大和缩小幻灯片,以及调整视图角度,这为 3D 虚拟空间中的组织学图像提供了一个有趣的视角。以下是使用不同视角查看图像的屏幕截图:

当前基于 OpenGL 的框架的另一个独特功能通过以下屏幕截图展示,该截图是通过将新的图像过滤器实现到片段着色器中生成的,该过滤器突出显示图像中的边缘。这展示了使用 OpenGL 渲染管线实时交互式可视化和处理 2D 图像的无限可能性,同时不牺牲 CPU 性能。这里实现的过滤器将在下一节中讨论。

带过滤器的实时视频渲染
GLSL 着色器提供了一种简单的方法来执行高度并行的处理。在之前展示的纹理映射之上,我们将演示如何通过片段着色器实现一个简单的视频过滤器,该过滤器对缓冲帧的最终结果进行后处理。为了说明这项技术,我们实现了 Sobel 滤波器,并使用 OpenGL 管线渲染热图。之前在第三章中实现的,交互式 3D 数据可视化的热图函数现在将直接移植到 GLSL,仅做非常小的修改。
Sobel 算子是一种简单的图像处理技术,常用于计算机视觉算法,如边缘检测。这个算子可以定义为具有 3 x 3 核的卷积操作,如下所示:

和
分别是图像I在像素位置(x, y)的卷积操作得到的水平和垂直导数的结果。
我们还可以执行平方和操作来近似图像的梯度幅度:

准备工作
这个演示基于之前的章节,其中渲染了一个图像。在本节中,我们将演示使用 OpenCV 库调用处理视频的图像序列或视频的渲染。在common.h中,我们将添加以下行以包含 OpenCV 库:
#include <opencv2/opencv.hpp>
using namespace cv;
如何操作...
现在,让我们按照以下步骤完成实现:
-
首先,修改
main.cpp以启用 OpenCV 的视频处理。本质上,不是加载图像,而是将视频的单独帧输入到相同的管道中:char *filepath; if(argc<2){ filepath = (char*)malloc(sizeof(char)*512); sprintf(filepath, "video.mov"); } else{ filepath = argv[1]; } //Handling Video input with OpenCV VideoCapture cap(filepath); // open the default camera Mat frame; if (!cap.isOpened()){ // check if we succeeded printf("Cannot open files\n"); glfwTerminate(); exit(EXIT_FAILURE); }else{ cap >> frame; // get a new frame from camera printf("Got Video, %d x %d\n",frame.size().width, frame.size().height); } cap >> frame; // get a new frame from camera GLuint texture_id = initializeTexture(frame.data, frame.size().width, frame.size().height, GL_BGR); aspect_ratio = (float)frame.size().width/ (float)frame.size().height; -
然后,在
main循环中添加update函数以在每一帧中更新纹理:/* get the video feed, reset to beginning if it reaches the end of the video */ if(!cap.grab()){ printf("End of Video, Resetting\n"); cap.release(); cap.open(filepath); // open the default camera } cap >> frame; // get a new frame from camera //update the texture with the new frame updateTexture(frame.data, frame.size().width, frame.size().height, GL_BGR); -
接下来,修改片段着色器并将其重命名为
texture_sobel.frag(从texture.frag)。在main函数中,我们将概述整体处理过程(使用 Sobel 滤波器和热图渲染器处理纹理缓冲区):void main(){ //compute the results of Sobel filter float graylevel = sobel_filter(); color = heatMap(graylevel, 0.1, 3.0); } -
现在,实现 Sobel 滤波器算法,该算法通过计算相邻像素来得到结果:
float sobel_filter() { float dx = 1.0 / float(1280); float dy = 1.0 / float(720); float s00 = pixel_operator(-dx, dy); float s10 = pixel_operator(-dx, 0); float s20 = pixel_operator(-dx,-dy); float s01 = pixel_operator(0.0,dy); float s21 = pixel_operator(0.0, -dy); float s02 = pixel_operator(dx, dy); float s12 = pixel_operator(dx, 0.0); float s22 = pixel_operator(dx, -dy); float sx = s00 + 2 * s10 + s20 - (s02 + 2 * s12 + s22); float sy = s00 + 2 * s01 + s02 - (s20 + 2 * s21 + s22); float dist = sx * sx + sy * sy; return dist; } -
定义计算亮度值的辅助函数:
float rgb2gray(vec3 color ) { return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; } -
为像素操作操作创建辅助函数:
float pixel_operator(float dx, float dy){ return rgb2gray(texture( textureSampler, UV + vec2(dx,dy)).rgb); } -
最后,定义热图渲染器原型并实现算法以更好地可视化值范围:
vec4 heatMap(float v, float vmin, float vmax){ float dv; float r, g, b; if (v < vmin) v = vmin; if (v > vmax) v = vmax; dv = vmax - vmin; if(v == 0){ return vec4(0.0, 0.0, 0.0, 1.0); } if (v < (vmin + 0.25f * dv)) { r = 0.0f; g = 4.0f * (v - vmin) / dv; } else if (v < (vmin + 0.5f * dv)) { r = 0.0f; b = 1.0f + 4.0f * (vmin + 0.25f * dv - v) / dv; } else if (v < (vmin + 0.75f * dv)) { r = 4.0f * (v - vmin - 0.5f * dv) / dv; b = 0.0f; } else { g = 1.0f + 4.0f * (vmin + 0.75f * dv - v) / dv; b = 0.0f; } return vec4(r, g, b, 1.0); }
如何工作...
这个演示有效地打开了使用 OpenGL 管道在片段着色阶段进行实时处理以渲染任何图像序列的可能性。以下截图是一个示例,说明了如何使用这个强大的 OpenGL 框架显示视频的一帧(显示书籍的作者),而不启用 Sobel 滤波器:

现在,启用 Sobel 滤波器和热图渲染后,我们看到一种使用实时 OpenGL 纹理映射和自定义着色器处理来可视化世界的方法:

进一步微调阈值参数并将结果转换为灰度(在 texture_sobel.frag 文件中)将导致美观的输出:
void main(){
//compute the results of Sobel filter
float graylevel = sobel_filter();
color = vec4(graylevel, graylevel, graylevel, 1.0);
}

此外,我们可以通过修改着色器程序中的主函数(texture_sobel.frag)将这些结果与原始视频输入混合,以实时创建过滤效果:
void main(){
//compute the results of Sobel filter
float graylevel = sobel_filter();
//process the right side of the image
if(UV.x > 0.5)
color = heatMap(graylevel, 0.0, 3.0) + texture(textureSampler, UV);
else
color = vec4(graylevel, graylevel, graylevel, 1.0) + texture (textureSampler, UV);
}

为了说明使用完全相同的程序来可视化成像数据集,这里有一个示例,展示了使用 光学相干断层扫描(OCT)成像的人指的体素数据集,只需更改输入视频的文件名:

这张截图代表了这个体素 OCT 数据集中指甲床的 256 个横截面图像之一(以电影文件格式导出)。
这里还有一个示例,展示了使用 偏振敏感光学相干断层扫描(PS-OCT)成像的疤痕样本的体素数据集,它为疤痕区域提供了无标记的内在对比度:

在这个案例中,体素 PS-OCT 数据集是用 ImageJ 3D Viewer 渲染的,并转换成了电影文件。颜色表示偏振度(DOP),这是衡量光偏振状态随机性的一个指标(低 DOP 在黄色/绿色,高 DOP 在蓝色),在皮肤中。疤痕区域与正常皮肤相比,具有高 DOP 的特征。
正如我们在这里所展示的,这个程序可以很容易地采用(通过更改输入视频源)来显示许多类型的数据集,例如内窥镜视频或其他体素成像数据集。在需要实时处理非常大的数据集的严格要求应用中,OpenGL 的实用性变得明显。
第五章:3D 范围感应相机的点云数据渲染
在本章中,我们将涵盖以下主题:
-
开始使用微软 Kinect(PrimeSense)3D 范围感应相机
-
从深度感应相机捕获原始数据
-
带纹理映射和叠加的 OpenGL 点云渲染
简介
本章的目的是介绍可视化另一类有趣且新兴数据的技术:来自 3D 范围感应相机的深度信息。带有 3D 深度传感器的设备每天都在市场上出现,英特尔、微软、SoftKinetic、PMD、Structure Sensor 和 Meta(可穿戴增强现实眼镜)等公司都在使用这些新颖的 3D 感应设备来跟踪用户输入,例如手势交互和/或跟踪用户的环境。3D 传感器与 OpenGL 的有趣集成是能够从不同的视角查看场景,从而实现使用深度传感器捕获的场景的虚拟 3D 飞行浏览。在我们的案例中,对于数据可视化,能够在庞大的 3D 数据集中行走可能特别强大,在科学计算、城市规划以及许多涉及场景 3D 结构可视化的其他应用中。
在本章中,我们提出了一种简化的流程,它接受任何带有颜色(r,g,b)的 3D 点数据(X,Y,Z),并在屏幕上实时渲染这些点云。点云将直接从使用 3D 范围感应相机的现实世界数据中获得。我们还将提供绕点云飞行的方法和动态调整相机参数的方式。本章将在前一章讨论的 OpenGL 图形渲染管道的基础上构建,我们将向您展示一些额外的技巧来使用 GLSL 过滤数据。我们将使用我们的热图生成器显示深度信息,以在 2D 中查看深度,并使用纹理映射和透视投影将此数据重新映射到 3D 点云。这将使我们能够看到场景的真实深度渲染,并从任何视角在场景中导航。
开始使用微软 Kinect(PrimeSense)3D 范围感应相机
基于 PrimeSense 技术的微软 Kinect 3D 范围感应相机是一套有趣的设备,它通过使用光模式进行深度感应来估计场景的 3D 几何形状。3D 传感器具有一个主动红外激光投影仪,它发射编码的斑点光模式。这些传感器允许用户捕获彩色图像,并提供分辨率为 640 x 480 的 3D 深度图。由于 Kinect 传感器是一个主动传感器,它对室内照明条件(即,即使在黑暗中也能工作)不变,从而使得许多应用成为可能,例如手势和姿态跟踪以及 3D 扫描和重建。
在本节中,我们将演示如何设置此类范围感应相机,作为一个示例。虽然我们不需要读者为本章购买 3D 范围感应相机(因为我们将为运行我们的演示提供该设备捕获的原始数据),但我们将演示如何设置设备以直接捕获数据,主要针对那些对进一步实验实时 3D 数据感兴趣的人。
如何操作...
Windows 用户可以从structure.io/openni(或使用直接下载链接:com.occipital.openni.s3.amazonaws.com/OpenNI-Windows-x64-2.2.0.33.zip)下载 OpenNI 2 SDK 和驱动程序,并按照屏幕上的说明操作。Linux 用户可以从同一网站structure.io/openni下载 OpenNI 2 SDK。
Mac 用户可以按照以下步骤安装 OpenNI2 驱动程序:
-
使用 Macport 安装库:
sudo port install libtool sudo port install libusb +universal -
从
github.com/occipital/openni2下载 OpenNI2。 -
使用以下命令编译源代码:
cd OpenNI2-master make cd Bin/x64-Release/ -
运行
SimpleViewer可执行文件:./SimpleViewer
如果你使用的是具有 USB 3.0 接口的计算机,那么首先升级 PrimeSense 传感器的固件到版本 1.0.9dasl.mem.drexel.edu/wiki/images/5/51/FWUpdate_RD109-112_5.9.2.zip非常重要。此升级需要 Windows 平台。请注意,为了继续操作,您必须安装 PrimeSense 传感器的 Windows 驱动程序structure.io/openni。执行FWUpdate_RD109-112_5.9.2.exe文件,固件将自动升级。有关固件的更多详细信息,请参阅dasl.mem.drexel.edu/wiki/index.php/4._Updating_Firmware_for_Primesense。
参见
可以从msdn.microsoft.com/en-us/library/jj131033.aspx获取 Microsoft Kinect 3D 系统的详细技术规格,并且有关安装 OpenNI2 驱动程序的进一步说明和先决条件可以在github.com/occipital/openni2找到。
此外,Microsoft Kinect V2 也可用,并且与 Windows 兼容。新的传感器提供更高分辨率的图像和更好的深度保真度。有关传感器的更多信息以及 Microsoft Kinect SDK,请参阅www.microsoft.com/en-us/kinectforwindows。
从深度感应相机捕获原始数据
现在您已安装了必要的库和驱动程序,我们将演示如何从您的深度感应相机捕获原始数据。
如何操作...
要直接以二进制格式捕获传感器数据,实现以下函数:
void writeDepthBuffer(openni::VideoFrameRef depthFrame){
static int depth_buffer_counter=0;
char file_name [512];
sprintf(file_name, "%s%d.bin", "depth_frame", depth_buffer_counter);
openni::DepthPixel *depthPixels = new openni::DepthPixel[depthFrame.getHeight()*depthFrame.getWidth()];
memcpy(depthPixels, depthFrame.getData(), depthFrame.getHeight()*depthFrame.getWidth()*sizeof(uint16_t));
std::fstream myFile (file_name, std::ios::out |std::ios::binary);
myFile.write ((char*)depthPixels, depthFrame.getHeight()*depthFrame.getWidth()*sizeof(uint16_t));
depth_buffer_counter++;
printf("Dumped Depth Buffer %d\n",depth_buffer_counter);
myFile.close();
delete depthPixels;
}
同样,我们使用以下实现方法捕获原始的 RGB 颜色数据:
void writeColorBuffer(openni::VideoFrameRef colorFrame){
static int color_buffer_counter=0;
char file_name [512];
sprintf(file_name, "%s%d.bin", "color_frame", color_buffer_counter);
//basically unsigned char*
const openni::RGB888Pixel* imageBuffer = (const openni::RGB888Pixel*)colorFrame.getData();
std::fstream myFile (file_name, std::ios::out | std::ios::binary);
myFile.write ((char*)imageBuffer, colorFrame.getHeight()*colorFrame.getWidth()*sizeof(uint8_t)*3);
color_buffer_counter++;
printf("Dumped Color Buffer %d, %d, %d\n", colorFrame.getHeight(), colorFrame.getWidth(), color_buffer_counter);
myFile.close();
}
上述代码片段可以添加到 OpenNI2 SDK 中提供深度和颜色数据可视化的任何示例代码中(以启用原始数据捕获)。我们建议您修改OpenNI2-master/Samples/SimpleViewer文件夹中的Viewer.cpp文件。修改后的示例代码包含在我们的代码包中。要捕获原始数据,请按R键,数据将被存储在depth_frame0.bin和color_frame0.bin文件中。
如何工作...
深度传感器实时返回两个数据流。一个数据流是 3D 深度图,存储在 16 位无符号短数据类型中(见以下图左侧)。另一个数据流是彩色图像(见以下图右侧),存储在每像素 24 位,RGB888 格式中(即内存按 R、G、B 顺序对齐,每个像素使用8 位 3 通道 = 24 位)。

二进制数据直接写入硬盘,不进行压缩或修改数据格式。在客户端,我们读取二进制文件,就像有连续的数据流和颜色数据对同步从硬件设备到达一样。OpenNI2 驱动程序提供了与基于 PrimeSense 的传感器(Microsoft Kinect 或 PS1080)接口的机制。
例如,openni::VideoFrameRef depthFrame变量存储了对深度数据缓冲区的引用。通过调用depthFrame.getData()函数,我们获得一个指向DepthPixel格式缓冲区的指针,这相当于无符号短数据类型。然后,我们使用fstream库中的write()函数将二进制数据写入文件。同样,我们使用彩色图像执行相同的任务,但数据存储在 RGB888 格式中。
此外,我们可以在 OpenNI2 中启用setImageRegistrationMode(openni::IMAGE_REGISTRATION_DEPTH_TO_COLOR)深度图注册功能,以自动计算并将深度值映射到颜色图像上。深度图叠加在颜色图像上,如图所示:

在下一节中,我们将假设原始深度图已经通过 OpenNI2 的图像配准预校准,可以直接用于计算真实世界的坐标和 UV 映射索引。
带纹理映射和叠加的 OpenGL 点云渲染
我们将在上一章讨论的 OpenGL 框架的基础上构建本节中的点云渲染。上一章中引入的纹理映射技术也可以应用于点云格式。基本上,深度传感器提供了一组实际空间中的顶点(深度图),而颜色相机为我们提供了顶点的颜色信息。一旦深度图和颜色相机校准,UV 映射就是一个简单的查找表。
准备工作
读者应使用提供的原始数据用于后续演示或从 3D 范围感应相机获取自己的原始数据。在任一情况下,我们假设这些文件名将用于表示原始数据文件:depth_frame0.bin和color_frame0.bin。
如何操作...
与上一章类似,我们将程序分为三个主要组件:主程序(main.cpp)、着色器程序(shader.cpp,shader.hpp,pointcloud.vert,pointcloud.frag)和纹理映射函数(texture.cpp,texture.hpp)。主程序执行设置演示的基本任务,而着色器程序执行专门的加工。纹理映射函数提供了一种将颜色信息加载并映射到顶点的机制。最后,我们修改control.cpp文件,通过各种额外的键盘输入(使用上、下、左、右箭头键进行缩放,以及使用a、s、x和z键调整旋转角度)提供对飞行体验的更精细控制。
首先,让我们看一下着色器程序。我们将在pointcloud.vert和pointcloud.frag文件中创建两个顶点和片段着色器程序,这些程序在程序运行时通过shader.cpp文件中的LoadShaders函数编译和加载。
对于pointcloud.vert文件,我们实现以下内容:
#version 150 core
// Input vertex data
in vec3 vertexPosition_modelspace;
in vec2 vertexUV;
// Output data: interpolated for each fragment.
out vec2 UV;
out vec4 color_based_on_position;
// Values that stay constant for the whole mesh
uniform mat4 MVP;
//heat map generator
vec4 heatMap(float v, float vmin, float vmax){
float dv;
float r=1.0f, g=1.0f, b=1.0f;
if (v < vmin)
v = vmin;
if (v > vmax)
v = vmax;
dv = vmax - vmin;
if (v < (vmin + 0.25f * dv)) {
r = 0.0f;
g = 4.0f * (v - vmin) / dv;
} else if (v < (vmin + 0.5f * dv)) {
r = 0.0f;
b = 1.0f+4.0f*(vmin+0.25f*dv-v)/dv;
} else if (v < (vmin + 0.75f * dv)) {
r = 4.0f*(v-vmin-0.5f*dv)/dv;
b = 0.0f;
} else {
g = 1.0f+4.0f*(vmin+0.75f*dv-v)/dv;
b = 0.0f;
}
return vec4(r, g, b, 1.0);
}
void main(){
// Output position of the vertex, in clip space: MVP * position
gl_Position = MVP * vec4(vertexPosition_modelspace,1);
color_based_on_position = heatMap(vertexPosition_modelspace.z, -3.0, 0.0f);
UV = vertexUV;
}
对于pointcloud.frag文件,我们实现以下内容:
#version 150 core
in vec2 UV;
out vec4 color;
uniform sampler2D textureSampler;
in vec4 color_based_on_position;
void main(){
//blend the depth map color with RGB
color = 0.5f*texture(textureSampler, UV).rgba+0.5f*color_based_on_position;
}
最后,让我们使用main.cpp文件将所有内容组合在一起:
-
在公共文件夹中包含先决库和着色器程序头文件:
#include <stdio.h> #include <stdlib.h> //GLFW and GLEW libraries #include <GL/glew.h> #include <GLFW/glfw3.h> //GLM library #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include "../common/shader.hpp" #include "../common/texture.hpp" #include "../common/controls.hpp" #include "../common/common.h" #include <fstream> -
为 GLFW 窗口创建一个全局变量:
GLFWwindow* window; -
定义输入深度数据集的宽度和高度以及其他用于渲染的窗口/相机属性:
const int WINDOWS_WIDTH = 640; const int WINDOWS_HEIGHT = 480; const int IMAGE_WIDTH = 320; const int IMAGE_HEIGHT = 240; float z_offset = 0.0f; float rotateY = 0.0f; float rotateX = 0.0f; -
定义辅助函数以解析原始深度和颜色数据:
unsigned short *readDepthFrame(const char *file_path){ int depth_buffer_size = DEPTH_WIDTH*DEPTH_HEIGHT*sizeof(unsigned short); unsigned short *depth_frame = (unsigned short*)malloc(depth_buffer_size); char *depth_frame_pointer = (char*)depth_frame; //read the binary file ifstream myfile; myfile.open (file_path, ios::binary | ios::in); myfile.read(depth_frame_pointer, depth_buffer_size); return depth_frame; } unsigned char *readColorFrame(const char *file_path){ int color_buffer_size = DEPTH_WIDTH*DEPTH_HEIGHT*sizeof(unsigned char)*3; unsigned char *color_frame = (unsigned char*)malloc(color_buffer_size); //read the binary file ifstream myfile; myfile.open (file_path, ios::binary | ios::in); myfile.read((char *)color_frame, color_buffer_size); return color_frame; } -
创建回调函数以处理按键:
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { if (action != GLFW_PRESS && action != GLFW_REPEAT) return; switch (key) { case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(window, GL_TRUE); break; case GLFW_KEY_SPACE: rotateX=0; rotateY=0; break; case GLFW_KEY_Z: rotateX+=0.01; break; case GLFW_KEY_X: rotateX-=0.01; break; case GLFW_KEY_A: rotateY+=0.01; break; case GLFW_KEY_S: rotateY-=0.01; break; default: break; } } -
使用 GLFW 库的初始化启动主程序:
int main(int argc, char **argv) { if(!glfwInit()){ fprintf( stderr, "Failed to initialize GLFW\n" ); exit(EXIT_FAILURE); } -
设置 GLFW 窗口:
glfwWindowHint(GLFW_SAMPLES, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); -
创建 GLFW 窗口对象并使其对调用线程当前:
g_window = glfwCreateWindow(WINDOWS_WIDTH, WINDOWS_HEIGHT, "Chapter 5 - 3D Point Cloud Rendering", NULL, NULL); if(!g_window){ fprintf( stderr, "Failed to open GLFW window. If you have an Intel GPU, they are not 3.3 compatible. Try the 2.1 version of the tutorials.\n" ); glfwTerminate(); exit(EXIT_FAILURE); } glfwMakeContextCurrent(g_window); glfwSwapInterval(1); -
初始化 GLEW 库并包含对实验性驱动程序的支持:
glewExperimental = true; if (glewInit() != GLEW_OK) { fprintf(stderr, "Final to Initialize GLEW\n"); glfwTerminate(); exit(EXIT_FAILURE); } -
设置键盘回调:
glfwSetInputMode(g_window,GLFW_STICKY_KEYS,GL_TRUE); glfwSetKeyCallback(g_window, key_callback); -
设置着色器程序:
GLuint program_id = LoadShaders("pointcloud.vert", "pointcloud.frag"); -
为所有深度像素创建顶点(x,y,z):
GLfloat *g_vertex_buffer_data = (GLfloat*)malloc(IMAGE_WIDTH*IMAGE_HEIGHT * 3*sizeof(GLfloat)); GLfloat *g_uv_buffer_data = (GLfloat*)malloc(IMAGE_WIDTH*IMAGE_HEIGHT * 2*sizeof(GLfloat)); -
使用先前定义的辅助函数读取原始数据:
unsigned short *depth_frame = readDepthFrame("depth_frame0.bin"); unsigned char *color_frame = readColorFrame("color_frame0.bin"); -
将颜色信息加载到纹理对象中:
GLuint texture_id = loadRGBImageToTexture(color_frame, IMAGE_WIDTH, IMAGE_HEIGHT); -
根据深度图在真实空间中创建一组顶点,并定义颜色映射的 UV 映射:
//divided by two due to 320x240 instead of 640x480 resolution float cx = 320.0f/2.0f; float cy = 240.0f/2.0f; float fx = 574.0f/2.0f; float fy = 574.0f/2.0f; for(int y=0; y<IMAGE_HEIGHT; y++){ for(int x=0; x<IMAGE_WIDTH; x++){ int index = y*IMAGE_WIDTH+x; float depth_value = (float)depth_frame[index]/1000.0f; //in meter int ver_index = index*3; int uv_index = index*2; if(depth_value != 0){ g_vertex_buffer_data[ver_index+0] = ((float)x- cx)*depth_value/fx; g_vertex_buffer_data[ver_index+1] = ((float)y- cy)*depth_value/fy; g_vertex_buffer_data[ver_index+2] = -depth_value; g_uv_buffer_data[uv_index+0] = (float)x/IMAGE_WIDTH; g_uv_buffer_data[uv_index+1] = (float)y/IMAGE_HEIGHT; } } } //Enable depth test to ensure occlusion: //uncommented glEnable(GL_DEPTH_TEST); -
获取各种统一和属性变量的位置:
GLuint matrix_id = glGetUniformLocation(program_id, "MVP"); GLuint texture_sampler_id = glGetUniformLocation(program_id, "textureSampler"); GLint attribute_vertex, attribute_uv; attribute_vertex = glGetAttribLocation(program_id, "vertexPosition_modelspace"); attribute_uv = glGetAttribLocation(program_id, "vertexUV"); -
生成顶点数组对象:
GLuint vertex_array_id; glGenVertexArrays(1, &vertex_array_id); glBindVertexArray(vertex_array_id); -
初始化顶点缓冲区内存:
GLuint vertex_buffer; glGenBuffers(1, &vertex_buffer); glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); glBufferData(GL_ARRAY_BUFFER, IMAGE_WIDTH*IMAGE_HEIGHT*2* sizeof(GLfloat), g_uv_buffer_data, GL_STATIC_DRAW); -
创建并绑定 UV 缓冲区内存:
GLuint uv_buffer; glGenBuffers(1, &uv_buffer); glBindBuffer(GL_ARRAY_BUFFER, uv_buffer); glBufferData(GL_ARRAY_BUFFER, IMAGE_WIDTH*IMAGE_HEIGHT*3* sizeof(GLfloat), g_vertex_buffer_data, GL_STATIC_DRAW); -
使用我们的着色器程序:
glUseProgram(program_id); -
在纹理单元 0 中绑定纹理:
glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture_id); glUniform1i(texture_sampler_id, 0); -
为顶点和 UV 映射设置属性缓冲区:
glEnableVertexAttribArray(attribute_vertex); glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); glVertexAttribPointer(attribute_vertex, 3, GL_FLOAT, GL_FALSE, 0, (void*)0); glEnableVertexAttribArray(attribute_uv); glBindBuffer(GL_ARRAY_BUFFER, uv_buffer); glVertexAttribPointer(attribute_uv, 2, GL_FLOAT, GL_FALSE, 0, (void*)0); -
运行绘制函数和循环:
do{ //clear the screen glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(1.0f, 1.0f, 1.0f, 0.0f); //compute the MVP matrix from keyboard and mouse input computeViewProjectionMatrices(g_window); //get the View and Model Matrix and apply to the rendering glm::mat4 projection_matrix = getProjectionMatrix(); glm::mat4 view_matrix = getViewMatrix(); glm::mat4 model_matrix = glm::mat4(1.0); model_matrix = glm::rotate(model_matrix, glm::pi<float>() * rotateY, glm::vec3(0.0f, 1.0f, 0.0f)); model_matrix = glm::rotate(model_matrix, glm::pi<float>() * rotateX, glm::vec3(1.0f, 0.0f, 0.0f)); glm::mat4 mvp = projection_matrix * view_matrix * model_matrix; //send our transformation to the currently bound //shader in the "MVP" uniform variable glUniformMatrix4fv(matrix_id, 1, GL_FALSE, &mvp[0][0]); glPointSize(2.0f); //draw all points in space glDrawArrays(GL_POINTS, 0, IMAGE_WIDTH*IMAGE_HEIGHT); //swap buffers glfwSwapBuffers(g_window); glfwPollEvents(); } // Check if the ESC key was pressed or the window was closed while(!glfwWindowShouldClose(g_window) && glfwGetKey(g_window, GLFW_KEY_ESCAPE )!=GLFW_PRESS); -
清理并退出程序:
glDisableVertexAttribArray(attribute_vertex); glDisableVertexAttribArray(attribute_uv); glDeleteBuffers(1, &vertex_buffer); glDeleteBuffers(1, &uv_buffer); glDeleteProgram(program_id); glDeleteTextures(1, &texture_id); glDeleteVertexArrays(1, &vertex_array_id); glfwDestroyWindow(g_window); glfwTerminate(); exit(EXIT_SUCCESS); } -
在
texture.cpp中,我们基于上一章实现了额外的图像加载函数:/* Handle loading images to texture memory and setting up the parameters */ GLuint loadRGBImageToTexture(const unsigned char * image_buffer, int width, int height){ int channels; GLuint textureID=0; textureID=initializeTexture(image_buffer, width, height, GL_RGB); return textureID; } GLuint initializeTexture(const unsigned char *image_data, int width, int height, GLenum input_format){ GLuint textureID=0; //for the first time we create the image, //create one texture element glGenTextures(1, &textureID); //bind the one element glBindTexture(GL_TEXTURE_2D, textureID); glPixelStorei(GL_UNPACK_ALIGNMENT,1); /* Specify the target texture. Parameters describe the format and type of image data */ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, input_format, GL_UNSIGNED_BYTE, image_data); /* Set the magnification method to linear, which returns an weighted average of 4 texture elements */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); /* Set the magnification method to linear, which //returns an weighted average of 4 texture elements */ //closest to the center of the pixel glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); /* Choose the mipmap that most closely matches the size of the pixel being textured and use the GL_NEAREST criterion (texture element nearest to the center of the pixel) to produce texture value. */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glGenerateMipmap(GL_TEXTURE_2D); return textureID; } -
在
texture.hpp中,我们简单地定义了函数原型:GLuint loadRGBImageToTexture(const unsigned char *image_data, int width, int height); GLuint initializeTexture(const unsigned char *image_data, int width, int height, GLenum input_format = GL_RGBA); -
在
control.cpp中,我们使用以下代码修改computeViewProjectionMatrices函数以支持额外的平移控制://initial position of the camera glm::vec3 g_position = glm::vec3( 0, 0, 3.0 ); const float speed = 3.0f; // 3 units / second float g_initial_fov = glm::pi<float>()*0.25f; //compute the view matrix and projection matrix based on //user input void computeViewProjectionMatrices(GLFWwindow* window){ static double last_time = glfwGetTime(); // Compute time difference between current and last frame double current_time = glfwGetTime(); float delta_time = float(current_time - last_time); int width, height; glfwGetWindowSize(window, &width, &height); //direction vector for movement glm::vec3 direction_z(0, 0, -0.5); glm::vec3 direction_y(0, 0.5, 0); glm::vec3 direction_x(0.5, 0, 0); //up vector glm::vec3 up = glm::vec3(0,-1,0); if (glfwGetKey( window, GLFW_KEY_UP ) == GLFW_PRESS){ g_position += direction_y * delta_time * speed; } else if (glfwGetKey( window, GLFW_KEY_DOWN ) == GLFW_PRESS){ g_position -= direction_y * delta_time * speed; } else if (glfwGetKey( window, GLFW_KEY_RIGHT ) == GLFW_PRESS){ g_position += direction_z * delta_time * speed; } else if (glfwGetKey( window, GLFW_KEY_LEFT ) == GLFW_PRESS){ g_position -= direction_z * delta_time * speed; } else if (glfwGetKey( window, GLFW_KEY_PERIOD ) == GLFW_PRESS){ g_position -= direction_x * delta_time * speed; } else if (glfwGetKey( window, GLFW_KEY_COMMA ) == GLFW_PRESS){ g_position += direction_x * delta_time * speed; } /* update projection matrix: Field of View, aspect ratio, display range : 0.1 unit <-> 100 units */ g_projection_matrix = glm::perspective(g_initial_fov, (float)width/(float)height, 0.01f, 100.0f); // update the view matrix g_view_matrix = glm::lookAt( g_position, // camera position g_position+direction_z, //viewing direction up // up direction ); last_time = current_time; }
现在我们已经创建了一种以 3D 飞行浏览风格可视化深度传感器信息的方法;以下图显示了以帧中央位置为虚拟摄像机的点云渲染:

通过旋转和移动虚拟摄像机,我们可以从不同的视角创建场景的各种表示。从场景的鸟瞰图或侧视图,我们可以分别从这两个角度更明显地看到面部和手的轮廓:

这是同一场景的侧视图:

通过在重映射循环中添加一个额外的条件,我们可以渲染场景中的未知区域(空洞),这些区域由于遮挡、视场限制、范围限制以及/或表面特性(如反射率)等原因,深度相机无法重建:
if(depth_value != 0){
g_vertex_buffer_data[ver_index+0] = ((float)x-cx)*depth_value/fx;
g_vertex_buffer_data[ver_index+1] = ((float)y-cy)*depth_value/fy;
g_vertex_buffer_data[ver_index+2] = -depth_value;
g_uv_buffer_data[uv_index+0] = (float)x/IMAGE_WIDTH;
g_uv_buffer_data[uv_index+1] = (float)y/IMAGE_HEIGHT;
}
else{
g_vertex_buffer_data[ver_index+0] = ((float)x-cx)*0.2f/fx;
g_vertex_buffer_data[ver_index+1] = ((float)y-cy)*0.2f/fy;
g_vertex_buffer_data[ver_index+2] = 0;
}
这种条件允许我们将深度值为 0 的区域分割并投影到距离虚拟摄像机 0.2 米远的平面上,如图所示:

如何工作...
在本章中,我们利用 GLSL 管道和纹理映射技术创建了一个交互式的点云可视化工具,该工具能够使用 3D 范围感应相机捕获的场景进行 3D 导航。着色器程序还将结果与彩色图像结合以产生我们期望的效果。程序读取两个二进制图像:校准的深度图图像和 RGB 彩色图像。颜色直接使用新的 loadRGBImageToTexture() 函数加载到纹理对象中,该函数将数据从 GL_RGB 转换为 GL_RGBA。然后,根据相机的内在值以及每个像素的深度值,将深度图数据转换为基于实际坐标的点云数据,如下所示:

在这里,d 是毫米级的深度值,x 和 y 是深度值在像素(投影)空间中的位置,
和
是深度相机的原理轴,
和
是相机的焦距,而
是点云在真实世界坐标系中的位置。
在我们的例子中,我们不需要精细的对齐或注册,因为我们的可视化器使用了对内在参数的原始估计:

这些数值可以使用 OpenCV 中的相机标定工具进行估计。这些工具的详细内容超出了本章的范围。
对于我们的应用,我们提供了一组 3D 点(x,y,z)以及相应的颜色信息(r,g,b)来计算点云表示。然而,点可视化不支持动态照明和其他更高级的渲染技术。为了解决这个问题,我们可以将点云进一步扩展成网格(即,一组三角形来表示表面),这将在下一章中讨论。
第六章:使用 OpenGL 渲染立体 3D 模型
在本章中,我们将涵盖以下主题:
-
安装 Open Asset Import Library (Assimp)
-
加载第一个 Wavefront Object (.obj) 格式的 3D 模型
-
使用点、线和三角形渲染 3D 模型
-
立体 3D 渲染
简介
在本章中,我们将展示如何使用 OpenGL 将数据以惊人的立体 3D 技术可视化。立体 3D 设备越来越受欢迎,最新一代的可穿戴计算设备(如 NVIDIA、Epson 的 3D 视觉眼镜,以及最近 Meta 的增强现实 3D 眼镜)现在可以原生支持此功能。
在立体 3D 环境中可视化数据的能力为许多应用程序中数据的交互式显示提供了一个强大且高度直观的平台。例如,我们可能从模型的 3D 扫描(如建筑、工程、牙科或医学)中获取数据,并希望实时可视化或操作 3D 对象。
不幸的是,OpenGL 不提供任何加载、保存或操作 3D 模型的机制。因此,为了支持这一点,我们将把一个名为 Open Asset Import Library (Assimp) 的新库集成到我们的代码中。本章中的源代码建立在 第五章 中 OpenGL 点云渲染带纹理映射和叠加 的配方之上,即 为 3D 范围感应相机渲染点云数据。主要依赖项包括需要 OpenGL 版本 3.2 及更高版本的 GLFW 库。我们将假设您已经从早期章节安装了所有先决软件包。
安装 Open Asset Import Library (Assimp)
Assimp 是一个开源库,可以从各种 3D 模型数据格式中加载和处理 3D 几何场景。该库提供了一个统一的接口来加载许多不同的数据格式,例如 Wavefront Object (.obj)、3ds Max 3DS (.3ds) 和 Stereolithography (.stl)。此外,这个库是用可移植的、符合 ISO 标准的 C++ 编写的,因此它允许进一步的定制和长期支持。由于该库是跨平台的,我们可以很容易地按照下一节中给出的说明在 Mac OS X、Linux 以及 Windows 中安装它。
如何操作...
要获取 Assimp 3.0 的库源文件或二进制库,请直接从 Assimp 的官方网站 sourceforge.net/projects/assimp/files/assimp-3.0/ 下载。或者,对于 Linux 和 Mac OS X 用户,使用命令行界面简化下一节中描述的安装步骤。
在 Mac OS X 中,使用 MacPort 的命令行界面安装 Assimp。它自动解决所有依赖项,因此这是推荐的:
sudo port install assimp
在 Linux 中,使用 apt-get 命令界面安装 Assimp:
sudo apt-get install install libassimp-dev
安装完成后,修改 Makefile 以确保库文件通过将以下内容追加到 LIBS 变量中来链接到源文件:
`pkg-config --static --libs assimp`
分别添加到 INCLUDES 路径变量中:
`pkg-config --cflags assimp`
最终的 Makefile 如下所示,供您参考:
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/
CFILES = ../common/shader.cpp ../common/controls.cpp ../common/ObjLoader.cpp main.cpp
CFLAGS = -c
OPT = -O3
INCLUDES = -I../common -I/usr/include -I/usr/include/SOIL -I. `pkg-config --cflags glfw3` `pkg-config --cflags assimp`
LIBS = -lm -L/usr/local/lib -lGLEW `pkg-config --static --libs glfw3` `pkg-config --static --libs assimp`
CC = g++
OBJECTS=$(CFILES:.cpp=.o)
EXECUTABLE=main
all: $(CFILES) $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS)
$(CC) $(OPT) $(INCLUDES) $(OBJECTS) -o $@ $(LIBS)
.cpp.o:
$(CC) $(OPT) $(CFLAGS) $(INCLUDES) $< -o $@
clean:
rm -v -f *~ ../common/*.o *.o $(EXECUTABLE)
在 Windows 上安装 Assimp,首先,从以下链接下载二进制库:sourceforge.net/projects/assimp/files/assimp-3.0/assimp--3.0.1270-full.zip/download。
然后,我们按照以下步骤配置环境:
-
解压
assimp--3.0.1270-full.zip并将其保存到C:/Program Files (x86)/。 -
将 DLL 路径,
C:/Program Files (x86)/assimp--3.0.1270-sdk/bin/assimp_release-dll_win32,添加到 PATH 环境变量中。 -
将
CMakeLists.txt文件添加到项目中:cmake_minimum_required (VERSION 2.8) set(CMAKE_CONFIGURATION_TYPES Debug Release) set(PROGRAM_PATH "C:/Program Files \(x86\)") set(OpenCV_DIR ${PROGRAM_PATH}/opencv/build) project (code) #modify these path based on your configuration #OpenCV find_package(OpenCV REQUIRED ) INCLUDE_DIRECTORIES(${OpenCV_INCLUDE_DIRS}) INCLUDE_DIRECTORIES(${PROGRAM_PATH}/glm) INCLUDE_DIRECTORIES(${PROGRAM_PATH}/glew-1.10.0/include) LINK_DIRECTORIES(${PROGRAM_PATH}/glew-1.10.0/lib/Release/Win32) INCLUDE_DIRECTORIES(${PROGRAM_PATH}/glfw-3.0.4/include) LINK_DIRECTORIES(${PROGRAM_PATH}/glfw-3.0.4/lib) INCLUDE_DIRECTORIES(${PROGRAM_PATH}/Simple\ OpenGL\ Image\ Library/src) INCLUDE_DIRECTORIES(${PROGRAM_PATH}/assimp--3.0.1270-sdk/include/assimp) LINK_DIRECTORIES(${PROGRAM_PATH}/assimp--3.0.1270-sdk/lib/assimp_release-dll_win32) add_subdirectory (../common common) add_executable (main main.cpp) target_link_libraries (main LINK_PUBLIC shader controls texture glew32s glfw3 opengl32 assimp ObjLoader)
最后,按照 第四章 中描述的相同步骤,使用纹理映射渲染 2D 图像和视频 和 第五章 中描述的步骤,为 3D 范围感应相机渲染点云数据,生成构建文件。
参见
除了导入 3D 模型对象外,Assimp 还支持以 .obj、.stl 和 .ply 格式导出 3D 模型。通过将此库与 OpenGL 图形渲染引擎结合,我们创建了一个简单而强大的机制,用于协作或远程可视化并交换 3D 模型。在导入模型后,Assimp 库还可以处理一些 3D 场景的后处理任务(例如,将大型网格分割以克服某些 GPU 对顶点数的限制)。这些附加功能在官方网站上有文档说明,可能对高级用户感兴趣(assimp.sourceforge.net/lib_html/index.html)。
加载第一个 Wavefront Object (.obj) 格式的 3D 模型
现在,我们已准备好将 3D 对象加载器集成到我们的代码中。第一步是创建一个名为 ObjLoader 的空类,以及相应的源文件(.cpp)和头文件(.h)。此类处理与 3D 对象加载、解析和绘制相关的所有函数,使用 OpenGL 和 Assimp 库。类的头文件将包括用于处理数据结构和所有 3D 数据格式 I/O 机制的 Assimp 核心函数:
#include <cimport.h>
#include <scene.h>
#include <postprocess.h>
在 ObjLoader.h 文件中,我们为主程序提供了创建、销毁、加载和显示 3D 数据的接口。在 ObjLoader.cpp 文件中,我们实现了一套函数,使用 Assimp 的内置函数来解析场景(以网格和面为术语的 3D 对象的分层表示)。
Assimp 库可以支持各种 3D 模型数据格式;然而,在我们的示例中,我们将专注于 Wavefront Object(.obj)格式,因为它简单。.obj文件是一个简单的几何定义文件,最初由 Wavefront Technologies 开发。该文件包含图形的核心元素,如顶点、顶点位置、法线面等,并以简单的文本格式存储。由于文件以 ASCII 文本存储,我们可以轻松地打开和检查文件而无需任何解析器。例如,以下是一个面向前的正方形.obj文件:
# This is a comment.
# Front facing square.
# vertices [x, y, z]
v 0 0 0 # Bottom left.
v 1 0 0 # Bottom right.
v 1 1 0 # Top right.
v 0 1 0 # Top left.
# List of faces:
f 1 2 3 4 # Square.
如前例所示,表示对于初学者来说非常简单直观。顶点可以逐行读取和提取,然后可以修改。
在下一节中,我们将展示完整的实现过程,它允许用户加载.obj文件,将场景存储在顶点缓冲对象中,并显示场景。
如何做到这一点...
首先,我们在公共文件夹中创建ObjLoader.h文件,并附加将在我们的实现中使用的类函数定义和变量:
#ifndef OBJLOADER_H_
#define OBJLOADER_H_
/* Assimp include files. These three are usually needed. */
#include <cimport.h>
#include <scene.h>
#include <postprocess.h>
#include <common.h>
#define aisgl_min(x,y) (x<y?x:y)
#define aisgl_max(x,y) (y>x?y:x)
class ObjLoader {
public:
ObjLoader();
virtual ~ObjLoader();
int loadAsset(const char* path);
void setScale(float scale);
unsigned int getNumVertices();
void draw(const GLenum draw_mode);
void loadVertices(GLfloat *g_vertex_buffer_data);
private:
//helper functions and variables
const struct aiScene* scene;
GLuint scene_list;
aiVector3D scene_min, scene_max, scene_center;
float g_scale;
unsigned int num_vertices;
unsigned int recursiveDrawing(const struct aiNode* nd, unsigned int v_count, const GLenum);
unsigned int recursiveVertexLoading(const struct aiNode *nd, GLfloat *g_vertex_buffer_data, unsigned int v_counter);
unsigned int recursiveGetNumVertices(const struct aiNode *nd);
void get_bounding_box (aiVector3D* min, aiVector3D* max);
void get_bounding_box_for_node (const struct aiNode* nd, aiVector3D* min, aiVector3D* max, aiMatrix4x4* trafo);
};
#endif
Assimp 库中的类名前面带有前缀ai-(例如,aiScene和aiVector3D)。ObjLoader文件提供了动态加载和绘制内存中加载的对象的方法。它还处理简单的动态缩放,以便对象可以适应屏幕。
在源文件ObjLoader.cpp中,我们首先添加类的构造函数:
#include <ObjLoader.h>
ObjLoader::ObjLoader() {
g_scale=1.0f;
scene = NULL; //empty scene
scene_list = 0;
num_vertices = 0;
}
然后,我们使用aiImportFile函数实现文件加载机制。场景被处理以提取边界框大小,以便正确缩放以适应屏幕。然后,场景的顶点数被用于允许在后续步骤中动态创建顶点缓冲区:
int ObjLoader::loadAsset(const char *path){
scene = aiImportFile(path, aiProcessPreset_TargetRealtime_MaxQuality);
if (scene) {
get_bounding_box(&scene_min,&scene_max);
scene_center.x = (scene_min.x + scene_max.x) / 2.0f;
scene_center.y = (scene_min.y + scene_max.y) / 2.0f;
scene_center.z = (scene_min.z + scene_max.z) / 2.0f;
printf("Loaded file %s\n", path);
g_scale =4.0/(scene_max.x-scene_min.x);
printf("Scaling: %lf", g_scale);
num_vertices = recursiveGetNumVertices(scene->mRootNode);
printf("This Scene has %d vertices.\n", num_vertices);
return 0;
}
return 1;
}
为了提取绘制场景所需的顶点总数,我们递归地遍历树形层次结构中的每个节点。实现需要一个简单的递归函数,该函数返回每个节点中的顶点数,然后根据所有节点的总和计算总数:
unsigned int ObjLoader::recursiveGetNumVertices(const struct aiNode *nd){
unsigned int counter=0;
unsigned int i;
unsigned int n = 0, t;
// draw all meshes assigned to this node
for (; n < nd->mNumMeshes; ++n) {
const struct aiMesh* mesh = scene-> mMeshes[nd->mMeshes[n]];
for (t = 0; t < mesh->mNumFaces; ++t) {
const struct aiFace* face = &mesh-> mFaces[t];
counter+=3*face->mNumIndices;
}
printf("recursiveGetNumVertices: mNumFaces %d\n", mesh->mNumFaces);
}
//traverse all children nodes
for (n = 0; n < nd->mNumChildren; ++n) {
counter+=recursiveGetNumVertices(nd-> mChildren[n]);
}
printf("recursiveGetNumVertices: counter %d\n", counter);
return counter;
}
同样,为了计算边界框的大小(即包含场景所需的最小体积),我们递归地检查每个节点并提取距离对象中心最远的点:
void ObjLoader::get_bounding_box (aiVector3D* min, aiVector3D* max)
{
aiMatrix4x4 trafo;
aiIdentityMatrix4(&trafo);
min->x = min->y = min->z = 1e10f;
max->x = max->y = max->z = -1e10f;
get_bounding_box_for_node(scene-> mRootNode,min,max,&trafo);
}
void ObjLoader::get_bounding_box_for_node (const struct aiNode* nd, aiVector3D* min, aiVector3D* max, aiMatrix4x4* trafo)
{
aiMatrix4x4 prev;
unsigned int n = 0, t;
prev = *trafo;
aiMultiplyMatrix4(trafo,&nd->mTransformation);
for (; n < nd->mNumMeshes; ++n) {
const struct aiMesh* mesh = scene-> mMeshes[nd->mMeshes[n]];
for (t = 0; t < mesh->mNumVertices; ++t) {
aiVector3D tmp = mesh->mVertices[t];
aiTransformVecByMatrix4(&tmp,trafo);
min->x = aisgl_min(min->x,tmp.x);
min->y = aisgl_min(min->y,tmp.y);
min->z = aisgl_min(min->z,tmp.z);
max->x = aisgl_max(max->x,tmp.x);
max->y = aisgl_max(max->y,tmp.y);
max->z = aisgl_max(max->z,tmp.z);
}
}
for (n = 0; n < nd->mNumChildren; ++n) {
get_bounding_box_for_node(nd-> mChildren[n],min,max,trafo);
}
*trafo = prev;
}
生成的边界框使我们能够计算缩放因子并将对象坐标重新居中,以适应可查看的屏幕。
在main.cpp文件中,我们首先通过插入头文件来集成代码:
#include <ObjLoader.h>
然后,我们在主函数中创建ObjLoader对象,并使用给定的文件名加载模型:
ObjLoader *obj_loader = new ObjLoader();
int result = 0;
if(argc > 1){
result = obj_loader->loadAsset(argv[1]);
}
else{
result = obj_loader-> loadAsset("dragon.obj");
}
if(result){
fprintf(stderr, "Final to Load the 3D file\n");
glfwTerminate();
exit(EXIT_FAILURE);
}
ObjLoader包含一个算法,该算法递归地检查每个网格,计算场景的边界框和顶点数。然后,我们根据顶点数动态分配顶点缓冲区并将顶点加载到缓冲区中:
GLfloat *g_vertex_buffer_data = (GLfloat*)
malloc (obj_loader->getNumVertices()*sizeof(GLfloat));
//load the scene data to the vertex buffer
obj_loader->loadVertices(g_vertex_buffer_data);
现在,我们已经有了所有必要的顶点信息,可以使用我们用 OpenGL 编写的自定义着色器程序进行显示。
它是如何工作的...
Assimp 提供了高效加载和解析 3D 数据格式的机制。我们利用的关键特性是以分层方式导入 3D 对象,这使得我们可以统一我们的渲染管线,无论 3D 格式如何。aiImportFile函数读取指定的文件,并将其内容以aiScene结构返回。此函数的第二个参数指定了在成功导入后要执行的可选后处理步骤。aiProcessPreset_TargetRealtime_MaxQuality标志是一个预定义变量,它组合了以下参数集:
( \
aiProcessPreset_TargetRealtime_Quality | \
aiProcess_FindInstances | \
aiProcess_ValidateDataStructure | \
aiProcess_OptimizeMeshes | \
aiProcess_Debone | \
0 )
这些后处理选项的详细描述请参阅assimp.sourceforge.net/lib_html/postprocess_8h.html#a64795260b95f5a4b3f3dc1be4f52e410。高级用户可以查看每个选项,并了解是否需要启用或禁用这些功能。
在这一点上,我们有一个简单的机制来将图形加载到 Assimp 的aiScene对象中,显示边界框大小,以及提取渲染场景所需的顶点数量。接下来,我们将创建一个简单的着色器程序以及各种绘图函数,以不同的风格可视化内容。简而言之,通过将其与 OpenGL 图形渲染引擎集成,我们现在有了一种灵活的方式来使用我们在前几章中开发的各种工具来可视化 3D 模型。
使用点、线和三角形渲染 3D 模型
在导入 3D 模型之后的下一步是,以直观和美观的方式在屏幕上显示内容。许多复杂的场景由多个表面(网格)和许多顶点组成。在前一章中,我们实现了一个简单的着色器程序,根据热图在各个深度值上可视化点云。在本节中,我们将使用非常简单的原语(点、线和三角形)以及透明度来创建类似骨骼的渲染效果。
如何做到这一点...
我们将继续实现ObjLoader类,以支持加载顶点并在场景中的每个网格上绘制图形。
在ObjLoader.cpp的源文件中,我们添加了一个递归函数来从场景中提取所有顶点并将它们存储在一个单独的顶点缓冲区数组中。这使我们能够减少要管理的顶点缓冲区数量,从而降低代码的复杂性:
void ObjLoader::loadVertices(GLfloat *g_vertex_buffer_data)
{
recursiveVertexLoading(scene->mRootNode, g_vertex_buffer_data, 0);
}
unsigned int ObjLoader::recursiveVertexLoading (const struct aiNode *nd, GLfloat *g_vertex_buffer_data, unsigned int v_counter)
{
unsigned int i;
unsigned int n = 0, t;
/* save all data to the vertex array, perform offset and scaling to reduce the computation */
for (; n < nd->mNumMeshes; ++n) {
const struct aiMesh* mesh = scene-> mMeshes[nd->mMeshes[n]];
for (t = 0; t < mesh->mNumFaces; ++t) {
const struct aiFace* face = &mesh->mFaces[t];
for(i = 0; i < face->mNumIndices; i++) {
int index = face->mIndices[i];
g_vertex_buffer_data[v_counter]=
(mesh->mVertices[index].x-scene_center.x)*g_scale;
g_vertex_buffer_data[v_counter+1]=
(mesh->mVertices[index].y-scene_center.y)*g_scale;
g_vertex_buffer_data[v_counter+2]=
(mesh->mVertices[index].z-scene_center.z)*g_scale;
v_counter+=3;
}
}
}
//traverse all children nodes
for (n = 0; n < nd->mNumChildren; ++n) {
v_counter = recursiveVertexLoading(nd-> mChildren[n], g_vertex_buffer_data, v_counter);
}
return v_counter;
}
要绘制图形,我们遍历从根节点开始的aiScene对象,一次绘制一个网格:
void ObjLoader::draw(const GLenum draw_mode){
recursiveDrawing(scene->mRootNode, 0, draw_mode);
}
unsigned int ObjLoader::recursiveDrawing(const struct aiNode* nd, unsigned int v_counter, const GLenum draw_mode){
/* break up the drawing, and shift the pointer to draw different parts of the scene */
unsigned int i;
unsigned int n = 0, t;
unsigned int total_count = v_counter;
// draw all meshes assigned to this node
for (; n < nd->mNumMeshes; ++n) {
unsigned int count=0;
const struct aiMesh* mesh = scene-> mMeshes[nd->mMeshes[n]];
for (t = 0; t < mesh->mNumFaces; ++t) {
const struct aiFace* face = &mesh-> mFaces[t];
count+=3*face->mNumIndices;
}
glDrawArrays(draw_mode, total_count, count);
total_count+=count;
}
v_counter = total_count;
// draw all children nodes recursively
for (n = 0; n < nd->mNumChildren; ++n) {
v_counter = recursiveDrawing(nd-> mChildren[n], v_counter, draw_mode);
}
return v_counter;
}
在顶点着色器pointcloud.vert中,我们根据顶点在空间中的位置计算顶点的颜色。重映射算法创建了一个空间中对象的热图表示,这为人类眼睛提供了重要的深度线索(深度感知):
#version 150 core
// Input
in vec3 vertexPosition_modelspace;
// Output
out vec4 color_based_on_position;
// Uniform/constant variable.
uniform mat4 MVP;
//heat map generator
vec4 heatMap(float v, float vmin, float vmax){
float dv;
float r=1.0f, g=1.0f, b=1.0f;
if (v < vmin)
v = vmin;
if (v > vmax)
v = vmax;
dv = vmax - vmin;
if (v < (vmin + 0.25f * dv)) {
r = 0.0f;
g = 4.0f * (v - vmin) / dv;
} else if (v < (vmin + 0.5f * dv)) {
r = 0.0f;
b = 1.0f + 4.0f * (vmin + 0.25f * dv - v) / dv;
} else if (v < (vmin + 0.75f * dv)) {
r = 4.0f * (v - vmin - 0.5f * dv) / dv;
b = 0.0f;
} else {
g = 1.0f + 4.0f * (vmin + 0.75f * dv - v) / dv;
b = 0.0f;
}
//with 0.2 transparency - can be dynamic if we pass in variables
return vec4(r, g, b, 0.2f);
}
void main () {
// Output position of the vertex, in clip space : MVP * position
gl_Position = MVP * vec4(vertexPosition_modelspace, 1.0f);
// remapping the color based on the depth (z) value.
color_based_on_position = heatMap(vertexPosition_modelspace.z, -1.0f, 1.0f);
}
顶点着色器通过color_based_on_position变量将热图颜色信息传递给片段着色器。然后,最终颜色通过片段着色器(pointcloud.frag)直接返回,无需进一步处理。这样一个简单管道的实现如下所示:
#version 150 core
out vec4 color;
in vec4 color_based_on_position;
void main(){
color = color_based_on_position;
}
最后,我们以各种风格绘制场景:线条、点和三角形(表面)带有透明度。以下是在绘图循环中的代码片段:
//draw the left eye (but full screen)
glViewport(0, 0, width, height);
//compute the MVP matrix from the IOD and virtual image plane distance
computeStereoViewProjectionMatrices(g_window, IOD, depthZ, true);
//get the View and Model Matrix and apply to the rendering
glm::mat4 projection_matrix = getProjectionMatrix();
glm::mat4 view_matrix = getViewMatrix();
glm::mat4 model_matrix = glm::mat4(1.0);
model_matrix = glm::translate(model_matrix, glm::vec3(0.0f, 0.0f, -depthZ));
model_matrix = glm::rotate(model_matrix,
glm::pi<float>()*rotateY, glm::vec3(0.0f, 1.0f, 0.0f));
model_matrix = glm::rotate(model_matrix,
glm::pi<float>()*rotateX, glm::vec3(1.0f, 0.0f, 0.0f));
glm::mat4 mvp = projection_matrix * view_matrix * model_matrix;
//send our transformation to the currently bound shader,
//in the "MVP" uniform variable
glUniformMatrix4fv(matrix_id, 1, GL_FALSE, &mvp[0][0]);
/* render scene with different modes that can be enabled separately to get different effects */
obj_loader->draw(GL_TRIANGLES);
if(drawPoints)
obj_loader->draw(GL_POINTS);
if(drawLines)
obj_loader->draw(GL_LINES);
下面的系列截图展示了我们可以使用自定义着色器实现的令人愉悦的结果。基于热图着色器的深度位置颜色映射提供了强烈的深度感知,帮助我们更容易地理解物体的 3D 结构。此外,我们可以分别启用和禁用各种渲染选项以实现各种效果。例如,同一个物体可以用不同的风格渲染:点、线和三角形(表面)带有透明度。
为了演示效果,我们首先仅用点渲染两个物体。第一个例子是一个龙模型:

第二个例子是一个建筑模型:

基于点的渲染风格非常适合可视化具有未知关系或分布的大量数据集。接下来,我们将仅用线条渲染相同的物体:

这里是仅用线条渲染的建筑模型:

使用线条后,我们现在可以更轻松地看到物体的结构。这种渲染技术非常适合简单的结构,例如建筑模型和其他定义良好的模型。此外,我们还可以同时启用点和线条来渲染场景,如下所示:

这里是启用点和线条渲染的建筑模型:

点和线的组合为物体的结构提供了额外的视觉提示(即强调交点)。最后,我们以所有选项启用的方式渲染场景:点、线和三角形(表面)带有透明度:

这里是使用点、线和三角形(表面)以及透明度渲染的建筑模型:

启用所有选项的最终组合提供了对物体体积以及整体 3D 结构的更直观的可视化。或者,我们也可以启用深度测试,以不透明的方式渲染实体模型:

如何在运行时启用/禁用这些选项的说明在源代码中有文档记录。
它是如何工作的...
通过结合 Assimp 库和 OpenGL,我们现在可以在屏幕上动态加载 3D 模型,并通过基于 OpenGL 的交互式可视化工具创建视觉上吸引人的 3D 效果。
在 ObjLoader.cpp 中,loadVertices 函数将场景转换为单个顶点缓冲区数组,以减少内存管理的复杂性。特别是,这种方法减少了 OpenGL 内存复制的次数和渲染侧(即 glBufferData 和 glGenBuffers)的内存缓冲区数量。此外,加载函数根据边界框处理顶点的缩放和居中。这一步至关重要,因为大多数 3D 格式都没有标准化它们的坐标系。
接下来,ObjLoader.cpp 中的 draw 函数遍历 aiScene 对象,并使用顶点缓冲区绘制场景的每个部分。在基于点的渲染情况下,我们可以跳过此步骤,直接使用 glDrawArray 绘制整个数组,因为相邻顶点之间没有依赖关系。
顶点着色器(pointcloud.vert)包含了热图颜色生成器的实现。heatmap 函数接受三个参数:输入值(即深度或 z 值)、最小值和最大值。它返回 RGBA 格式的热图颜色表示。
在绘图循环内部,computeStereoViewProjectionMatrices 函数构建视图和投影矩阵。详细内容将在下一节中解释。
最后,我们可以混合和匹配各种渲染技术;例如,只为基于骨骼的渲染启用点和线。通过支持对象的旋转或平移,可以轻松添加各种深度视觉提示,如遮挡和运动视差。为了进一步提高结果,可以根据应用需求添加其他渲染技术,如光照或阴影。
参见
除了 .obj 文件外,Assimp 库还支持许多文件格式。例如,我们可以将 .stl 文件加载到我们的系统中,而无需更改任何源代码。
要下载更多 3D 模型,请访问各种 3D 模型共享网站,如 Makerbot ThingiVerse (www.thingiverse.com/) 或 Turbosquid (www.turbosquid.com/):

立体 3D 渲染
随着消费电子的最新趋势和可穿戴计算技术的进步,3D 电视和 3D 眼镜变得越来越普遍。在市场上,目前有许多硬件选项允许我们使用立体 3D 技术可视化信息。一种常见的格式是左右并排 3D,许多 3D 眼镜都支持这种格式,因为每只眼睛都从不同的视角看到同一场景的图像。在 OpenGL 中,创建左右并排 3D 渲染需要非对称调整以及视口调整(即要渲染的区域)——非对称截锥体平行投影或相当于摄影中的镜头偏移。这项技术不引入垂直视差,并且在立体渲染中得到广泛应用。为了说明这个概念,以下图显示了用户从右眼看到的场景的几何形状:

眼内距离(IOD)是两眼之间的距离。从图中我们可以看出,截锥体偏移表示非对称截锥体调整的倾斜/偏移量。同样,对于左眼图像,我们使用镜像设置进行变换。该设置的实现将在下一节中描述。
如何操作...
以下代码说明了构建用于立体 3D 可视化的投影和视图矩阵的步骤。该代码使用眼内距离、图像平面距离和近裁剪平面距离来计算适当的截锥体偏移值。在源文件common/controls.cpp中,我们添加了立体 3D 矩阵设置的实现:
void computeStereoViewProjectionMatrices(GLFWwindow* window, float IOD, float depthZ, bool left_eye){
int width, height;
glfwGetWindowSize(window, &width, &height);
//up vector
glm::vec3 up = glm::vec3(0,-1,0);
glm::vec3 direction_z(0, 0, -1);
//mirror the parameters with the right eye
float left_right_direction = -1.0f;
if(left_eye)
left_right_direction = 1.0f;
float aspect_ratio = (float)width/(float)height;
float nearZ = 1.0f;
float farZ = 100.0f;
double frustumshift = (IOD/2)*nearZ/depthZ;
float top = tan(g_initial_fov/2)*nearZ;
float right =
aspect_ratio*top+frustumshift*left_right_direction;
//half screen
float left = -aspect_ratio*top+frustumshift*left_right_direction;
float bottom = -top;
g_projection_matrix = glm::frustum(left, right, bottom, top, nearZ, farZ);
// update the view matrix
g_view_matrix =
glm::lookAt(
g_position-direction_z+
glm::vec3(left_right_direction*IOD/2, 0, 0),
//eye position
g_position+
glm::vec3(left_right_direction*IOD/2, 0, 0),
//centre position
up //up direction
);
在main.cpp中的渲染循环中,我们为每只眼睛(左和右)定义视口,并相应地设置投影和视图矩阵。对于每只眼睛,我们根据前一个图所示,将我们的相机位置沿眼内距离的一半进行平移:
if(stereo){
//draw the LEFT eye, left half of the screen
glViewport(0, 0, width/2, height);
//computes the MVP matrix from the IOD and virtual image plane distance
computeStereoViewProjectionMatrices(g_window, IOD, depthZ, true);
//gets the View and Model Matrix and apply to the rendering
glm::mat4 projection_matrix = getProjectionMatrix();
glm::mat4 view_matrix = getViewMatrix();
glm::mat4 model_matrix = glm::mat4(1.0);
model_matrix = glm::translate(model_matrix, glm::vec3(0.0f, 0.0f, -depthZ));
model_matrix = glm::rotate(model_matrix, glm::pi<float>() * rotateY, glm::vec3(0.0f, 1.0f, 0.0f));
model_matrix = glm::rotate(model_matrix, glm::pi<float>() * rotateX, glm::vec3(1.0f, 0.0f, 0.0f));
glm::mat4 mvp = projection_matrix * view_matrix * model_matrix;
//sends our transformation to the currently bound shader,
//in the "MVP" uniform variable
glUniformMatrix4fv(matrix_id, 1, GL_FALSE, &mvp[0][0]);
//render scene, with different drawing modes
if(drawTriangles)
obj_loader->draw(GL_TRIANGLES);
if(drawPoints)
obj_loader->draw(GL_POINTS);
if(drawLines)
obj_loader->draw(GL_LINES);
//Draw the RIGHT eye, right half of the screen
glViewport(width/2, 0, width/2, height);
computeStereoViewProjectionMatrices(g_window, IOD, depthZ, false);
projection_matrix = getProjectionMatrix();
view_matrix = getViewMatrix();
model_matrix = glm::mat4(1.0);
model_matrix = glm::translate(model_matrix, glm::vec3(0.0f, 0.0f, -depthZ));
model_matrix = glm::rotate(model_matrix, glm::pi<float>() * rotateY, glm::vec3(0.0f, 1.0f, 0.0f));
model_matrix = glm::rotate(model_matrix, glm::pi<float>() * rotateX, glm::vec3(1.0f, 0.0f, 0.0f));
mvp = projection_matrix * view_matrix * model_matrix;
glUniformMatrix4fv(matrix_id, 1, GL_FALSE, &mvp[0][0]);
if(drawTriangles)
obj_loader->draw(GL_TRIANGLES);
if(drawPoints)
obj_loader->draw(GL_POINTS);
if(drawLines)
obj_loader->draw(GL_LINES);
}
最终渲染结果由显示器的两侧的两个单独图像组成,并且请注意,每个图像都通过一个缩放因子为二的水平压缩。对于某些显示系统,显示器的每一侧都需要根据显示器的规格保持相同的纵横比。
这里是使用立体 3D 渲染在真实 3D 中显示的相同模型的最终截图:

这是建筑模型的立体 3D 渲染:

工作原理...
立体 3D 渲染技术基于平行轴和非对称截锥体透视投影原理。简单来说,我们为每只眼睛渲染了一个单独的图像,就像物体从不同的眼睛位置看到,但视图在同一平面上。参数如眼内距离和截锥体偏移可以动态调整,以提供所需的 3D 立体效果。
例如,通过增加或减少截锥体不对称参数,物体看起来就像是在屏幕的前面或后面移动。默认情况下,零视差平面被设置为视体积的中间。也就是说,物体被设置得使其中心位置位于屏幕水平,物体的某些部分将出现在屏幕的前面或后面。通过增加截锥体不对称(即正视差),场景看起来就像被推到了屏幕后面。同样,通过减少截锥体不对称(即负视差),场景看起来就像被拉到了屏幕前面。
glm::frustum函数设置投影矩阵,我们实现了图中所示的不对称截锥体投影概念。然后,我们使用glm::lookAt函数根据我们选择的 IOP 值调整眼睛位置。
为了实现图像并排显示,我们使用glViewport函数限制图形可以渲染的区域。该函数基本上执行一个仿射变换(即缩放和平移),将归一化设备坐标映射到窗口坐标。请注意,最终结果是一个并排图像,其中图形在垂直方向上按因子二缩放(或水平方向上压缩)。根据硬件配置,我们可能需要调整纵横比。
当前实现支持并排 3D,这在大多数可穿戴的增强现实(AR)或虚拟现实(VR)眼镜中很常见。从根本上说,渲染技术,即我们章节中描述的不对称截锥体透视投影,是平台无关的。例如,我们在 Meta 1 开发者套件(www.getameta.com/products)上成功测试了我们的实现,并在光学透视立体 3D 显示器上渲染了最终结果:

下面是 Meta 1 开发者套件的前视图,显示了光学透视立体 3D 显示器和 3D 范围感应相机(在第五章中介绍,3D 范围感应相机的点云数据渲染),Rendering of Point Cloud Data for 3D Range-sensing Cameras):

结果如下所示,立体 3D 图形被渲染到真实世界中(这构成了增强现实的基础):


在接下来的章节中,我们将转向越来越强大且无处不在的移动平台,并介绍如何使用移动设备内置的运动传感器,通过 OpenGL 以有趣的方式可视化数据。关于实现增强现实应用的更多细节将在第九章 基于增强现实的移动或可穿戴平台可视化 中介绍。
参见
此外,我们可以轻松扩展我们的代码以支持基于快门眼镜的 3D 显示器,通过利用四缓冲 OpenGL API(参考glDrawBuffer函数中的GL_BACK_RIGHT和GL_BACK_LEFT标志)。不幸的是,这种 3D 格式需要特定的硬件同步,并且通常需要更高的帧率显示(例如,120Hz)以及专业显卡。有关如何在您的应用程序中实现立体 3D 的更多信息,请参阅www.nvidia.com/content/GTC-2010/pdfs/2010_GTC2010.pdf。
第七章:使用 OpenGL ES 3.0 在移动平台上进行实时图形渲染的介绍
在本章中,我们将涵盖以下主题:
-
设置 Android SDK
-
设置 Android 本地开发工具包(NDK)
-
开发用于集成 Android NDK 的基本框架
-
使用 OpenGL ES 3.0 创建您的第一个 Android 应用程序
简介
在本章中,我们将通过展示如何使用 嵌入式系统中的 OpenGL(OpenGL ES)在最新的移动设备上可视化数据,从智能手机到平板电脑,过渡到一个越来越强大且无处不在的计算平台。随着移动设备的普及和计算能力的增强,我们现在有了一个前所未有的机会,可以直接使用集成到现代移动设备中的高性能图形硬件来开发新颖的交互式数据可视化工具。
OpenGL ES 在标准化 2D 和 3D 图形 API 中发挥着重要作用,允许在具有各种硬件设置的嵌入式系统上大规模部署移动应用程序。在各种移动平台(主要是 Google Android、Apple iOS 和 Microsoft Windows Phone)中,Android 移动操作系统目前是最受欢迎的之一。因此,在本章中,我们将主要关注使用 OpenGL ES 3.0 开发基于 Android 的应用程序(API 18 及以上版本)的开发,它提供了 GLSL 的新版本支持(包括对整数和 32 位浮点操作的全面支持)以及增强的纹理渲染支持。尽管如此,OpenGL ES 3.0 也支持其他移动平台,如 Apple iOS 和 Microsoft Phone。
在这里,我们将首先介绍如何设置 Android 开发平台,包括提供构建移动应用程序基本工具的 SDK,以及允许通过直接硬件加速使用本地代码语言(C/C++)进行高性能科学计算和仿真的 NDK。我们将提供一个脚本,以简化在您的移动设备上部署第一个基于 Android 的应用程序的过程。
设置 Android SDK
Google Android OS 网站提供了一个名为 Android SDK 的独立包,用于 Android 应用程序开发。它包含开发 Android 应用程序所需的所有必要的编译和调试工具(除了由 Android NDK 提供的本地代码支持)。接下来的步骤解释了在 Mac OS X 或类似地,在 Linux 上的安装过程,需要对脚本和二进制包进行一些小的修改。
如何操作...
要安装 Android SDK,请按照以下步骤操作:
-
从 Android 开发者网站下载独立包,网址为
dl.google.com/android/android-sdk_r24.3.3-macosx.zip。 -
创建一个名为
3rd_party/android的新目录,并将设置文件移动到该文件夹:mkdir 3rd_party/android mv android-sdk_r24.3.3-macosx.zip 3rd_party/android -
解压包:
cd 3rd_party/android && unzip android-sdk_r24.3.3-macosx.zip -
执行 Android SDK Manager:
./android-sdk-macosx/tools/android -
从包列表中选择Android 4.3.1 (API 18),除了默认选项外。取消选择Android M (API22, MBC preview)和Android 5.1.1 (API 22)。在Android SDK Manager界面上按下安装 9 个包...按钮,如图所示:
![如何操作...]()
-
选择接受****许可并点击安装按钮:
![如何操作...]()
-
要验证安装,请在终端中输入以下命令:
./android-sdk-macosx/tools/android list -
这是一个说明 Android 4.3.1 平台成功安装的示例:
Available Android targets: ---------- id: 1 or "android-18" Name: Android 4.3.1 Type: Platform API level: 18 Revision: 3 Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in Tag/ABIs : default/armeabi-v7a, default/x86 ... -
最后,我们将安装 Apache Ant 以自动化 Android 应用程序开发的软件构建过程。我们可以通过使用 MacPort 命令行或从其官方网站
ant.apache.org/轻易地获取 Apache Ant 包:sudo port install apache-ant
相关内容
要在 Linux 或 Windows 上安装 Android SDK,下载相应的安装文件,并遵循 Android 开发者网站上的说明,网址为 developer.android.com/sdk/index.html。
在 Linux 上设置 Android SDK 的设置程序与使用命令行界面基本相同,只是应该使用此链接下载不同的独立包:dl.google.com/android/android-sdk_r24.3.3-linux.tgz。
此外,对于 Windows 用户,可以通过此链接获取独立包:dl.google.com/android/installer_r24.3.3-windows.exe。
要验证您的手机是否具有适当的 OpenGL ES 3.0 支持,请参考 Android 文档中关于如何在运行时检查 OpenGL ES 版本的说明:developer.android.com/guide/topics/graphics/opengl.html#version-check。
设置 Android 原生开发工具包 (NDK)
Android NDK 环境对于原生代码语言开发至关重要。在这里,我们将再次概述 Mac OS X 平台的设置步骤。
如何操作...
要安装 Android NDK,请按照以下步骤操作:
-
从 Android 开发者网站下载 NDK 安装包,网址为
dl.google.com/android/ndk/android-ndk-r10e-darwin-x86_64.bin。 -
将设置文件移动到相同的安装文件夹:
mv android-ndk-r10e-darwin-x86_64.bin 3rd_party/android -
将文件的权限设置为可执行:
cd 3rd_party/android && chmod +x android-ndk-r10e-darwin-x86_64.bin -
运行 NDK 安装包:
./android-ndk-r10e-darwin-x86_64.bin -
安装过程完全自动化,以下输出确认了 Android NDK 安装的成功:
... Extracting android-ndk-r10e/build/tools Extracting android-ndk-r10e/build/gmsl Extracting android-ndk-r10e/build/core Extracting android-ndk-r10e/build/awk Extracting android-ndk-r10e/build Extracting android-ndk-r10e Everything is Ok
相关内容
要在 Linux 或 Windows 上安装 Android NDK,下载相应的安装文件,并遵循 Android 开发者网站上的说明:developer.android.com/tools/sdk/ndk/index.html。
开发用于集成 Android NDK 的基本框架
现在我们已经成功安装了 Android SDK 和 NDK,我们将演示如何开发一个基本框架,将本地 C/C++代码集成到基于 Java 的 Android 应用程序中。在这里,我们描述了使用 OpenGL ES 3.0 在移动设备上部署高性能代码的一般机制。
OpenGL ES 3.0 支持 Java 和 C/C++接口。根据应用程序的具体要求,您可能因为其灵活性和可移植性而选择在 Java 中实现解决方案。对于高性能计算和需要高内存带宽的应用程序,您最好使用 NDK 进行细粒度优化和内存管理。此外,我们可以使用静态库链接将现有的库,如使用 Android NDK 的 OpenCV,移植到我们的项目中。跨平台编译能力为在移动平台上以最小的开发工作量进行实时图像和信号处理开辟了许多可能性。
在这里,我们介绍了一个由三个类组成的简单框架:GL3JNIActivity、GL3JNIView和GL3JNIActivity。我们将在以下图中展示一个简化的类图,说明这些类之间的关系。本地代码(C/C++)将单独实现,并在下一节中详细描述:

如何操作...
首先,我们将创建对 Android 应用程序至关重要的核心 Java 源文件。这些文件作为我们 OpenGL ES 3.0 本地代码的包装器:
-
在项目目录中,使用以下命令创建一个名为
src/com/android/gl3jni的文件夹:mkdir src/com/android/gl3jni -
在新文件夹
src/com/android/gl3jni/中的 Java 源文件GL3JNIActivity.java中创建第一个类GL3JNIActivity:package com.android.gl3jni; import android.app.Activity; import android.os.Bundle; /** * Main application for Android */ public class GL3JNIActivity extends Activity { GL3JNIView mView; @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); mView = new GL3JNIView(getApplication()); setContentView(mView); } @Override protected void onPause() { super.onPause(); mView.onPause(); } @Override protected void onResume() { super.onResume(); mView.onResume(); } } -
接下来,实现
GL3JNIView类,该类在src/com/android/gl3jni/目录下的GL3JNIView.java源文件中处理 OpenGL 渲染设置:package com.android.gl3jni; import android.content.Context; import android.opengl.GLSurfaceView; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** * A simple application that uses OpenGL ES3 and GLSurface */ class GL3JNIView extends GLSurfaceView { public GL3JNIView(Context context) { super(context); /* Pick an EGLConfig with RGB8 color, 16-bit depth, no stencil, supporting OpenGL ES 3.0 or later */ setEGLConfigChooser(8, 8, 8, 0, 16, 0); setEGLContextClientVersion(3); setRenderer(new Renderer()); } private static class Renderer implements GLSurfaceView.Renderer { public void onDrawFrame(GL10 gl) { GL3JNILib.step(); } public void onSurfaceChanged(GL10 gl, int width, int height) { GL3JNILib.init(width, height); } public void onSurfaceCreated(GL10 gl, EGLConfig config) { } } } -
最后,在
src/com/android/gl3jni目录下的GL3JNILib.java中创建GL3JNILib类来处理本地库的加载和调用:package com.android.gl3jni; public class GL3JNILib { static { System.loadLibrary("gl3jni"); } public static native void init(int width, int height); public static native void step(); } -
现在,在项目的项目目录中添加
AndroidManifest.xml文件,该文件包含关于您在 Android 系统中的应用程序的所有必要信息:<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android= "http://schemas.android.com/apk/res/android" package="com.android.gl3jni"> <application android:label= "@string/gl3jni_activity"> <activity android:name="GL3JNIActivity" android:theme= "@android:style/Theme.NoTitleBar.Fullscreen" android:launchMode="singleTask" android:configChanges= "orientation|keyboardHidden"> <intent-filter> <action android:name= "android.intent.action.MAIN" /> <category android:name= "android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <uses-feature android:glEsVersion="0x00030000"/> <uses-sdk android:minSdkVersion="18"/> </manifest> -
在
res/values/目录下,添加strings.xml文件,该文件保存了我们的应用程序名称:<?xml version="1.0" encoding="utf-8"?> <resources> <string name="gl3jni_activity">OpenGL ES Demo</string> </resources>
它是如何工作的...
下面的类图展示了核心函数和类之间的关系。类似于所有其他具有用户界面的 Android 应用程序,我们定义了 Activity 类,它处理核心交互。GL3JNIActivity 的实现很简单。它捕获 Android 应用程序的事件(例如,onPause 和 onResume),并创建一个 GL3JNIView 类的实例,该实例处理图形渲染。我们不是添加 UI 元素,如文本框或标签,而是基于 GLSurfaceView 创建一个表面,该表面处理硬件加速的 OpenGL 渲染:

GL3JNIView 类是 GLSurfaceView 类的子类,它提供了一个专门用于 OpenGL 渲染的表面。我们通过 setEGLConfigChooser 函数选择了 RGB8 颜色模式、16 位深度缓冲区,并且没有使用模板缓冲区,并通过 setEGLContextClientVersion 函数确保环境已设置为 OpenGL ES 3.0。然后,setRenderer 函数注册了自定义的 Renderer 类,该类负责实际的 OpenGL 渲染。
Renderer 类在渲染循环中实现了关键事件函数——onDrawFrame、onSurfaceChanged 和 onSurfaceCreated——这些函数连接到由 GL3JNILib 类处理的代码的原生实现(C/C++)。
最后,GL3JNILib 类创建了一个与原生代码函数通信的接口。首先,它加载名为 gl3jni 的原生库,该库包含实际的 OpenGL ES 3.0 实现。函数原型 step 和 init 用于与原生代码接口,这些代码将在下一节中单独定义。请注意,我们还可以将画布宽度和高度值作为参数传递给原生函数。
AndroidManifest.xml 和 strings.xml 文件是 Android 应用程序所需的配置文件,它们必须以 XML 格式存储在项目的根目录中。AndroidManifest.xml 文件定义了所有必要的信息,包括 Java 包的名称、权限要求的声明(例如,文件读写访问),以及应用程序所需的最低 Android API 版本。
参见
关于 Android 应用程序开发的更多信息,Android 开发者网站提供了关于 API 的详细文档,请参阅 developer.android.com/guide/index.html。
关于在 Android 应用程序中使用 OpenGL ES 的更多信息,Android 编程指南详细描述了编程工作流程,并在 developer.android.com/training/graphics/opengl/environment.html 提供了有用的示例。
使用 OpenGL ES 创建您的第一个 Android 应用程序
在本节中,我们将使用 C/C++的本地代码来完成我们的实现,创建第一个使用 OpenGL ES 3.0 的 Android 应用程序。如图所示的简化类图所示,Java 代码仅在移动设备上提供基本接口。现在,在 C/C++方面,我们实现 Java 侧之前定义的所有功能,并包含 OpenGL ES 3.0 所需的所有库(在main_simple.cpp文件中)。main_simple.cpp文件还通过使用Java 本地接口(JNI)定义了 C/C++和 Java 侧之间的关键接口:

准备工作
我们假设您已经安装了 Android SDK 和 NDK 中的所有先决工具,并且已经设置了上一节中介绍的基本框架。此外,在继续之前,您应该复习前面章节中介绍的着色器编程基础知识。
如何操作...
在这里,我们描述了 OpenGL ES 3.0 本地代码的实现,以完成演示应用程序:
-
在项目目录中,使用以下命令创建一个名为
jni的文件夹:mkdir jni -
创建一个名为
main_simple.cpp的文件,并将其存储在jni目录中。 -
包含 JNI 和 OpenGL ES 3.0 所需的所有必要头文件:
//header for JNI #include <jni.h> //header for the OpenGL ES3 library #include <GLES3/gl3.h> -
包含日志头文件并定义宏以显示调试消息:
#include <android/log.h> #include <stdio.h> #include <stdlib.h> #include <math.h> //android error log interface #define LOG_TAG "libgl3jni" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) -
声明我们的演示应用程序的着色程序变量:
GLuint gProgram; GLuint gvPositionHandle; GLuint gvColorHandle; int width = 1280; int height = 720; -
定义顶点着色器和片段着色器的着色程序代码:
// Vertex shader source code static const char g_vshader_code[] = "#version 300 es\n" "in vec4 vPosition;\n" "in vec4 vColor;\n" "out vec4 color;\n" "void main() {\n" " gl_Position = vPosition;\n" " color = vColor;\n" "}\n"; // fragment shader source code static const char g_fshader_code[] = "#version 300 es\n" "precision mediump float;\n" "in vec4 color;\n" "out vec4 color_out;\n" "void main() {\n" " color_out = color;\n" "}\n"; -
使用 Android 日志实现 OpenGL ES 的错误调用处理程序:
/** * Print out the error string from OpenGL */ static void printGLString(const char *name, GLenum s) { const char *v = (const char *) glGetString(s); LOGI("GL %s = %s\n", name, v); } /** * Error checking with OpenGL calls */ static void checkGlError(const char* op) { for (GLint error = glGetError(); error; error = glGetError()) { LOGI("After %s() glError (0x%x)\n", op, error); } } -
实现顶点或片段程序加载机制。警告和错误消息被重定向到 Android 日志输出:
GLuint loadShader(GLenum shader_type, const char* p_source) { GLuint shader = glCreateShader(shader_type); if (shader) { glShaderSource(shader, 1, &p_source, 0); glCompileShader(shader); GLint compiled = 0; glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); //Report error and delete the shader if (!compiled) { GLint infoLen = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen); if (infoLen) { char* buf = (char*) malloc(infoLen); if (buf) { glGetShaderInfoLog(shader, infoLen, 0, buf); LOGE("Could not compile shader %d:\n%s\n", shader_type, buf); free(buf); } glDeleteShader(shader); shader = 0; } } } return shader; } -
实现着色程序创建机制。该函数还附加并链接着色程序:
GLuint createShaderProgram(const char *vertex_shader_code, const char *fragment_shader_code){ //create the vertex and fragment shaders GLuint vertex_shader_id = loadShader(GL_VERTEX_SHADER, vertex_shader_code); if (!vertex_shader_id) { return 0; } GLuint fragment_shader_id = loadShader(GL_FRAGMENT_SHADER, fragment_shader_code); if (!fragment_shader_id) { return 0; } GLint result = GL_FALSE; //link the program GLuint program_id = glCreateProgram(); glAttachShader(program_id, vertex_shader_id); checkGlError("glAttachShader"); glAttachShader(program_id, fragment_shader_id); checkGlError("glAttachShader"); glLinkProgram(program_id); //check the program and ensure that the program is linked properly glGetProgramiv(program_id, GL_LINK_STATUS, &result); if ( result != GL_TRUE ){ //error handling with Android GLint bufLength = 0; glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &bufLength); if (bufLength) { char* buf = (char*) malloc(bufLength); if (buf) { glGetProgramInfoLog(program_id, bufLength, 0, buf); LOGE("Could not link program:\n%s\n", buf); free(buf); } } glDeleteProgram(program_id); program_id = 0; } else { LOGI("Linked program Successfully\n"); } glDeleteShader(vertex_shader_id); glDeleteShader(fragment_shader_id); return program_id; } -
创建一个处理初始化的函数。这是一个辅助函数,用于处理来自 Java 侧的请求:
bool setupGraphics(int w, int h) { printGLString("Version", GL_VERSION); printGLString("Vendor", GL_VENDOR); printGLString("Renderer", GL_RENDERER); printGLString("Extensions", GL_EXTENSIONS); LOGI("setupGraphics(%d, %d)", w, h); gProgram = createShaderProgram(g_vshader_code, g_fshader_code); if (!gProgram) { LOGE("Could not create program."); return false; } gvPositionHandle = glGetAttribLocation(gProgram, "vPosition"); checkGlError("glGetAttribLocation"); LOGI("glGetAttribLocation(\"vPosition\") = %d\n", gvPositionHandle); gvColorHandle = glGetAttribLocation(gProgram, "vColor"); checkGlError("glGetAttribLocation"); LOGI("glGetAttribLocation(\"vColor\") = %d\n", gvColorHandle); glViewport(0, 0, w, h); width = w; height = h; checkGlError("glViewport"); return true; } -
设置绘制屏幕上红色、绿色和蓝色顶点的渲染函数:
//vertices GLfloat gTriangle[9]={-1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f}; GLfloat gColor[9]={1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f}; void renderFrame() { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); checkGlError("glClearColor"); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); checkGlError("glClear"); glUseProgram(gProgram); checkGlError("glUseProgram"); glVertexAttribPointer(gvPositionHandle, 3, GL_FLOAT, GL_FALSE, 0, gTriangle); checkGlError("glVertexAttribPointer"); glVertexAttribPointer(gvColorHandle, 3, GL_FLOAT, GL_FALSE, 0, gColor); checkGlError("glVertexAttribPointer"); glEnableVertexAttribArray(gvPositionHandle); checkGlError("glEnableVertexAttribArray"); glEnableVertexAttribArray(gvColorHandle); checkGlError("glEnableVertexAttribArray"); glDrawArrays(GL_TRIANGLES, 0, 9); checkGlError("glDrawArrays"); } -
定义连接到 Java 侧的 JNI 原型。这些调用是 Java 代码和 C/C++本地代码之间的接口:
//external calls for Java extern "C" { JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_init(JNIEnv * env, jobject obj, jint width, jint height); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_step(JNIEnv * env, jobject obj); }; -
使用辅助函数设置内部函数调用:
//link to internal calls JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_init(JNIEnv * env, jobject obj, jint width, jint height) { setupGraphics(width, height); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_step(JNIEnv * env, jobject obj) { renderFrame(); } //end of file -
现在我们已经完成了本地代码的实现,我们必须编译代码并将其链接到 Android 应用程序。要编译代码,创建一个类似于
Makefile的build文件,在jni文件夹中命名为Android.mk:LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := libgl3jni LOCAL_CFLAGS := -Werror #for simplified demo LOCAL_SRC_FILES := main_simple.cpp LOCAL_LDLIBS := -llog -lGLESv3 include $(BUILD_SHARED_LIBRARY) -
此外,我们必须创建一个
Application.mk文件,该文件提供有关构建类型的信息,例如应用程序二进制接口(ABI)。Application.mk文件必须存储在jni目录中:APP_ABI := armeabi-v7a #required for GLM and other static libraries APP_STL := gnustl_static -
在此阶段,根目录中应具有以下文件列表:
src/com/android/gl3jni/GL3JNIActivity.java src/com/android/gl3jni/GL3JNILib.java src/com/android/gl3jni/GL3JNIView.java AndroidManifest.xml res/value/strings.xml jni/Android.mk jni/Application.mk jni/main_simple.cpp
要编译原生源代码并在手机上部署我们的应用程序,请在终端中运行以下build脚本,如下所示:
-
设置 SDK 和 NDK 的环境变量。(请注意,以下相对路径假设 SDK 和 NDK 安装在当前目录外 3 级,其中
compile.sh和install.sh脚本在代码包中执行。根据需要,应修改这些路径以匹配您的代码目录结构):export ANDROID_SDK_PATH="../../../3rd_party/android/android-sdk-macosx" export ANDROID_NDK_PATH="../../../3rd_party/android/android-ndk-r10e" -
使用 android
update命令初始化项目以进行首次编译。这将生成后续步骤所需的所有必要文件(如build.xml文件):$ANDROID_SDK_PATH/tools/android update project -p . -s --target "android-18" -
使用
build命令编译 JNI 原生代码:$ANDROID_NDK_PATH/ndk-build -
运行
build命令。Apache Ant 会读取build.xml脚本并构建准备部署的Android 应用程序包(APK)文件:ant debug -
使用Android 调试桥接器(adb)命令安装 Android 应用程序:
$ANDROID_SDK_PATH/platform-tools/adb install -r bin/GL3JNIActivity-debug.apk
要使此命令生效,在通过 USB 端口连接移动设备之前,请确保已启用 USB 调试模式并接受任何与安全相关的警告提示。在大多数设备上,您可以通过导航到设置 | 应用程序 | 开发或设置 | 开发者来找到此选项。然而,在 Android 4.2 或更高版本中,此选项默认隐藏,必须通过导航到设置 | 关于手机(或关于平板电脑)并多次点击构建号来启用。有关进一步详情,请遵循官方 Android 开发者网站提供的说明,网址为[developer.android.com/tools/device.html](http://developer.android.com/tools/device.html)。以下是成功配置 USB 调试模式的 Android 手机示例截图:

应用程序安装后,我们可以像使用任何其他 Android 应用程序一样执行它,只需直接通过手机上的应用程序图标打开即可,如图所示:

下图展示了启动应用程序后的截图。请注意,CPU 监视器已被启用以显示 CPU 利用率。默认情况下,此功能未启用,但可以在开发者选项中找到。应用程序支持纵向和横向模式,并且在更改帧缓冲区大小时,图形会自动缩放到窗口大小:

这是横向模式的另一张截图:

它是如何工作的...
本章展示了我们在前几章中方法的可移植性。本质上,本章开发的原生代码与我们之前章节中介绍的内容相似。特别是,着色器程序的创建和加载机制几乎完全相同,只是我们在 Android 中使用了一个预定义的字符串(static char[])来简化加载文件的复杂性。然而,也有一些细微的差异。在这里,我们将列出这些差异和新特性。
在片段程序和顶点程序中,我们需要添加 #version 300 es 指令以确保着色器代码可以访问新特性,例如统一块和整数及浮点运算的全面支持。例如,OpenGL ES 3.0 用 in 和 out 关键字替换了属性和变元限定符。这种标准化使得在各个平台上开发 OpenGL 代码的速度大大加快。
另一个显著的差异是,我们完全用 EGL 库替换了 GLFW 库,EGL 库是 Android 中的标准库,用于上下文管理。所有事件处理,如窗口管理和用户输入,现在都通过 Android API 和原生代码来处理,原生代码只负责图形渲染。
Android 日志和错误报告系统现在可以通过 Android adb 程序访问。交互类似于终端输出,我们可以使用以下命令实时查看日志:
adb logcat
例如,我们的应用程序在日志中报告 OpenGL ES 版本以及移动设备支持的扩展。使用前面的命令,我们可以提取以下信息:
I/libgl3jni( 6681): GL Version = OpenGL ES 3.0 V@66.0 AU@04.04.02.048.042 LNXBUILD_AU_LINUX_ANDROID_LNX.LA.3.5.1_RB1.04.04.02.048.042+PATCH[ES]_msm8974_LNX.LA.3.5.1_RB1__release_ENGG (CL@)
I/libgl3jni( 6681): GL Vendor = Qualcomm
I/libgl3jni( 6681): GL Renderer = Adreno (TM) 330
I/libgl3jni( 6681): GL Extensions = GL_AMD_compressed_ATC_texture GL_AMD_performance_monitor GL_AMD_program_binary_Z400 GL_EXT_debug_label GL_EXT_debug_marker GL_EXT_discard_framebuffer GL_EXT_robustness GL_EXT_texture_format_BGRA8888 GL_EXT_texture_type_2_10_10_10_REV GL_NV_fence GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth_texture GL_OES_depth24 GL_OES_EGL_image GL_OES_EGL_image_external GL_OES_element_index_uint GL_OES_fbo_render_mipmap GL_OES_fragment_precision_high GL_OES_get_program_binary GL_OES_packed_depth_stencil GL_OES_depth_texture_cube_map GL_OES_rgb8_rgba8 GL_OES_standard_derivatives GL_OES_texture_3D GL_OES_texture_float GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_texture_npot GL_OES_vertex_half_float GL_OES_vertex_type_10_10_10_2 GL_OES_vertex_array_object GL_QCOM_alpha_test GL_QCOM_binning_control GL_QCOM_driver_control GL_QCOM_perfmon_global_mode GL_QCOM_extended_get GL_QCOM_extended_get2 GL_QCOM_tiled_rendering GL_QCOM_writeonly_rendering GL_EXT_sRGB GL_EXT_sRGB_write_control GL_EXT_
I/libgl3jni( 6681): setupGraphics(1440, 2560)
实时日志数据对于调试非常有用,并允许开发者快速分析问题。
一个常见的问题是 Java 和 C/C++ 元素之间如何相互通信。JNI 语法一开始可能难以理解,但我们可以通过仔细分析以下代码片段来解码它:
JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_init
(JNIEnv *env, jobject obj, jint width, jint height)
JNIEXPORT 和 JNICALL 标签允许函数在运行时定位到共享库中。类名由 com_android_gl3jni_GL3JNILib (com.android.gl3jni.GL3JNILib) 指定,init 是 Java 原生函数的方法名。正如我们所见,类名中的点被下划线替换了。此外,我们还有两个额外的参数,即帧缓冲区的宽度和高度。根据需要,可以简单地将更多参数追加到函数参数列表的末尾。
在向后兼容性方面,我们可以看到 OpenGL 4.3 是 OpenGL ES 3.0 的完整超集。在 OpenGL 3.1 及更高版本中,我们可以看到嵌入式系统版本的 OpenGL 和标准桌面版本的 OpenGL 正在缓慢趋同,这减少了在应用程序生命周期中维护各种 OpenGL 版本的底层复杂性。
参见
Android 操作系统架构的详细描述超出了本书的范围。然而,你被鼓励查阅官方的开发者工作流程指南,链接为developer.android.com/tools/workflow/index.html。
关于 OpenGL ES 着色语言的更多信息可以在www.khronos.org/registry/gles/specs/3.0/GLSL_ES_Specification_3.00.3.pdf找到。
第八章. 移动设备上的交互式实时数据可视化
在本章中,我们将涵盖以下主题:
-
可视化内置惯性测量单元(IMUs)的实时数据
-
第一部分 – 处理多点触控界面和运动传感器输入
-
第二部分 – 使用移动 GPU 进行交互式、实时数据可视化
简介
在本章中,我们将演示如何使用内置的运动传感器,称为惯性测量单元(IMUs)以及移动设备上的多点触控界面,来交互式地可视化数据。我们将进一步探讨使用着色器程序来加速计算密集型操作,以便使用移动图形硬件实时可视化 3D 数据。我们假设读者熟悉前一章中介绍的基于 Android 的 OpenGL ES 3.0 应用程序的基本框架,并在本章的实现中添加了显著更多的复杂性,以实现使用运动传感器和多点触控手势界面进行交互式、实时 3D 可视化高斯函数。最终的演示设计为适用于任何具有适当传感器硬件支持的基于 Android 的移动设备。
在这里,我们将首先介绍如何直接从 IMUs 提取数据并绘制在 Android 设备上获取的实时数据流。鉴于其复杂性,我们将最终演示分为两部分。在第一部分中,我们将演示如何在 Java 端处理多点触控界面和运动传感器输入。在第二部分中,我们将演示如何在 OpenGL ES 3.0 中实现着色器程序以及原生代码的其他组件,以完成我们的交互式演示。
可视化内置惯性测量单元(IMUs)的实时数据
许多现代移动设备现在集成了大量的内置传感器,包括各种运动和位置传感器(如加速度计、陀螺仪和磁力计/数字罗盘)以实现新的用户交互形式(如复杂的手势和运动控制)以及其他环境传感器,这些传感器可以测量环境条件(如环境光传感器和接近传感器),以实现智能可穿戴应用。Android 传感器框架提供了一个全面的接口来访问许多类型的传感器,这些传感器可以是基于硬件的(物理传感器)或基于软件的(从硬件传感器获取输入的虚拟传感器)。一般来说,有三大类传感器——运动传感器、位置传感器和环境传感器。
在本节中,我们将演示如何利用 Android 传感器框架与设备上的传感器进行通信,注册传感器事件监听器以监控传感器变化,并获取原始传感器数据以在您的移动设备上显示。为了创建此演示,我们将使用上一章中介绍的相同框架设计实现 Java 代码和本地代码。以下框图说明了演示中的核心功能和将要实现的类之间的关系:

准备工作
此演示需要一个支持 OpenGL ES 3.0 的 Android 设备和物理传感器硬件支持。不幸的是,目前这些函数无法使用 Android SDK 提供的模拟器进行模拟。具体来说,需要一个具有以下传感器集的 Android 移动设备来运行此演示:加速度计、陀螺仪和磁力计(数字指南针)。
此外,我们假设 Android SDK 和 Android NDK 已按照第七章中讨论的配置进行配置,《在移动平台上使用 OpenGL ES 3.0 进行实时图形渲染的介绍》。
如何操作...
首先,我们将创建与上一章类似的 Java 核心源文件。由于大部分代码是相似的,我们只讨论当前代码中引入的新和重要元素。其余代码使用“…”符号省略。请从官方 Packt Publishing 网站下载完整的源代码。
在GL3JNIActivity.java文件中,我们首先集成 Android 传感器管理器,这使得我们可以读取和解析传感器数据。以下步骤是完成集成的必要步骤:
-
导入 Android 传感器管理器的类:
package com.android.gl3jni; … import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; … -
添加
SensorEventListener接口以与传感器交互:public class GL3JNIActivity extends Activity implements SensorEventListener{ -
定义
SensorManager和Sensor变量以处理加速度计、陀螺仪和磁力计的数据:… private SensorManager mSensorManager; private Sensor mAccelerometer; private Sensor mGyro; private Sensor mMag; -
初始化
SensorManager以及所有其他传感器服务:@Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mGyro = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); mMag = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); mView = new GL3JNIView(getApplication()); setContentView(mView); } -
注册回调函数并开始监听这些事件:
@Override protected void onPause() { super.onPause(); mView.onPause(); //unregister accelerometer and other sensors mSensorManager.unregisterListener(this, mAccelerometer); mSensorManager.unregisterListener(this, mGyro); mSensorManager.unregisterListener(this, mMag); } @Override protected void onResume() { super.onResume(); mView.onResume(); /* register and activate the sensors. Start streaming data and handle with callback functions */ mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mGyro, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(this, mMag, SensorManager.SENSOR_DELAY_GAME); } -
处理
sensor事件。onSensorChanged和onAccuracyChanged函数捕获检测到的任何变化,而SensorEvent变量包含有关传感器类型、时间戳、精度等信息:@Override public void onAccuracyChanged(Sensor sensor, int accuracy) { //included for completeness } @Override public void onSensorChanged(SensorEvent event) { //handle the accelerometer data //All values are in SI units (m/s²) if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { float ax, ay, az; ax = event.values[0]; ay = event.values[1]; az = event.values[2]; GL3JNILib.addAccelData(ax, ay, az); } /* All values are in radians/second and measure the rate of rotation around the device's local X, Y, and Z axes */ if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { float gx, gy, gz; //angular speed gx = event.values[0]; gy = event.values[1]; gz = event.values[2]; GL3JNILib.addGyroData(gx, gy, gz); } //All values are in micro-Tesla (uT) and measure the ambient magnetic field in the X, Y and Z axes. if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) { float mx, my, mz; mx = event.values[0]; my = event.values[1]; mz = event.values[2]; GL3JNILib.addMagData(mx, my, mz); } }
接下来,在 src/com/android/gl3jni/ 目录中的 GL3JNIView.java 源文件中实现 GL3JNIView 类,该类处理 OpenGL 渲染。由于此实现与前一章内容相同,即 第七章,在移动平台上使用 OpenGL ES 3.0 进行实时图形渲染的介绍,我们在此不再讨论:
最后,将 GL3JNILib 类中所有新功能集成到处理原生库加载和调用的 GL3JNILib.java 文件中,该文件位于 src/com/android/gl3jni 目录内:
package com.android.gl3jni;
public class GL3JNILib {
static {
System.loadLibrary("gl3jni");
}
public static native void init(int width, int height);
public static native void step();
public static native void addAccelData(float ax, float ay, float az);
public static native void addGyroData(float gx, float gy, float gz);
public static native void addMagData(float mx, float my, float mz);
}
现在,在 JNI/C++端,创建一个名为 Sensor 的类来管理每个传感器的数据缓冲区,包括加速度计、陀螺仪和磁力计(数字罗盘)。首先,为 Sensor 类创建一个名为 Sensor.h 的头文件:
#ifndef SENSOR_H_
#define SENSOR_H_
#include <stdlib.h>
#include <jni.h>
#include <GLES3/gl3.h>
#include <math.h>
class Sensor {
public:
Sensor();
Sensor(unsigned int size);
virtual ~Sensor();
//Resize buffer size dynamically with this function
void init(unsigned int size);
//Append new data to the buffer
void appendAccelData(GLfloat x, GLfloat y,GLfloat z);
void appendGyroData(GLfloat x, GLfloat y, GLfloat z);
void appendMagData(GLfloat x, GLfloat y, GLfloat z);
//Get sensor data buffer
GLfloat *getAccelDataPtr(int channel);
GLfloat *getGyroDataPtr(int channel);
GLfloat *getMagDataPtr(int channel);
GLfloat *getAxisPtr();
//Auto rescale factors based on max and min
GLfloat getAccScale();
GLfloat getGyroScale();
GLfloat getMagScale();
unsigned int getBufferSize();
private:
unsigned int buffer_size;
GLfloat **accel_data;
GLfloat **gyro_data;
GLfloat **mag_data;
GLfloat *x_axis;
GLfloat abs_max_acc;
GLfloat abs_max_mag;
GLfloat abs_max_gyro;
void createBuffers(unsigned int size);
void free_all();
void findAbsMax(GLfloat *src, GLfloat *max);
void appendData(GLfloat *src, GLfloat data);
void setNormalizedAxis(GLfloat *data, unsigned int size, float min, float max);
};
#endif /* SENSOR_H_ */
然后,在 Sensor.cpp 文件中实现 Sensor 类,步骤如下:
-
实现
Sensor类的构造函数和析构函数。将缓冲区的默认大小设置为256:#include "Sensor.h" Sensor::Sensor() { //use default size init(256); } // Initialize with different buffer size Sensor::Sensor(unsigned int size) { init(size); } Sensor::~Sensor() { free_all(); } -
添加初始化函数,设置所有默认参数,并在运行时分配和释放内存:
void Sensor::init(unsigned int size){ buffer_size = size; //delete the old memory if already exist free_all(); //allocate the memory for the buffer createBuffers(size); setNormalizedAxis(x_axis, size, -1.0f, 1.0f); abs_max_acc = 0; abs_max_gyro = 0; abs_max_mag = 0; } -
实现内存分配的
createBuffers函数:// Allocate memory for all sensor data buffers void Sensor::createBuffers(unsigned int buffer_size){ accel_data = (GLfloat**)malloc(3*sizeof(GLfloat*)); gyro_data = (GLfloat**)malloc(3*sizeof(GLfloat*)); mag_data = (GLfloat**)malloc(3*sizeof(GLfloat*)); //3 channels for accelerometer accel_data[0] = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); accel_data[1] = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); accel_data[2] = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); //3 channels for gyroscope gyro_data[0] = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); gyro_data[1] = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); gyro_data[2] = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); //3 channels for digital compass mag_data[0] = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); mag_data[1] = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); mag_data[2] = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); //x-axis precomputed x_axis = (GLfloat*)calloc(buffer_size,sizeof(GLfloat)); } -
实现
free_all函数以释放内存:// Deallocate all memory void Sensor::free_all(){ if(accel_data){ free(accel_data[0]); free(accel_data[1]); free(accel_data[2]); free(accel_data); } if(gyro_data){ free(gyro_data[0]); free(gyro_data[1]); free(gyro_data[2]); free(gyro_data); } if(mag_data){ free(mag_data[0]); free(mag_data[1]); free(mag_data[2]); free(mag_data); } if(x_axis){ free(x_axis); } } -
创建将数据追加到每个传感器数据缓冲区的例程:
// Append acceleration data to the buffer void Sensor::appendAccelData(GLfloat x, GLfloat y, GLfloat z){ abs_max_acc = 0; float data[3] = {x, y, z}; for(int i=0; i<3; i++){ appendData(accel_data[i], data[i]); findAbsMax(accel_data[i], &abs_max_acc); } } // Append the gyroscope data to the buffer void Sensor::appendGyroData(GLfloat x, GLfloat y, GLfloat z){ abs_max_gyro = 0; float data[3] = {x, y, z}; for(int i=0; i<3; i++){ appendData(gyro_data[i], data[i]); findAbsMax(gyro_data[i], &abs_max_gyro); } } // Append the magnetic field data to the buffer void Sensor::appendMagData(GLfloat x, GLfloat y, GLfloat z){ abs_max_mag = 0; float data[3] = {x, y, z}; for(int i=0; i<3; i++){ appendData(mag_data[i], data[i]); findAbsMax(mag_data[i], &abs_max_mag); } } // Append Data to the end of the buffer void Sensor::appendData(GLfloat *src, GLfloat data){ //shift the data by one int i; for(i=0; i<buffer_size-1; i++){ src[i]=src[i+1]; } //set the last element with the new data src[buffer_size-1]=data; } -
创建返回每个传感器内存缓冲区指针的例程:
// Return the x-axis buffer GLfloat* Sensor::getAxisPtr() { return x_axis; } // Get the acceleration data buffer GLfloat* Sensor::getAccelDataPtr(int channel) { return accel_data[channel]; } // Get the Gyroscope data buffer GLfloat* Sensor::getGyroDataPtr(int channel) { return gyro_data[channel]; } // Get the Magnetic field data buffer GLfloat* Sensor::getMagDataPtr(int channel) { return mag_data[channel]; } -
实现正确显示/绘制每个传感器数据流的函数(例如,确定每个传感器数据流的最大值以正确缩放数据):
// Return buffer size unsigned int Sensor::getBufferSize() { return buffer_size; } /* Return the global max for the acceleration data buffer (for rescaling and fitting purpose) */ GLfloat Sensor::getAccScale() { return abs_max_acc; } /* Return the global max for the gyroscope data buffer (for rescaling and fitting purpose) */ GLfloat Sensor::getGyroScale() { return abs_max_gyro; } /* Return the global max for the magnetic field data buffer (for rescaling and fitting purpose) */ GLfloat Sensor::getMagScale() { return abs_max_mag; } // Pre-compute the x-axis for the plot void Sensor::setNormalizedAxis(GLfloat *data, unsigned int size, float min, float max){ float step_size = (max - min)/(float)size; for(int i=0; i<size; i++){ data[i]=min+step_size*i; } } // Find the absolute maximum from the buffer void Sensor::findAbsMax(GLfloat *src, GLfloat *max){ int i=0; for(i=0; i<buffer_size; i++){ if(*max < fabs(src[i])){ *max= fabs(src[i]); } } }
最后,我们描述了 OpenGL ES 3.0 原生代码的实现,以完成演示应用程序(main_sensor.cpp)。该代码基于前一章中介绍的结构构建,因此以下步骤中仅描述新的更改和修改:
-
在项目目录中创建一个名为
main_sensor.cpp的文件,并将其存储在jni目录内。 -
在文件开头包含所有必要的头文件,包括
Sensor.h:#include <Sensor.h> ... -
声明着色器程序处理程序和变量,用于处理传感器数据:
GLuint gProgram; GLuint gxPositionHandle; GLuint gyPositionHandle; GLuint gColorHandle; GLuint gOffsetHandle; GLuint gScaleHandle; static Sensor g_sensor_data; -
定义用于渲染点和线的顶点着色器和片段着色器的着色器程序代码:
// Vertex shader source code static const char g_vshader_code[] = "#version 300 es\n" "in float yPosition;\n" "in float xPosition;\n" "uniform float scale;\n" "uniform float offset;\n" "void main() {\n" " vec4 position = vec4(xPosition, yPosition*scale+offset, 0.0, 1.0);\n" " gl_Position = position;\n" "}\n"; // fragment shader source code static const char g_fshader_code[] = "#version 300 es\n" "precision mediump float;\n" "uniform vec4 color;\n" "out vec4 color_out;\n" "void main() {\n" " color_out = color;\n" "}\n"; -
在
setupGraphics函数中设置所有属性变量。这些变量将用于与着色器程序通信:bool setupGraphics(int w, int h) { ... gxPositionHandle = glGetAttribLocation(gProgram,"xPosition"); checkGlError("glGetAttribLocation"); LOGI("glGetAttribLocation(\"vPosition\") = %d\n", gxPositionHandle); gyPositionHandle = glGetAttribLocation(gProgram, "yPosition"); checkGlError("glGetAttribLocation"); LOGI("glGetAttribLocation(\"vPosition\") = %d\n", gyPositionHandle); gColorHandle = glGetUniformLocation(gProgram, "color"); checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"color\") = %d\n", gColorHandle); gOffsetHandle = glGetUniformLocation(gProgram, "offset"); checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"offset\") = %d\n", gOffsetHandle); gScaleHandle = glGetUniformLocation(gProgram, "scale"); checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"scale\") = %d\n", gScaleHandle); glViewport(0, 0, w, h); width = w; height = h; checkGlError("glViewport"); return true; } -
创建一个用于绘制 2D 图表的函数,以显示实时传感器数据:
void draw2DPlot(GLfloat *data, unsigned int size, GLfloat scale, GLfloat offset){ glVertexAttribPointer(gyPositionHandle, 1, GL_FLOAT, GL_FALSE, 0, data); checkGlError("glVertexAttribPointer"); glEnableVertexAttribArray(gyPositionHandle); checkGlError("glEnableVertexAttribArray"); glUniform1f(gOffsetHandle, offset); checkGlError("glUniform1f"); glUniform1f(gScaleHandle, scale); checkGlError("glUniform1f"); glDrawArrays(GL_LINE_STRIP, 0, g_sensor_data.getBufferSize()); checkGlError("glDrawArrays"); } -
设置渲染函数,用于绘制来自传感器的数据流的各种 2D 时间序列:
void renderFrame() { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); checkGlError("glClearColor"); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); checkGlError("glClear"); glUseProgram(gProgram); checkGlError("glUseProgram"); glVertexAttribPointer(gxPositionHandle, 1, GL_FLOAT, GL_FALSE, 0, g_sensor_data.getAxisPtr()); checkGlError("glVertexAttribPointer"); glEnableVertexAttribArray(gxPositionHandle); checkGlError("glEnableVertexAttribArray"); //Obtain the scaling factor based on the dataset //0.33f for 1/3 of the screen for each graph float acc_scale = 0.33f/g_sensor_data.getAccScale(); float gyro_scale = 0.33f/g_sensor_data.getGyroScale(); float mag_scale = 0.33f/g_sensor_data.getMagScale(); glLineWidth(4.0f); //set the rendering color glUniform4f(gColorHandle, 1.0f, 0.0f, 0.0f, 1.0f); checkGlError("glUniform1f"); /* Render the accelerometer, gyro, and digital compass data. As the vertex shader does not use any projection matrix, every visible vertex has to be in the range of [-1, 1]. 0.67f, 0.0f, and -0.67f define the vertical positions of each graph */ draw2DPlot(g_sensor_data.getAccelDataPtr(0), g_sensor_data.getBufferSize(), acc_scale, 0.67f); draw2DPlot(g_sensor_data.getGyroDataPtr(0), g_sensor_data.getBufferSize(), gyro_scale, 0.0f); draw2DPlot(g_sensor_data.getMagDataPtr(0), g_sensor_data.getBufferSize(), mag_scale, -0.67f); glUniform4f(gColorHandle, 0.0f, 1.0f, 0.0f, 1.0f); checkGlError("glUniform1f"); draw2DPlot(g_sensor_data.getAccelDataPtr(1), g_sensor_data.getBufferSize(), acc_scale, 0.67f); draw2DPlot(g_sensor_data.getGyroDataPtr(1), g_sensor_data.getBufferSize(), gyro_scale, 0.0f); draw2DPlot(g_sensor_data.getMagDataPtr(1), g_sensor_data.getBufferSize(), mag_scale, -0.67f); glUniform4f(gColorHandle, 0.0f, 0.0f, 1.0f, 1.0f); checkGlError("glUniform1f"); draw2DPlot(g_sensor_data.getAccelDataPtr(2), g_sensor_data.getBufferSize(), acc_scale, 0.67f); draw2DPlot(g_sensor_data.getGyroDataPtr(2), g_sensor_data.getBufferSize(), gyro_scale, 0.0f); draw2DPlot(g_sensor_data.getMagDataPtr(2), g_sensor_data.getBufferSize(), mag_scale, -0.67f); } -
定义连接到 Java 端的 JNI 原型。这些调用是 Java 代码和 C/C++原生代码之间通信的接口:
//external calls for Java extern "C" { JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_init(JNIEnv * env, jobject obj, jint width, jint height); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_step(JNIEnv * env, jobject obj); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_addAccelData (JNIEnv * env, jobject obj, jfloat ax, jfloat ay, jfloat az); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_addGyroData (JNIE nv * env, jobject obj, jfloat gx, jfloat gy, jfloat gz); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_addMagData (JNIEnv * env, jobject obj, jfloat mx, jfloat my, jfloat mz) { g_sensor_data.appendMagData(mx, my, mz); } }; //link to internal calls JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_init(JNIEnv * env, jobject obj, jint width, jint height) { setupGraphics(width, height); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_step(JNIEnv * env, jobject obj) { renderFrame(); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_addAccelData(JNIEnv * env, jobject obj, jfloat ax, jfloat ay, jfloat az){ g_sensor_data.appendAccelData(ax, ay, az); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_addGyroData(JNIEnv * env, jobject obj, jfloat gx, jfloat gy, jfloat gz){ g_sensor_data.appendGyroData(gx, gy, gz); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_addMagData(JNIEnv * env, jobject obj, jfloat mx, jfloat my, jfloat mz){ g_sensor_data.appendMagData(mx, my, mz); }
最后,我们需要按照前一章中概述的相同说明编译和安装 Android 应用程序:
以下截图显示了我们的 Android 设备上加速度计、陀螺仪和数字罗盘(分别位于顶部面板、中间面板和底部面板)的实时传感器数据流。红色、绿色和蓝色用于区分来自每个传感器数据流的通道。例如,顶部面板中的红色曲线表示设备沿x轴的加速度值(y轴的蓝色曲线和z轴的绿色曲线)。在第一个例子中,我们自由旋转手机,以各种方向进行旋转,曲线显示了传感器值的相应变化。可视化器还提供了一个自动缩放功能,该功能自动计算最大值以相应地缩放曲线:

接下来,我们将手机放置在静止的表面上,并绘制了传感器的值。与观察随时间保持恒定的值不同,时间序列图显示由于传感器噪声,传感器值存在一些非常小的变化(抖动)。根据应用的不同,你通常会需要应用滤波技术以确保用户体验无抖动。一个简单的解决方案是应用低通滤波器以平滑掉任何高频噪声。关于此类滤波器实现的更多详细信息,请参阅developer.android.com/guide/topics/sensors/sensors_motion.html。

它是如何工作的…
Android 传感器框架允许用户访问移动设备上各种类型传感器的原始数据。此框架是android.hardware包的一部分,传感器包包括一组用于特定传感器功能的类和接口。
SensorManager类提供了一种接口和方法,用于访问和列出设备上可用的传感器。一些常见的硬件传感器包括加速度计、陀螺仪、接近传感器和磁力计(数字罗盘)。这些传感器由常量变量(如加速度计的TYPE_ACCELEROMETER,磁力计的TYPE_MAGNETIC_FIELD,陀螺仪的TYPE_GYROSCOPE)表示,而getDefaultSensor函数根据请求的类型返回Sensor对象的一个实例。
要启用数据流,我们必须将传感器注册到SensorEventListener类,以便在更新时将原始数据报告回应用程序。然后registerListener函数创建回调以处理传感器值或传感器精度的更新。SensorEvent变量存储了传感器名称、事件的戳记和精度,以及原始数据。
每个传感器的原始数据流通过onSensorChange函数返回。由于传感器数据可能以高频率获取和流式传输,因此我们确保不在onSensorChange函数中阻塞回调函数调用或执行任何计算密集型过程非常重要。此外,根据您的应用需求降低传感器数据速率是一种良好的实践。在我们的案例中,我们通过将常量预设变量SENSOR_DELAY_GAME传递给registerListener函数,将传感器设置为以游戏目的的最优速率运行。
然后,GL3JNILib类使用新函数处理所有数据传递到本地代码。为了简化,我们为每种传感器类型创建了单独的函数,这使得读者更容易理解每种传感器的数据流。
在这个阶段,我们已经创建了将数据重定向到本地端的接口。然而,为了在屏幕上绘制传感器数据,我们需要创建一个简单的缓冲机制,以便在一段时间内存储数据点。我们使用 C++创建了一个自定义的Sensor类来处理数据创建、更新和处理,以管理这些交互。类的实现非常直接,我们默认预设缓冲区大小为存储 256 个数据点。
在 OpenGL ES 方面,我们通过将数据流附加到我们的顶点缓冲区来创建 2D 图表。数据流的刻度根据当前值动态调整,以确保值适合屏幕。请注意,我们还在顶点着色器中执行了所有数据缩放和转换,以减少 CPU 计算中的任何开销。
参见
- 更多关于 Android 传感器框架的信息,请查阅在线文档
developer.android.com/guide/topics/sensors/sensors_overview.html。
第一部分 – 处理多触控界面和运动传感器输入
现在我们已经介绍了处理传感器输入的基础知识,我们将开发一个基于传感器的交互式数据可视化工具。除了使用运动传感器外,我们还将引入多触控界面以供用户交互。以下是对最终应用的预览,整合了本章的所有元素:

在本节中,我们将专注于实现中的 Java 部分,而本地代码将在第二部分中描述。以下类图说明了 Java 代码(第一部分)的各个组件,这些组件为移动设备上的用户交互提供了基本接口,并展示了本地代码(第二部分)如何完成整个实现:

如何实现...
首先,我们将创建对 Android 应用程序至关重要的核心 Java 源文件。这些文件作为我们 OpenGL ES 3.0 原生代码的包装器。代码结构基于前一小节中描述的 gl3jni 包。在这里,我们将突出显示对代码所做的主要更改,并讨论这些新组件的交互:
在项目目录中,修改 src/com/android/gl3jni 目录下的 GL3JNIActivity.java 文件中的 GL3JNIActivity 类。我们不再使用原始的传感器数据,而是将利用 Android 传感器融合算法,该算法智能地结合所有传感器数据以恢复设备的方向作为旋转向量。启用此功能的步骤如下所述:
-
在
GL3JNIActivity类中,添加用于处理旋转矩阵和向量的新变量:public class GL3JNIActivity extends Activity implements SensorEventListener{ GL3JNIView mView; private SensorManager mSensorManager; private Sensor mRotate; private float[] mRotationMatrix=new float[16]; private float[] orientationVals=new float[3]; -
使用
TYPE_ROTATION_VECTOR类型初始化Sensor变量,该类型返回设备方向作为旋转向量/矩阵:@Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); //lock the screen orientation for this demo //otherwise the canvas will rotate setRequestedOrientation (ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); //TYPE_ROTATION_VECTOR for device orientation mRotate = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); mView = new GL3JNIView(getApplication()); setContentView(mView); } -
注册传感器管理器对象并将传感器响应速率设置为
SENSOR_DELAY_GAME,该速率用于游戏或实时应用程序:@Override protected void onResume() { super.onResume(); mView.onResume(); mSensorManager.registerListener(this, mRotate, SensorManager.SENSOR_DELAY_GAME); } -
获取设备方向并将事件数据保存为旋转矩阵。然后将旋转矩阵转换为欧拉角,传递给原生代码:
@Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR){ SensorManager.getRotationMatrixFromVector (mRotationMatrix,event.values); SensorManager.getOrientation (mRotationMatrix, orientationVals); GL3JNILib.addRotData(orientationVals[0], orientationVals[1],orientationVals[2]); } }
接下来,修改 GL3JNIView 类,该类在 src/com/android/gl3jni/ 目录下的 GL3JNIView.java 文件中处理 OpenGL 渲染。为了使应用程序交互式,我们还集成了基于触摸的手势检测器,该检测器处理多触点事件。特别是,我们添加了 ScaleGestureDetector 类,它允许缩放手势来缩放 3D 图形。为了实现此功能,我们对 GL3JNIView.java 文件进行了以下修改:
-
导入
MotionEvent和ScaleGestureDetector类:package com.android.gl3jni; ... import android.view.MotionEvent; import android.view.ScaleGestureDetector; ... -
创建一个
ScaleGestureDetector变量并使用ScaleListener初始化:class GL3JNIView extends GLSurfaceView { private ScaleGestureDetector mScaleDetector; ... public GL3JNIView(Context context) { super(context); ... //handle gesture input mScaleDetector = new ScaleGestureDetector (context, new ScaleListener()); } -
当触摸屏事件发生时 (
onTouchEvent),将运动事件传递给手势检测器:@Override public boolean onTouchEvent(MotionEvent ev) { // Let ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev); return true; } -
实现
SimpleOnScaleGestureListener并处理捏合手势事件中的回调 (onScale):private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { private float mScaleFactor = 1.f; @Override public boolean onScale(ScaleGestureDetector detector) { //scaling factor mScaleFactor *= detector.getScaleFactor(); //Don't let the object get too small/too large. mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f)); invalidate(); GL3JNILib.setScale(mScaleFactor); return true; } }
最后,在 src/com/android/gl3jni 目录下的 GL3JNILib.java 文件中,我们在 GL3JNILib 类中实现处理原生库加载和调用的函数:
package com.android.gl3jni;
public class GL3JNILib {
static {
System.loadLibrary("gl3jni");
}
public static native void init(int width, int height);
public static native void step();
/* pass the rotation angles and scaling factor to the native code */
public static native void addRotData(float rx, float ry, float rz);
public static native void setScale(float scale);
}
它是如何工作的……
与之前的演示类似,我们将使用 Android 传感器框架来处理传感器输入。请注意,在这个演示中,我们在GL3JNIActivity.java文件中的getDefaultSensor函数内部指定了传感器类型为TYPE_ROTATION_VECTOR,这允许我们检测设备方向。这是一个软件类型传感器,其中所有 IMU 数据(从加速度计、陀螺仪和磁力计)都被融合在一起以创建旋转向量。设备方向数据首先使用getRotationMatrixFromVector函数存储在旋转矩阵mRotationMatrix中,然后使用getOrientation函数检索方位角、俯仰角和翻滚角(分别绕x、y和z轴旋转)。最后,我们通过GL3JNILib.addRotData调用将三个方向角传递到实现的原生代码部分。这允许我们根据设备的方向来控制 3D 图形。
接下来,我们将解释多点触控界面是如何工作的。在GL3JNIView类中,您会注意到我们创建了一个名为ScaleGestureDetector的新类的实例(mScaleDetector)。ScaleGestureDetector类使用多点触控屏幕的MotionEvent类检测缩放变换手势(用两个手指捏合)。该算法返回可以重定向到 OpenGL 管道的缩放因子,以实时更新图形。SimpleOnScaleGestureListener类提供了一个onScale事件的回调函数,我们通过GL3JNILib.setScale调用将缩放因子(mScaleFactor)传递到原生代码。
参见
- 关于 Android 多点触控界面的更多信息,请参阅
developer.android.com/training/gestures/index.html的详细文档。
第二部分 – 使用移动 GPU 进行交互式、实时数据可视化
现在我们将使用原生代码实现来完成我们的演示,创建一个基于 Android 的数据可视化应用程序,该程序使用 OpenGL ES 3.0 以及 Android 传感器和手势控制界面。
下面的类图突出了在 C/C++方面还需要实现的内容:

如何做到这一点...
在这里,我们描述了完成演示应用程序的 OpenGL ES 3.0 原生代码的实现。我们将保留与第七章相同的代码结构,在移动平台上使用 OpenGL ES 3.0 进行实时图形渲染的介绍。在以下步骤中,仅突出显示新代码,所有更改都在jni文件夹内的main.cpp文件中实现:
-
包含所有必要的头文件,包括
JNI、OpenGL ES 3.0 和GLM库:#define GLM_FORCE_RADIANS //header for JNI #include <jni.h> ... //header for GLM library #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> -
声明着色程序变量:
//shader program handlers GLuint gProgram; GLuint gvPositionHandle; GLuint matrixHandle; GLuint sigmaHandle; GLuint scaleHandle; -
声明用于设置相机以及其他相关变量(如旋转角度和网格)的变量:
//the view matrix and projection matrix glm::mat4 g_view_matrix; glm::mat4 g_projection_matrix; //initial position of the camera glm::vec3 g_position = glm::vec3( 0, 0, 4 ); //FOV of the camera float g_initial_fov = glm::pi<float>()*0.25f; //rotation angles, set by sensors or by touch screen float rx, ry, rz; float scale=1.0f; //vertices for the grid const unsigned int GRID_SIZE=400; GLfloat gGrid[GRID_SIZE*GRID_SIZE*3]={0}; -
定义顶点着色器和片段着色器的着色器程序代码。注意在 OpenGL ES 3.0 的此实现和标准 OpenGL 早期实现(见第 4-6 章)之间热图生成代码的相似性:
// Vertex shader source code static const char g_vshader_code[] = "#version 300 es\n" "in vec4 vPosition;\n" "uniform mat4 MVP;\n" "uniform float sigma;\n" "uniform float scale;\n" "out vec4 color_based_on_position;\n" "// Heat map generator \n" "vec4 heatMap(float v, float vmin, float vmax){\n" " float dv;\n" " float r=1.0, g=1.0, b=1.0;\n" " if (v < vmin){\n" " v = vmin;}\n" " if (v > vmax){\n" " v = vmax;}\n" " dv = vmax - vmin;\n" " if (v < (vmin + 0.25 * dv)) {\n" " r = 0.0;\n" " g = 4.0 * (v - vmin) / dv;\n" " } else if (v < (vmin + 0.5 * dv)) {\n" " r = 0.0;\n" " b = 1.0 + 4.0 * (vmin + 0.25 * dv - v) / dv;\n" " } else if (v < (vmin + 0.75 * dv)) {\n" " r = 4.0 * (v - vmin - 0.5 * dv) / dv;\n" " b = 0.0;\n" " } else {\n" " g = 1.0 + 4.0 * (vmin + 0.75 * dv - v) / dv;\n" " b = 0.0;\n" " }\n" " return vec4(r, g, b, 0.1);\n" "}\n" "void main() {\n" " //Simulation on GPU \n" " float x_data = vPosition.x;\n" " float y_data = vPosition.y;\n" " float sigma2 = sigma*sigma;\n" " float z = exp(-0.5*(x_data*x_data)/(sigma2)-0.5*(y_data*y_data)/(sigma2));\n" " vec4 position = vPosition;\n" // scale the graphics based on user gesture input " position.z = z*scale;\n" " position.x = position.x*scale;\n" " position.y = position.y*scale;\n" " gl_Position = MVP*position;\n" " color_based_on_position = heatMap(position.z, 0.0, 0.5);\n" " gl_PointSize = 5.0*scale;\n" "}\n"; // fragment shader source code static const char g_fshader_code[] = "#version 300 es\n" "precision mediump float;\n" "in vec4 color_based_on_position;\n" "out vec4 color;\n" "void main() {\n" " color = color_based_on_position;\n" "}\n"; -
初始化数据可视化的网格模式:
void computeGrid(){ float grid_x = GRID_SIZE; float grid_y = GRID_SIZE; unsigned int data_counter = 0; //define a grid ranging from -1 to +1 for(float x = -grid_x/2.0f; x<grid_x/2.0f; x+=1.0f){ for(float y = -grid_y/2.0f; y<grid_y/2.0f; y+=1.0f){ float x_data = 2.0f*x/grid_x; float y_data = 2.0f*y/grid_y; gGrid[data_counter] = x_data; gGrid[data_counter+1] = y_data; gGrid[data_counter+2] = 0; data_counter+=3; } } } -
设置用于控制模型观察角度的旋转角度。这些角度(设备方向)从 Java 端传递过来:
void setAngles(float irx, float iry, float irz){ rx = irx; ry = iry; rz = irz; } -
根据相机参数计算投影和视图矩阵:
void computeProjectionMatrices(){ //direction vector for z glm::vec3 direction_z(0, 0, -1.0); //up vector glm::vec3 up = glm::vec3(0,-1,0); float aspect_ratio = (float)width/(float)height; float nearZ = 0.1f; float farZ = 100.0f; float top = tan(g_initial_fov/2*nearZ); float right = aspect_ratio*top; float left = -right; float bottom = -top; g_projection_matrix = glm::frustum(left, right, bottom, top, nearZ, farZ); // update the view matrix g_view_matrix = glm::lookAt( g_position, // camera position g_position+direction_z, // view direction up // up direction ); } -
创建一个函数来处理着色程序和其他一次性设置(如网格的内存分配和初始化)中所有属性变量的初始化:
bool setupGraphics(int w, int h) { ... gvPositionHandle = glGetAttribLocation(gProgram, "vPosition"); checkGlError("glGetAttribLocation"); LOGI("glGetAttribLocation(\"vPosition\") = %d\n", gvPositionHandle); matrixHandle = glGetUniformLocation(gProgram, "MVP"); checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"MVP\") = %d\n", matrixHandle); sigmaHandle = glGetUniformLocation(gProgram, "sigma"); checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"sigma\") = %d\n", sigmaHandle); scaleHandle = glGetUniformLocation(gProgram, "scale"); checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"scale\") = %d\n", scaleHandle); ... computeGrid(); return true; } -
设置高斯函数 3D 绘图的渲染函数:
void renderFrame() { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); static float sigma; //update the variables for animations sigma+=0.002f; if(sigma>0.5f){ sigma = 0.002f; } /* gets the View and Model Matrix and apply to the rendering */ computeProjectionMatrices(); glm::mat4 projection_matrix = g_projection_matrix; glm::mat4 view_matrix = g_view_matrix; glm::mat4 model_matrix = glm::mat4(1.0); model_matrix = glm::rotate(model_matrix, rz, glm::vec3(-1.0f, 0.0f, 0.0f)); model_matrix = glm::rotate(model_matrix, ry, glm::vec3(0.0f, -1.0f, 0.0f)); model_matrix = glm::rotate(model_matrix, rx, glm::vec3(0.0f, 0.0f, 1.0f)); glm::mat4 mvp = projection_matrix * view_matrix * model_matrix; glClearColor(0.0f, 0.0f, 0.0f, 1.0f); checkGlError("glClearColor"); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); checkGlError("glClear"); glUseProgram(gProgram); checkGlError("glUseProgram"); glUniformMatrix4fv(matrixHandle, 1, GL_FALSE, &mvp[0][0]); checkGlError("glUniformMatrix4fv"); glUniform1f(sigmaHandle, sigma); checkGlError("glUniform1f"); glUniform1f(scaleHandle, scale); checkGlError("glUniform1f"); glVertexAttribPointer(gvPositionHandle, 3, GL_FLOAT, GL_FALSE, 0, gGrid); checkGlError("glVertexAttribPointer"); glEnableVertexAttribArray(gvPositionHandle); checkGlError("glEnableVertexAttribArray"); glDrawArrays(GL_POINTS, 0, GRID_SIZE*GRID_SIZE); checkGlError("glDrawArrays"); } -
定义连接到 Java 端的 JNI 原型。这些调用是 Java 代码和 C/C++本地代码之间通信的接口:
extern "C" { JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_init(JNIEnv * env, jobject obj, jint width, jint height); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_step(JNIEnv * env, jobject obj); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_addRotData(JNIEnv * env, jobject obj, jfloat rx, jfloat ry, jfloat rz); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setScale(JNIEnv * env, jobject obj, jfloat jscale); }; -
使用辅助函数设置内部函数调用:
JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_init(JNIEnv * env, jobject obj, jint width, jint height) { setupGraphics(width, height); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_step(JNIEnv * env, jobject obj) { renderFrame(); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_addRotData(JNIEnv * env, jobject obj, jfloat rx, jfloat ry, jfloat rz) { setAngles(rx, ry, rz); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setScale(JNIEnv * env, jobject obj, jfloat jscale) { scale = jscale; LOGI("Scale is %lf", scale); }
最后,在编译步骤方面,根据以下内容相应地修改Android.mk和Application.mk构建文件:
-
在
Android.mk中将 GLM 路径添加到LOCAL_C_INCLUDES变量中:LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := libgl3jni LOCAL_CFLAGS := -Werror LOCAL_SRC_FILES := main.cpp LOCAL_LDLIBS := -llog -lGLESv3 #The GLM library is installed in one of these two folders by default LOCAL_C_INCLUDES := /opt/local/include /usr/local/include include $(BUILD_SHARED_LIBRARY) -
将
gnustl_static添加到APP_STL变量中,以使用 GNU STL 作为静态库。这允许 C++的所有运行时支持,这是 GLM 库所需的。更多信息请参阅www.kandroid.org/ndk/docs/CPLUSPLUS-SUPPORT.html:APP_ABI := armeabi-v7a #required for GLM and other static libraries APP_STL := gnustl_static -
运行编译脚本(这与我们在上一章中做的是类似的)。请注意,
ANDROID_SDK_PATH和ANDROID_NDK_PATH变量应根据本地环境设置更改为正确的目录:#!/bin/bash ANDROID_SDK_PATH="../../../3rd_party/android/android-sdk-macosx" ANDROID_NDK_PATH="../../../3rd_party/android/android-ndk-r10e" $ANDROID_SDK_PATH/tools/android update project -p . -s --target "android-18" $ANDROID_NDK_PATH/ndk-build ant debug -
在 Android 手机上安装Android 应用包(APK),使用以下终端命令:
ANDROID_SDK_PATH="../../../3rd_party/android/android-sdk-macosx" $ANDROID_SDK_PATH/platform-tools/adb install -r bin/GL3JNIActivity-debug.apk
我们实现的最终结果如下。通过改变手机的方向,可以从不同的角度查看高斯函数。这提供了一种非常直观的方式来可视化 3D 数据集。以下是当设备方向平行于地面时高斯函数的照片:

最后,我们通过在触摸屏上用两根手指捏合来测试我们的多指手势界面。这提供了一种直观的方式来缩放 3D 数据。以下是放大数据后的近距离视图的第一张照片:

这是另一张照片,展示了通过捏合手指缩小视图时数据的外观:

最后,这是一张演示应用程序的截图,展示了使用我们的 OpenGL ES 3.0 着色器程序实时渲染的 3D 高斯分布:

工作原理…
在演示的第二部分,我们展示了使用 OpenGL ES 3.0 编写的着色程序来执行所有模拟和基于热图的三维渲染步骤,以在移动 GPU 上可视化高斯分布。重要的是,OpenGL ES 3.0 中的着色器代码与标准 OpenGL 3.2 及以上版本中编写的代码非常相似(参见第 4 到 6 章)。然而,我们建议您查阅规范以确保感兴趣的特性在两个版本中都存在。有关 OpenGL ES 3.0 规范的更多详细信息,请参阅www.khronos.org/registry/gles/specs/3.0/es_spec_3.0.0.pdf。
代码的硬件加速部分在顶点着色程序中编程,并存储在 g_vshader_code 变量中;然后片段着色程序将处理后的颜色信息传递到屏幕的颜色缓冲区。顶点程序处理与模拟相关的计算(在我们的案例中,我们有一个具有时间变化的标准差值的高斯函数,如第三章中所示 Chapter 3,交互式三维数据可视化),在图形硬件中进行。我们将标准差值作为统一变量传递,并用于计算表面高度。此外,我们还在着色程序中根据高度值计算热图颜色值。通过这种方法,我们通过完全消除这些大量浮点运算的 CPU 循环使用,显著提高了图形渲染步骤的速度。
此外,我们将上一章中使用的 GLM 库集成到 Android 平台,通过在构建脚本 Android.mk 中添加头文件以及 GLM 路径来实现。GLM 库负责处理视图和投影矩阵的计算,并允许我们将大部分之前的工作,例如设置 3D 渲染,迁移到 Android 平台。
最后,我们的基于 Android 的应用程序还利用了来自多点触控屏幕界面的输入以及来自运动传感器数据的设备方向。这些值直接通过 JNI 作为统一变量传递到着色程序中。
第九章.移动或可穿戴平台上的基于增强现实的可视化
在本章中,我们将涵盖以下主题:
-
入门 I:在 Android 上设置 OpenCV
-
入门 II:使用 OpenCV 访问相机实时流
-
使用纹理映射显示实时视频处理
-
基于增强现实的真实场景数据可视化
简介
数字图形领域自计算机发明以来,传统上一直生活在自己的虚拟世界中。通常,计算机生成的内容没有意识到用户,以及信息在现实世界中与用户的关联性。应用程序总是简单地等待用户的命令,如鼠标或键盘输入。在计算机应用程序早期设计中,一个主要的限制因素是计算机通常坐在办公室或家庭环境中的桌子上。缺乏移动性和无法与环境或用户互动,最终限制了现实世界交互式可视化应用程序的发展。
今天,随着移动计算的演变,我们重新定义了我们与世界日常互动的许多方面——例如,通过使用手机通过 GPS 进行导航的应用程序。然而,移动设备并没有使用户能够无缝地与世界互动,反而将用户从现实世界中引开。特别是,就像在桌面计算的前几代中一样,用户仍然需要从现实世界转向虚拟世界(在许多情况下,只是一个微小的移动屏幕)。
增强现实(AR)的概念是通过融合虚拟世界(由计算机生成)与现实世界,重新连接用户与真实世界的一步。这与虚拟现实截然不同,在虚拟现实中,用户沉浸于虚拟世界,脱离了现实世界。例如,AR 的一个典型实现是使用视频透视显示器,其中虚拟内容(如计算机生成的地图)与真实场景(通过内置摄像头连续捕获)相结合。现在,用户与真实世界互动——更接近真正以人为中心的应用。
最终,AR 功能的可穿戴计算设备(如 Meta 的 AR 眼镜,具有世界上第一个具有 3D 手势检测和 3D 立体显示的全息界面)的出现将创造一个新的计算时代,将极大地改变人类与计算机互动的方式。对数据可视化感兴趣的开发商现在有一套更以人为中心且直观的工具。这种设计,不用说,真正将人、机器和现实世界连接在一起。将信息直接叠加到现实世界(例如,通过叠加虚拟导航地图)要强大得多,也更有意义。
本章介绍了创建第一个基于 AR 的应用程序的基本构建块,该应用程序运行在基于 Android 的移动设备上:OpenCV 用于计算机视觉,OpenGL 用于图形渲染,以及 Android 的传感器框架用于交互。有了这些工具,以前只在好莱坞电影制作中存在的图形渲染能力现在可以随时供每个人使用。虽然我们本章将只关注基于 Android 的移动设备的使用,但本章介绍的基于 AR 的数据可视化的概念框架可以类似地扩展到最先进的可穿戴计算平台,如 Meta 的 AR 眼镜。
开始:在 Android 上设置 OpenCV
在本节中,我们将概述在 Android 平台上设置 OpenCV 库的步骤,这是启用访问任何增强现实应用程序的核心实时相机流所必需的。
准备工作
我们假设 Android SDK 和 NDK 的配置与第七章中讨论的完全一致,即《在移动平台上使用 OpenGL ES 3.0 进行实时图形渲染的介绍》,在移动平台上使用 OpenGL ES 3.0 进行实时图形渲染的介绍。在这里,我们增加了对 Android OpenCV 的支持。我们将从上一章的现有代码结构中导入和集成 OpenCV 库。
如何操作...
在这里,我们描述了设置 OpenCV 库的主要步骤,主要是路径设置和 Java SDK 项目预配置:
-
在
sourceforge.net/projects/opencvlibrary/files/opencv-android/3.0.0/OpenCV-3.0.0-android-sdk-1.zip下载 OpenCV for Android SDK 包,版本 3.0.0(OpenCV-3.0.0-android-sdk-1.zip)。 -
将包(
OpenCV-3.0.0-android-sdk-1.zip)移动到第七章中创建的3rd_party/android文件夹中,即《在移动平台上使用 OpenGL ES 3.0 进行实时图形渲染的介绍》,在移动平台上使用 OpenGL ES 3.0 进行实时图形渲染的介绍。 -
使用以下命令解压包
cd 3rd_party/android && unzip OpenCV-3.0.0-android-sdk-1.zip -
然后在项目文件夹中(例如
ch9/code/opencv_demo_1),运行以下脚本以初始化 Android 项目。请注意,3rd_party文件夹假设与上一章中的顶级目录相同:#!/bin/bash ANDROID_SDK_PATH="../../../3rd_party/android/android-sdk-macosx" OPENCV_SDK_PATH="../../../3rd_party/android/OpenCV-android-sdk" #initialize the SDK Java library $ANDROID_SDK_PATH/tools/android update project -p $OPENCV_SDK_PATH/sdk/java -s --target "android-18" $ANDROID_SDK_PATH/tools/android update project -p . -s --target "android-18" --library $OPENCV_SDK_PATH/sdk/java -
最后,在构建脚本
jni/Android.mk中包含 OpenCV 路径。LOCAL_PATH:= $(call my-dir) #build the OpenGL + OpenCV code in JNI include $(CLEAR_VARS) #including OpenCV SDK include ../../../3rd_party/android/OpenCV-android-sdk/sdk/native/jni/OpenCV.mk
现在,该项目已与 OpenCV 库相连,既包括 Java 端也包括本地端。
接下来,我们必须在手机上安装 OpenCV 管理器。OpenCV 管理器允许我们创建应用程序,而无需将所有必需的库静态链接,这是推荐的。要安装软件包,我们可以从同一项目文件夹(ch9/code/opencv_demo_1)中执行以下 adb 命令。再次注意 3rd_party 文件夹的相对位置。您也可以在 Android SDK 文件夹中执行此命令,并相应地修改 3rd_party 文件夹的相对路径。
$ANDROID_SDK_PATH/platform-tools/adb install ../../../3rd_party/android/OpenCV-android-sdk/apk/OpenCV_3.0.0_Manager_3.00_armeabi-v7a.apk
在我们成功完成设置后,我们就准备好在手机上创建我们的第一个 OpenCV Android 应用程序了。
参见
Windows 用户应参考以下关于使用 OpenCV 进行 Android 开发的教程,以获取设置说明:docs.opencv.org/doc/tutorials/introduction/android_binary_package/android_dev_intro.html 和 docs.opencv.org/doc/tutorials/introduction/android_binary_package/dev_with_OCV_on_Android.html#native-c。
关于在 Android 应用中使用 OpenCV 的更多信息,请参阅 opencv.org/platforms/android.html 在线文档。
开始 II:使用 OpenCV 访问相机实时流
接下来,我们需要演示如何将 OpenCV 集成到我们的基于 Android 的开发框架中。以下块图说明了本章将实现的核心功能和类之间的关系(本节将仅讨论与 OpenCV 介绍相关的功能或类):

尤其是我们将演示如何从相机视频流中提取图像帧,以进行后续的图像处理步骤。OpenCV 库提供了对访问实时相机流的相机支持(视频数据流的原始数据缓冲区)以及控制相机参数。此功能允许我们以最佳分辨率、帧率和图像格式从实时预览相机中获取原始帧数据。
准备工作
本章中的示例基于第八章示例代码中介绍的基本结构,即 移动设备上的交互式实时数据可视化,它利用多点触控界面和运动传感器输入,在移动设备上实现交互式实时数据可视化。为了支持 OpenCV 所做的重大更改将在本章中突出显示。完整的代码,请从 Packt Publishing 网站下载代码包。
如何操作...
首先,我们将突出显示修改 Java 源文件所需的更改,以启用 OpenCV 和 OpenCV 相机模块的使用。将GL3JNIActivity.java(src/com/android/gl3jni/)重命名为GL3OpenCVDemo.java,并按以下方式修改代码:
-
包含 OpenCV 库的包:
package com.android.gl3jni; ... import org.opencv.android.BaseLoaderCallback; import org.opencv.android.LoaderCallbackInterface; import org.opencv.android.OpenCVLoader; import org.opencv.android.CameraBridgeViewBase; import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame; import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2; import org.opencv.core.CvType; import org.opencv.core.Mat; import android.widget.RelativeLayout; import android.view.SurfaceView; -
将
CvCameraViewListener2接口添加到GL3OpenCVDemo类:public class GL3OpenCVDemo extends Activity implements SensorEventListener, CvCameraViewListener2{ -
创建处理相机视图的变量:
private GL3JNIView mView=null; ... private boolean gl3_loaded = false; private CameraBridgeViewBase mOpenCvCameraView; private RelativeLayout l_layout; -
实现
BaseLoaderCallback函数,用于OpenCVLoader:private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) { @Override public void onManagerConnected(int status) { switch (status) { case LoaderCallbackInterface.SUCCESS:{ Log.i("OpenCVDemo", "OpenCV loaded successfully"); // load the library *AFTER* we have OpenCV lib ready! System.loadLibrary("gl3jni"); gl3_loaded = true; //load the view as we have all JNI loaded mView = new GL3JNIView(getApplication()); l_layout.addView(mView); setContentView(l_layout); /* enable the camera, and push the images to the OpenGL layer */ mOpenCvCameraView.enableView(); } break; default:{ super.onManagerConnected(status); } break; } } }; -
实现 OpenCV 相机回调函数,并将图像数据传递到 JNI C/C++侧进行处理和渲染:
public void onCameraViewStarted(int width, int height) { } public void onCameraViewStopped() { } public Mat onCameraFrame(CvCameraViewFrame inputFrame) { //Log.i("OpenCVDemo", "Got Frame\n"); Mat input = inputFrame.rgba(); if(gl3_loaded){ GL3JNILib.setImage(input.nativeObj); } //don't show on the java side return null; } -
在应用程序启动时,在
onCreate函数中初始化相机:@Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); ... //setup the Java Camera with OpenCV setContentView(R.layout.ar); l_layout = (RelativeLayout)findViewById(R.id.linearLayoutRest); mOpenCvCameraView = (CameraBridgeViewBase)findViewById(R.id.opencv_camera_surface_view); mOpenCvCameraView.setVisibility( SurfaceView.VISIBLE ); mOpenCvCameraView.setMaxFrameSize(1280, 720); /* cap it at 720 for performance issue */ mOpenCvCameraView.setCvCameraViewListener(this); mOpenCvCameraView.disableView(); } -
使用
OpenCVLoader类中的异步初始化函数initAsync加载 OpenCV 库。此事件由之前定义的BaseLoaderCallback mLoaderCallback函数捕获:@Override protected void onResume() { super.onResume(); OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, mLoaderCallback); ... } -
最后,处理
onPause事件,当应用程序不再在前台运行时,暂停相机预览:@Override protected void onPause() { super.onPause(); mSensorManager.unregisterListener(this); //stop the camera if(mView!=null){ mView.onPause(); } if (mOpenCvCameraView != null) mOpenCvCameraView.disableView(); gl3_loaded = false; } -
现在在
GL3JNILib.java(src/com/android/gl3jni/)中,添加原生的setImage函数以传递相机原始数据。由于源文件的简单性,此处显示了整个源文件:package com.android.gl3jni; public class GL3JNILib { public static native void init(int width, int height); public static native void step(); //pass the image to JNI C++ side public static native void setImage(long imageRGBA); //pass the device rotation angles and the scaling factor public static native void resetRotDataOffset(); public static native void setRotMatrix(float[] rotMatrix); public static native void setScale(float scale); } -
最后,
GL3JNIView.java文件内的源代码几乎完全相同,除了我们提供了重置旋转数据并调用setZOrderOnTop函数的选项,以确保 OpenGL 层位于 Java 层之上:class GL3JNIView extends GLSurfaceView { ... public GL3JNIView(Context context) { super(context); // Pick an EGLConfig with RGB8 color, 16-bit depth, no stencil setZOrderOnTop(true); setEGLConfigChooser(8, 8, 8, 8, 16, 0); setEGLContextClientVersion(3); getHolder().setFormat(PixelFormat.TRANSLUCENT); renderer = new Renderer(); setRenderer(renderer); //handle gesture input mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); } ... @Override public boolean onTouchEvent(MotionEvent ev) { mScaleDetector.onTouchEvent(ev); int action = ev.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: GL3JNILib.resetRotDataOffset(); break; } return true; } ... } -
最后,在
main.cpp文件中定义 JNI 原型,该文件连接所有组件,以与 Java 侧进行接口://external calls for Java extern "C" { JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setImage(JNIEnv * jenv, jobject, jlong imageRGBA); }; JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setImage( JNIEnv * jenv, jobject, jlong imageRGBA) { cv::Mat* image = (cv::Mat*) imageRGBA; /* use mutex lock to ensure the write/read operations are synced (to avoid corrupting the frame) */ pthread_mutex_lock(&count_mutex); frame = image->clone(); pthread_mutex_unlock(&count_mutex); //LOGI("Got Image: %dx%d\n", frame.rows, frame.cols); } -
要访问设备相机,必须在
AndroidManifest.xml文件中声明以下元素,以确保我们有控制相机的权限。在我们的当前示例中,我们请求访问具有自动对焦支持的前后摄像头。<uses-permission android:name="android.permission.CAMERA"/> <uses-feature android:name="android.hardware.camera" android:required="false"/> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/> <uses-feature android:name="android.hardware.camera.front" android:required="false"/> <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/>
到目前为止,我们已经开发了一个支持 OpenCV 和实时相机流的完整演示应用程序。在下一节中,我们将连接相机原始数据流到 OpenGL 层,并在 C/C++中使用 OpenCV 进行实时特征提取。
它是如何工作的...
在 Java 方面,我们已经集成了之前安装的 OpenCV Manager 来处理运行时所有库的动态加载。在启动应用程序时,我们必须调用OpenCVLoader.initAsync函数;所有与 OpenCV 相关的 JNI 库必须在 OpenCV 库成功加载后才能调用。为了同步这些操作,在我们的情况下,callback函数(BaseLoaderCallback)检查 OpenCV 初始化的状态,并且只有当 OpenCV 加载器返回成功(LoaderCallbackInterface.SUCCESS)时,我们才使用System.loadLibrary函数来初始化 OpenGL 和其他组件。为了简化,我们没有在这个示例中包含处理库加载异常的实现。
在传感器方面,我们还改变了SensorManager函数的实现,以返回旋转矩阵而不是欧拉角,以避免陀螺仪锁定问题(参考en.wikipedia.org/wiki/Gimbal_lock)。我们还使用SensorManager.remapCoordinateSystem函数重新映射坐标(从设备方向到 OpenGL 摄像头方向)。然后,旋转矩阵通过原生调用GL3JNILib.setRotMatrix传递到 OpenGL 端。此外,我们可以允许用户通过触摸屏幕重置默认方向。这是通过调用GL3JNILib.resetRotDataOffset函数实现的,该函数使用触摸事件重置旋转矩阵。
此外,我们还添加了OpenCV CvCameraViewListener2接口和CameraBridgeViewBase类,以实现原生摄像头访问。CameraBridgeViewBase类是一个基本类,用于处理与 Android Camera 类和 OpenCV 库的交互。它负责控制摄像头,例如分辨率,以及处理帧,例如更改图像格式。客户端实现CvCameraViewListener以接收回调事件。在当前实现中,我们手动将分辨率设置为 1280 x 720。然而,我们可以根据应用需求增加或减少分辨率。最后,颜色帧缓冲区以 RGBA 格式返回,数据流将被传输到 JNI C/C++端并使用纹理映射进行渲染。
使用纹理映射显示实时视频
今天,大多数手机都配备了能够捕捉高质量照片和视频的摄像头。例如,三星 Galaxy Note 4 配备了 1600 万像素的后置摄像头以及 370 万像素的前置摄像头,用于视频会议应用。有了这些内置摄像头,我们可以在户外和室内环境中录制具有卓越图像质量的高清视频。这些成像传感器的普遍存在以及移动处理器的计算能力不断提高,现在使我们能够开发出更多交互式应用,例如实时跟踪物体或人脸。
通过结合 OpenGL 和 OpenCV 库,我们可以创建交互式应用,这些应用可以对现实世界进行实时视频处理,以注册和增强 3D 虚拟信息到现实世界物体上。由于这两个库都是硬件加速的(GPU 和 CPU 优化),探索使用这些库以获得实时性能非常重要。
在上一节中,我们介绍了提供访问实时摄像头流的框架。在这里,我们将创建一个完整的演示,使用基于 OpenGL 的纹理映射技术(类似于在第四章(ch04.html "第四章. 使用纹理映射渲染 2D 图像和视频")到第六章(ch06.html "第六章. 使用 OpenGL 渲染立体 3D 模型")中介绍的)显示实时视频,并使用 OpenCV 处理视频流以执行角点检测。为了帮助读者理解完成演示所需的额外代码,以下是实现概述图:

准备工作
此演示需要完成所有准备工作步骤,以便在 Android 设备上使用 OpenCV 捕获实时视频流。着色器程序和纹理映射代码的实现基于第八章(ch08.html "第八章. 移动设备上的交互式实时数据可视化")中的演示。
如何做...
在本地代码方面,创建两个新的文件,分别命名为VideoRenderer.hpp和VideoRenderer.cpp。这些文件包含使用纹理映射渲染视频的实现。此外,我们还将从上一章导入Texture.cpp和Texture.hpp文件来处理纹理创建。
在VideoRenderer.hpp文件中,按照以下方式定义VideoRenderer类(每个函数的详细信息将在下一节讨论):
#ifndef VIDEORENDERER_H_
#define VIDEORENDERER_H_
//The shader program and basic OpenGL calls
#include <Shader.hpp>
//for texture support
#include <Texture.hpp>
//opencv support
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
class VideoRenderer {
public:
VideoRenderer();
virtual ~VideoRenderer();
//setup all shader program and texture mapping variables
bool setup();
bool initTexture(cv::Mat frame);
//render the frame on screen
void render(cv::Mat frame);
private:
//this handles the generic camera feed view
GLuint gProgram;
GLuint gvPositionHandle;
GLuint vertexUVHandle;
GLuint textureSamplerID;
GLuint texture_id;
Shader shader;
};
#endif /* VIDEORENDERER_H_ */
在VideoRenderer.cpp文件中,我们实现了三个关键成员函数(setup、initTexture和render)。以下是完整的实现:
-
包含
VideoRenderer.hpp头文件,定义打印调试信息的函数,并定义构造函数和析构函数:#include "VideoRenderer.hpp" #define LOG_TAG "VideoRenderer" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) VideoRenderer::VideoRenderer() { } VideoRenderer::~VideoRenderer() { } -
定义顶点和片段着色器以及相关的配置步骤(类似于第八章,移动设备上的交互式实时数据可视化):
bool VideoRenderer::setup(){ // Vertex shader source code const char g_vshader_code[] = "#version 300 es\n" "layout(location = 1) in vec4 vPosition;\n" "layout(location = 2) in vec2 vertexUV;\n" "out vec2 UV;\n" "void main() {\n" " gl_Position = vPosition;\n" " UV=vertexUV;\n" "}\n"; // fragment shader source code const char g_fshader_code[] = "#version 300 es\n" "precision mediump float;\n" "out vec4 color;\n" "uniform sampler2D textureSampler;\n" "in vec2 UV;\n" "void main() {\n" " color = vec4(texture(textureSampler, UV).rgb, 1.0);\n" "}\n"; LOGI("setupVideoRenderer"); gProgram = shader.createShaderProgram(g_vshader_code, g_fshader_code); if (!gProgram) { LOGE("Could not create program."); return false; } gvPositionHandle = glGetAttribLocation(gProgram, "vPosition"); shader.checkGlError("glGetAttribLocation"); LOGI("glGetAttribLocation(\"vPosition\") = %d\n", gvPositionHandle); vertexUVHandle = glGetAttribLocation(gProgram, "vertexUV"); shader.checkGlError("glGetAttribLocation"); LOGI("glGetAttribLocation(\"vertexUV\") = %d\n", vertexUVHandle); textureSamplerID = glGetUniformLocation(gProgram, "textureSampler"); shader.checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"textureSampler\") = %d\n", textureSamplerID); return true; } -
初始化并绑定纹理:
bool VideoRenderer::initTexture(cv::Mat frame){ texture_id = initializeTexture(frame.data, frame.size().width, frame.size().height); //binds our texture in Texture Unit 0 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture_id); glUniform1i(textureSamplerID, 0); return true; } -
使用纹理映射在屏幕上渲染摄像头流:
void VideoRenderer::render(cv::Mat frame){ //our vertices const GLfloat g_vertex_buffer_data[] = { 1.0f,1.0f,0.0f, -1.0f,1.0f,0.0f, -1.0f,-1.0f,0.0f, 1.0f,1.0f ,0.0f, -1.0f,-1.0f,0.0f, 1.0f,-1.0f,0.0f }; //UV map for the vertices const GLfloat g_uv_buffer_data[] = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f }; glUseProgram(gProgram); shader.checkGlError("glUseProgram"); glEnableVertexAttribArray(gvPositionHandle); shader.checkGlError("glEnableVertexAttribArray"); glEnableVertexAttribArray(vertexUVHandle); shader.checkGlError("glEnableVertexAttribArray"); glVertexAttribPointer(gvPositionHandle, 3, GL_FLOAT, GL_FALSE, 0, g_vertex_buffer_data); shader.checkGlError("glVertexAttribPointer"); glVertexAttribPointer(vertexUVHandle, 2, GL_FLOAT, GL_FALSE, 0, g_uv_buffer_data); shader.checkGlError("glVertexAttribPointer"); updateTexture(frame.data, frame.size().width, frame.size().height, GL_RGBA); //draw the camera feed on the screen glDrawArrays(GL_TRIANGLES, 0, 6); shader.checkGlError("glDrawArrays"); glDisableVertexAttribArray(gvPositionHandle); glDisableVertexAttribArray(vertexUVHandle); }
为了进一步提高代码的可读性,我们将着色器程序和纹理映射的处理封装在Shader.hpp(Shader.cpp)和Texture.hpp(Texture.cpp)中,分别。这里我们只展示头文件以示完整,并请读者参考 Packt Publishing 网站上的代码包以获取每个函数的详细实现。
下面是Shader.hpp文件:
#ifndef SHADER_H_
#define SHADER_H_
#define GLM_FORCE_RADIANS
#include <jni.h>
#include <android/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <GLES3/gl3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
class Shader {
public:
Shader();
virtual ~Shader();
GLuint loadShader(GLenum shader_type, const char*p_source);
GLuint createShaderProgram(const char*vertex_shader_code, const char*fragment_shader_code);
void printGLString(const char *name, GLenum s) ;
void checkGlError(const char* op);
};
#endif /* SHADER_H_ */
Texture.hpp文件应如下所示:
#ifndef TEXTURE_HPP
#define TEXTURE_HPP
#include <GLES3/gl3.h>
class Texture {
public:
Texture();
virtual ~Texture();
GLuint initializeTexture(const unsigned char *image_data, int width, int height);
void updateTexture(const unsigned char *image_data, int width, int height, GLenum format);
};
#endif
最后,我们按照以下步骤在main.cpp文件中整合所有内容:
-
包含所有头文件。特别是,包含
pthread.h以处理同步和 OpenCV 库以进行图像处理。... #include <pthread.h> #include <Texture.hpp> #include <Shader.hpp> #include <VideoRenderer.hpp> //including opencv headers #include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/features2d/features2d.hpp> ... -
定义
VideoRenderer和Shader对象,以及pthread_mutex_t锁变量,以使用互斥锁处理数据复制的同步。//mutex lock for data copying pthread_mutex_t count_mutex; ... //pre-set image size. const int IMAGE_WIDTH = 1280; const int IMAGE_HEIGHT = 720; bool enable_process = true; //main camera feed from the Java side cv::Mat frame; //all shader related code Shader shader; //for video rendering VideoRenderer videorenderer; -
在
setupGraphics函数中设置VideoRenderer对象并初始化纹理。bool setupGraphics(int w, int h) { ... videorenderer.setup(); //template for the first texture cv::Mat frameM(IMAGE_HEIGHT, IMAGE_WIDTH, CV_8UC4, cv::Scalar(0,0,0,255)); videorenderer.initTexture(frameM); frame = frameM; ... return true; } -
创建一个
processFrame辅助函数,用于处理使用 OpenCV 的goodFeaturesToTrack函数的特征提取。该函数还直接在帧上绘制结果以进行可视化。void processFrame(cv::Mat *frame_local){ int maxCorners = 1000; if( maxCorners < 1 ) { maxCorners = 1; } cv::RNG rng(12345); // Parameters for Shi-Tomasi algorithm std::vector<cv::Point2f> corners; double qualityLevel = 0.05; double minDistance = 10; int blockSize = 3; bool useHarrisDetector = false; double k = 0.04; // Copy the source image cv::Mat src_gray; cv::Mat frame_small; cv::resize(*frame_local, frame_small, cv::Size(), 0.5, 0.5, CV_INTER_AREA); cv::cvtColor(frame_small, src_gray, CV_RGB2GRAY ); // Apply feature extraction cv::goodFeaturesToTrack( src_gray, corners, maxCorners, qualityLevel, minDistance, cv::Mat(), blockSize, useHarrisDetector, k ); // Draw corners detected on the image int r = 10; for( int i = 0; i < corners.size(); i++ ) { cv::circle(*frame_local, 2*corners[i], r, cv::Scalar(rng.uniform(0,255), rng.uniform(0,255), rng.uniform(0,255), 255), -1, 8, 0 ); } //LOGI("Found %d features", corners.size()); } -
在
renderFrame函数中实现带有互斥锁同步的帧复制(以避免由于共享内存和竞态条件导致的帧损坏)。使用 OpenCV 库处理帧,并使用 OpenGL 纹理映射技术渲染结果。void renderFrame() { shader.checkGlError("glClearColor"); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); shader.checkGlError("glClear"); pthread_mutex_lock(&count_mutex); cv::Mat frame_local = frame.clone(); pthread_mutex_unlock(&count_mutex); if(enable_process) processFrame(&frame_local); //render the video feed on screen videorenderer.render(frame_local); //LOGI("Rendering OpenGL Graphics"); } -
定义 JNI 原型并实现
setImage函数,该函数通过互斥锁确保数据复制受保护,从 Java 端接收原始相机图像数据。还实现了toggleFeatures函数,用于在触摸屏幕时开启和关闭特征跟踪。extern "C" { .. JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setImage(JNIEnv * jenv, jobject, jlong imageRGBA); //toggle features JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_toggleFeatures(JNIEnv * jenv, jobject); }; JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_toggleFeatures(JNIEnv * env, jobject obj){ //toggle the processing on/off enable_process = !enable_process; } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setImage( JNIEnv * jenv, jobject, jlong imageRGBA) { cv::Mat* image = (cv::Mat*) imageRGBA; /* use mutex lock to ensure the write/read operations are synced (to avoid corrupting the frame) */ pthread_mutex_lock(&count_mutex); frame = image->clone(); pthread_mutex_unlock(&count_mutex); //LOGI("Got Image: %dx%d\n", frame.rows, frame.cols); }![如何做...]()
生成的图像是来自 OpenCV 的后处理帧。除了显示原始视频帧外,我们还展示了我们的实现可以轻松扩展以支持使用 OpenCV 的实时视频处理。processFrame 函数使用 OpenCV 的 goodFeaturesToTrack 角点检测功能,并将从场景中提取的所有角点叠加到图像上。
图像特征是许多跟踪算法(如同时定位与建图(SLAM))以及识别算法(如基于图像的匹配)的基本元素。例如,使用 SLAM 算法,我们可以构建环境的地图,并同时跟踪设备在空间中的位置。这些技术在 AR 应用中特别有用,因为我们始终需要将虚拟世界与真实世界对齐。接下来,我们可以看到在手机上实时运行的特征提取算法(角点检测)。

它是如何工作的...
VideoRenderer 类有两个主要功能:
-
创建处理纹理映射的着色器程序(
Shader.cpp和Texture.cpp)。 -
使用 OpenCV 的原始相机帧更新纹理内存。每次从 OpenCV 获取新帧时,我们调用渲染函数,该函数更新纹理内存并在屏幕上绘制帧。
main.cpp 文件连接了实现的所有组件,并封装了所有交互逻辑。它与 Java 端(例如,setImage)接口,我们将所有计算密集型任务卸载到 C++原生端。例如,processFrame 函数处理 OpenCV 视频处理管道,我们可以高效地处理内存 I/O 和并行化。另一方面,VideoRenderer 类通过 OpenGL 加速渲染,以在移动平台上实现实时性能。
可能有人会注意到,Android 上的 OpenGL 和 OpenCV 的实现与桌面版本大多相同。这就是我们采用跨平台语言的关键原因,我们可以轻松地将代码扩展到任何未来平台,而无需付出太多努力。
参见
在移动平台上,计算资源尤其有限,因此优化所有可用硬件资源的利用非常重要。基于 OpenGL 的硬件加速可以减少我们在图形处理器上渲染 2D 和 3D 图形的大部分开销。在不久的将来,特别是随着支持 GPGPU 的移动处理器的出现(例如,Nvidia 的 K1 移动处理器),我们将为计算机视觉算法提供更多并行化处理,并为移动设备上的许多应用提供实时性能。例如,Nvidia 现在正式支持其所有即将推出的移动处理器上的 CUDA,因此我们将在移动平台上看到更多实时图像处理、机器学习(如深度学习算法)和高性能图形的出现。更多信息请参阅以下网站:developer.nvidia.com/embedded-computing。
基于增强现实的真实世界场景数据可视化
在我们的最终演示中,我们将通过在现实世界的物体和场景上叠加 3D 数据来介绍基于 AR 的数据可视化的基本框架。我们应用相同的 GPU 加速模拟模型,并使用基于传感器的跟踪方法将其注册到世界中。以下图表展示了本章实现中的最终架构:

准备工作
这个最终演示整合了本章之前介绍的所有概念,并需要在基于 Android 的手机上使用 OpenCV 捕获(并可能处理)实时视频流。为了减少代码的复杂性,我们创建了增强现实层(AROverlayRenderer),我们可以在未来使用更先进的算法来改进层的注册、对齐和校准。
如何实现...
让我们在 AROverlayRenderer.hpp 文件中定义一个新的类 AROverlayRenderer:
#ifndef AROVERLAYRENDERER_H_
#define AROVERLAYRENDERER_H_
#include<Shader.hpp>
class AROverlayRenderer {
public:
AROverlayRenderer();
virtual ~AROverlayRenderer();
void render();
bool setup();
void setScale(float s);
void setOldRotMatrix(glm::mat4 r_matrix);
void setRotMatrix(glm::mat4 r_matrix);
void resetRotMatrix();
void setScreenSize(int width, int height);
void setDxDy (float dx, float dy);
private:
//this renders the overlay view
GLuint gProgramOverlay;
GLuint gvOverlayPositionHandle;
GLuint gvOverlayColorHandle;
GLuint matrixHandle;
GLuint sigmaHandle;
GLuint scaleHandle;
//vertices for the grid
int grid_size;
GLfloat *gGrid;
GLfloat sigma;
//for handling the object rotation from user
GLfloat dx, dy;
GLfloat rotX, rotY;
//the view matrix and projection matrix
glm::mat4 g_view_matrix;
glm::mat4 g_projection_matrix;
//initial position of the camera
glm::vec3 g_position;
//FOV of the virtual camera in OpenGL
float g_initial_fov;
glm::mat4 rotMatrix;
glm::mat4 old_rotMatrix;
float scale;
int width;
int height;
Shader shader;
void computeProjectionMatrices();
void computeGrid();
};
#endif /* AROVERLAYRENDERER_H_ */
现在在 AROverlayRenderer.cpp 文件中实现 AROverlayRenderer 成员函数:
-
包含
AROverlayRenderer.hpp头文件,并定义打印消息以及构造函数和析构函数:#include "AROverlayRenderer.hpp" #define LOG_TAG "AROverlayRenderer" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) AROverlayRenderer::AROverlayRenderer() { //initial position of the camera g_position = glm::vec3( 0.0f, 0.0f, 0.0f ); //FOV of the virtual camera in OpenGL //45 degree FOV g_initial_fov = 45.0f*glm::pi<float>()/180.0f; /* scale for the panel and other objects, allow for zooming in with pinch. */ scale = 1.0f; dx=0.0f; dy=0.0f; rotX=0.0f, rotY=0.0f; sigma = 0; grid_size = 400; //allocate memory for the grid gGrid = (GLfloat*) malloc(sizeof(GLfloat)*grid_size*grid_size*3); } AROverlayRenderer::~AROverlayRenderer() { //delete all dynamically allocated objects here free(gGrid); } -
初始化模拟的网格模式:
void AROverlayRenderer::computeGrid(){ float grid_x = grid_size; float grid_y = grid_size; unsigned int data_counter = 0; //define a grid ranging from -1 to +1 for(float x = -grid_x/2.0f; x<grid_x/2.0f; x+=1.0f){ for(float y = -grid_y/2.0f; y<grid_y/2.0f; y+=1.0f){ float x_data = x/grid_x; float y_data = y/grid_y; gGrid[data_counter] = x_data; gGrid[data_counter+1] = y_data; gGrid[data_counter+2] = 0; data_counter+=3; } } } -
设置用于叠加图形的着色器程序:
bool AROverlayRenderer::setup(){ // Vertex shader source code static const char g_vshader_code_overlay[] = "#version 300 es\n" "in vec4 vPosition;\n" "uniform mat4 MVP;\n" "uniform float sigma;\n" "uniform float scale;\n" "out vec4 color_based_on_position;\n" "// Heat map generator \n" "vec4 heatMap(float v, float vmin, float vmax){\n" " float dv;\n" " float r=1.0, g=1.0, b=1.0;\n" " if (v < vmin){\n" " v = vmin;}\n" " if (v > vmax){\n" " v = vmax;}\n" " dv = vmax - vmin;\n" " if (v < (vmin + 0.25 * dv)) {\n" " r = 0.0;\n" " g = 4.0 * (v - vmin) / dv;\n" " } else if (v < (vmin + 0.5 * dv)) {\n" " r = 0.0;\n" " b = 1.0 + 4.0 * (vmin + 0.25 * dv - v) / dv;\n" " } else if (v < (vmin + 0.75 * dv)) {\n" " r = 4.0 * (v - vmin - 0.5 * dv) / dv;\n" " b = 0.0;\n" " } else {\n" " g = 1.0 + 4.0 * (vmin + 0.75 * dv - v) / dv;\n" " b = 0.0;\n" " }\n" " return vec4(r, g, b, 0.1);\n" "}\n" "void main() {\n" " //Simulation on GPU \n" " float x_data = vPosition.x;\n" " float y_data = vPosition.y;\n" " float sigma2 = sigma*sigma;\n" " float z = exp(-0.5*(x_data*x_data)/(sigma2)-0.5*(y_data*y_data)/(sigma2));\n" " vec4 position = vPosition;\n" " position.z = z*scale;\n" " position.x = position.x*scale;\n" " position.y = position.y*scale;\n" " gl_Position = MVP*position;\n" " color_based_on_position = heatMap(position.z, 0.0, 0.5);\n" " gl_PointSize = 5.0*scale;\n" "}\n"; // fragment shader source code static const char g_fshader_code_overlay[] = "#version 300 es\n" "precision mediump float;\n" "in vec4 color_based_on_position;\n" "out vec4 color;\n" "void main() {\n" " color = color_based_on_position;\n" "}\n"; //setup the shader for the overlay gProgramOverlay = shader.createShaderProgram(g_vshader_code_overlay, g_fshader_code_overlay); if (!gProgramOverlay) { LOGE("Could not create program for overlay."); return false; } //get handlers for the overlay side matrixHandle = glGetUniformLocation(gProgramOverlay, "MVP"); shader.checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"MVP\") = %d\n", matrixHandle); gvOverlayPositionHandle = glGetAttribLocation(gProgramOverlay, "vPosition"); shader.checkGlError("glGetAttribLocation"); LOGI("glGetAttribLocation(\"vPosition\") = %d\n", gvOverlayPositionHandle); sigmaHandle = glGetUniformLocation(gProgramOverlay, "sigma"); shader.checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"sigma\") = %d\n", sigmaHandle); scaleHandle = glGetUniformLocation(gProgramOverlay, "scale"); shader.checkGlError("glGetUniformLocation"); LOGI("glGetUniformLocation(\"scale\") = %d\n", scaleHandle); computeGrid(); } -
创建辅助函数,从触摸界面设置比例、屏幕大小和旋转变量:
void AROverlayRenderer::setScale(float s) { scale = s; } void AROverlayRenderer::setScreenSize(int w, int h) { width = w; height = h; } void AROverlayRenderer::setRotMatrix(glm::mat4 r_matrix){ rotMatrix= r_matrix; } void AROverlayRenderer::setOldRotMatrix(glm::mat4 r_matrix){ old_rotMatrix = r_matrix; } void AROverlayRenderer::resetRotMatrix(){ old_rotMatrix = rotMatrix; } void AROverlayRenderer::setDxDy(float dx, float dy){ //update the angle of rotation for each rotX += dx/width; rotY += dy/height; } -
根据相机参数计算投影和视图矩阵:
void AROverlayRenderer::computeProjectionMatrices(){ //direction vector for z glm::vec3 direction_z(0.0, 0.0, -1.0); //up vector glm::vec3 up = glm::vec3(0.0, -1.0, 0.0); float aspect_ratio = (float)width/(float)height; float nearZ = 0.01f; float farZ = 50.0f; float top = tan(g_initial_fov/2*nearZ); float right = aspect_ratio*top; float left = -right; float bottom = -top; g_projection_matrix = glm::frustum(left, right, bottom, top, nearZ, farZ); g_view_matrix = glm::lookAt( g_position, // camera position g_position+direction_z, //viewing direction up // up direction ); } -
在屏幕上渲染图形:
void AROverlayRenderer::render(){ //update the variables for animations sigma+=0.002f; if(sigma>0.5f){ sigma = 0.002f; } glUseProgram(gProgramOverlay); /* Retrieve the View and Model matrices and apply them to the rendering */ computeProjectionMatrices(); glm::mat4 projection_matrix = g_projection_matrix; glm::mat4 view_matrix = g_view_matrix; glm::mat4 model_matrix = glm::mat4(1.0); model_matrix = glm::translate(model_matrix, glm::vec3(0.0f, 0.0f, scale-5.0f)); //X,Y reversed for the screen orientation model_matrix = glm::rotate(model_matrix, rotY*glm::pi<float>(), glm::vec3(-1.0f, 0.0f, 0.0f)); model_matrix = glm::rotate(model_matrix, rotX*glm::pi<float>(), glm::vec3(0.0f, -1.0f, 0.0f)); model_matrix = glm::rotate(model_matrix, 90.0f*glm::pi<float>()/180.0f, glm::vec3(0.0f, 0.0f, 1.0f)); /* the inverse of rotational matrix is to counter- rotate the graphics to the center. This allows us to reset the camera orientation since R*inv(R) = I. */ view_matrix = rotMatrix*glm::inverse(old_rotMatrix)*view_matrix; //create the MVP (model view projection) matrix glm::mat4 mvp = projection_matrix * view_matrix * model_matrix; glUniformMatrix4fv(matrixHandle, 1, GL_FALSE, &mvp[0][0]); shader.checkGlError("glUniformMatrix4fv"); glEnableVertexAttribArray(gvOverlayPositionHandle); shader.checkGlError("glEnableVertexAttribArray"); glVertexAttribPointer(gvOverlayPositionHandle, 3, GL_FLOAT, GL_FALSE, 0, gGrid); shader.checkGlError("glVertexAttribPointer"); glUniform1f(sigmaHandle, sigma); shader.checkGlError("glUniform1f"); glUniform1f(scaleHandle, 1.0f); shader.checkGlError("glUniform1f"); //draw the overlay graphics glDrawArrays(GL_POINTS, 0, grid_size*grid_size); shader.checkGlError("glDrawArrays"); glDisableVertexAttribArray(gvOverlayPositionHandle); } -
最后,我们只需要对之前演示中使用的
main.cpp文件进行少量修改,即可在实时视频流(现实场景)上启用 AR 叠加。这里只展示了突出所需修改的相关代码片段(从 Packt Publishing 网站下载完整代码):... #include <AROverlayRenderer.hpp> ... AROverlayRenderer aroverlayrenderer; ... bool setupGraphics(int w, int h) { ... videorenderer.setup(); aroverlayrenderer.setup(); ... videorenderer.initTexture(frame); aroverlayrenderer.setScreenSize(width, height); } void renderFrame() { ... videorenderer.render(frame); aroverlayrenderer.render(); } ... extern "C" { ... JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setScale(JNIEnv * env, jobject obj, jfloat jscale); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_resetRotDataOffset(JNIEnv * env, jobject obj); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setRotMatrix (JNIEnv *env, jobject obj, jfloatArray ptr); JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setDxDy(JNIEnv *env, jobject obj, jfloat dx, jfloat dy); }; ... JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_resetRotDataOffset (JNIEnv * env, jobject obj){ aroverlayrenderer.resetRotMatrix(); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setScale (JNIEnv * env, jobject obj, jfloat jscale) { aroverlayrenderer.setScale(jscale); LOGI("Scale is %lf", scale); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_resetRotDataOffset (JNIEnv * env, jobject obj){ aroverlayrenderer.resetRotMatrix(); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setRotMatrix (JNIEnv *env, jobject obj, jfloatArray ptr) { jsize len = env->GetArrayLength(ptr); jfloat *body = env->GetFloatArrayElements(ptr,0); //should be 16 elements from the rotation matrix glm::mat4 rotMatrix(1.0f); int count = 0; for(int i = 0; i<4; i++){ for(int j=0; j<4; j++){ rotMatrix[i][j] = body[count]; count++; } } env->ReleaseFloatArrayElements(ptr, body, 0); aroverlayrenderer.setRotMatrix(rotMatrix); } JNIEXPORT void JNICALL Java_com_android_gl3jni_GL3JNILib_setDxDy(JNIEnv * env, jobject obj, jfloat dx, jfloat dy){ aroverlayrenderer.setDxDy(dx, dy); }
使用这个框架,可以在不同的现实世界物体或表面上叠加任何虚拟数据集,并启用真正交互式应用程序,使用移动设备内置的传感器和手势界面以及新兴的最先进可穿戴 AR 眼镜。以下是一些结果,展示了基于 AR 的实时、交互式 3-D 数据集(在这种情况下,高斯分布)叠加在现实场景中的可视化:

它是如何工作的...
启用 AR 应用程序的关键要素是将信息叠加到现实世界中的能力。AROverlayRenderer 类实现了所有 AR 应用程序必需的核心功能。首先,我们创建一个虚拟相机,其参数与手机上实际相机的参数相匹配。相机的参数,如视野(FOV)和宽高比,目前是硬编码的,但我们可以很容易地在 computeProjectionMatrices 函数中修改它们。然后,为了在现实世界和虚拟世界之间进行注册,我们根据设备的方向控制虚拟相机的方向。方向值通过从 Java 端传递的旋转矩阵(setRotMatrix 函数)输入,我们将其直接应用于 OpenGL 相机视图矩阵(view_matrix)。此外,我们使用手机的触摸屏界面来重置旋转矩阵的默认方向。这是通过在触摸事件(resetRotDataOffset 函数)上存储旋转矩阵值来实现的,并将旋转矩阵的逆应用于视图矩阵(这相当于将相机向相反方向旋转)。
在用户交互方面,我们已启用捏合和拖动选项以支持与虚拟对象的动态交互。在捏合事件发生时,我们获取缩放因子,并通过在 model_matrix 变量上应用 glm::translate 函数将渲染对象放置在更远的位置。此外,我们通过捕获 Java 端的拖动动作(setDxDy 函数)来旋转虚拟对象。用户可以通过在屏幕上拖动手指来控制虚拟对象的朝向。这些多指手势共同实现了一个高度交互的应用程序界面,使用户能够直观地改变渲染对象的视角。
由于校准过程的潜在复杂性,我们在此不涵盖这些细节。然而,高级用户可以参考以下网站以进行更深入的讨论:docs.opencv.org/doc/tutorials/calib3d/camera_calibration/camera_calibration.html。
此外,当前的注册过程完全基于 IMU,并且不支持平移(即虚拟对象不会与真实世界精确移动)。为了解决这个问题,我们可以应用各种图像处理技术,如均值漂移跟踪、基于特征的跟踪和基于标记的跟踪,以恢复摄像头的完整 6 自由度 (DOF) 模型。例如,SLAM 是恢复 6 自由度摄像头模型的一个很好的候选方案,但其详细实现超出了本章的范围。
参见
事实上,在本章中,我们只涵盖了增强现实 (AR) 的基础知识。AR 领域在学术界和工业界都变得越来越热门。如果您对在最新的可穿戴计算平台上实现 AR 数据可视化应用(例如,Meta 提供的具有 3D 手势输入和 3D 立体输出的平台)感兴趣,请访问以下网站:
关于 AR 眼镜的更多技术细节,请参阅以下出版物:
-
Raymond Lo, Alexander Chen, Valmiki Rampersad, Jason Huang, Han Wu, Steve Mann (2013). "基于 3D 摄像头自手势感知的增强现实系统," IEEE 国际技术与社会研讨会 (ISTAS) 2013,第 20-31 页。
-
Raymond Lo, Valmiki Rampersad, Jason Huang, Steve Mann (2013). "三维高动态范围监控用于 3D 范围感测摄像头," IEEE 国际技术与社会研讨会 (ISTAS) 2013,第 255-265 页。
-
Raymond Chun Hing Lo, Steve Mann, Jason Huang, Valmiki Rampersad, and Tao Ai. 2012. "高动态范围 (HDR) 视频图像处理用于数字玻璃." 在第 20 届 ACM 国际多媒体会议 (MM '12) 论文中。ACM,纽约,纽约,美国,第 1477-1480 页。
-
史蒂夫·曼恩,雷蒙德·洛,黄杰森,瓦尔米基·兰佩拉斯,瑞安·詹森,爱涛(2012)。"HDRchitecture: 极大动态范围的实时立体 HDR 成像",载于 ACM SIGGRAPH 2012 新兴技术(SIGGRAPH '12)。















浙公网安备 33010602011771号