Cinder-创造性编程秘籍-全-

Cinder 创造性编程秘籍(全)

原文:zh.annas-archive.org/md5/d98a60671a106726c4f2748589bfa183

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Cinder 是创意编码中最激动人心的框架之一。它是用 C++开发的,以提高性能,并允许快速创建视觉复杂和交互式应用程序。Cinder 的大优点是它可以针对许多平台,如 Mac、Windows 和 iOS,使用相同的代码。

Cinder 创意编码食谱集将向你展示如何使用简单易懂的食谱开发交互式和视觉动态的应用程序。

你将学习如何使用多媒体内容,在 2D 和 3D 中绘制生成图形,并以引人入胜的方式动画化它们。

从使用 Cinder 创建简单的项目开始,你将使用多媒体,创建动画,并与用户进行交互。

从使用粒子进行动画到使用视频、音频和图像,读者将获得使用 Cinder 创建创意应用程序的广泛知识。

通过包括 3D 绘图、图像处理以及从相机输入实时感知和跟踪的食谱,本书将教你如何开发可以在台式计算机、移动设备上运行或在交互式安装中成为一部分的交互式应用程序。

本书将为你提供开始使用 Cinder 创建包含动画和高级视觉效果的项目的必要知识。

本书涵盖内容

第一章,入门,教你使用 Cinder 创建应用程序的基础知识。

第二章,准备开发,介绍了在开发过程中非常有用的几个简单食谱。

第三章,使用图像处理技术,包含了在 Cinder 中实现和使用第三方库的图像处理技术示例。

第四章,使用多媒体内容,教我们如何加载、操作、显示、保存和分享视频、图形和网格数据。

第五章,构建粒子系统,解释了如何使用流行的多功能物理算法创建和动画粒子。

第六章,渲染和纹理化粒子系统,教我们如何渲染并应用纹理到我们的粒子上,以使它们更具吸引力。

第七章,使用 2D 图形,是关于如何使用 OpenGL 和内置的 Cinder 工具进行 2D 图形的工作和绘制。

第八章,使用 3D 图形,通过 OpenGL 和一些 Cinder 包含的用于高级 OpenGL 特性的有用包装器,介绍了创建 3D 图形的基础。

第九章,添加动画,介绍了动画 2D 和 3D 对象的技术。我们还将介绍 Cinder 在此领域的功能,如时间轴和数学函数。

第十章,与用户交互,创建了通过鼠标和触摸交互对用户做出反应的图形对象。它还教我们如何创建具有自己事件的简单图形界面,以实现更大的灵活性,并集成流行的物理库 Bullet Physics。

第十一章,从摄像头感应和跟踪输入,解释了如何接收和处理来自摄像头或 Microsoft Kinect 传感器等输入设备的数据。

第十二章,使用音频输入和输出,通过示例生成声音,其中声音是在物理模拟中对象碰撞时生成的。我们将展示使用音频反应动画可视化声音的示例。

附录与 Bullet 物理库集成,将帮助我们学习如何将 Bullet 物理库与 Cinder 集成。

本章可作为可下载文件在以下网址获取:www.packtpub.com/sites/default/files/downloads/Integrating_with_Bullet_Physics.pdf

您需要为本书准备什么

Mac OS X 或 Windows 操作系统。如果 Mac 用户希望使用 iOS 菜谱,则需要 XCode,它可以从 Apple 免费获得,以及 iOS SDK。Windows 用户需要 Visual C++ 2010 Express Edition,它也免费提供。Windows 用户还需要安装 Windows Platform SDK。在编写本书时,Cinder 的最新版本为 0.8.4。

本书面向对象

本书面向希望开始或已经使用 Cinder 构建创意应用的 C++ 开发者。对于使用其他创意编码框架并希望尝试 Cinder 的开发者来说,本书易于遵循。

阅读本书的读者应具备 C++ 编程语言的基本知识。

规范

在本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。

文本中的代码词如下所示:“我们可以通过使用include指令来包含其他上下文。”

代码块设置如下:

gl::setMatricesWindow(getWindowWidth(), getWindowHeight());
gl::color( ColorA(0.f,0.f,0.f, 0.05f) );
gl::drawSolidRect(getWindowBounds());
gl::color( ColorA(1.f,1.f,1.f, 1.f) );
mParticleSystem.draw();

任何命令行输入或输出都应如下编写:

$ ./fullbuild.sh

新术语重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“点击下一步按钮将您移动到下一屏幕”。

注意

警告或重要提示将以如下框中的形式出现。

小贴士

小技巧和技巧如下所示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者的反馈对我们开发你真正能从中获得最大收益的标题非常重要。

要发送一般反馈,只需发送电子邮件到 <feedback@packtpub.com>,并在邮件主题中提及书名。

如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为 www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。

下载示例代码

您可以从您在 www.packtpub.com 的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击错误清单提交表单链接,并输入您的错误清单详情。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站,或添加到该标题的错误清单部分。您可以通过从 www.packtpub.com/support 选择您的标题来查看任何现有的错误清单。

盗版

互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

如果您发现疑似盗版材料,请通过 <copyright@packtpub.com> 联系我们,并提供链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。

问题

如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. 入门

在本章中,我们将介绍以下内容:

  • 为基本应用程序创建项目

  • 为屏幕保护程序应用程序创建项目

  • 为 iOS 触摸应用程序创建项目

  • 理解应用程序的基本结构

  • 响应鼠标输入

  • 响应键盘输入

  • 响应触摸输入

  • 访问拖放到应用程序窗口上的文件

  • 调整场景以适应窗口大小

  • 在 Windows 上使用资源

  • 在 OSX 和 iOS 上使用资源

  • 使用资产

简介

在本章中,我们将学习使用 Cinder 创建应用程序的基础知识。

我们将首先使用一个名为 TinderBox 的强大工具在不同的平台上创建 Cinder 支持的不同类型的应用程序。

我们将介绍应用程序的基本结构,并查看如何响应用户输入事件。

最后,我们将学习如何在 Windows 和 Mac 上使用资源。

为基本应用程序创建项目

在本配方中,我们将学习如何为 Windows 和 Mac OSX 的基本桌面应用程序创建项目。

准备工作

可以使用一个名为 TinderBox 的强大工具来创建项目。TinderBox 包含在您的 Cinder 下载中,并为 Microsoft Visual C++ 2010 和 OSX Xcode 提供了创建不同应用程序项目的模板。

要找到 Tinderbox,请转到您的 Cinder 文件夹,其中包含一个名为 tools 的文件夹,其中包含 TinderBox 应用程序。

准备就绪

第一次打开 TinderBox 时,您将被要求指定 Cinder 的安装文件夹。您只需在第一次打开 TinderBox 时这样做。如果您需要重新定义 Cinder 安装的位置,您可以通过选择文件菜单然后选择首选项在 Windows 上,或者在 OS X 上选择TinderBox菜单然后选择首选项来完成。

如何操作…

我们将使用 TinderBox,这是一个与 Cinder 一起捆绑提供的实用工具,它允许轻松创建项目。按照以下步骤创建一个基本应用程序的项目:

  1. 打开 TinderBox 并选择您项目的位置。在主 TinderBox 窗口中,选择目标BasicApp模板OpenGL,如下面的截图所示:如何操作…

  2. 选择您项目的位置。命名前缀项目名称字段将默认为项目名称,如下面的截图所示:如何操作…

  3. 选择您项目要使用的编译器,可以是 Microsoft Visual C++ 2010 和/或 OS X Xcode。如何操作…

  4. 点击创建按钮,TinderBox 将显示您新项目所在的文件夹。TinderBox 将保持打开状态;您现在可以关闭它。

它是如何工作的...

TinderBox 将为所选平台(Visual C++ 2010 和 OS X Xcode)创建所选项目,并创建编译的 Cinder 库的引用。它还将创建应用程序类,并将其作为ci::app::AppBasic的子类。它还将创建一些带有基本示例的示例代码,以帮助你开始。

更多内容...

默认情况下,你的项目名称和命名前缀将是创建项目所在的文件夹名称。如果你想编辑,可以这样做,但请确保项目名称命名前缀字段没有空格,否则可能会出错。

命名前缀将被用来通过添加App后缀来命名你的应用程序类。例如,如果你将命名前缀字段设置为MyCinderTest,你的应用程序类将是MyCinderTestApp

创建屏幕保护程序应用程序的项目

在本食谱中,我们将学习如何为 Windows 和 Mac OS X 创建桌面屏幕保护程序的项目。

准备就绪

要准备好使用 TinderBox,请参阅上一节“为基本应用程序创建项目”中的“准备就绪”部分。

如何操作...

我们将使用 TinderBox,这是一个与 Cinder 捆绑提供的实用工具,它允许轻松创建项目。按照以下步骤创建一个屏幕保护程序应用程序的项目:

  1. 打开 TinderBox 并选择你的项目位置。在主TinderBox窗口中选择屏幕保护程序作为目标,选择OpenGL作为模板,如图所示:如何操作…

  2. 选择你想要创建项目的编译器,可以是 Microsoft Visual C++ 2010 和/或 OS X Xcode。

  3. 点击创建,TinderBox 将引导你到创建项目所在的文件夹。

它是如何工作的...

TinderBox 将为你创建一个项目,并将其链接到编译的 Cinder 库。它还将创建应用程序类,并将其作为ci::app::AppScreenSaver的子类,这是具有屏幕保护程序应用程序所有基本功能的类。它还将创建一些带有基本示例的示例代码,以帮助你开始。

创建 iOS 触摸应用程序的项目

在本食谱中,我们将学习如何为在 iOS 设备(如 iPhone 和 iPad)上运行的应用程序创建项目。

准备就绪

要准备好使用 TinderBox,请参阅“为基本应用程序创建项目”食谱中的“准备就绪”部分。

请注意,iOS 触摸应用程序只能在 iOS 设备(如 iPhone 和 iPad)上运行,并且使用 TinderBox 创建的项目仅适用于 OSX Xcode。

如何操作...

我们将使用 TinderBox,这是一个与 Cinder 捆绑提供的实用工具,它允许轻松创建项目。按照以下步骤创建一个 iOS 触摸应用程序的项目:

  1. 打开 TinderBox,选择您项目的位置。在主TinderBox窗口中,选择Cocoa Touch作为目标,选择简单作为模板,如图所示:如何实现…

  2. 选择您想要创建项目的编译器,无论是 Microsoft Visual C++ 2010 还是 OS X Xcode。

  3. 点击创建,TinderBox 将指导您到创建项目所在的文件夹。

它是如何工作的...

TinderBox 将创建一个 OS X Xcode 项目,并创建对编译后的 Cinder 库的引用。它还将创建应用程序类,作为ci::app::AppCocoaTouch的子类,这是具有屏幕保护程序应用程序所有基本功能的类。它还将创建一些带有基本示例的示例代码,以帮助您开始。

此应用程序基于苹果的 Cocoa Touch 框架构建,用于创建 iOS 应用程序。

理解应用程序的基本结构

您的应用程序类可以在程序执行的不同点调用多个方法。以下表格列出了这些方法:

方法 用途
prepareSettings 此方法在应用程序初始化之前,在创建渲染器之前被调用一次。在这里,我们可以在应用程序初始化之前定义一些参数,例如帧率或窗口大小。如果没有指定,应用程序将使用默认值初始化。
setup 此方法在应用程序生命周期开始时仅被调用一次。在这里,您初始化所有成员并为应用程序的运行做准备。
update 此方法在应用程序运行期间循环调用,在draw方法之前。它用于动画和更新应用程序组件的状态。尽管您可以在draw方法中更新它们,但出于组织上的考虑,建议您将更新和绘图例程分开。
draw 此方法在应用程序运行期间更新之后循环调用。所有绘图代码都应该放在这里。
shutdown 此方法在应用程序退出之前被调用。使用它来执行任何必要的清理,例如释放内存和分配的资源或关闭硬件设备。

要执行我们的代码,我们必须用我们自己的代码覆盖这些方法。

准备工作

不必覆盖所有前面的方法;您可以使用应用程序特定需要的那些。例如,如果您不想进行任何绘图,可以省略draw方法。

在本食谱中,为了学习目的,我们将实现所有这些方法。

在您的类声明中声明以下方法:

Void prepareSettings( Settings *settings );
Void setup();
Void update();
Void draw();
Void shutdown();

如何实现...

我们将实现几个方法,这些方法构成了应用程序的基本结构。执行以下步骤以实现:

  1. 实现prepareSettings方法。在这里,我们可以定义,例如,窗口的大小、其标题和帧率:

    void MyApp::prepareSettings( Settings *settings ){
      settings->setSize( 1024, 768 );
      settings->setTitle( "My Application Window" );
      settings->setFrameRate( 60 );
    }
    
  2. 实现setup方法。在这里,我们应该初始化应用程序类的所有成员。例如,为了初始化从网络摄像头捕获,我们会声明以下成员:

    int mCamWidth;
    int mCamHeight;
    Capture mCapture;
    And initialize them in the setup
    void Myapp::setup(){
      mCamWidth = 640;
      mCamHeight = 480;
      mCapture = Capture( mCamWidth, mCamHeight );
    }
    
  3. 实现更新update方法。例如,我们将打印当前帧数到控制台:

    void MyApp::update(){
      console() < < geElapsedFrames() < < std::endl;
    }
    
  4. 使用所有绘图命令实现draw方法。在这里,我们用黑色清除背景并绘制一个红色圆圈:

    void MyApp::draw(){
      gl::clear( Color::black() );
      gl::color( Color( 1.0f, 0.0f, 0.0f ) );
      gl::drawSolidCircle( Vec2f( 300.0f, 300.0f ), 100.0f  );
    }
    
  5. 实现关闭shutdown方法。此方法应包含清理代码,例如,关闭线程或保存应用程序的状态。

  6. 这里是一个将一些参数保存为 XML 格式的示例代码:

    void MyApp::shutdown(){
      XmlTree doc = XmlTree::createDoc();
      XmlTree settings = xmlTree( "Settings", "" );
      //add some attributes to the settings node
      doc.push_back( settings );
      doc.write( writeFile( "Settings.xml" ) );
    }
    

工作原理...

我们应用程序的超类实现了前面的方法作为虚拟空方法。

当应用程序运行时,会调用这些方法,调用我们实现的代码或如果未实现,则调用父类的空方法。

在步骤 1 中,我们在prepareSettings方法中定义了几个应用程序参数。不建议使用setup方法来初始化这些参数,因为这意味着渲染器必须使用默认值初始化,然后在设置过程中进行调整。结果是额外的初始化时间。

还有更多...

还有其他回调响应用户输入,如鼠标和键盘事件、窗口大小调整以及将文件拖放到应用程序窗口上。这些在响应鼠标输入响应键盘输入响应触摸输入访问拖放到应用程序窗口上的文件调整窗口大小后的场景配方中进行了更详细的描述。

参见

要了解如何使用 TinderBox 创建基本应用程序,请阅读为基本应用程序创建项目配方。

响应鼠标输入

应用程序可以通过几个事件处理程序响应鼠标交互,这些处理程序根据执行的操作而调用。

下表列出了响应鼠标交互的现有处理程序:

方法 用法
mouseDown 当用户按下鼠标按钮时调用
mouseUp 当用户释放鼠标按钮时调用
mouseWheel 当用户旋转鼠标滚轮时调用
mouseMove 当鼠标移动而没有按钮按下时调用
mouseDrag 当鼠标移动时按下任何按钮时调用

实现前面的所有方法不是强制性的;您只需实现应用程序所需的方法。

准备工作

根据您需要响应的鼠标事件实现必要的处理程序。例如,要创建一个响应所有可用鼠标事件的应用程序,您必须在主类声明中实现以下代码:

void mouseDown( MouseEvent event );
void mouseUp( MouseEvent event );
void mouseWheel( MouseEvent event );
void mouseMove( MouseEvent event );
void mouseDrag( MouseEvent event );

作为参数传递的MouseEvent对象包含有关鼠标事件的信息。

如何实现...

我们将学习如何使用 ci::app::MouseEvent 类来响应鼠标事件。执行以下步骤来完成此操作:

  1. 要获取事件发生的屏幕坐标位置,可以输入以下代码行:

    Vec2i mousePos = event.getPos();
    

    或者,我们可以通过调用 getXgetY 方法来获取单独的 x 和 y 坐标:

    int mouseX = event.getX();
    int mouseY = event.getY();
    
  2. MouseEvent 对象还通过调用 isLeftisMiddleisRight 方法来告诉我们哪个鼠标按钮触发了事件。它们返回一个 bool 值,分别指示是否是左键、中键或右键。

    bool leftButton = event.isLeft();
    bool rightButton = event.isRight();
    bool middleButton = event.isMiddle();
    
  3. 要知道事件是否是由按下鼠标按钮触发的,我们可以调用 isLeftDownisRightDownisMiddleDown 方法,这些方法根据鼠标的左键、右键或中键是否被按下返回 true

    bool leftDown = event.isLeftDown();
    bool rightDown = event.isRightDown();
    bool middleDown = event.isMiddleDown();
    
  4. getWheelIncrement 方法返回一个表示鼠标滚轮移动增量的 float 值。

    float wheelIncrement = event.getWheelIncrement();
    
  5. 还可以知道在事件期间是否按下了特殊键。isShiftDown 方法在按下 Shift 键时返回 trueisAltDown 方法在按下 Alt 键时返回 trueisControlDown 在按下 control 键时返回 trueisMetaDown 在 Windows 上按下 Windows 键或在 OS X 上按下 option 键时返回 trueisAccelDown 在 Windows 上按下 Ctrl 键或在 OS X 上按下 command 键时返回 true

工作原理

Cinder 应用内部响应系统的原生鼠标事件。然后,它使用原生信息创建一个 ci::app::MouseEvent 对象,并调用应用程序类必要的鼠标事件处理器。

还有更多...

也可以通过调用 getNativeModifiers 方法来访问原生修饰符掩码。这些是 Cinder 内部使用的平台特定值,可能对高级应用有所帮助。

响应按键输入

Cinder 应用可以通过多个回调来响应按键事件。

下表列出了由键盘交互触发的可用回调:

方法 用法
keyDown 当用户首次按下键时调用,如果键被持续按下,则重复调用。
keyUp 当按键释放时调用。

这两个方法都接收一个 ci::app::KeyEvent 对象作为参数,其中包含有关事件的信息,例如按下的键码或是否按下了任何特殊键(如 Shiftcontrol)。

实现所有前面的按键事件处理器不是强制性的;你可以只实现应用程序需要的那些。

准备工作

根据需要响应的按键事件实现必要的事件处理器。例如,要创建一个响应键按下和键释放事件的程序,必须声明以下方法:

void keyDown( KeyEvent event );
void keyUp( KeyEvent event );

ci::app::KeyEvent 参数包含有关按键事件的信息。

如何实现...

我们将学习如何使用 ci::app::KeyEvent 类来了解如何理解按键事件。执行以下步骤:

  1. 要获取触发按键事件的字符的 ASCII 码,你可以输入以下代码行:

    char character = event.getChar();
    
  2. 要响应不映射到 ASCII 字符表的特殊键,我们必须调用 getCode 方法,该方法检索一个可以映射到 ci::app::KeyEvent 类中字符表的 int 值。例如,要测试按键事件是否由 Esc 键触发,你可以输入以下代码行:

    bool escPressed = event.getCode() == KeyEvent::KEY_ESCAPE;
    

    如果 Esc 键触发了事件,则 escPressed 将为 true,否则为 false

  3. ci::app::KeyEvent 参数还包含有关在事件期间按下的修饰键的信息。isShiftDown 方法在按下 Shift 键时返回 trueisAltDown 在按下 Alt 键时返回 trueisControlDown 在按下 control 键时返回 trueisMetaDown 在 Windows 上按下 Windows 键或在 OS X 上按下 command 键时返回 true,而 isAccelDown 在 Windows 上按下 Ctrl 键或在 OS X 上按下 command 键时返回 true

它是如何工作的…

Cinder 应用程序内部响应系统的原生按键事件。当接收到原生按键事件时,它基于原生信息创建一个 ci::app::KeyEvent 对象,并在我们的应用程序类上调用相应的回调。

还有更多...

还可以通过调用 getNativeKeyCode 方法来访问原生键码。此方法返回一个包含键的原生、平台特定代码的 int 值。对于高级用途来说,这可能很重要。

响应触摸输入

Cinder 应用程序可以接收多个触摸事件。

在以下表中列出了由触摸交互调用的可用触摸事件处理器:

方法 用法
touchesBegan 当检测到新的触摸时调用此方法
touchesMoved 当现有触摸移动时调用此方法
touchesEnded 当现有触摸被移除时调用此方法

所有的前面方法都接收一个 ci::app::TouchEvent 对象作为参数,该对象包含一个 std::vector,其中包含 ci::app::TouchEvent::Touch 对象,包含每个检测到的触摸信息。由于许多设备可以同时检测和响应多个触摸,因此触摸事件可能包含多个触摸是可能且常见的。

实现所有前面的事件处理器不是强制性的;你可以使用你应用程序特别需要的那些。

Cinder 应用程序可以响应在运行 Windows 7、OS X 或 iOS 的任何触摸设备上的触摸事件。

准备工作

根据你想要响应的触摸事件实现必要的触摸事件处理器。例如,要响应所有可用的触摸事件(触摸添加、触摸移动和触摸移除),你需要声明并实现以下方法:

void touchesBegan( TouchEvent event );
void touchesMoved( TouchEvent event );
void touchesEnded( TouchEvent event );

如何做到这一点…

我们将学习如何使用ci::app::TouchEvent类来理解触摸事件。执行以下步骤来完成此操作:

  1. 要访问触摸列表,您可以输入以下代码行:

    const std::vector<TouchEvent::Touch>& touches = event.getTouches();
    

    遍历容器以访问每个单独的元素。

    for( std::vector<TouchEvent::Touch>::const_iterator it = touches.begin(); it != touches.end(); ++it ){
      const TouchEvent::Touch& touch = *it;
      //do something with the touch object
    }
    
  2. 您可以通过调用返回包含其位置的Vec2f值的getPos方法来获取触摸的位置,或者使用getXgetY方法分别接收 x 和 y 坐标,例如:

    for( std::vector<TouchEvent::Touch>::const_iterator it = touches.begin(); it != touches.end(); ++it ){
      const TouchEvent::Touch& touch = *it;
      vec2f pos = touch.getPos();
      float x = touch.getX();
      float y = touch.getY(); 
    }
    
  3. getId方法返回一个包含touch对象的唯一 ID 的uint32_t值。此 ID 在触摸的生命周期内是持久的,这意味着您可以使用它来跟踪在不同的触摸事件中访问的特定触摸。

    例如,要创建一个我们可以用手指画线的应用程序,我们可以创建一个std::map,将每条线(以ci::PolyLine<Vec2f>对象的形式)与一个uint32_t键关联,该键是触摸的唯一 ID。

    我们需要通过在源文件开头添加以下代码片段将包含std::mapPolyLine的文件包含到我们的项目中:

    #include "cinder/polyline.h"
    #include <map>
    
  4. 我们现在可以声明容器:

    std::map< uint32_t, PolyLine<Vec2f> > mLines;
    
  5. touchesBegan方法中,我们为每个检测到的触摸创建一条新线并将其映射到每个触摸的唯一 ID:

    const std::vector<TouchEvent::Touch>& touches = event.getTouches();
    for( std::vector<TouchEvent::Touch>::const_iterator it = touches.begin(); it != touches.end(); ++it ){
      const TouchEvent::Touch& touch = *it;
      mLines[ touch.getId() ] = PolyLine<Vec2f>();
    }
    
  6. touchesMoved方法中,我们将每个触摸的位置添加到其对应的线条中:

    const std::vector<TouchEvent::Touch>& touches = event.getTouches();
    for( std::vector<TouchEvent::Touch>::const_iterator it = touches.begin(); it != touches.end(); ++it ){
      const TouchEvent::Touch& touch = *it;
      mLines[ touch.getId() ].push_back( touch.getPos() ); 
    }
    
  7. touchesEnded方法中,我们移除与被移除的触摸对应的线条:

    const std::vector<TouchEvent::Touch>& touches = event.getTouches();
    for( std::vector<TouchEvent::Touch>::const_iterator it = touches.begin(); it != touches.end(); ++it ){
      const TouchEvent::Touch& touch = *it;
      mLines.erase( touch.getId() );
    }
    
  8. 最后,可以绘制线条。在这里,我们用黑色清除背景,用白色绘制线条。以下是对draw方法的实现:

    gl::clear( Color::black() );
    gl::color( Color::white() );
    for( std::map<uint32_t, PolyLine<Vec2f> >::iterator it = mLines.begin(); it != mLines.end(); ++it ){
      gl::draw( it->second );
    }
    

    以下是我们绘制了一些线条后应用程序运行的截图:

    如何操作…

它是如何工作的…

Cinder 应用程序会内部响应系统对任何触摸事件的调用。然后,它将创建一个包含事件信息的ci::app::TouchEvent对象,并调用我们应用程序类中的相应事件处理器。在 Windows 和 Mac 平台上,响应触摸事件的方式变得统一。

ci::app::TouchEvent类只包含一个访问器方法,该方法返回一个对std::vector<TouchEvent::Touch>容器的const引用。该容器为每个检测到的触摸包含一个ci::app::TouchEvent::Touch对象,并包含有关触摸的信息。

ci::app::TouchEvent::Touch对象包含有关触摸的信息,包括位置和前一个位置、唯一 ID、时间戳以及指向原生事件对象的指针,该指针映射到 Cocoa Touch 上的UITouch和 Windows 7 上的TOUCHPOINT

更多内容…

在任何时候,也可以通过调用getActiveTouches方法来获取所有活动触摸的容器。它返回一个对std::vector<TouchEvent::Touch>容器的const引用。当处理触摸应用程序时,它提供了灵活性,因为它可以在触摸事件方法之外访问。

例如,如果你想在每个活动触摸周围绘制一个实心红色圆圈,你可以在你的draw方法中添加以下代码片段:

const std::vector<TouchEvent::Touch>&activeTouches = getActiveTouches();
gl::color( Color( 1.0f, 0.0f, 0.0f ) );
for( std::vector<TouchEvent::Touch>::const_iterator it = activeTouches.begin(); it != activeTouches.end(); ++it ){
  const TouchEvent::Touch& touch = *it;
gl::drawSolidCircle( touch.getPos(), 10.0f );
}

访问拖放到应用程序窗口上的文件

Cinder 应用程序可以通过fileDrop回调来响应拖放到应用程序窗口上的文件。此方法接受一个包含事件信息的ci::app::FileDropEvent对象作为参数。

准备工作

你的应用程序必须实现一个接受ci::app::FileDropEvent对象作为参数的fileDrop方法。

将以下方法添加到应用程序的类声明中:

void fileDrop( FileDropEvent event );

如何操作...

我们将学习如何使用ci::app::FileDropEvent对象来处理文件拖放事件。执行以下步骤:

  1. 在方法实现中,你可以通过调用getFiles方法来访问应用程序上拖放的文件列表,使用ci::app::FileDropEvent参数。此方法返回一个包含fs::path对象的conststd::vector容器:

    const vector<fs::path >& files = event.getFiles();
    
  2. 文件被拖放到窗口中的位置可以通过以下回调方法访问:

    • 要获取包含文件拖放位置的ci::Vec2i对象,请输入以下代码行:

      Vec2i dropPosition = event.getPos();
      
    • 要分别获取 x 和 y 坐标,你可以使用getXgetY方法,例如:

      int pOS X = event.getX();
      int posY = event.getY();
      
  3. 你可以通过使用getNumFiles方法来查找拖放文件的数量:

    int numFiles = event.getNumFiles();
    
  4. 要访问特定文件,如果你已经知道它的索引,你可以使用getFile方法并将索引作为参数传递。

    例如,要访问索引为2的文件,你可以使用以下代码行:

    const fs::path& file = event.getFile( 2 );
    

它是如何工作的...

Cinder 应用程序将响应系统的本地文件拖放事件。然后,它将创建一个包含事件信息的ci::app::FileDropEvent对象,并在我们的应用程序中调用fileDrop回调。这样,Cinder 在 Windows 和 OS X 平台之间创建了一种统一的响应文件拖放事件的方式。

还有更多...

Cinder 使用ci::fs::path对象来定义路径。这些是boost::filesystem::path对象的typedef实例,在处理路径时提供了更大的灵活性。要了解更多关于fs::path对象的信息,请参阅www.boost.org/doc/libs/1_50_0/libs/filesystem/doc/index.htm上的boost::filesystem库参考。

调整窗口大小后的场景调整

Cinder 应用程序可以通过实现调整大小事件来响应窗口调整大小。此方法接受一个包含事件信息的ci::app::ResizeEvent参数。

准备工作

如果你的应用程序没有resize方法,请实现一个。在应用程序的类声明中添加以下代码行:

void resize( ResizeEvent event );

在方法实现中,你可以使用ResizeEvent参数来获取关于窗口新大小和格式的信息。

如何操作...

我们将学习如何使用ci::app::ResizeEvent参数来响应窗口大小调整事件。执行以下步骤:

  1. 要找到窗口的新大小,你可以使用getSize方法,该方法返回一个ci::Vec2iwith对象,窗口的宽度作为 x 分量,高度作为 y 分量。

    Vec2i windowSize = event.getSize();
    

    getWidthgetHeight方法都返回int值,分别代表窗口的宽度和高度,例如:

    int width = event.getWidth();
    int height = event.getHeight();
    
  2. getAspectRatio方法返回一个float值,表示窗口的宽高比,即其宽度和高度的比例:

    float ratio = event.getAspectRatio();
    
  3. 任何需要调整的屏幕元素都必须使用新的窗口大小来重新计算其属性。例如,为了有一个在窗口中心绘制且所有边都有 20 像素边距的矩形,我们必须首先在类声明中声明一个ci::Rectf对象:

    Rect frect;
    

    在设置中,我们设置其属性,使其在窗口的所有边都有 20 像素的边距:

    rect.x1 = 20.0f;
    rect.y1 = 20.0f;
    rect.x2 = getWindowWidth() – 20.0f;
    rect.y2 = getWindowHeight() – 20.0f;
    
  4. 要用红色绘制矩形,请将以下代码片段添加到draw方法中:

    gl::color( Color( 1.0f, 0.0f, 0.0f ) );
    gl::drawSolidRect( rect );
    
  5. resize方法中,我们必须重新计算矩形的属性,以便它调整大小以保持窗口所有边的 20 像素边距:

    rect.x1 = 20.0f;
    rect.y1 = 20.0f;
    rect.x2 = event.getWidth() – 20.0f;
    rect.y2 = event.getHeight() – 20.0f;
    
  6. 运行应用程序并调整窗口大小。矩形将根据窗口大小保持其相对大小和位置。如何操作…

它是如何工作的…

Cinder 应用程序会内部响应系统的窗口大小调整事件。然后它将创建ci::app::ResizeEvent对象,并在我们的应用程序类上调用resize方法。这样 Cinder 创建了一种在 Windows 和 Mac 平台之间处理大小调整事件的一致方式。

在 Windows 上使用资源

对于 Windows 应用程序来说,使用外部文件来加载图像、播放音频或视频,或在 XML 文件中加载或保存设置是很常见的。

资源是应用程序的外部文件,它们嵌入在应用程序的可执行文件中。资源文件对用户隐藏,以避免修改。

准备工作

资源应存储在项目文件夹中名为resources的文件夹中。如果此文件夹不存在,请创建它。

在 Windows 上,资源必须在名为Resources.rc的文件中引用。此文件应放置在vc10文件夹中,紧邻 Visual C++解决方案。如果此文件不存在,你必须创建它作为一个空文件。如果resources.rs文件尚未包含在你的项目解决方案中,你必须通过右键单击资源过滤器并选择添加然后现有项来添加它。导航到该文件并选择它。按照惯例,此文件应与项目解决方案在同一文件夹中。

如何操作…

我们将使用 Visual C++ 2010 向 Windows 应用程序添加资源。执行以下步骤:

  1. 头文件过滤器中打开 Visual C++解决方案,并打开resources.h文件。

  2. #pragma once 宏添加到你的文件中,以防止它在你的项目中多次包含,并包含 CinderResources.h 文件。

    #pragma once
    #include "cinder/CinderResources.h"
    
  3. 在 Windows 上,每个资源都必须有一个唯一的 ID 号。按照惯例,ID 被定义为从 128 开始的连续数字,但如果你有更好的选择,可以使用其他 ID。务必确保不要重复使用相同的 ID。你还必须定义一个类型字符串。类型字符串用于识别同一类型的资源,例如,在声明图像资源时可以使用字符串 IMAGE,声明视频资源时使用 VIDEO 等。

  4. 为了简化编写多平台代码,Cinder 有一个宏可以声明在 Windows 和 Mac 上都可以使用的资源。

    例如,要声明名为 image.png 的图像文件的资源,我们会在以下代码行中输入:

    #define RES_IMAGE CINDER_RESOURCE(../resources/, image.png, 128, IMAGE)
    

    CINDER_RESOURCE 宏的第一个参数是资源文件所在文件夹的相对路径,在这种情况下是默认的 resources 文件夹。

    第二个参数是文件名,之后是此资源的唯一 ID,最后是其类型字符串。

  5. 现在,我们需要将我们的 resources 宏添加到 resources.rs 文件中,如下所示:

    #include "..\include\Resources.h"
    RES_IMAGE
    
  6. 此资源现在已准备好在我们的应用程序中使用。要将此图像加载到 ci::gl::Texture 中,我们只需在我们的应用程序源代码中包含 Texture.h 文件:

    #include "cinder/gl/Texture.h"
    
  7. 我们现在可以声明纹理:

    gl::Texture mImage;
    
  8. 在设置中,我们通过加载资源创建纹理:

    mImage = gl::Texture( loadImage( loadResource( RES_IMAGE ) );
    
  9. 纹理现在已准备好在屏幕上绘制。要在位置 (20, 20) 绘制图像,我们将在 draw 方法内部输入以下代码行:

    gl::draw( mImage, Vec2f( 20.0f, 20.0f ) );
    

它是如何工作的...

resources.rc 文件由资源编译器使用,将资源嵌入到可执行文件中作为二进制数据。

更多内容...

Cinder 允许编写代码以使用资源,这些资源在所有支持的平台上是一致的,但在 Windows 和 OS X/iOS 上处理资源的方式略有不同。要了解如何在 Mac 上使用资源,请阅读 在 iOS 和 OS X 上使用资源 烹饪配方。

在 iOS 和 OS X 上使用资源

对于 Windows 应用程序来说,使用外部文件来加载图像、播放音频或视频,或在 XML 文件上加载或保存设置是很常见的。

资源是应用程序外部文件,包含在应用程序包中。资源文件对用户隐藏,以避免修改。

Cinder 允许编写代码以使用资源,这种方式在编写 Windows 或 Mac 应用程序时是相同的,但处理资源的方式略有不同。要了解如何在 Windows 上使用资源,请阅读 在 Windows 上使用资源 烹饪配方。

准备工作

应将资源存储在 project 文件夹中名为 resources 的文件夹中。如果此文件夹不存在,请创建它。

如何做到这一点…

我们将使用 Xcode 在 iOS 和 OS X 上添加资源到我们的应用程序。执行以下步骤:

  1. 将任何你希望使用的资源文件放置在 resources 文件夹中。

  2. 通过在 Xcode 项目的 Resources 过滤器上右键单击并选择 添加 然后选择 现有文件,导航到 resources 文件夹,并选择你想要添加的资源文件。

  3. 要在你的代码中加载资源,你使用 loadResource 方法并传递资源文件的名称。例如,要加载名为 image.png 的图像,你应在类声明中首先创建 gl::Texture 成员:

    gl::Texture mImage;
    
  4. setup 方法中,我们使用以下资源初始化纹理:

    mImage = loadImage( loadResource( "image.png" ));
    
  5. 纹理现在已准备好在窗口中绘制。要在位置 (20, 20) 绘制它,请在 draw 方法中输入以下代码行:

    gl::draw( mImage, Vec2f( 20.0f, 20.0f ) );
    

它是如何工作的……

在 iOS 和 OS X 上,应用程序实际上是包含运行应用程序所需所有文件的文件夹,例如 Unix 可执行文件、使用的框架和资源。你可以通过点击任何 Mac 应用程序并选择 显示包内容 来访问这些文件夹的内容。

当你在你的 Xcode 项目中的 resources 文件夹中添加资源时,这些文件会在构建阶段被复制到你的应用程序包的 resources 文件夹中。

更多内容……

你也可以使用在 Windows 应用程序中使用的相同 loadResource 方法来加载资源。这对于编写跨平台应用程序非常有用,这样你的代码就不需要做任何更改。

你应该在 Resources.h 文件中创建 resource 宏,并添加唯一资源 ID 及其类型字符串。例如,要加载图像 image.png,你可以输入以下代码片段:

#pragma once
#include "cinder/CinderResources.h"
#define RES_IMAGE CINDER_RESOURCE(../resources/, image.png, 128, IMAGE)

这就是 Resources.rc 文件应该看起来像的:

#include "..\include\Resources.h"

RES_IMAGE

使用前面的示例来加载图像,唯一的区别是我们将使用以下代码行来加载纹理:

mImage = loadImage( loadResource( RES_IMAGE ) );

资源唯一 ID 和类型字符串在 Mac 应用程序中将被忽略,但添加它们允许创建跨平台代码。

使用资源

在这个菜谱中,我们将学习我们如何加载和使用资源。

准备工作

作为这个菜谱的示例,我们将加载并显示一个资源图像。

在你的项目目录中的 assets 文件夹内放置一个图像文件,并将其命名为 image.png

在你的源代码顶部包含以下文件:

#include "cinder/gl/Texture.h"
#include "cinder/ImageIO.h"

还应添加以下有用的 using 语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何做……

作为示例,我们将学习我们如何加载并显示图像资源。执行以下步骤:

  1. 声明一个 ci::gl::Texture 对象:

    gl::Texture image;
    
  2. setup 方法中,让我们加载图像资源。如果无法加载资源,我们将使用 try/catch 块。

        try{
            image = loadImage( loadAsset( "image.png" ) );
        } catch( ... ){
            console() << "asset not found" << endl;
        }
    
  3. draw 方法中,我们将绘制纹理。我们将使用一个 if 语句来检查纹理是否已成功初始化:

    if( image ){
      gl::draw( image, getWindowBounds() );
        }
    

它是如何工作的……

第一个应用使用了一个名为 Cinder 的资产,它将尝试找到其默认的assets文件夹。它将首先在可执行文件或应用程序包文件夹中搜索,具体取决于平台,然后继续向上搜索其父文件夹,最多五层。这样做是为了适应不同的项目设置。

还有更多...

你可以使用addAssetDirectory方法添加一个额外的assets文件夹,该方法接受一个ci::fs::path对象作为参数。每次 Cinder 搜索资产时,它都会首先在其默认的asset文件夹中查找,然后查找用户可能添加的每个文件夹。

你也可以在assets文件夹内创建子文件夹,例如,如果我们的图片位于名为My Images的子文件夹中,我们就会在setup方法中输入以下代码片段:

try{
     image = loadImage( loadAsset( "My Images/image.png" ) );
}catch( ... ){
     console() << "asset not found" << endl;
 }

你也可以知道特定文件夹的路径。为此,使用getAssetPath方法,该方法接受一个带有文件名的ci::fs::path对象作为参数。

第二章:准备开发

在本章中,我们将涵盖:

  • 设置用于调整参数的图形用户界面(GUI)

  • 保存和加载配置

  • 捕获当前参数状态的快照

  • 使用 MayaCamUI

  • 使用 3D 空间指南

  • 与其他软件通信

  • 为 iOS 准备您的应用程序

简介

在本章中,我们将介绍一些在开发过程中非常有用的简单配方。

设置用于调整参数的 GUI

图形用户界面GUI)通常用于控制和调整您的 Cinder 应用程序。在许多情况下,您花费更多的时间调整应用程序参数以实现所需的结果,而不是编写代码。这在您正在处理一些生成图形时尤其如此。

Cinder 通过InterfaceGl类提供了一个方便且易于使用的 GUI。

设置用于调整参数的 GUI

准备就绪

要使InterfaceGl类在您的 Cinder 应用程序中可用,您只需包含一个头文件即可。

#include "cinder/params/Params.h"

如何操作…

按照此处给出的步骤将 GUI 添加到您的 Cinder 应用程序中。

  1. 让我们从在我们的主类中准备不同类型的变量开始,我们将使用 GUI 来操作这些变量。

    float mObjSize;
    Quatf mObjOrientation;
    Vec3f mLightDirection;
    ColorA mColor;
    
  2. 接下来,声明InterfaceGl类成员如下:

    params::InterfaceGl mParams;
    
  3. 现在我们转向setup方法,并初始化我们的 GUI 窗口,将"Parameters"作为窗口标题传递给InterfaceGl构造函数:

    mParams = params::InterfaceGl("Parameters", Vec2i(200,400));
    
  4. 现在我们可以添加和配置变量的控件:

    mParams.addParam( "Cube Size", &mObjSize, "min=0.1 max=20.5 step=0.5 keyIncr=z keyDecr=Z" );
    mParams.addParam( "Cube Rotation", &mObjOrientation ); // Quatf type
    mParams.addParam( "Cube Color", &mColor, "" ); // ColorA
    mParams.addSeparator(); // add horizontal line separating controls
    mParams.addParam( "Light Direction", &mLightDirection, "" ); // Vec3f
    mParams.addParam( "String ", &mString, "" ); // string
    

    查看addParam方法和其参数。第一个参数只是字段标题。第二个参数是存储值的变量的指针。有许多支持的变量类型,例如boolfloatdoubleintVec3fQuatfColorColorAstd::string

    可能的变量类型及其接口表示在以下表中列出:

    类型 表示
    std::string 如何操作…
    Numerical: int, float, double 如何操作…
    bool 如何操作…
    ci::Vec3f 如何操作…
    ci::Quatf 如何操作…
    ci::Color 如何操作…
    ci::ColorA 如何操作…
    枚举参数 如何操作…

    第三个参数定义了控制选项。在下面的表中,您可以找到一些常用选项及其简短说明:

    名称 说明
    min 数值变量的可能最小值
    max 数值变量的可能最大值
    step 定义浮点变量小数点后打印的显著数字的数量
    key 调用按钮回调的键盘快捷键
    keyIncr 增加值的键盘快捷键
    keyDecr 减少值的键盘快捷键
    readonly 将值设置为true使变量在 GUI 中为只读
    precision 定义浮点变量小数点后打印的显著数字的数量

    提示

    您可以在以下地址的 AntTweakBar 页面找到可用选项的完整文档:anttweakbar.sourceforge.net/doc/tools:anttweakbar:varparamsyntax

  5. 最后一件要做的事情是调用InterfaceGl::draw()方法。我们将在主类中的draw方法末尾通过输入以下代码行来完成此操作:

    params::InterfaceGl::draw();
    

它是如何工作的...

setup方法中,我们将设置 GUI 窗口并添加控件,在addParam方法的第一个参数中设置一个名称。在第二个参数中,我们指向我们想要链接 GUI 元素的变量。每次我们通过 GUI 更改值时,链接的变量都会更新。

更多...

对于InterfaceGl,如果您需要更多控制内置 GUI 机制,请参阅AntTweakBar文档,您可以在本菜谱的也见部分提到的项目页面上找到。

按钮

您还可以向 InterfaceGl (CIT)面板添加按钮,并为其分配一些函数的回调。例如:

mParams.addButton("Start", std::bind(&MainApp::start, this));

在 GUI 中点击开始按钮将触发MainApp类的start方法。

面板位置

控制 GUI 面板位置的便捷方式是通过使用AntTweekBar工具。您必须包含一个额外的头文件:

#include "AntTweakBar.h"

现在您可以使用以下代码行更改 GUI 面板的位置:

TwDefine("Parameters position='100 200' ");

在这种情况下,Parameters是 GUI 面板名称,position选项接受 x 和 y 作为值。

也见

CinderBlocks 中提供了一些看起来不错的 GUI 库。Cinder 有一个名为 blocks 的扩展系统。CinderBlocks 背后的理念是提供与许多第三方库的易于使用的集成。您可以在与其他软件通信菜谱中找到如何将 CinderBlocks 示例添加到您的项目的说明。

SimpleGUI

您可以在github.com/vorg/MowaLibs/tree/master/SimpleGUI找到由Marcin Ignac开发的作为 CinderBlock 的替代 GUI。

ciUI

您可以查看由Reza Ali开发的作为 CinderBlock 的替代用户界面,地址为www.syedrezaali.com/blog/?p=2366

AntTweakBar

Cinder 中的InterfaceGl是在AntTweakBar之上构建的;您可以在www.antisphere.com/Wiki/tools:anttweakbar找到其文档。

保存和加载配置

你将要开发的大多数应用程序都会操作用户设置的输入参数。例如,这可能是某些图形元素的色彩或位置,或者用于设置与其他应用程序通信的参数。从外部文件读取配置对于你的应用程序是必要的。我们将使用 Cinder 内置的读取和写入 XML 文件的支持来实现配置持久化机制。

准备工作

在主类中创建两个可配置的变量:我们正在与之通信的主机的 IP 地址和端口号。

string mHostIP;
int mHostPort;

如何操作...

现在,我们将实现 loadConfigsaveConfig 方法,并在应用程序启动时加载配置,在关闭时保存更改。

  1. 包含以下两个额外的头文件:

    #include "cinder/Utilities.h"
    #include "cinder/Xml.h"
    
  2. 我们将为加载和保存 XML 配置文件准备两种方法。

    void MainApp::loadConfig() 
    {
      try {
        XmlTree doc( loadFile( getAppPath() / fs::path("config.xml") ) );
        XmlTree &generalNode = doc.getChild( "general" );
    
        mHostIP = generalNode.getChild("hostIP").getValue();
        mHostPort = generalNode.getChild("hostPort").getValue<int>();
    
      } catch(Exception e) {
        console() << "ERROR: loading/reading configuration file." << endl;
      }
    }
    
    void MainApp::saveConfig() 
    {
      std::string beginXmlStr( "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" );
      XmlTree doc( beginXmlStr );
    
      XmlTree generalNode;
      generalNode.setTag("general");
      generalNode.push_back( XmlTree("hostIP", mHostIP) );
      generalNode.push_back( XmlTree("hostPort", toString(mHostPort)) );
      doc.push_back(generalNode);
    
      doc.write(writeFile( getAppPath() / fs::path("config.xml")) );
    }
    
  3. 现在,在主类的 setup 方法中,我们将放置以下内容:

    // setup default values
    mHostIP = "127.0.0.1";
    mHostPort = 1234;
    
    loadConfig();
    
  4. 在此之后,我们将按照以下方式实现 shutdown 方法:

    void MainApp::shutdown()
    {
      saveConfig();
    }
    
  5. 并且不要忘记在主类中声明 shutdown 方法:

    void shutdown();
    

它是如何工作的...

前两个方法,loadConfigsaveConfig,是必不可少的。loadConfig 方法尝试打开 config.xml 文件并找到 general 节点。在 general 节点中应该有 hostIPhostPort 节点。这些节点的值将被分配到我们应用程序中相应的变量:mHostIPmHostPort

shutdown 方法在 Cinder 应用程序关闭前自动触发,因此当我们退出应用程序时,我们的配置值将被存储在 XML 文件中。最后,我们的配置 XML 文件看起来像这样:

<?xml version="1.0" encoding="UTF-8" ?>
<general>
<hostIP>127.0.0.1</hostIP>
<hostPort>1234</hostPort>
</general>

你可以清楚地看到节点正在引用应用程序变量。

参见

你可以编写自己的配置加载器和保存器,或者使用现有的 CinderBlock。

Cinder-Config

Cinder-Config 是一个小的 CinderBlock,用于创建配置文件以及 InterfaceGl

github.com/dawidgorny/Cinder-Config

制作当前参数状态的快照

我们将实现一个简单但有用的机制来保存和加载参数的状态。示例中使用的代码将基于之前的配方。

准备工作

假设我们有一个频繁更改的变量。在这种情况下,它将是我们在绘图中更改的某个元素的色彩,主类将具有以下成员变量:

ColorA mColor;

如何操作...

我们将使用内置的 XML 解析器和 fileDrop 事件处理器。

  1. 我们必须包含以下额外的头文件:

    #include "cinder/params/Params.h"
    #include "cinder/ImageIo.h"
    #include "cinder/Utilities.h"
    #include "cinder/Xml.h"
    
  2. 首先,我们实现两个用于加载和保存参数的方法:

    void MainApp::loadParameters(std::string filename)
    {
      try {
        XmlTree doc( loadFile( fs::path(filename) ) );
        XmlTree &generalNode = doc.getChild( "general" );
    
            mColor.r = generalNode.getChild("ColorR").getValue<float>();
            mColor.g = generalNode.getChild("ColorG").getValue<float>();
            mColor.b = generalNode.getChild("ColorB").getValue<float>();
    
      } catch(XmlTree::Exception e) {
        console() << "ERROR: loading/reading configuration file." << e.what() << std::endl;
      }
    }
    
    void MainApp::saveParameters(std::string filename)
    {
      std::string beginXmlStr( "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" );
      XmlTree doc( beginXmlStr );
    
      XmlTree generalNode;
      generalNode.setTag("general");
      generalNode.push_back(XmlTree("ColorR", toString(mColor.r)));
      generalNode.push_back(XmlTree("ColorG", toString(mColor.g)));
      generalNode.push_back(XmlTree("ColorB", toString(mColor.b)));
    
      doc.push_back(generalNode);
    
      doc.write( writeFile( getAppPath() / fs::path("..") / fs::path(filename) ) );
    }
    
  3. 现在,我们声明一个类成员。它将是一个触发快照创建的标志:

    bool mMakeSnapshot;
    
  4. setup 方法中为其赋值:

    mMakeSnapshot = false;
    
  5. draw 方法的末尾,我们在 params::InterfaceGl::draw(); 行之前放置以下代码:

    if(mMakeSnapshot) {
      mMakeSnapshot = false;
    
      double timestamp = getElapsedSeconds();
      std::string timestampStr = toString(timestamp);
    
      writeImage(getAppPath() / fs::path("..") / fs::path("snapshot_" + timestampStr + ".png"), copyWindowSurface());
      saveParameters("snapshot_" + timestampStr + ".xml");
    }
    
  6. 我们想在 InterfaceGl 窗口中创建一个按钮:

    mParams.addButton( "Make snapshot", std::bind( &MainApp::makeSnapshotClick, this ) );
    

    正如你所见,我们还没有makeSnapshotClick方法。实现起来很简单:

    void MainApp::makeSnapshotClick()
    {
        mMakeSnapshot = true;
    }
    
  7. 最后一步将是添加以下方法以支持拖放

    void MainApp::fileDrop( FileDropEvent event )
    {
        std::string filepath = event.getFile( event.getNumFiles() - 1 ).generic_string();
        loadParameters(filepath);
    }
    

它是如何工作的...

我们有两种方法用于在 XML 文件中加载和存储mColor值。这些方法是loadParameterssaveParameters

我们放在draw方法内部的代码需要一些解释。我们正在等待mMakeSnapshot方法被设置为true,然后我们创建一个时间戳以避免覆盖之前的快照。接下来的两行通过调用saveParameters方法存储所选值,并使用writeImage函数将当前窗口视图保存为 PNG 文件。请注意,我们在调用InterfaceGl::draw之前放置了这段代码,所以我们保存的窗口视图没有 GUI。

这里有一个很好的功能是加载快照文件的拖放功能。它在fileDrop方法中实现;每当文件被拖放到你的应用程序窗口时,Cinder 都会调用此方法。首先,我们获取被拖放文件的路径;在多个文件的情况下,我们只取一个。然后我们使用被拖放文件的路径作为参数调用loadParameters方法。

使用 MayaCamUI

我们将向你的 3D 场景添加一个导航功能,这是我们自从建模 3D 软件以来就熟知的。使用MayaCamUI,你只需几行代码就能做到这一点。

准备工作

我们需要在场景中有些 3D 对象。你可以使用 Cinder 提供的某些原语,例如:

gl::drawColorCube(Vec3f::zero(), Vec3f(4.f, 4.f, 4.f));

一个彩色立方体是一个每个面都有不同颜色的立方体,因此很容易确定方向。

准备工作

如何做到这一点...

执行以下步骤以创建相机导航:

  1. 我们需要MayaCam.h头文件:

    #include "cinder/MayaCamUI.h"
    
  2. 我们还需要在主类中添加一些成员声明:

    CameraPersp  mCam;
    MayaCamUI    mMayaCam;
    
  3. setup方法内部,我们将设置相机的初始状态:

    mCam.setPerspective(45.0f, getWindowAspectRatio(), 0.1, 10000);
    mMayaCam.setCurrentCam(mCam);
    
  4. 现在我们必须实现三个方法:

    void MainApp::resize( ResizeEvent event )
    {
        mCam = mMayaCam.getCamera();
        mCam.setAspectRatio(getWindowAspectRatio());
        mMayaCam.setCurrentCam(mCam);
    }
    
    void MainApp::mouseDown( MouseEvent event )
    {
      mMayaCam.mouseDown( event.getPos() );
    }
    
    void MainApp::mouseDrag( MouseEvent event )
    {
      mMayaCam.mouseDrag( event.getPos(), event.isLeftDown(), event.isMiddleDown(), event.isRightDown() );
    }
    
  5. draw方法内部应用相机矩阵:

    gl::setMatrices(mMayaCam.getCamera());
    

它是如何工作的...

setup方法内部,我们设置初始的相机设置。当窗口调整大小时,我们必须更新相机的纵横比,因此我们将这段代码放在resize方法中。每当我们的应用程序窗口调整大小时,Cinder 都会自动调用此方法。我们在mouseDownmouseDrag方法内部捕获鼠标事件。你可以点击并拖动鼠标进行旋转,右键点击进行缩放,使用中间按钮进行平移。现在你已经在自己的应用程序中拥有了类似于常见 3D 建模软件的交互功能。

使用 3D 空间指南

我们将尝试使用内置的 Cinder 方法来可视化我们正在工作的场景的一些基本信息。这应该会使在 3D 空间中工作更加舒适。

准备工作

我们将需要我们在上一个菜谱中实现的MayaCamUI导航。

如何做到这一点...

我们将绘制一些有助于可视化和找到 3D 场景方向的物体。

  1. 我们将在 MayaCamUI 之外添加另一个相机。让我们先在主类中添加成员声明:

    CameraPersp     mSceneCam;
    int             mCurrentCamera;
    
  2. 然后,我们将在 setup 方法内部设置初始值:

    mCurrentCamera = 0;
    
    mSceneCam.setEyePoint(Vec3f(0.f, 5.f, 10.f));
    mSceneCam.setViewDirection(Vec3f(0.f, 0.f, -1.f) );
    mSceneCam.setPerspective(45.0f, getWindowAspectRatio(), 0.1, 20);
    
  3. 我们必须在 resize 方法中更新 mSceneCamera 的纵横比:

    mSceneCam.setAspectRatio(getWindowAspectRatio());
    
  4. 现在,我们将实现 keyDown 方法,通过按键盘上的 12 键在两个相机之间切换:

    void MainApp::keyDown( KeyEvent event )
    {
        if(event.getChar() == '1') {
            mCurrentCamera = 0;    
        } else if(event.getChar() == '2') {
            mCurrentCamera = 1;         
        }
    }
    
  5. 我们将要使用的方法是 drawGrid,它看起来是这样的:

    void MainApp::drawGrid(float size, float step)
    {
      gl::color( Color(0.7f, 0.7f, 0.7f) );
    
        //draw grid
        for(float i=-size;i<=size;i+=step) {
        gl::drawLine(Vec3f(i, 0.f, -size), Vec3f(i, 0.f, size));
        gl::drawLine(Vec3f(-size, 0.f, i), Vec3f(size, 0.f, i));
      }
    
        // draw bold center lines
        glLineWidth(2.f);
        gl::color(Color::white());
        gl::drawLine(Vec3f(0.f, 0.f, -size), Vec3f(0.f, 0.f, size));
        gl::drawLine(Vec3f(-size, 0.f, 0.f), Vec3f(size, 0.f, 0.f));
    
        glLineWidth(1.f);
    }
    
  6. 之后,我们可以实现我们的主要绘图程序,所以这里是整个 draw 方法:

    void MainApp::draw()
    {
      gl::enable(GL_CULL_FACE);
      gl::enableDepthRead();
      gl::enableDepthWrite();
      gl::clear( Color( 0.1f, 0.1f, 0.1f ) );
    
      if(mCurrentCamera == 0) {
            gl::setMatrices(mMayaCam.getCamera());
    
            // draw grid
            drawGrid(100.0f, 10.0f);
    
            // draw coordinate guide
            gl::pushMatrices();
            gl::translate(0.f, 0.4f, 0.f);
            gl::drawCoordinateFrame(5.0f, 1.5f, 0.3f);
            gl::popMatrices();
    
            // draw scene camera frustum
            gl::color(Color::white());
            gl::drawFrustum(mSceneCam);
    
            // draw vector guide
            gl::color(Color(1.f,0.f,0.f));
            gl::drawVector(Vec3f(-3.f, 7.f, -6.f), 
            Vec3f(3.f, 10.f, -9.f), 1.5f, 0.3);
    
        } else {
            gl::setMatrices(mSceneCam);
        }
    
        // draw some 3D object
        gl::rotate(30);
        gl::drawColorCube(Vec3f(0.f, 5.f, -5.f), 
        Vec3f(2.f, 2.f, 2.f));
    }
    

它是如何工作的...

我们有两个相机;mSceneCam 用于最终渲染,mMayaCam 用于场景中物体的预览。你可以通过按 12 键在它们之间切换。默认相机是 MayaCam

它是如何工作的...

在前面的截图中,你可以看到整个场景设置,包括坐标系统的原点、帮助你轻松保持 3D 空间方向的构造网格,以及 mSceneCam 之间的视锥体和向量可视化。你可以使用 MayaCamUI 在这个空间中导航。

如果你按下 2 键,你将切换到 mSceneCam 的视图,因此你将只看到你的 3D 对象,没有引导,如下面的截图所示:

它是如何工作的...

与其他软件通信

我们将实现两个 Cinder 应用程序之间的示例通信,以说明我们如何发送和接收信号。这两个应用程序中的每一个都可以很容易地被非 Cinder 应用程序替换。

我们将要使用 Open Sound Control (OSC) 消息格式,它是为网络中广泛的多媒体设备之间的通信而设计的。OSC 使用 UDP 协议,提供灵活性和性能。每个消息由类似 URL 的地址和整数、浮点或字符串类型的参数组成。OSC 的流行使其成为连接使用不同技术开发的网络或本地机器上的不同环境或应用程序的绝佳工具。

准备工作

在下载 Cinder 包的同时,我们也在下载四个主要块。其中之一是位于 blocks 目录中的 osc 块。首先,我们将向我们的 XCode 项目根目录添加一个新的组,并将其命名为 Blocks,然后我们将拖动 osc 文件夹到 Blocks 组中。确保选中 Create groups for any added folders 选项和 MainAppAdd to targets 部分中。

准备工作

我们只需要包含 osc 文件夹中的 src,因此我们将从我们的项目树中删除对 libsamples 文件夹的引用。最终的项目结构应该看起来像下面的截图:

准备工作

现在,我们必须在项目的构建设置中将 OSC 库文件的路径添加为另一个链接器标志的位置:

$(CINDER_PATH)/blocks/osc/lib/macosx/osc.a

小贴士

CINDER_PATH 应该在项目的构建设置中设置为用户定义的设置,并且它应该是 Cinder 根目录的路径。

如何实现...

首先,我们将介绍关于 发送者 的说明,然后是 监听者

发送者

我们将实现一个发送 OSC 消息的应用程序。

  1. 我们必须包含一个额外的头文件:

    #include "OSCSender.h"
    
  2. 之后,我们可以使用 osc::Sender 类,因此让我们在主类中声明所需的属性:

    osc::Sender mOSCSender;
    std::string mDestinationHost;
    int         mDestinationPort;
    
    Vec2f       mObjPosition;
    
  3. 现在,我们必须在 setup 方法中设置我们的发送者:

    mDestinationHost = "localhost";
    mDestinationPort = 3000;
    mOSCSender.setup(mDestinationHost, mDestinationPort);
    
  4. mObjectPosition 的默认值设置为窗口的中心:

    mObjPosition = Vec2f(getWindowWidth()*0.5f,
                         getWindowHeight()*0.5f);
    
  5. 我们现在可以实现 mouseDrag 方法,它包括两个主要操作——根据鼠标位置更新对象位置,并通过 OSC 发送位置信息。

    void MainApp::mouseDrag(MouseEvent event)
    {
        mObjPosition.x = event.getX();
        mObjPosition.y = event.getY();
    
      osc::Message msg;
      msg.setAddress("/obj/position");
      msg.addFloatArg(mObjPosition.x);
      msg.addFloatArg(mObjPosition.y);
      msg.setRemoteEndpoint(mDestinationHost, mDestinationPort);
      mOSCSender.sendMessage(msg);
    }
    
  6. 我们最后需要做的是绘制一个方法,仅用于可视化对象的位置:

    void MainApp::draw()
    {
      gl::clear(Color(0.1f, 0.1f, 0.1f));
        gl::color(Color::white());
        gl::drawStrokedCircle(mObjPosition, 50.f);
    }
    

监听者

我们将实现一个接收 OSC 消息的应用程序。

  1. 我们必须包含一个额外的头文件:

    #include "OSCListener.h"
    
  2. 之后,我们可以使用 osc::Listener 类,因此让我们在主类中声明所需的属性:

    osc::Listener  mOSCListener;
    Vec2f          mObjPosition;
    
  3. 现在,我们必须在 setup 方法中设置我们的监听者对象,传递监听端口作为参数:

    mOSCListener.setup(3000);
    
  4. 并且将 mObjectPosition 的默认值设置为窗口的中心:

    mObjPosition = Vec2f(getWindowWidth()*0.5f,
                         getWindowHeight()*0.5f);
    
  5. update 方法内部,我们将监听传入的 OSC 消息:

    void MainApp::update()
    {
        while (mOSCListener.hasWaitingMessages()) {
            osc::Message msg;
            mOSCListener.getNextMessage(&msg);
    
            if(msg.getAddress() == "/obj/position" &&
               msg.getNumArgs() == 2 &&
               msg.getArgType(0) == osc::TYPE_FLOAT &&
               msg.getArgType(1) == osc::TYPE_FLOAT)
            {
                mObjPosition.x = msg.getArgAsFloat(0);
                mObjPosition.y = msg.getArgAsFloat(1);
            }
        }
    }
    
  6. 我们的 draw 方法将与发送者版本几乎相同,但我们将绘制一个填充的圆圈而不是描边的圆圈:

    void MainApp::draw()
    {
      gl::clear( Color( 0.1f, 0.1f, 0.1f ) );
      gl::color(Color::white());
      gl::drawSolidCircle(mObjPosition, 50.f);
    }
    

工作原理...

我们已经实现了发送应用程序,该应用程序通过 OSC 协议发送鼠标位置。这些消息,带有地址 /obj/position,可以被任何在许多其他框架和编程语言中实现的非 Cinder OSC 应用程序接收。消息的第一个参数是鼠标的 x 轴位置,第二个参数是 y 轴位置。两者都是 float 类型。

工作原理...

在我们的例子中,接收消息的应用程序是另一个 Cinder 应用程序,它会在你指向发送应用程序窗口中的确切位置绘制一个填充的圆圈。

工作原理...

还有更多...

这只是 OSC 提供的可能性的一个简短示例。这种简单的通信方法甚至可以应用于非常复杂的项目。当多个设备作为独立单元工作时,OSC 工作得非常好。但到了某个时候,来自它们的数据需要被处理;例如,来自摄像机的帧可以被计算机视觉软件处理,并将结果通过网络发送到另一台投影可视化的机器。基于 UDP 协议的实现不仅因为传输数据比使用 TCP 快而提供性能,而且由于不需要连接握手,实现也更为简单。

广播

您可以通过设置一个广播地址作为目标主机来向您网络上的所有主机发送 OSC 消息:255.255.255.255。例如,在子网的情况下,您可以使用 192.168.1.255

小贴士

如果您在 Mac OS X 10.7 下编译时遇到链接错误,请尝试在您的项目构建设置中将 内联方法隐藏 设置为

参见

您可以通过查看以下链接来获取有关 OSC 实现的更多信息。

Flash 中的 OSC

要在您的 ActionScript 3.0 代码中支持接收和发送 OSC 消息,您可以使用以下库:bubblebird.at/tuioflash/

处理中的 OSC

要在您的 Processing 草图中支持 OSC 协议,您可以使用以下库:www.sojamo.de/libraries/oscP5/

openFrameworks 中的 OSC

要在您的 openFrameworks 项目中支持接收和发送 OSC 消息,您可以使用 ofxOsc 扩展程序:ofxaddons.com/repos/112

OpenSoundControl 协议

您可以在其官方网站上找到有关 OSC 协议和相关工具的更多信息:opensoundcontrol.org/

为 iOS 准备应用程序

使用 Cinder 的主要好处是生成的多平台代码。在大多数情况下,您的应用程序可以在 Windows、Mac OS X 和 iOS 上编译,而无需进行重大修改。

准备工作

如果您想在 iOS 设备上运行应用程序,您需要注册为 Apple 开发者并购买 iOS 开发者计划。

如何操作...

在注册为 Apple 开发者或购买 iOS 开发者计划后,您可以使用 Tinderbox 创建一个初始的 XCode iOS 项目。

  1. 在运行 Tinderbox 后,您必须将 目标 设置为 Cocoa Touch如何操作...

  2. 它将为您生成一个项目结构,支持针对多点触控屏幕的特定 iOS 事件。

    我们可以使用事件来处理多个触摸操作,并轻松访问加速度计数据。触摸事件和鼠标事件之间的主要区别在于,在只有一个鼠标光标的情况下,可以有多个活跃的触摸点。正因为如此,每个触摸会话都有一个 ID,可以从 TouchEvent 对象中读取。

    方法 描述
    touchesBegan( TouchEvent event ) 多指触摸序列的开始
    touchesMoved( TouchEvent event ) 在多指触摸序列中拖动
    touchesEnded( TouchEvent event ) 多指触摸序列的结束
    getActiveTouches() 返回所有活动触摸
    accelerated( AccelEvent event ) 加速度方向的 3D 向量

相关内容

我建议你查看 Cinder 包中包含的示例项目:MultiTouchBasiciPhoneAccelerometer

苹果开发者中心

你可以在这里找到有关 iOS 开发者计划的更多信息:developer.apple.com/

第三章。使用图像处理技术

在本章中,我们将涵盖:

  • 调整图像的对比度和亮度

  • 与 OpenCV 集成

  • 检测边缘

  • 检测面部

  • 在图像中检测特征

  • 将图像转换为矢量图形

简介

在本章中,我们将展示使用 Cinder 中实现图像处理技术的示例,以及使用第三方库的示例。在大多数示例中,我们将使用以下著名的测试图像,该图像广泛用于说明计算机视觉算法和技术:

简介

您可以从维基百科下载 Lenna 的图像(en.wikipedia.org/wiki/File:Lenna.png)。

调整图像的对比度和亮度

在本配方中,我们将介绍使用Surface类进行像素操作的基本图像颜色转换。

准备工作

要更改对比度和亮度的值,我们将使用第二章中介绍的InterfaceGl在设置 GUI 以调整参数时做准备配方。我们需要一个样本图像来继续操作;将其保存到您的assets文件夹中,命名为image.png

如何操作...

我们将创建一个具有简单 GUI 的应用程序,用于在样本图像上调整对比度和亮度。执行以下步骤:

  1. 包含必要的头文件:

    #include "cinder/gl/gl.h"
    #include "cinder/gl/Texture.h"
    #include "cinder/Surface.h"
    #include "cinder/ImageIo.h"
    
  2. 向主类添加属性:

    float mContrast,mContrastOld;
    float mBrightness,mBrightnessOld;
    Surface32f  mImage, mImageOutput;
    
  3. setup方法中,加载一张图像进行处理,并准备Surface对象以存储处理后的图像:

    mImage = loadImage( loadAsset("image.png") );
    mImageOutput = Surface32f(mImage.getWidth(), 
            mImage.getHeight(), false);
    
  4. 设置窗口大小为默认值:

    setWindowSize(1025, 512);
    mContrast = 0.f;
    mContrastOld = -1.f;
    mBrightness = 0.f;
    mBrightnessOld = -1.f;
    
  5. 将参数控制添加到InterfaceGl窗口中:

    mParams.addParam("Contrast", &mContrast, 
    "min=-0.5 max=1.0 step=0.01");
    mParams.addParam("Brightness", &mBrightness, 
          "min=-0.5 max=0.5 step=0.01");
    
  6. 按如下方式实现update方法:

    if(mContrastOld != mContrast || mBrightnessOld != mBrightness) {
    float c = 1.f + mContrast;
        Surface32f::IterpixelIter = mImage.getIter();
        Surface32f::IterpixelOutIter = mImageOutput.getIter();
    
        while( pixelIter.line() ) {
        pixelOutIter.line();
        while( pixelIter.pixel() ) {
        pixelOutIter.pixel();
    
        // contrast transformation
        pixelOutIter.r() = (pixelIter.r() - 0.5f) * c + 0.5f;
        pixelOutIter.g() = (pixelIter.g() - 0.5f) * c + 0.5f;
        pixelOutIter.b() = (pixelIter.b() - 0.5f) * c + 0.5f;
    
        // brightness transformation
        pixelOutIter.r() += mBrightness;
        pixelOutIter.g() += mBrightness;
        pixelOutIter.b() += mBrightness;
    
            }
        }
    
    mContrastOld = mContrast;
    mBrightnessOld = mBrightness;
    }
    
  7. 最后,我们将在draw方法内部添加以下代码行来绘制原始图像和处理后的图像:

    gl::draw(mImage);
    gl::draw(mImageOutput, Vec2f(512.f+1.f, 0.f));
    

如何工作...

最重要的一部分在update方法中。在第 6 步中,我们检查对比度和亮度的参数是否已更改。如果已更改,我们将遍历原始图像的所有像素,并将重新计算的颜色值存储在mImageOutput中。调整亮度只是增加或减少每个颜色组件,而计算对比度则稍微复杂一些。对于每个颜色组件,我们使用乘法公式,颜色 = (颜色 - 0.5) * 对比度 + 0.5,其中对比度是一个介于 0.5 和 2 之间的数字。在 GUI 中,我们设置一个介于-0.5 和 1.0 之间的值,这是一个更自然的范围;然后在第 6 步的开始进行重新计算。在处理图像时,我们必须更改所有像素的颜色值,因此在第 6 步的后面,您可以看到我们使用两个while循环遍历每行的后续列。为了移动到下一行,我们在Surface迭代器上调用line方法,然后调用pixel方法来移动到当前行的下一个像素。这种方法比使用例如getPixelsetPixel方法要快得多。

如何工作...

我们的应用程序在左侧渲染原始图像,在右侧渲染处理后的图像,因此你可以比较颜色调整的结果。

与 OpenCV 集成

OpenCV 是一个非常强大的开源计算机视觉库。该库是用 C++编写的,因此可以轻松集成到你的 Cinder 应用程序中。Cinder 包中提供了一个非常有用的 OpenCV Cinder 块,可在 GitHub 仓库中找到(github.com/cinder/Cinder-OpenCV)。

准备工作

确保 Xcode 正在运行,并且已经打开了一个 Cinder 项目。

如何操作…

我们将向你的项目中添加 OpenCV Cinder 块,这同时也说明了将任何其他 Cinder 块添加到项目中的通常方法。执行以下步骤:

  1. 在我们的 Xcode 项目根目录中添加一个新的组,并将其命名为Blocks。然后,将opencv文件夹拖到Blocks组中。务必选择以下截图所示的为添加的任何文件夹创建组单选按钮:如何操作…

  2. 你只需要项目结构中opencv文件夹内的include文件夹,因此删除对其他文件夹的任何引用。最终的项目结构应如下截图所示:如何操作…

  3. 在你的项目构建设置的其他链接器标志部分添加 OpenCV 库文件的路径,例如:

    $(CINDER_PATH)/blocks/opencv/lib/macosx/libopencv_imgproc.a
    $(CINDER_PATH)/blocks/opencv/lib/macosx/libopencv_core.a
    $(CINDER_PATH)/blocks/opencv/lib/macosx/libopencv_objdetect.a
    

    这些路径如下截图所示:

    如何操作…

  4. 在你的项目构建设置的用户头文件搜索路径部分添加你将要使用的 OpenCV Cinder 块头文件的路径:

    $(CINDER_PATH)/blocks/opencv/include
    

    如下截图所示,这是该路径:

    如何操作…

  5. 包含 OpenCV Cinder 块头文件:

    #include "CinderOpenCV.h"
    

它是如何工作的…

OpenCV Cinder 块提供了toOcvfromOcv函数,用于 Cinder 和 OpenCV 之间的数据交换。在设置好项目后,你可以使用它们,如下面的简短示例所示:

Surface mImage, mImageOutput;
mImage = loadImage( loadAsset("image.png") );
cv::Mat ocvImage(toOcv(mImage));
cv::cvtColor(ocvImage, ocvImage, CV_BGR2GRAY ); 
mImageOutput = Surface(fromOcv(ocvImage));

你可以使用toOcvfromOcv函数在 Cinder 和 OpenCV 类型之间进行转换,存储通过ImageSourceRef类型处理的图像数据,如SurfaceChannel;还有其他类型,如下表所示:

Cinder 类型 OpenCV 类型
ImageSourceRef Mat
Color Scalar
Vec2f Point2f
Vec2i Point
Area Rect

在此示例中,我们链接了 OpenCV 包中的以下三个文件:

  • libopencv_imgproc.a:此图像处理模块包括图像操作函数、过滤器、特征检测等

  • libopencv_core.a:此模块提供核心功能和数据结构

  • libopencv_objdetect.a:此模块包含如级联分类器等目标检测工具

你可以在docs.opencv.org/index.html找到所有 OpenCV 模块的文档。

还有更多…

在 OpenCV Cinder 块中打包的预编译 OpenCV 库中,有一些功能是不可用的,但你可以始终编译自己的 OpenCV 库,并在你的项目中仍然使用 OpenCV Cinder 块中的交换函数。

检测边缘

在这个菜谱中,我们将演示如何使用边缘检测函数,这是 Cinder 直接实现的一种图像处理函数。

准备工作

确保 Xcode 正在运行,并且已经打开了一个空的 Cinder 项目。我们将需要一个示例图像来继续,所以将其保存到你的资源文件夹中,命名为image.png

如何做到这一点…

我们将使用边缘检测函数处理示例图像。执行以下步骤来完成此操作:

  1. 包含必要的头文件:

    #include "cinder/gl/Texture.h"
    #include "cinder/Surface.h"
    #include "cinder/ImageIo.h"
    
    #include "cinder/ip/EdgeDetect.h"
    #include "cinder/ip/Grayscale.h"
    
  2. 向你的主类添加两个属性:

    Surface8u mImageOutput;
    
  3. setup方法中加载源图像并设置处理图像的Surface

    mImage = loadImage( loadAsset("image.png") );
    mImageOutput = Surface8u(mImage.getWidth(), mImage.getHeight(), false);
    
  4. 使用图像处理函数:

    ip::grayscale(mImage, &mImage);
    ip::edgeDetectSobel(mImage, &mImageOutput);
    
  5. draw方法内部添加以下两行代码以绘制图像:

    gl::draw(mImage);
    gl::draw(mImageOutput, Vec2f(512.f+1.f, 0.f));
    

它是如何工作的…

如您所见,由于 Cinder 直接实现了基本的图像处理函数,因此在 Cinder 中检测边缘非常简单,因此您不需要包含任何第三方库。在这种情况下,我们使用grayscale函数将原始图像的颜色空间转换为灰度。这是图像处理中常用的功能,因为许多算法在灰度图像上运行得更有效率,或者甚至是为仅与灰度源图像一起工作而设计的。边缘检测是通过edgeDetectSobel函数实现的,并使用 Sobel 算法。在这种情况下,第一个参数是源原始灰度图像,第二个参数是输出Surface对象,结果将存储在其中。

draw方法内部,我们绘制了两个图像,如下面的截图所示:

它是如何工作的…

还有更多…

你可能会发现 Cinder 中实现的图像处理函数不足,因此你也可以将第三方库,如 OpenCV,包含到你的项目中。我们在先前的菜谱中解释了如何将 Cinder 和 OpenCV 一起使用,与 OpenCV 集成

在边缘检测的上下文中,其他有用的函数是CannyfindContours。以下是如何使用它们的示例:

vector<vector<cv::Point> > contours; 
cv::Mat inputMat( toOcv( frame ) );
// blur
cv::cvtColor( inputMat, inputMat, CV_BGR2GRAY );
cv::Mat blurMat;
cv::medianBlur(inputMat, blurMat, 11);

// threshold
cv::Mat thresholdMat;
cv::threshold(blurMat, thresholdMat, 50, 255, CV_8U );

// erode
cv::Mat erodeMat;
cv::erode(thresholdMat, erodeMat, 11);

// Detect edges
cv::Mat cannyMat;
int thresh = 100;
cv::Canny(erodeMat, cannyMat, thresh, thresh*2, 3 );

// Find contours
cv::findContours(cannyMat, contours, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE);

执行前面的代码后,形成轮廓的点存储在contours变量中。

检测人脸

在这个菜谱中,我们将检查我们的应用程序如何被用来识别人脸。多亏了 OpenCV 库,这真的非常简单。

准备工作

我们将使用 OpenCV 库,因此请参阅与 OpenCV 集成配方以获取有关如何设置项目的信息。我们将需要一个示例图像来继续,所以将其保存到您的assets文件夹中作为image.png。将用于正面人脸识别的 Haar 级联分类器文件放入assets目录中。级联文件可以在下载的 OpenCV 包中找到,或者在位于github.com/Itseez/opencv/blob/master/data/haarcascades/haarcascade_frontalface_alt.xml的在线公共存储库中找到。

如何做…

我们将创建一个应用程序,演示如何使用 Cinder 与 OpenCV 的级联分类器。执行以下步骤:

  1. 包含必要的头文件:

    #include "cinder/gl/Texture.h"
    #include "cinder/Surface.h"
    #include "cinder/ImageIo.h"
    
  2. 将以下成员添加到您的主类中:

    Surface8u mImage;
    cv::CascadeClassifier  mFaceCC;
    std::vector<Rectf>  mFaces;
    
  3. 将以下代码片段添加到setup方法中:

    mImage = loadImage( loadAsset("image.png") );
    mFaceCC.load( getAssetPath( "haarcascade_frontalface_alt.xml" ).string() );
    
  4. 还需要在setup方法的末尾添加以下代码片段:

    cv::Mat cvImage( toOcv( mImage, CV_8UC1 ) );
    std::vector<cv::Rect> faces;
    mFaceCC.detectMultiScale( cvImage, faces );
    std::vector<cv::Rect>::const_iterator faceIter;
    for(faceIter = faces.begin(); faceIter != faces.end(); ++faceIter ) {
      Rectf faceRect( fromOcv( *faceIter ) );
      mFaces.push_back( faceRect );
    }
    
  5. draw方法的末尾添加以下代码片段:

    gl::color( Color::white() );
    gl::draw(mImage);
    gl::color( ColorA( 1.f, 0.f, 0.f, 0.45f ) );
    std::vector<Rectf>::const_iterator faceIter;
    for(faceIter = mFaces.begin(); faceIter != mFaces.end(); ++faceIter ) {
      gl::drawStrokedRect( *faceIter );
    }
    

它是如何工作的…

在第 3 步中,我们加载了一个图像文件用于处理和一个 XML 分类器文件,该文件描述了要识别的对象特征。在第 4 步中,我们通过在mFaceCC对象上调用detectMultiScale函数执行图像检测,我们将cvImage作为输入指向,并将结果存储在一个向量结构中,cvImage是从mImage转换而来的 8 位单通道图像(CV_8UC1)。我们接下来所做的是遍历所有检测到的人脸并存储Rectf变量,该变量描述了检测到的人脸周围的边界框。最后,在第 5 步中,我们绘制了原始图像和所有识别到的人脸作为描边的矩形。

我们使用的是 OpenCV 中实现的级联分类器,它可以训练来检测图像中的特定对象。有关训练和使用级联分类器进行对象检测的更多信息,请参阅位于docs.opencv.org/modules/objdetect/doc/cascade_classification.html的 OpenCV 文档。

如何工作…

还有更多…

您可以使用来自相机的视频流并处理每一帧以实时跟踪人脸。请参阅第十一章中的从相机捕获配方,从相机获取感应和跟踪输入

在图像中检测特征

在这个配方中,我们将使用图像中查找特征特征的方法之一。我们将使用 OpenCV 库实现的 SURF 算法。

准备工作

我们将使用 OpenCV 库,因此请参考与 OpenCV 集成菜谱,了解如何设置您的项目。我们将需要一个样本图像来继续,所以将其保存到您的assets文件夹中,命名为image.png,然后保存样本图像的副本为image2.png,并对它进行一些变换,例如旋转。

如何做到这一点…

我们将创建一个应用程序,用于可视化两张图像之间的匹配特征。执行以下步骤来完成此操作:

  1. 在您项目的构建设置中其他链接器标志部分添加 OpenCV 库文件的路径,例如:

    $(CINDER_PATH)/blocks/opencv/lib/macosx/libopencv_imgproc.a
    $(CINDER_PATH)/blocks/opencv/lib/macosx/libopencv_core.a
    $(CINDER_PATH)/blocks/opencv/lib/macosx/libopencv_objdetect.a
    $(CINDER_PATH)/blocks/opencv/lib/macosx/libopencv_features2d.a
    $(CINDER_PATH)/blocks/opencv/lib/macosx/libopencv_flann.a
    
  2. 包含必要的标题:

    #include "cinder/gl/Texture.h"
    #include "cinder/Surface.h"
    #include "cinder/ImageIo.h"
    
  3. 在您的主类声明中添加方法和属性:

    int matchImages(Surface8u img1, Surface8u img2);
    
    Surface8u   mImage, mImage2;
    gl::Texture mMatchesImage;
    
  4. setup方法中加载图像并调用匹配方法:

    mImage = loadImage( loadAsset("image.png") );
    mImage2 = loadImage( loadAsset("image2.png") );
    
    int numberOfmatches = matchImages(mImage, mImage2);
    
  5. 现在您必须实现之前声明的matchImages方法:

    int MainApp::matchImages(Surface8u img1, Surface8u img2)
    {
      cv::Mat image1(toOcv(img1));
      cv::cvtColor( image1, image1, CV_BGR2GRAY );
    
      cv::Mat image2(toOcv(img2));
      cv::cvtColor( image2, image2, CV_BGR2GRAY );
    
      // Detect the keypoints using SURF Detector
      std::vector<cv::KeyPoint> keypoints1, keypoints2;
    
      cv::SurfFeatureDetector detector;
      detector.detect( image1, keypoints1 );
      detector.detect( image2, keypoints2 );
    
      // Calculate descriptors (feature vectors)
      cv::SurfDescriptorExtractor extractor;
      cv::Mat descriptors1, descriptors2;
    
      extractor.compute( image1, keypoints1, descriptors1 );
      extractor.compute( image2, keypoints2, descriptors2 );
    
      // Matching
      cv::FlannBasedMatcher matcher;
      std::vector<cv::DMatch> matches;
      matcher.match( descriptors1, descriptors2, matches );
    
      double max_dist = 0; 
      double min_dist = 100;
    
      for( int i = 0; i< descriptors1.rows; i++ )
        {
      double dist = matches[i].distance;
      if( dist<min_dist ) min_dist = dist;
      if( dist>max_dist ) max_dist = dist;
          }
    
      std::vector<cv::DMatch> good_matches;
    
      for( int i = 0; i< descriptors1.rows; i++ )
          {
      if( matches[i].distance<2*min_dist )
      good_matches.push_back( matches[i]);
          }
    
      // Draw matches
      cv::Matimg_matches;
      cv::drawMatches(image1, keypoints1, image2, keypoints2,
      good_matches, img_matches, cv::Scalar::all(-1),cv::Scalar::all(-1),
      std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS );
    
      mMatchesImage = gl::Texture(fromOcv(img_matches));
    
      return good_matches.size();
        }
    
  6. 最后,我们需要可视化匹配,所以将以下代码行放在draw方法中:

    gl::draw(mMatchesImage);
    

工作原理…

让我们讨论第 5 步下的代码。首先,我们将image1image2转换为 OpenCV Mat 结构。然后,我们将这两张图像转换为灰度图。现在我们可以开始使用 SURF 处理图像了,因此我们正在检测关键点——该算法计算出的图像的特征点。我们可以使用这两个图像计算出的关键点,并使用 FLANN 进行匹配,或者更确切地说,使用FlannBasedMatcher类。在过滤掉适当的匹配并将它们存储在good_matches向量中后,我们可以按以下方式可视化它们:

工作原理…

请注意,第二张图像已旋转,然而算法仍然可以找到并链接相应的关键点。

更多内容…

在图像中检测特征对于匹配图片至关重要,也是用于增强现实应用中的更高级算法的一部分。

如果图像匹配

有可能确定一张图像是否是另一张图像的副本,或者是否已旋转。您可以使用matchImages方法返回的匹配数量。

其他可能性

SURF 算法对于实时匹配来说相当慢,所以如果您需要实时处理来自摄像机的帧,可以尝试您项目的 FAST 算法。FAST 算法也包含在 OpenCV 库中。

参见

将图像转换为矢量图形

在这个菜谱中,我们将尝试使用 OpenCV 库和 Cairo 库的矢量绘图和导出功能,将简单的手绘草图转换为矢量图形。

入门

我们将使用 OpenCV 库,因此请参考本章前面的 与 OpenCV 集成 菜谱,获取有关如何设置你的项目的信息。你可能想准备自己的绘图以便处理。在这个例子中,我们使用了一张在纸上绘制的简单几何形状的照片。

入门

如何做到这一点…

我们将创建一个应用程序来展示矢量形状的转换。执行以下步骤:

  1. 包含必要的头文件:

    #include "cinder/gl/Texture.h"
    #include "cinder/Surface.h"
    #include "cinder/ImageIo.h"
    #include "cinder/cairo/Cairo.h"
    
  2. 在主类中添加以下声明:

    void renderDrawing( cairo::Context&ctx );
    
    Surface mImage, mIPImage;
    std::vector<std::vector<cv::Point> >mContours, mContoursApprox;
    double mApproxEps;
    int mCannyThresh;
    
  3. setup 方法中加载你的绘图并设置默认值:

    mImage = loadImage( loadAsset("drawing.jpg") );
    
    mApproxEps = 1.0;
    mCannyThresh = 200;
    
  4. setup 方法的末尾添加以下代码片段:

    cv::Mat inputMat( toOcv( mImage ) );
    
    cv::Mat bgr, gray, outputFrame;
    cv::cvtColor(inputMat, bgr, CV_BGRA2BGR);
    double sp = 50.0;
    double sr = 55.0;
    cv::pyrMeanShiftFiltering(bgr.clone(), bgr, sp, sr);
    
    cv::cvtColor(bgr, gray, CV_BGR2GRAY);
    cv::cvtColor(bgr, outputFrame, CV_BGR2BGRA);
    mIPImage = Surface(fromOcv(outputFrame));
    cv::medianBlur(gray, gray, 7);
    
    // Detect edges using
    cv::MatcannyMat;
    cv::Canny(gray, cannyMat, mCannyThresh, mCannyThresh*2.f, 3 );
    mIPImage = Surface(fromOcv(cannyMat));
    
    // Find contours
    cv::findContours(cannyMat, mContours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);
    
    // prepare outline
    for( int i = 0; i<mContours.size(); i++ )
    {
    std::vector<cv::Point> approxCurve;
    cv::approxPolyDP(mContours[i], approxCurve, mApproxEps, true);
    mContoursApprox.push_back(approxCurve);
    }
    
  5. 添加 renderDrawing 方法的实现:

    void MainApp::renderDrawing( cairo::Context&ctx )
    {
      ctx.setSource( ColorA( 0, 0, 0, 1 ) );
      ctx.paint();
    
      ctx.setSource( ColorA( 1, 1, 1, 1 ) );
      for( int i = 0; i<mContoursApprox.size(); i++ )
        {
      ctx.newSubPath();
      ctx.moveTo(mContoursApprox[i][0].x, mContoursApprox[i][0].y);
      for( int j = 1; j <mContoursApprox[i].size(); j++ )
            {
    ctx.lineTo(mContoursApprox[i][j].x, mContoursApprox[i][j].y);
            }
    ctx.closePath();
    ctx.fill();
    
    ctx.setSource(Color( 1, 0, 0 ));
    for( int j = 1; j <mContoursApprox[i].size(); j++ )
            {
    ctx.circle(mContoursApprox[i][j].x, mContoursApprox[i][j].y, 2.f);
            }
    ctx.fill();
        }
    }
    
  6. 按照以下方式实现你的 draw 方法:

      gl::clear( Color( 0.1f, 0.1f, 0.1f ) );
    
      gl::color(Color::white());
    
      gl::pushMatrices();
      gl::scale(Vec3f(0.5f,0.5f,0.5f));
      gl::draw(mImage);
      gl::draw(mIPImage, Vec2i(0, mImage.getHeight()+1));
      gl::popMatrices();
    
      gl::pushMatrices();
      gl::translate(Vec2f(mImage.getWidth()*0.5f+1.f, 0.f));
      gl::color( Color::white() );
    
      cairo::SurfaceImage vecSurface( mImage.getWidth(), mImage.getHeight() );
      cairo::Context ctx( vecSurface );
      renderDrawing(ctx);
      gl::draw(vecSurface.getSurface());
    
      gl::popMatrices();
    
  7. keyDown 方法中插入以下代码片段:

    if( event.getChar() == 's' ) {
    cairo::Context ctx( cairo::SurfaceSvg( getAppPath() / fs::path("..") / "output.svg",mImage.getWidth(), mImage.getHeight() ) );
    renderDrawing( ctx );
    }
    

它是如何工作的…

关键部分在步骤 4 中实现,我们在图像中检测边缘然后找到轮廓。我们在 renderDrawing 方法中步骤 5 内绘制处理形状的矢量表示。为了绘制矢量图形,我们使用 Cairo 库,它还能将结果保存为多种矢量格式。如图所示,在屏幕左上角是原始图像,其下方是检测到的轮廓预览。我们简单手绘图像的矢量版本在右侧:

它是如何工作的…

每个形状都是一个黑色填充路径。路径由步骤 4 中计算出的点组成。以下是对突出显示的点的可视化:

它是如何工作的…

你可以通过按 S 键将矢量图形保存为文件。文件将保存在应用程序可执行文件相同的文件夹下,命名为 output.svg。SVG 只是以下可用导出选项之一:

方法 用法
SurfaceSvg 准备 SVG 文件渲染的上下文
SurfacePdf 准备 PDF 文件渲染的上下文
SurfacePs 准备 PostScript 文件渲染的上下文
SurfaceEps 准备 Illustrator EPS 文件渲染的上下文

导出的图形如下所示:

它是如何工作的…

参见

第四章。使用多媒体内容

在本章中,我们将学习以下内容:

  • 加载和显示视频

  • 创建一个简单的视频控制器

  • 将窗口内容保存为图像

  • 将窗口动画保存为视频

  • 将窗口内容保存为矢量图形图像

  • 使用瓦片渲染器保存高分辨率图像

  • 在应用程序之间共享图形

简介

大多数有趣的应用都以某种形式使用多媒体内容。在本章中,我们将首先学习如何加载、操作和显示视频。然后,我们将继续将我们的图形保存为图像、图像序列或视频,然后我们将转向录音可视化。

最后,我们将学习如何在应用程序之间共享图形以及如何保存网格数据。

加载和显示视频

在这个菜谱中,我们将学习如何使用 Quicktime 和 OpenGL 从文件中加载视频并在屏幕上显示。我们将学习如何将文件作为资源加载,或者通过文件打开对话框由用户选择文件来加载。

准备工作

您需要安装 QuickTime,并且还需要一个与 QuickTime 兼容格式的视频文件。

要将视频作为资源加载,需要将其复制到项目中的resources文件夹。要了解更多关于资源的信息,请阅读来自第一章的菜谱在 Windows 上使用资源在 OSX 和 iOS 上使用资源入门

如何做到这一点…

我们将使用 Cinder 的 QuickTime 包装器来加载和显示视频。

  1. 通过在源文件开头添加以下内容来包含包含 Quicktime 和 OpenGL 功能的头文件:

    #include "cinder/qtime/QuickTime.h"
    #include "cinder/gl/gl.h"
    #include "cinder/gl/Texture.h"
    
  2. 在您应用程序的类声明中声明一个ci::qtime::MovieGl成员。此示例只需要setupupdatedraw方法,所以请确保至少声明这些方法:

    using namespace ci;
    using namespace ci::app;
    
    class MyApp : public AppBasic {
    public:
      void setup();
      void update();
      void draw();
    
    qtime::MovieGl mMovie;
    gl::Texture mMovieTexture;
    };
    
  3. 要将视频作为资源加载,请使用ci::app::loadResource方法,将文件名作为parameter,并在构造电影对象时传递结果ci::app::DataSourceRef。将加载资源放在trycatch段中也是一个好习惯,以便捕获任何资源加载错误。请在您的setup方法中放置以下代码:

    try{
    mMovie = qtime::MovieGl( loadResource( "movie.mov" ) );
        } catch( Exception e){
    console() <<e.what()<<std::endl;
        }
    
  4. 您也可以通过使用文件打开对话框并在构造mMovie对象时传递文件路径作为参数来加载视频。您的setup方法将具有以下代码:

    try{
    fs::path path = getOpenFilePath();
    mMovie = qtime::MovieGl( path );
        } catch( Exception e){
    console() <<e.what()<<std::endl;
        }
    
  5. 要播放视频,请调用电影对象的play方法。您可以通过将其放在一个if语句中来测试mMovie的成功实例化,就像一个普通的指针一样:

    If( mMovie ){
    mMovie.play();
    }
    
  6. update方法中,我们将当前电影帧的纹理复制到我们的mMovieTexture中,以便稍后绘制:

    void MyApp::update(){
    if( mMovie ){
    mMovieTexture = mMovie.getTexture();
    }
    
  7. 要绘制电影,我们只需使用gl::draw方法在屏幕上绘制我们的纹理。我们需要检查纹理是否有效,因为mMovie可能需要一段时间才能加载。我们还将创建ci::Rectf与纹理大小,并将其居中在屏幕上,以保持绘制的视频居中而不拉伸:

    gl::clear( Color( 0, 0, 0 ) ); 
    if( mMovieTexture ){
    Rect frect = Rectf( mMovieTexture.getBounds() ).getCenteredFit( getWindowBounds(), true );
    gl::draw( mMovieTexture, rect );
    }
    

它是如何工作的…

ci::qtime::MovieGl类通过封装 QuickTime 框架允许播放和控制电影。电影帧被复制到 OpenGl 纹理中,以便于绘制。要访问电影当前帧的纹理,请使用ci::qtime::MovieGl::getTexture()方法,它返回一个ci::gl::Texture对象。ci::qtime::MovieGl使用的纹理始终绑定到GL_TEXTURE_RECTANGLE_ARB目标。

还有更多

如果你希望对电影中的像素进行迭代,请考虑使用ci::qtime::MovieSurface类。这个类通过封装 QuickTime 框架来播放电影,但将电影帧转换为ci::Surface对象。要访问当前帧的表面,请使用ci::qtime::MovieSurface::getSurface()方法,它返回一个ci::Surface对象。

创建一个简单的视频控制器

在这个菜谱中,我们将学习如何使用 Cinder 的内置 GUI 功能创建一个简单的视频控制器。

我们将控制电影播放,包括电影是否循环、播放速度、音量和位置。

准备工作

你必须安装 Apple 的 QuickTime,并且有一个与 QuickTime 兼容的电影文件。

要了解如何加载和显示电影,请参考之前的菜谱加载和显示视频

如何实现...

我们将创建一个简单的界面,使用 Cinder params类来控制视频。

  1. 通过在源文件顶部添加以下内容,包含必要的文件以使用 Cinder params(QuickTime 和 OpenGl):

    #include "cinder/gl/gl.h"
    #include "cinder/gl/Texture.h"
    #include "cinder/qtime/QuickTime.h"
    #include "cinder/params/Params.h"
    #include "cinder/Utilities.h"
    
  2. 在应用程序类声明之前添加using语句,以简化调用 Cinder 命令,如下所示:

    using namespace ci;
    using namespace ci::app;
    using namespace ci::gl;
    
  3. 声明一个ci::qtime::MovieGlci::gl::Textureci::params::InterfaceGl对象,分别用于播放、渲染和控制视频。在你的类声明中添加以下内容:

    Texture mMovieTexture;
    qtime::MovieGl mMovie;
    params::InterfaceGl mParams;
    
  4. 通过打开一个打开文件对话框选择视频文件,并使用该路径初始化我们的mMovie。以下代码应在setup方法中:

    try{
    fs::path path = getOpenFilePath();
    mMovie = qtime::MovieGl( path );
    }catch( … ){
      console() << "could not open video file" <<std::endl;
    }
    
  5. 我们还需要一些变量来存储我们将要操作的值。每个可控制的视频参数将有两个变量来表示该参数的当前值和前一个值。现在声明以下变量:

    float mMoviePosition, mPrevMoviePosition;
    float mMovieRate, mPrevMovieRate;
    float mMovieVolume, mPrevMovieVolume;
    bool mMoviePlay, mPrevMoviePlay;
    bool mMovieLoop, mPrevMovieLoop;
    
  6. setup方法中设置默认值:

    mMoviePosition = 0.0f;
    mPrevMoviePosition = mMoviePosition;
    mMovieRate = 1.0f;
    mPrevMovieRate = mMovieRate;
    mMoviePlay = false;
    mPrevMoviePlay = mMoviePlay;
    mMovieLoop = false;
    mPrevMovieLoop = mMovieLoop;
    mMovieVolume = 1.0f;
    mPrevMovieVolume = mMovieVolume;
    
  7. 现在让我们初始化mParams并为之前定义的每个变量添加一个控件,并在必要时设置maxminstep值。以下代码必须在setup方法中:

    mParams = params::InterfaceGl( "Movie Controller", Vec2i( 200, 300 ) ); 
    if( mMovie ){
    string max = ci::toString( mMovie.getDuration() );
    mParams.addParam( "Position", &mMoviePosition, "min=0.0 max=" + max + " step=0.5" );
    
    mParams.addParam( "Rate", &mMovieRate, "step=0.01" );
    
    mParams.addParam( "Play/Pause", &mMoviePlay );
    
    mParams.addParam( "Loop", &mMovieLoop );
    
    mParams.addParam( "Volume", &mMovieVolume, "min=0.0 max=1.0 step=0.01" );
    }
    
  8. update方法中,我们将检查电影是否有效,并将每个参数与其前一个状态进行比较,以查看它们是否已更改。如果已更改,我们将更新mMovie并将参数设置为新的值。以下代码行应放在update方法中:

    if( mMovie ){
    
    if( mMoviePosition != mPrevMoviePosition ){
    mPrevMoviePosition = mMoviePosition;
    mMovie.seekToTime( mMoviePosition );
            } else {
    mMoviePosition = mMovie.getCurrentTime();
    mPrevMoviePosition = mMoviePosition;
            }
    if( mMovieRate != mPrevMovieRate ){
    mPrevMovieRate = mMovieRate;
    mMovie.setRate( mMovieRate );
            }
    if( mMoviePlay != mPrevMoviePlay ){
    mPrevMoviePlay = mMoviePlay;
    if( mMoviePlay ){
    mMovie.play();
                } else {
    mMovie.stop();
                }
            }
    if( mMovieLoop != mPrevMovieLoop ){
    mPrevMovieLoop = mMovieLoop;
    mMovie.setLoop( mMovieLoop );
            }
    if( mMovieVolume != mPrevMovieVolume ){
    mPrevMovieVolume = mMovieVolume;
    mMovie.setVolume( mMovieVolume );
            }
        }
    
  9. update方法中,还需要获取电影纹理并将其复制到之前声明的mMovieTexture。在update方法中,我们编写:

    if( mMovie ){
    mMovieTexture = mMovie.getTexture();
    }
    
  10. 剩下的就是绘制我们的内容了。在 draw 方法中,我们将使用黑色清除背景。我们将检查 mMovieTexture 的有效性,并在一个适合窗口的矩形内绘制它。我们还调用 mParamsdraw 命令来在视频上方绘制控件:

    gl::clear( Color( 0, 0, 0 ) ); 
    
    if( mMovieTexture ){
    Rectf rect = Rectf( mMovieTexture.getBounds() ).getCenteredFit( getWindowBounds(), true );
    gl::draw( mMovieTexture, rect );
        }
    
    mParams.draw();
    
  11. 绘制它,您将看到具有黑色背景和控件的应用程序窗口。在参数菜单中更改各种参数,您将看到它影响视频:如何做到这一点…

它是如何工作的…

我们创建了一个 ci::params::InterfaceGl 对象,并为每个我们想要操作的参数添加了一个控件。

我们为每个我们想要操作的参数创建了一个变量,并为存储它们的上一个值创建了一个变量。在更新时,我们检查这些值是否不同,这只会发生在用户使用 mParams 菜单更改它们的值时。

当参数更改时,我们使用用户设置的值更改 mMovie 参数。

一些参数必须保持在特定的范围内。电影位置设置为从 0 秒到视频最大持续时间的秒数。音量必须是一个介于 01 之间的值,0 表示没有音频,而 1 是最大音量。

将窗口内容保存为图像

在这个例子中,我们将向您展示如何将窗口内容保存到图形文件,以及如何在您的 Cinder 应用程序中实现此功能。这可以用于保存图形算法的输出。

如何做到这一点...

我们将在您的应用程序中添加一个窗口内容保存功能:

  1. 添加必要的头文件:

    #include "cinder/ImageIo.h"
    #include "cinder/Utilities.h"
    
  2. 向您的应用程序主类添加属性:

    bool mMakeScreenshot;
    
  3. setup 方法中设置默认值:

    mMakeScreenshot = false;
    
  4. 按如下方式实现 keyDown 方法:

    void MainApp::keyDown(KeyEvent event)
      {
      if(event.getChar() == 's') {
      mMakeScreenshot = true;
        }
      }
    
  5. draw 方法的末尾添加以下代码:

    if(mMakeScreenshot) {
    mMakeScreenshot = false;
    writeImage( getDocumentsDirectory() / fs::path("MainApp_screenshot.png"), copyWindowSurface() );
    }
    

它是如何工作的…

每次将 mMakeScreenshot 设置为 true 时,您的应用程序的截图将被选中并保存。在这种情况下,应用程序等待按下 S 键,然后将标志 mMakeScreenshot 设置为 true。当前应用程序窗口的截图将被保存在您的文档目录下,文件名为 MainApp_screenshot.png

还有更多...

这只是 writeImage 函数常见用法的简单示例。还有许多其他实际应用。

将窗口动画保存为图像序列

假设您想要记录一系列图像。执行以下步骤来完成此操作:

  1. 修改步骤 5 中显示的先前代码片段,以将窗口内容保存如下:

    if(mMakeScreenshot || mRecordFrames) {
    mMakeScreenshot = false;
    writeImage( getDocumentsDirectory() / fs::path("MainApp_screenshot_" + toString(mFramesCounter) + ".png"), copyWindowSurface() );
    mFramesCounter++;
    }
    
  2. 您必须将 mRecordFramesmFrameCounter 定义为您的应用程序主类的属性:

    bool mRecordFrames;
    int mFramesCounter;
    
  3. setup 方法中设置初始值:

    mRecordFrames = false;
    mFramesCounter = 1;
    

录音声音可视化

我们假设您正在使用 audio 命名空间中的 TrackRef 来播放您的声音。执行以下步骤:

  1. 实现之前的步骤以将窗口动画保存为图像序列。

  2. update 方法的开头输入以下代码行:

    if(mRecordFrames) {
    mTrack->setTime(mFramesCounter / 30.f);
    }
    

我们正在根据经过的帧数计算所需的音频轨道位置。我们这样做是为了使动画与音乐轨道同步。在这种情况下,我们希望产生 30 fps 的动画,所以我们把 mFramesCounter 除以 30

将窗口动画保存为视频

在这个菜谱中,我们将从绘制一个简单的动画开始,并学习如何将其导出为视频。我们将创建一个视频,按下任意键将开始或停止录制。

准备工作

你必须安装苹果的 QuickTime。确保你知道你想要将视频保存的位置,因为你将不得不在开始时指定其位置。

这可以是使用 OpenGl 绘制的任何东西,但在这个例子中,我们将在窗口中心创建一个黄色的圆圈,其半径会变化。半径是通过自应用程序启动以来经过的秒数的正弦值的绝对值来计算的。我们将此值乘以 200 以放大它。现在将以下内容添加到 draw 方法中:

gl::clear( Color( 0, 0, 0 ) );     
float radius = fabsf( sinf( getElapsedSeconds() ) ) * 200.0f;
Vec2f center = getWindowCenter();
gl::color( Color( 1.0f, 1.0f, 0.0f ) );
gl::drawSolidCircle( center, radius );

如何操作…

我们将使用 ci::qtime::MovieWriter 类来创建我们的渲染视频。

  1. 在源文件的开头包含 OpenGl 和 QuickTime 文件,通过添加以下内容:

    #include "cinder/gl/gl.h"
    #include "cinder/qtime/MovieWriter.h"
    
  2. 现在让我们声明一个 ci::qtime::MovieWriter 对象和一个初始化它的方法。将以下内容添加到你的类声明中:

    qtime::MovieWriter mMovieWriter;
    void initMovieWriter();
    
  3. initMovieWriter 的实现中,我们首先要求用户使用保存文件对话框指定一个路径,并使用它来初始化电影写入器。电影写入器还需要知道窗口的宽度和高度。这是 initMovieWriter 的实现。

    void MyApp::initMovieWriter(){
    fs::path path = getSaveFilePath();
    if( path.empty() == false ){
    mMovieWriter = qtime::MovieWriter( path, getWindowWidth(), getWindowHeight() );
        }
    }
    
  4. 让我们通过声明 keyUp 方法来声明一个按键事件处理器。

    void keyUp( KeyEvent event );
    
  5. 在实现中,我们将通过检查 mMovieWriter 的有效性来查看是否已经在录制电影。如果它是一个有效的对象,那么我们必须通过销毁对象来保存当前的电影。我们可以通过调用 ci::qtime::MovieWriter 默认构造函数来实现;这将创建一个空实例。如果 mMovieWriter 不是一个有效的对象,那么我们通过调用 initMovieWriter() 方法来初始化一个新的电影写入器。

    void MovieWriterApp::keyUp( KeyEvent event ){
    if( mMovieWriter ){
    mMovieWriter = qtime::MovieWriter();
        } else {
    initMovieWriter();
        }
    }
    
  6. 最后两个步骤是检查 mMovieWriter 是否有效,并通过调用带有窗口表面的 addFrame 方法来添加一个帧。这个方法必须在 draw 方法中调用,在我们的绘图程序之后。这是最终的 draw 方法,包括圆圈绘制代码。

    void MyApp::draw()
    {
      gl::clear( Color( 0, 0, 0 ) ); 
    
    float radius = fabsf( sinf( getElapsedSeconds() ) ) * 200.0f;
        Vec2f center = getWindowCenter();
    gl::color( Color( 1.0f, 1.0f, 0.0f ) );
    gl::drawSolidCircle( center, radius );
    
    if( mMovieWriter ){
    mMovieWriter.addFrame( copyWindowSurface() );
        }
    }
    
  7. 构建并运行应用程序。按下任意键将开始或结束视频录制。每次开始新的录制时,用户将看到一个保存文件对话框,用于设置电影将保存的位置。如何操作…

它是如何工作的…

ci::qtime::MovieWriter 对象允许使用苹果的 QuickTime 容易地写入电影。录制开始于初始化一个 ci::qtime::MovieWriter 对象,并在对象被销毁时保存。通过调用 addFrame 方法,可以添加新的帧。

还有更多...

你还可以通过创建一个ci::qtime::MovieWriter::Format对象并将其作为可选参数传递给电影编写器的构造函数来定义视频的格式。如果没有指定格式,电影编写器将使用默认的 PNG 编解码器和每秒 30 帧。

例如,要创建一个使用 H264 编解码器、50%质量和 24 帧每秒的电影编写器,你可以编写以下代码:

qtime::MovieWriter::Format format;
format.setCodec( qtime::MovieWriter::CODEC_H264 );
format.setQuality( 0.5f );
format.setDefaultDuration( 1.0f / 24.0f );
qtime::MovieWriter mMovieWriter = ci::Qtime::MovieWriter( "mymovie.mov", getWindowWidth(), getWindowHeight(), format );

你可以选择打开一个设置窗口,并允许用户通过调用静态方法qtime::MovieWriter::getUserCompressionSettings来定义视频设置。此方法将填充一个qtime::MovieWriter::Format对象,并在成功时返回true,如果用户取消了设置更改则返回false

要使用此方法定义设置并创建一个电影编写器,你可以编写以下代码:

qtime::MovieWriter::Format format;
qtime::MovieWriter mMovieWriter;
boolformatDefined = qtime::MovieWriter::getUserCompressionSettings( &format );
if( formatDefined ){
mMovieWriter = qtime::MovieWriter( "mymovie.mov", getWindowWidth(), getWindowHeight(), format );
}

还可以启用多遍编码。对于 Cinder 的当前版本,它仅通过 H264 编解码器可用。多遍编码将提高电影的质量,但会以性能下降为代价。因此,默认情况下它是禁用的。

要启用多遍编码写入电影,我们可以编写以下代码:

qtime::MovieWriter::Format format;
format.setCodec( qtime::MovieWriter::CODEC_H264 );
format.enableMultiPass( true );
qtime::MovieWritermMovieWriter = ci::Qtime::MovieWriter( "mymovie.mov", getWindowWidth(), getWindowHeight(), format );

可以使用ci::qtime::MovieWriter::Format类设置许多设置和格式,要了解完整的选项列表,请查看该类在libcinder.org/docs/v0.8.4/guide__qtime___movie_writer.html的文档。

将窗口内容保存为矢量图形图像

在这个菜谱中,我们将学习如何使用 cairo 渲染器在屏幕上绘制 2D 图形并将其保存为矢量图形格式的图像。

矢量图形在创建用于打印的视觉效果时非常有用,因为它们可以缩放而不失真。

Cinder 集成了 cairo 图形库;一个功能强大且功能齐全的 2D 渲染器,能够输出到包括流行的矢量图形格式在内的多种格式。

要了解更多关于 cairo 库的信息,请访问其官方网站:www.cairographics.org

在这个例子中,我们将创建一个应用程序,当用户按下鼠标时,它会绘制一个新的圆。当按下任何键时,应用程序将打开一个保存文件对话框,并将内容以文件扩展名定义的格式保存。

准备工作

要绘制使用 cairo 渲染器创建的图形,我们必须将我们的渲染器定义为Renderer2d

在我们的应用程序类的源文件末尾有一个用于初始化应用程序的,其中第二个参数定义了渲染器。如果你的应用程序名为MyApp,你必须将宏更改为以下内容:

CINDER_APP_BASIC( MyApp, Renderer2d )

cairo 渲染器允许导出 PDF、SVG、EPS 和 PostScript 格式。在指定要保存的文件时,确保你写了一个受支持的扩展名:pdfsvgepsps

在源文件顶部包含以下文件:

#include "cinder/Rand.h"
#include "cinder/cairo/Cairo.h"

如何实现...

我们将使用 Cinder 的 cairo 包装器从我们的渲染中创建矢量格式的图像。

  1. 每当用户按下鼠标时创建一个新圆,我们必须首先创建一个 Circle 类。这个类将包含位置、半径和颜色参数。它的构造函数将接受 ci::Vec2f 来定义其位置,并将生成一个随机半径和颜色。

    在应用程序的类声明之前写入以下代码:

    class Circle{
    public:
        Circle( const Vec2f&pos ){
    this->pos = pos;
    radius = randFloat( 20.0f, 50.0f );
    color = ColorA( randFloat( 1.0f ), randFloat( 1.0f ), randFloat( 1.0f ), 0.5f );
        }
    
        Vec2f pos;
    float radius;
    ColorA color;
    };
    
  2. 我们现在应该声明一个存储创建的圆的 std::vector 的圆,并将以下代码添加到类声明中:

    std::vector< Circle >mCircles;
    
  3. 让我们创建一个将 cairo::Context 作为参数的方法来绘制圆:

    void renderScene( cairo::Context &context );
    
  4. 在方法定义中,遍历 mCircles 并在上下文中绘制每一个:

    void MyApp::renderScene( cairo::Context &context ){
    for( std::vector< Circle >::iterator it = mCircles.begin(); it != mCircles.end(); ++it ){
    context.circle( it->pos, it->radius );
    context.setSource( it->color );
    context.fill();
        }
    }
    
  5. 在这一点上,我们只需要在用户按下鼠标时添加一个圆。为此,我们必须通过在类声明中声明它来实现 mouseDown 事件处理程序。

    void mouseDown( MouseEvent event );
    
  6. 在其实现中,我们使用鼠标位置将一个 Circle 类添加到 mCircles 中。

    void MyApp::mouseDown( MouseEvent event ){
      Circle circle( event.getPos() );
    mCircles.push_back( circle );
    }
    
  7. 我们现在可以通过创建绑定到窗口表面的 cairo::Context 来在窗口上绘制圆。这将让我们可视化我们正在绘制的。以下是 draw 方法的实现:

    void CairoSaveApp::draw()
    {
    cairo::Context context( cairo::createWindowSurface() );
    renderScene( context );
    }
    
  8. 要将场景保存到图像文件,我们必须创建一个绑定到表示矢量图形格式中文件的表面的上下文。让我们通过声明 keyUp 事件处理程序来实现这一点,每当用户释放一个键时执行此操作。

    void keyUp( KeyEvent event );
    
  9. keyUp 实现中,我们创建 ci::fs::path 并通过调用保存文件对话框来填充它。我们还将创建一个空的 ci::cairo::SurfaceBase,它是 cairo 渲染器可以绘制到的所有表面的基础。

    fs::path filePath = getSaveFilePath();
    cairo::SurfaceBase surface;
    
  10. 我们现在将比较路径的扩展名与支持的格式,并相应地初始化表面。它可以初始化为 ci::cairo::SurfacePdfci::cairo::SurfaceSvgci::cairo::SurfaceEpsci::cairo::SurfacePs

    if( filePath.extension() == ".pdf" ){
    surface = cairo::SurfacePdf( filePath, getWindowWidth(), getWindowHeight() );
        } else if( filePath.extension() == ".svg" ){
    surface = cairo::SurfaceSvg( filePath, getWindowWidth(), getWindowHeight() );
        } else if( filePath.extension() == ".eps" ){
    surface = cairo::SurfaceEps( filePath, getWindowWidth(), getWindowHeight() );
        } else if( filePath.extension() == ".ps" ){
    surface = cairo::SurfacePs( filePath, getWindowWidth(), getWindowHeight() );
        }
    
  11. 现在我们可以创建 ci::cairo::Context 并通过调用 renderScene 方法并将上下文作为参数传递来将其渲染到场景中。圆将被渲染到上下文中,并将在指定的格式中创建一个文件。以下是最终的 keyUp 方法实现:

    void CairoSaveApp::keyUp( KeyEvent event ){
    fs::path filePath = getSaveFilePath();
    cairo::SurfaceBase surface;
    if( filePath.extension() == ".pdf" ){
    surface = cairo::SurfacePdf( filePath, getWindowWidth(), getWindowHeight() );
        } else if( filePath.extension() == ".svg" ){
    surface = cairo::SurfaceSvg( filePath, getWindowWidth(), getWindowHeight() );
        } else if( filePath.extension() == ".eps" ){
    surface = cairo::SurfaceEps( filePath, getWindowWidth(), getWindowHeight() );
        } else if( filePath.extension() == ".ps" ){
    surface = cairo::SurfacePs( filePath, getWindowWidth(), getWindowHeight() );
        }
    cairo::Context context( surface );
    renderScene( context );
    }
    

    如何实现…

它是如何工作的...

Cinder 包装并集成了 cairo 2D 矢量渲染器。它允许使用 Cinder 的类型来绘制和与 cairo 交互。

完整的绘图是通过调用 ci::cairo::Context 对象的绘图方法来完成的。上下文反过来必须通过传递一个扩展 ci::cairo::SurfaceBase 的表面对象来创建。所有绘图都将在这个表面上完成,并根据表面的类型进行光栅化。

以下表面允许以矢量图形格式保存图像:

表面类型 格式
ci::cairo::SurfacePdf PDF
ci::cairo::SurfaceSvg SVG
ci::cairo::SurfaceEps EPS
ci::cairo::SurfacePs PostScript

还有更多...

也可以使用其他渲染器进行绘制。尽管渲染器无法创建矢量图像,但在其他情况下它们可能很有用。

这里是其他可用的表面:

表面类型 格式
ci::cairo::SurfaceImage 基于像素的抗锯齿光栅化器
ci::cairo::SurfaceQuartz 苹果的 Quartz
ci::cairo::SurfaceCgBitmapContext 苹果的 CoreGraphics
ci::cairo::SurfaceGdi Windows GDI

使用瓦片渲染器保存高分辨率图像

在这个菜谱中,我们将学习如何使用ci::gl::TileRender类导出屏幕上绘制的内容的高分辨率图像。这在创建用于打印的图形时非常有用。

我们将首先创建一个简单的场景并在屏幕上绘制它。接下来,我们将编写示例代码,以便每当用户按下任何键时,都会出现一个保存文件对话框,并将高分辨率图像保存到指定的路径。

准备中

TileRender类可以从使用 OpenGl 调用的任何屏幕绘制内容创建高分辨率图像。

要使用TileRender保存图像,我们首先必须在屏幕上绘制一些内容。这可以是任何内容,但为了这个示例,让我们创建一个简单的图案,用圆形填充整个屏幕。

draw方法的实现中,写入以下代码:

void MyApp::draw()
{
  gl::clear( Color( 0, 0, 0 ) ); 
gl::color( Color::white() );
for( float i=0; i<getWindowWidth(); i+=10.0f ){
for( float j=0; j<getWindowHeight(); j += 10.0f ){
float radius = j * 0.01f;
gl::drawSolidCircle( Vec2f( i, j ), radius );
        }
    }
}

记住,这可以是使用 OpenGl 在屏幕上绘制的任何内容。

准备中

如何实现...

我们将使用ci::gl::TileRender类来生成 OpenGL 渲染的高分辨率图像。

  1. 通过在源文件顶部添加以下内容来包含必要的头文件:

    #include "cinder/gl/TileRender.h"
    #include "cinder/ImageIo.h"
    
  2. 由于我们将在用户按下任何键时保存高分辨率图像,让我们通过在类声明中声明来实现keyUp事件处理器。

    void keyUp( KeyEvent event );
    
  3. keyUp实现中,我们首先创建一个ci::gl::TileRender对象,然后设置我们即将创建的图像的宽度和高度。我们将将其设置为应用程序窗口大小的四倍。它可以是你想要的任何大小,但请注意,如果你不尊重窗口的宽高比,图像将会被拉伸。

    gl::TileRender tileRender( getWindowWidth() * 4, getWindowHeight() * 4 );
    
  4. 我们必须定义场景的ModelviewProjection矩阵以匹配我们的窗口。如果我们只使用 2D 图形,我们可以调用setMatricesWindow方法,如下所示:

    tileRender.setMatricesWindow( getWindowWidth(), getWindowHeight() );
    

    为了在绘制 3D 内容时定义场景的ModelviewProjection矩阵以匹配窗口,必须调用setMatricesWindowPersp方法:

    tileRender.setMatricesWindowPersp( getWindowWidth(), getWindowHeight() );
    
  5. 接下来,我们将使用nextTile方法在创建新瓦片时绘制场景。当所有瓦片都创建完毕后,该方法将返回false。我们可以通过在while循环中重新绘制场景并询问是否有下一个瓦片来创建所有瓦片,如下所示:

    while( tileRender.nextTile() ){
    draw();
        }
    
  6. 现在,场景已经完全在 TileRender 中渲染,我们必须保存它。让我们通过打开一个保存文件对话框来让用户指定保存位置。必须指定图像文件的扩展名,因为它将用于内部定义图像格式。

    fs::path filePath = getSaveFilePath();
    
  7. 我们检查 filePath 是否为空,并使用 writeImage 方法将标题渲染表面作为图像写入。

    if( filePath.empty() == false ){
    writeImage( filePath, tileRender.getSurface() );
    }
    
  8. 保存图像后,需要重新定义窗口的 ModelviewProjection 矩阵。如果在 2D 中绘图,可以通过使用带有窗口尺寸的 setMatricesWindow 方法将矩阵设置为默认值,如下所示:

    gl::setMatricesWindow( getWindowWidth(), getWindowHeight() );
    

它的工作原理…

ci::gl::TileRender 类使得通过将我们的绘图的部分缩放到整个窗口大小并将它们存储为 ci::Surface,从而生成我们渲染的高分辨率版本成为可能。在将整个场景存储在各个部分之后,它们被拼接成瓷砖,形成一个单独的高分辨率 ci::Surface,然后可以将其保存为图像。

在应用程序之间共享图形

在这个菜谱中,我们将向您展示在 Mac OS X 下实时在应用程序之间共享图形的方法。为此,我们将使用 Syphon 和其针对 Cinder 的实现。Syphon 是一个开源工具,允许应用程序以静态帧或实时更新的帧序列共享图形。您可以在以下位置了解更多关于 Syphon 的信息:syphon.v002.info/

准备工作

为了测试我们应用程序共享的图形是否可用,我们将使用 Syphon Recorder,您可以在以下位置找到它:syphon.v002.info/recorder/

如何操作…

  1. syphon-implementations 仓库检出 Syphon CinderBlock code.google.com/p/syphon-implementations/.

  2. 在您的项目树中创建一个新的组,并将其命名为 Blocks

  3. 将 Syphon CinderBlock 拖放到你新创建的 Blocks 组中。如何操作…

  4. 确保在 target 设置的 Build PhasesCopy Files 部分添加了 Syphon.framework

  5. 添加必要的头文件:

    #include "cinderSyphon.h"
    
  6. 向您的应用程序主类添加属性:

    syphonServer mScreenSyphon;
    
  7. setup 方法的末尾,添加以下代码:

    mScreenSyphon.setName("Cinder Screen");
    gl::clear(Color::white());
    
  8. draw 方法内部添加以下代码:

    gl::enableAlphaBlending();
    
    gl::color( ColorA(1.f, 1.f, 1.f, 0.05f) );
    gl::drawSolidRect( getWindowBounds() );
    
    gl::color( ColorA::black() );
    Vec2f pos = Vec2f( cos(getElapsedSeconds()), sin(getElapsedSeconds())) * 100.f;
    gl::drawSolidCircle(getWindowCenter() + pos, 10.f);
    
    mScreenSyphon.publishScreen();
    

它是如何工作的…

应用程序绘制一个简单的旋转动画,并通过 Syphon 库共享整个窗口区域。我们的应用程序窗口如下截图所示:

它的工作原理…

要测试图形是否可以被其他应用程序接收,我们将使用 Syphon Recorder。运行 Syphon Recorder 并在“Cinder Screen – MainApp”名称下的下拉菜单中找到我们的 Cinder 应用程序。我们在“如何做...”部分的步骤 6 中设置了该名称的第一部分,而第二部分是可执行文件名。现在,我们的 Cinder 应用程序的预览应该可用,并且看起来如下截图所示:

How it works…

还有更多...

Syphon 库非常实用,易于使用,并且适用于其他应用程序和库。

接收来自其他应用程序的图形

您还可以接收来自其他应用程序的纹理。为此,您必须使用 syphonClient 类,如下步骤所示:

  1. 在您的应用程序主类中添加一个属性:

    syphonClient mClientSyphon;
    
  2. 在 CIT 方法中初始化 mClientSyphon

    mClientSyphon.setApplicationName("MainApp Server");
    mClientSyphon.setServerName("");
    mClientSyphon.bind();
    
  3. draw 方法的末尾添加以下行,该行绘制其他应用程序共享的图形:

    mClientSyphon.draw(Vec2f::zero());
    

第五章:构建粒子系统

在本章中,我们将涵盖:

  • 在 2D 中创建粒子系统

  • 应用排斥力和吸引力

  • 模拟粒子随风飘动

  • 模拟集群行为

  • 使我们的粒子对声音做出反应

  • 将粒子与处理后的图像对齐

  • 将粒子与网格表面对齐

  • 创建弹簧

简介

粒子系统是一种计算技术,使用大量小图形对象执行不同类型的模拟,如爆炸、风、火、水和集群。

在本章中,我们将学习如何使用流行的多功能物理算法创建和动画化粒子。

在 2D 中创建粒子系统

在这个配方中,我们将学习如何使用 Verlet 算法在二维空间中构建一个基本的粒子系统。

准备工作

我们需要创建两个类,一个 Particle 类代表单个粒子,一个 ParticleSystem 类来管理我们的粒子。

使用您选择的 IDE 创建以下文件:

  • Particle.h

  • Particle.cpp

  • ParticleSystem.h

  • ParticleSystem.cpp

如何做呢...

我们将学习如何创建一个基本的粒子系统。执行以下步骤:

  1. 首先,让我们在 Particle.h 文件中声明我们的 Particle 类并包含必要的 Cinder 文件:

    #pragma once
    
    #include "cinder/gl/gl.h"
    #include "cinder/Vector.h"
    
    class Particle{
    };
    
  2. 让我们在类声明中添加必要的成员变量 - 使用 ci::Vec2f 存储位置、前一个位置和施加的力;以及使用 float 存储粒子半径、质量和阻力。

    ci::Vec2f position, prevPosition;
    ci::Vec2f forces;
    float radius;
    float mass;
    float drag;
    
  3. 为了最终完成 Particle 声明,还需要添加一个构造函数,该构造函数接受粒子的初始位置、半径、质量和阻力,以及更新和绘制粒子的方法。

    以下是最终的 Particle 类声明:

    class Particle{
    public:
    
    Particle( const ci::Vec2f& position, float radius, 
    float mass, float drag );
    
    void update();
    void draw();
    
    ci::Vec2f position, prevPosition;
    ci::Vec2f forces;
    float radius;
    float mass;
    float drag;
    };
    
  4. 让我们继续到 Particle.cpp 文件并实现 Particle 类。

    第一个必要的步骤是包含 Particle.h 文件,如下所示:

    #include "Particle.h"
    
  5. 我们将成员变量初始化为构造函数中传递的值。我们还初始化 forceszeroprevPosition 为初始位置。

    Particle::Particle( const ci::Vec2f& position, float radius, float mass, float drag ){
      this->position = position;
      this->radius = radius;
      this->mass = mass;
      this->drag = drag;
      prevPosition = position;
      forces = ci::Vec2f::zero();
    }
    
  6. update 方法中,我们需要创建一个临时的 ci::Vec2f 变量来存储更新前的粒子位置。

    ci::Vec2f temp = position;
    
  7. 我们通过计算当前位置与前一个位置之间的差异并乘以 drag 来计算粒子的速度。为了清晰起见,我们将此值暂时存储在 ci::Vec2f 中。

    ci::Vec2f vel = ( position – prevPosition ) * drag;
    
  8. 要更新粒子的位置,我们将之前计算的速度加上 forces 除以 mass

    position += vel + forces / mass;
    
  9. update 方法中的最后一步是将之前存储的位置复制到 prevPosition 并将 forces 重置为零向量。

    以下是完全的 update 方法实现:

    void Particle::update(){
        ci::Vec2f temp = position;
        ci::Vec2f vel = ( position - prevPosition ) * drag;
        position += vel + forces / mass;
        prevPosition = temp;
        forces = ci::Vec2f::zero();
    }
    
  10. draw 实现中,我们只需在粒子的位置处绘制一个圆,使用其半径。

    void Particle::draw(){
        ci::gl::drawSolidCircle( position, radius );
    }
    
  11. 现在随着Particle类的完成,我们需要开始着手于ParticleSystem类的开发。切换到ParticleSystem.h文件,包含必要的文件,并创建ParticleSystem类的声明。

    #pragma once
    
    #include "Particle.h"
    #include <vector>
    
    classParticleSystem{
    public:
    
    };
    
  12. 让我们添加一个析构函数和更新和绘制粒子的方法。我们还需要创建添加和销毁粒子的方法,以及一个std::vector变量来存储系统中的粒子。以下是最终的类声明:

    Class ParticleSystem{
    public:
      ~ParticleSystem();
    
      void update();
      void draw();
    
      void addParticle( Particle *particle );
      void destroyParticle( Particle *particle );
    
        std::vector<Particle*> particles;
    
    };
    
  13. 切换到ParticleSystem.cpp文件,让我们开始实现。首先我们需要做的是包含包含类声明的文件。

    #include "ParticleSystem.h"
    
  14. 现在,让我们逐一实现这些方法。在析构函数中,我们遍历所有粒子并将它们删除。

    ParticleSystem::~ParticleSystem(){
      for( std::vector<Particle*>::iterator it = particles.begin(); it!= particles.end(); ++it ){
      delete *it;
        }
      particles.clear();
    }
    
  15. update方法将用于遍历所有粒子并对每个粒子调用update

    void ParticleSystem::update(){
      for( std::vector<Particle*>::iterator it = particles.begin(); it != particles.end(); ++it ){
            (*it)->update();
        }
    }
    
  16. draw方法将遍历所有粒子,并对每个粒子调用draw

    void ParticleSystem::draw(){
      for( std::vector<Particle*>::iterator it = particles.begin(); it != particles.end(); ++it ){
            (*it)->draw();
        }
    }
    
  17. addParticle方法将粒子插入到particles容器中。

    void ParticleSystem::addParticle( Particle *particle ){
      particles.push_back( particle );
    }
    
  18. 最后,destroyParticle将删除粒子并从粒子列表中移除。

    我们将找到粒子的迭代器,并使用它来从容器中删除和稍后移除对象。

    void ParticleSystem::destroyParticle( Particle *particle ){
      std::vector<Particle*>::iterator it = std::find( particles.begin(), particles.end(), particle );
      delete *it;
      particles.erase( it );
    }
    
  19. 在我们的类准备就绪后,让我们转到应用程序类并创建一些粒子。

    在我们的应用程序类中,我们需要在源文件顶部包含ParticleSystem头文件和必要的头文件以使用随机数:

    #include "ParticleSystem.h"
    #include "cinder/Rand.h"
    
  20. 在类声明中声明一个ParticleSystem对象。

    ParticleSystem mParticleSystem;
    
  21. setup方法中,我们可以在窗口上创建 100 个具有随机位置和随机半径的粒子。我们将质量定义为与半径相同,以此作为大小和质量的关联方式。drag将被设置为 9.5。

    setup方法内部添加以下代码片段:

    int numParticle = 100;
      for( int i=0; i<numParticle; i++ ){
      float x = ci::randFloat( 0.0f, getWindowWidth() );
      float y = ci::randFloat( 0.0f, getWindowHeight() );
      float radius = ci::randFloat( 5.0f, 15.0f );
      float mass = radius;radius;
      float drag = 0.95f;
            Particle *particle = new Particle
            ( Vec2f( x, y ), radius, mass, drag );
            mParticleSystem.addParticle( particle );
    }
    
  22. update方法中,我们需要通过在mParticleSystem上调用update方法来更新粒子。

    void MyApp::update(){
      mParticleSystem.update();
    }
    
  23. draw方法中,我们需要清除屏幕,设置窗口的矩阵,并在mParticleSystem上调用draw方法。

    void ParticlesApp::draw()
    {
      gl::clear( Color( 0, 0, 0 ) ); 
      gl::setMatricesWindow( getWindowWidth(), getWindowHeight() );
      mParticleSystem.draw();
    }
    
  24. 构建并运行应用程序,你将在屏幕上看到 100 个随机圆圈,如下面的截图所示:如何做到这一点…

在接下来的菜谱中,我们将学习如何以有机和吸引人的方式动画化粒子。

它是如何工作的...

之前描述的方法使用了一个流行且通用的 Verlet 积分器。其主要特点是对速度的隐式近似。这是通过在每次模拟更新时计算自上次模拟更新以来所经过的距离来实现的。这允许有更高的稳定性,因为速度是隐式地与位置相关联的,并且不太可能发生不同步。

drag 成员变量代表运动阻力,应该是一个介于 0.0 和 1.0 之间的数字。0.0 的值表示如此大的阻力,以至于粒子将无法移动。1.0 的值表示没有阻力,将使粒子无限期地移动。我们在第 7 步中应用了 drag,其中我们将 drag 乘以速度:

ci::Vec2f vel = ( position – prevPosition ) * drag;

还有更多…

要在 3D 中创建粒子系统,必须使用 3D 向量而不是 2D 向量。

由于 Cinder 的 2D 向量和 3D 向量类具有非常相似的类结构,我们只需将 positionprevPositionforces 改为 ci::Vec3f 对象。

构造函数也需要接受一个 ci::Vec3f 对象作为参数。

以下是根据这些更改的类声明:

class Particle{
public:

    Particle( const ci::Vec3f& position, 
    float radius, float mass, float drag );

    void update();
    void draw();

    ci::Vec3f position, prevPosition;
    ci::Vec3f forces;
    float radius;
    float mass;
    float drag;
};

draw 方法也应更改以允许 3D 绘制;例如,我们可以绘制一个球体而不是圆形:

void Particle::draw(){
  ci::gl::drawSphere( position, radius );
} 

参见

应用排斥力和吸引力

在这个菜谱中,我们将展示如何将排斥力和吸引力应用到我们在前一个菜谱中实现的粒子系统中。

准备工作

在这个菜谱中,我们将使用 Creating particle system in 2D 菜谱中的代码。

如何做到这一点…

我们将展示如何将力应用到粒子系统中。执行以下步骤:

  1. 向你的应用程序的主类添加属性。

    Vec2f attrPosition;
    float attrFactor, repulsionFactor, repulsionRadius;
    
  2. setup 方法中设置默认值。

    attrPosition = getWindowCenter();
    attrFactor = 0.05f;
    repulsionRadius = 100.f;
    repulsionFactor = -5.f;
    
  3. 实现以下 mouseMovemouseDown 方法:

    void MainApp::mouseMove(MouseEvent event)
    {
      attrPosition.x = event.getPos().x;
      attrPosition.y = event.getPos().y;
    }
    
    void MainApp::mouseDown(MouseEvent event)
    {
    for( std::vector<Particle*>::iterator it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ) {
      Vec2f repulsionForce = (*it)->position - event.getPos();
      repulsionForce = repulsionForce.normalized() * math<float>::max(0.f, repulsionRadius - repulsionForce.length());
              (*it)->forces += repulsionForce;
          }
    }
    
  4. update方法的开始处,添加以下代码片段:

    for( std::vector<Particle*>::iterator it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ) {
      Vec2f attrForce = attrPosition - (*it)->position;
      attrForce *= attrFactor;
        (*it)->forces += attrForce;
    }
    

它是如何工作的…

在这个例子中,我们为第一个菜谱中引入的粒子引擎添加了交互。吸引力指向你的鼠标光标位置,而排斥向量指向相反方向。这些力在第 3 和 4 步中计算并应用到每个粒子上,然后我们让粒子跟随你的鼠标光标,但是当你点击左键时,它们会突然远离鼠标光标。这种效果可以通过基本的向量运算实现。Cinder 允许你以与通常对标量进行操作相同的方式执行向量计算。

排斥力在第 3 步计算。我们使用从鼠标光标位置到粒子位置的归一化向量,乘以基于粒子与鼠标光标位置之间的距离计算的排斥因子。使用 repulsionRadius 值,我们可以限制排斥力的范围。

我们在第 4 步计算吸引力,取从粒子位置开始到鼠标光标位置的向量。我们将此向量乘以attrFactor值,该值控制吸引力的强度。

如何工作…

模拟风中飞行的粒子

在这个配方中,我们将解释如何将布朗运动应用于您的粒子。粒子将表现得像雪花或随风飘动的树叶。

准备工作

在这个配方中,我们将使用在 2D 中创建粒子系统配方的代码库。

如何实现它...

我们将添加来自 Perlin 噪声和正弦函数计算的粒子运动。执行以下步骤来完成此操作:

  1. 添加必要的头文件。

    #include "cinder/Perlin.h"
    
  2. 向应用程序的主类添加属性。

    float    mFrequency;
    Perlin    mPerlin;
    
  3. setup方法中设置默认值。

    mFrequency = 0.01f;
    mPerlin = Perlin();
    
  4. 改变粒子的数量、半径和质量。

    int numParticle = 300;
    float radius = 1.f;
    float mass = Rand::randFloat(1.f, 5.f);
    
  5. update方法的开头添加以下代码片段:

    Vec2f oscilationVec;
    oscilationVec.x = sin(getElapsedSeconds()*0.6f)*0.2f;
    oscilationVec.y = sin(getElapsedSeconds()*0.2f)*0.1f;
    std::vector<Particle*>::iterator it;
    for(it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ) {
      Vec2f windForce = mPerlin.dfBm( (*it)->position * mFrequency );
        (*it)->forces += windForce * 0.1f;
        (*it)->forces += oscilationVec;
    }
    

如何工作…

主要的运动计算和力在第 5 步应用。如您所见,我们正在使用作为 Cinder 一部分实现的 Perlin 噪声算法。它为每个粒子提供检索布朗运动向量的方法。我们还添加了oscilationVec,使粒子从左到右和向后摆动,增加更真实的行为。

如何工作…

参见

模拟群聚行为

群聚是应用于组织成鸟群或其他飞行动物的行为的术语。

从我们的角度来看,特别有趣的是,通过仅对每个粒子(Boid)应用三条规则就可以模拟群聚行为。这些规则如下:

  • 分离: 避免过于靠近的邻居

  • 对齐: 驶向邻居的平均速度

  • 聚合: 驶向邻居的平均位置

准备工作

在这个配方中,我们将使用来自在 2D 中创建粒子系统配方的代码。

如何实现它…

我们将实现群聚行为的规则。执行以下步骤来完成此操作:

  1. 改变粒子的数量、半径和质量。

    int numParticle = 50;
    float radius = 5.f;
    float mass = 1.f;
    
  2. Particle.h头文件中为Particle类添加新方法和属性的定义。

    void flock(std::vector<Particle*>& particles);
    ci::Vec2f steer(ci::Vec2f target, bool slowdown);
    void borders(float width, float height);
    ci::Vec2f separate(std::vector<Particle*>& particles);
    ci::Vec2f align(std::vector<Particle*>& particles);
    ci::Vec2f cohesion(std::vector<Particle*>& particles);
    
    float maxspeed;
    float maxforce;
    ci::Vec2f vel;
    
  3. Particle.cpp源文件中的Particle构造函数末尾设置maxspeedmaxforce的默认值。

    this->maxspeed = 3.f;
    this->maxforce = 0.05f;
    
  4. Particle.cpp源文件中实现Particle类的新方法。

    void Particle::flock(std::vector<Particle*>& particles) {
      ci::Vec2f acc;
      acc += separate(particles) * 1.5f;
      acc += align(particles) * 1.0f;
      acc += cohesion(particles) * 1.0f;
      vel += acc;
      vel.limit(maxspeed);
    }
    
    ci::Vec2f Particle::steer(ci::Vec2f target, bool slowdown) {
    ci::Vec2f steer;
    ci::Vec2f desired = target - position;
    float d = desired.length();
    if (d >0) {
      desired.normalize();
      if ((slowdown) && (d <100.0)) desired *= (maxspeed*(d/100.0));
      else desired *= maxspeed;
      steer = desired - vel;
      steer.limit(maxforce);
        }
    else {
      steer = ci::Vec2f::zero();
        }
      return steer;
    }
    
    void Particle::borders(float width, float height) {
      if (position.x< -radius) position.x = width+radius;
      if (position.y< -radius) position.y = height+radius;
      if (position.x>width+radius) position.x = -radius;
      if (position.y>height+radius) position.y = -radius;
    }
    
  5. 添加分离规则的方法。

    ci::Vec2f Particle::separate(std::vector<Particle*>& particles) {
    ci::Vec2f resultVec = ci::Vec2f::zero();
    float targetSeparation = 30.f;
    int count = 0;
    for( std::vector<Particle*>::iterator it = particles.begin(); it != particles.end(); ++it ) {
      ci::Vec2f diffVec = position - (*it)->position;
      if( diffVec.length() >0&&diffVec.length() <targetSeparation ) {
        resultVec += diffVec.normalized() / diffVec.length();
        count++;
            }
        }
    
    if (count >0) {
      resultVec /= (float)count;
        }
    
    if (resultVec.length() >0) {
      resultVec.normalize();
      resultVec *= maxspeed;
      resultVec -= vel;
      resultVec.limit(maxforce);
        }
    
    return resultVec;
    }
    
  6. 添加对齐规则的方法。

    ci::Vec2f Particle::align(std::vector<Particle*>& particles) {
    ci::Vec2f resultVec = ci::Vec2f::zero();
    float neighborDist = 50.f;
    int count = 0;
    for( std::vector<Particle*>::iterator it = particles.begin(); it != particles.end(); ++it ) {
    ci::Vec2f diffVec = position - (*it)->position;
    if( diffVec.length() >0 && diffVec.length() <neighborDist ) {
    resultVec += (*it)->vel;
    count++;
            }
        }
    
    if (count >0) {
      resultVec /= (float)count;
    }
    
      if (resultVec.length() >0) {
      resultVec.normalize();
      resultVec *= maxspeed;
      resultVec -= vel;
      resultVec.limit(maxforce);
        }
    
      return resultVec;
    }
    
  7. 添加聚合规则的方法。

    ci::Vec2f Particle::cohesion(std::vector<Particle*>& particles) {
    ci::Vec2f resultVec = ci::Vec2f::zero();
    float neighborDist = 50.f;
    int count = 0;
    for( std::vector<Particle*>::iterator it = particles.begin(); it != particles.end(); ++it ) {
      float d = position.distance( (*it)->position );
      if( d >0 && d <neighborDist ) {
        resultVec += (*it)->position;
        count++;
            }
        }
    
    if (count >0) {
      resultVec /= (float)count;
      return steer(resultVec, false);
        }
    
      return resultVec;
    }
    
  8. update方法更改为以下内容

    void Particle::update(){
      ci::Vec2f temp = position;
      position += vel + forces / mass;
      prevPosition = temp;
      forces = ci::Vec2f::zero();
    }
    
  9. 改变Particledrawing方法,如下所示:

    void Particle::draw(){
      ci::gl::color(1.f, 1.f, 1.f);
      ci::gl::drawSolidCircle( position, radius );
      ci::gl::color(1.f, 0.f, 0.f);
      ci::gl::drawLine(position,
      position+( position - prevPosition).normalized()*(radius+5.f) );
    }
    
  10. ParticleSystem.cpp源文件中更改ParticleSystemupdate方法,如下所示:

    void ParticleSystem::update(){
      for( std::vector<Particle*>::iterator it = particles.begin(); it!= particles.end(); ++it ){
            (*it)->flock(particles);
            (*it)->update();
            (*it)->borders(640.f, 480.f);
        }
    }
    

它是如何工作的…

从第 4 步开始实现了三个群聚规则——分离、对齐和凝聚力——并在第 10 步应用于每个粒子。在这一步中,我们还通过重置它们的位置来防止 Boids 超出窗口边界。

它是如何工作的…

参见

使我们的粒子响应用户

在这个菜谱中,我们将基于从音频文件中进行的快速傅里叶变换FFT)分析来选择之前的粒子系统并添加动画。

FFT 分析将返回一个表示几个频率窗口振幅的值的列表。我们将每个粒子与一个频率窗口相匹配,并使用其值来动画化每个粒子对其他所有粒子施加的排斥力。

这个例子使用了 Cinder 的 FFT 处理器,它仅在 Mac OS X 上可用。

准备工作

我们将使用之前菜谱中开发的相同粒子系统,在 2D 中创建粒子系统。创建该菜谱中描述的ParticleParticleSystem类,并在应用程序源文件的顶部包含ParticleSystem.h文件。

如何做到这一点…

使用 FFT 分析中的值来动画化我们的粒子。执行以下步骤来完成:

  1. 在应用程序的类中声明一个ParticleSystem对象和一个变量来存储我们将创建的粒子数量。

    ParticleSystem mParticleSystem;
    int mNumParticles;
    
  2. setup方法中,我们将创建 256 个随机粒子。粒子的数量将与我们从音频分析中接收到的值的数量相匹配。

    粒子将在窗口的随机位置开始,具有随机的大小和质量。drag将设置为0.9

    mNumParticles = 256;
    for( int i=0; i<mNumParticles; i++ ){
      float x = ci::randFloat( 0.0f, getWindowWidth() );
      float y = ci::randFloat( 0.0f, getWindowHeight() );
      float radius = ci::randFloat( 5.0f, 15.0f );
      float mass = radius;
      float drag = 0.9f;
            Particle *particle = new Particle
            ( Vec2f( x, y ), radius, mass, drag );
    mParticleSystem.addParticle( particle );
    }
    
  3. update方法中,我们必须调用粒子系统的update方法。

    void MyApp::update(){
    mParticleSystem.update();
    }
    
  4. draw方法中,我们必须清除背景,计算窗口的矩阵,并调用粒子系统的draw方法。

    void MyApp::draw()
    {
      gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatricesWindow( getWindowWidth(), getWindowHeight() );
    mParticleSystem.draw();
    }
    
  5. 现在让我们加载并播放一个音频文件。我们首先包括加载、播放和执行 FFT 分析的必要文件。在源文件顶部添加以下代码片段:

    #include "cinder/audio/Io.h"
    #include "cinder/audio/FftProcessor.h"
    #include "cinder/audio/PcmBuffer.h"
    #include "cinder/audio/Output.h"
    
  6. 现在声明ci::audio::TrackRef,它是一个音频轨道的引用。

    Audio::TrackRef mAudio;
    
  7. setup方法中,我们将打开一个文件对话框,允许用户选择要播放的音频文件。

    如果检索到的路径不为空,我们将使用它来加载并添加一个新的音频轨道。

    fs::path audioPath = getOpenFilePath();
    if( audioPath.empty() == false ){
      mAudio = audio::Output::addTrack( audio::load( audioPath.string()   ) );
    }
    
  8. 我们将检查mAudio是否成功加载并播放。我们还将启用 PCM 缓冲区和循环。

    if( mAudio ){
      mAudio->enablePcmBuffering( true );
      mAudio->setLooping( true );
      mAudio->play();
    }
    
  9. 现在我们已经播放了一个音频文件,我们需要开始动画化粒子。首先,我们需要向窗口中心应用一个弹性力。我们通过迭代所有粒子并添加一个力来完成,这个力是粒子位置与窗口中心位置差值的十分之一。

    将以下代码片段添加到update方法中:

    Vec2f center = getWindowCenter();
    for( vector<Particle*>::iterator it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ){
            Particle *particle = *it;
            Vec2f force = 
            ( center - particle->position ) * 0.1f;
    particle->forces += force;
        }
    
  10. 现在我们必须计算 FFT 分析。这将在每次更新帧后进行一次。

    声明一个局部变量std::shared_ptr<float>,用于存储 FFT 的结果。

    我们将获取mAudio的 PCM 缓冲区的引用,并在其左通道上执行 FFT 分析。对mAudio及其缓冲区进行测试以检查其有效性是一个好的实践。

    std::shared_ptr<float>fft;
    if( mAudio ){
      audio::PcmBuffer32fRef pcmBuffer = mAudio->getPcmBuffer();
    if( pcmBuffer ){
        fft = audio::calculateFft( pcmBuffer->getChannelData( audio::CHANNEL_FRONT_LEFT ), mNumParticles );
      }
        }
    
  11. 我们将使用 FFT 分析的结果来调整每个粒子施加的排斥力。

    将以下代码片段添加到update方法中:

    if( fft ){
    float *values = fft.get();
    for( int i=0; i<mParticleSystem.particles.size()-1; i++ ){
    for( int j=i+1; j<mParticleSystem.particles.size(); j++ ){
      Particle *particleA = 
      mParticleSystem.particles[i];
      Particle *particleB = 
      mParticleSystem.particles[j];
      Vec2f delta = particleA->position - 
      particleB->position;
      float distanceSquared = delta.lengthSquared();
      particleA->forces += ( delta / distanceSquared ) * particleB->mass * values[j] * 0.5f;
      particleB->forces -= ( delta / distanceSquared ) * particleA->mass * values[i] * 0.5f;
    
  12. 构建并运行应用程序;您将被提示选择一个音频文件。选择它,它将开始播放。粒子将根据音频的频率移动并相互推挤。如何操作…

它是如何工作的…

我们为 FFT 分析返回的每个值创建了一个粒子,并根据相应的频率窗口幅度使每个粒子排斥其他粒子。随着音乐的演变,动画将相应地做出反应。

参见

将粒子对齐到处理后的图像

在本食谱中,我们将展示如何使用在前面的食谱中介绍的技术使粒子对齐到图像中检测到的边缘。

准备工作

在本食谱中,我们将使用来自在 2D 中创建粒子系统食谱的粒子实现;来自第三章的检测边缘食谱中的图像处理示例;以及应用排斥和吸引力量食谱中涵盖的模拟排斥。

如何操作…

我们将创建与图像中检测到的边缘对齐的粒子。为此,请执行以下步骤:

  1. Particle.h文件中为Particle类添加一个anchor属性。

    ci::Vec2f anchor;
    
  2. Particle.cpp源文件的Particle类构造函数末尾设置anchor值。

    anchor = position;
    
  3. 向您应用程序的主类添加一个新属性。

    float maxAlignSpeed;
    
  4. setup方法末尾,在图像处理之后,添加新粒子,如下所示:

    mMouseDown = false;
    repulsionFactor = -1.f;
    maxAlignSpeed = 10.f;
    
    mImage = loadImage( loadAsset("image.png") );
    mImageOutput = Surface8u(mImage.getWidth(), mImage.getHeight(), false);
    
    ip::grayscale(mImage, &mImage);
    ip::edgeDetectSobel(mImage, &mImageOutput);
    
    Surface8u::Iter pixelIter = mImageOutput.getIter(Area(1,1,mImageOutput.getWidth()-1,mImageOutput.getHeight()-1));
    
    while( pixelIter.line() ) {
        while( pixelIter.pixel() ) {
            if(pixelIter.getPos().x < mImageOutput.getWidth()
              && pixelIter.getPos().y < 
              mImageOutput.getHeight()
              && pixelIter.r() > 99) {
                float radius = 1.5f;
                float mass = Rand::randFloat(10.f, 20.f);
                float drag = 0.9f;
                Particle *particle = new Particle( 
                pixelIter.getPos(), radius, mass, drag );
                mParticleSystem.addParticle( particle );
            }
        }
    }
    
  5. 为您的主类实现update方法,如下所示:

    void MainApp::update() {
      for( std::vector<Particle*>::iterator it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ) {
    
        if(mMouseDown) {
          Vec2f repulsionForce = (*it)->position - getMousePos();
          repulsionForce = repulsionForce.normalized() * math<float>::max(0.f, 100.f - repulsionForce.length());
                      (*it)->forces += repulsionForce;
            }
    
        Vec2f alignForce = (*it)->anchor - (*it)->position;
        alignForce.limit(maxAlignSpeed);
            (*it)->forces += alignForce;
        }
    
      mParticleSystem.update();
    }
    
  6. Particle.cpp源文件中的Particle类的draw方法更改为以下内容

    void Particle::draw(){
      glBegin(GL_POINTS);
      glVertex2f(position);
      glEnd();
    }
    

它是如何工作的…

第一个主要步骤是在图像的一些特征点上分配粒子。为此,我们检测了边缘,这在第三章的检测边缘食谱中有介绍,使用图像处理技术。在第 4 步中,您可以看到我们遍历了每个处理图像的每个像素,并在检测到的特征处放置粒子。

你可以在第 5 步找到一个重要的计算,我们尝试将粒子移动回存储在anchor属性中的原始位置。为了使粒子无序,我们使用了与应用排斥和吸引力的力菜谱中相同的排斥代码。

如何工作…

参见

将粒子对齐到网格表面

在这个菜谱中,我们将使用来自在 2D 中创建粒子系统菜谱的粒子代码库的 3D 版本。为了在 3D 空间中导航,我们将使用在第二章为开发做准备中介绍的使用 MayaCamUI菜谱中的MayaCamUI。请参阅第二章。

准备工作

为了模拟排斥力,我们使用了来自应用排斥和吸引力的力菜谱的代码,并对三维空间进行了轻微修改。对于这个例子,我们使用了位于 Cinder 包内 Picking3D 样本的resources目录中的ducky.mesh网格文件。请将此文件复制到您项目中的assets文件夹。

如何做…

我们将创建与网格对齐的粒子。执行以下步骤:

  1. Particle.h文件中将anchor属性添加到Particle类中。

    ci::Vec3f anchor;
    
  2. Particle.cpp源文件的Particle类构造函数的末尾设置anchor值。

    anchor = position;
    
  3. 在您的主类中添加必要的头文件。

    #include "cinder/TriMesh.h"
    
  4. 将新属性添加到您应用程序的主类中。

    ParticleSystem mParticleSystem;
    
    float repulsionFactor;
    float maxAlignSpeed;
    
    CameraPersp  mCam;
    MayaCamUI       mMayaCam;
    
    TriMesh  mMesh;
    Vec3f    mRepPosition;
    
  5. setup方法中设置默认值。

    repulsionFactor = -1.f;
    maxAlignSpeed = 10.f;
    mRepPosition = Vec3f::zero();
    
    mMesh.read( loadAsset("ducky.msh") );
    
    mCam.setPerspective(45.0f, getWindowAspectRatio(), 0.1, 10000);
    mCam.setEyePoint(Vec3f(7.f,7.f,7.f));
    mCam.setCenterOfInterestPoint(Vec3f::zero());
    mMayaCam.setCurrentCam(mCam);
    
  6. setup方法的末尾添加以下代码片段:

    for(vector<Vec3f>::iterator it = mMesh.getVertices().begin(); it != mMesh.getVertices().end(); ++it) {
      float mass = Rand::randFloat(2.f, 15.f);
      float drag = 0.95f;
      Particle *particle = new Particle
      ( (*it), 0.f, mass, drag );
      mParticleSystem.addParticle( particle );
    }
    
  7. 添加相机导航的方法。

    void MainApp::resize( ResizeEvent event ){
        mCam = mMayaCam.getCamera();
        mCam.setAspectRatio(getWindowAspectRatio());
        mMayaCam.setCurrentCam(mCam);
    }
    
    void MainApp::mouseDown(MouseEvent event){
        mMayaCam.mouseDown( event.getPos() );
    }
    
    void MainApp::mouseDrag( MouseEvent event ){
      mMayaCam.mouseDrag( event.getPos(), event.isLeftDown(), 
      event.isMiddleDown(), event.isRightDown() );
    }
    
  8. 为您的应用程序主类实现updatedraw方法。

    void MainApp::update() {
    
    mRepPosition.x = cos(getElapsedSeconds()) * 3.f;
    mRepPosition.y = sin(getElapsedSeconds()*2.f) * 3.f;
    mRepPosition.z = cos(getElapsedSeconds()*1.5f) * 3.f;
    
    for( std::vector<Particle*>::iterator it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ) {
    
      Vec3f repulsionForce = (*it)->position - mRepPosition;
      repulsionForce = repulsionForce.normalized() * math<float>::max(0.f, 3.f - repulsionForce.length());
      (*it)->forces += repulsionForce;
    
      Vec3f alignForce = (*it)->anchor - (*it)->position;
      alignForce.limit(maxAlignSpeed);
            (*it)->forces += alignForce;
        }
    
      mParticleSystem.update();
    }
    
    void MainApp::draw()
    {
      gl::enableDepthRead();
      gl::enableDepthWrite();
      gl::clear( Color::black() );
      gl::setViewport(getWindowBounds());
      gl::setMatrices(mMayaCam.getCamera());
    
      gl::color(Color(1.f,0.f,0.f));
      gl::drawSphere(mRepPosition, 0.25f);
    
      gl::color(Color::white());
      mParticleSystem.draw();
    }
    
  9. Particle.cpp源文件中的Particledraw方法替换为以下内容

    void Particle::draw(){
      glBegin(GL_POINTS);
      glVertex2f(position);
      glEnd();
    }
    

如何工作…

首先,我们在第 6 步中创建的粒子代替了网格的顶点。

如何工作…

你可以在第 8 步找到一个重要的计算,我们尝试将粒子移动回存储在anchor属性中的原始位置。为了使粒子偏移,我们使用了与应用排斥和吸引力的力菜谱中相同的排斥代码,但对其进行了三维空间的轻微修改。基本上,它涉及到使用Vec3f类型而不是Vec2f

如何工作…

创建弹簧

在这个菜谱中,我们将学习如何创建弹簧。

弹簧是连接两个粒子并使它们保持在定义的静止距离的对象。

在这个例子中,我们将创建随机粒子,并且每当用户按下鼠标按钮时,两个随机粒子将通过一个新的弹簧连接,弹簧的静止距离是随机的。

准备工作

我们将使用之前菜谱中开发的相同粒子系统,即在 2D 中创建粒子系统。创建该菜谱中描述的ParticleParticleSystem类,并在应用程序源文件顶部包含ParticleSystem.h文件。

我们将创建一个Spring类,因此有必要创建以下文件:

  • Spring.h

  • Spring.cpp

如何实现它...

我们将创建约束粒子运动的弹簧。执行以下步骤以实现此目的:

  1. Spring.h文件中,我们将声明一个Spring类。首先,我们需要添加#pragma once宏并包含必要的文件。

    #pragma once
    #include "Particle.h"
    #include "cinder/gl/gl.h"
    
  2. 接下来,声明Spring类。

    class Spring{
    
    };
    
  3. 我们将添加成员变量,两个Particle指针以引用将通过此弹簧连接的粒子,以及reststrengthfloat变量。

    class Spring{
    public:
      Particle *particleA;
      Particle *particleB;
      float strength, rest;
    };
    
  4. 现在我们将声明一个构造函数,它将接受两个Particle对象的指针以及reststrength值。

    我们还将声明updatedraw方法。

    以下为最终的Spring类声明:

    class Spring{
    public:
    
        Spring( Particle *particleA, Particle *particleB, 
        float rest, float strength );
    
        void update();
        void draw();
    
        Particle *particleA;
        Particle *particleB;
        float strength, rest;
    
    };
    
  5. 让我们在Spring.cpp文件中实现Spring类。

    在构造函数中,我们将成员变量的值设置为通过参数传入的值。

    Spring::Spring( Particle *particleA, Particle *particleB, float rest, float strength ){
      this->particleA = particleA;
      this->particleB = particleB;
      this->rest = rest;
      this->strength = strength;
    }
    
  6. Spring类的update方法中,我们将计算粒子之间的距离与弹簧的平衡距离之间的差异,并相应地调整它们。

    void Spring::update(){
        ci::Vec2f delta = particleA->position - particleB->position;
        float length = delta.length();
        float invMassA = 1.0f / particleA->mass;
        float invMassB = 1.0f / particleB->mass;
        float normDist = ( length - rest ) / ( length * ( invMassA + invMassB ) ) * strength;
        particleA->position -= delta * normDist * invMassA;
        particleB->position += delta * normDist * invMassB;
    }
    
  7. Spring类的draw方法中,我们将简单地绘制一条连接两个粒子的线。

    void Spring::draw(){
        ci::gl::drawLine
        ( particleA->position, particleB->position );
    }
    
  8. 现在我们必须在ParticleSystem类中进行一些更改,以允许添加弹簧。

    ParticleSystem文件中,包含Spring.h文件。

    #include "Spring.h"
    
  9. 在类声明中声明std::vector<Spring*>成员。

    std::vector<Spring*> springs;
    
  10. 声明addSpringdestroySpring方法,用于向系统中添加和销毁弹簧。

    以下为最终的ParticleSystem类声明:

    classParticleSystem{
    public:
    
        ~ParticleSystem();
    
        void update();
        void draw();
    
        void addParticle( Particle *particle );
        void destroyParticle( Particle *particle );
        void addSpring( Spring *spring );
        void destroySpring( Spring *spring );
    
        std::vector<Particle*> particles;
        std::vector<Spring*> springs;
    
    };
    
  11. 让我们实现addSpring方法。在ParticleSystem.cpp文件中,添加以下代码片段:

    void ParticleSystem::addSpring( Spring *spring ){
      springs.push_back( spring );
    }
    
  12. destroySpring方法的实现中,我们将找到对应于参数Spring的迭代器,并将其从弹簧中移除。我们还将删除该对象。

    ParticleSystem.cpp文件中添加以下代码片段:

    void ParticleSystem::destroySpring( Spring *spring ){
      std::vector<Spring*>::iterator it = std::find( springs.begin(), springs.end(), spring );
      delete *it;
      springs.erase( it );
    }
    
  13. 必须修改update方法以更新所有弹簧。

    以下代码片段显示了最终的更新应该看起来像什么:

    void ParticleSystem::update(){
      for( std::vector<Particle*>::iterator it = particles.begin(); it != particles.end(); ++it ){
            (*it)->update();
        }
        for( std::vector<Spring*>::iterator it = 
        springs.begin(); it != springs.end(); ++it ){
            (*it)->update();
        }
    }
    
  14. draw方法中,我们还需要遍历所有弹簧并调用它们的draw方法。

    ParticleSystem::draw方法的最终实现应该如下所示:

    void ParticleSystem::draw(){
        for( std::vector<Particle*>::iterator it = particles.begin(); it != particles.end(); ++it ){
            (*it)->draw();
        }
        for( std::vector<Spring*>::iterator it = 
        springs.begin(); it != springs.end(); ++it ){
            (*it)->draw();
        }
    }
    
  15. 我们已经完成了Spring类的创建和对ParticleSystem类所有必要更改的制作。

    让我们转到我们的应用程序类并包含ParticleSystem.h文件:

    #include "ParticleSystem.h"
    
  16. 声明一个ParticleSystem对象。

    ParticleSystem mParticleSystem;
    
  17. 通过将以下代码片段添加到setup方法中,创建一些随机粒子:

    for( int i=0; i<100; i++ ){
            float x = randFloat( getWindowWidth() );
            float y = randFloat( getWindowHeight() );
            float radius = randFloat( 5.0f, 15.0f );
            float mass = radius;
            float drag = 0.9f;
            Particle *particle = 
            new Particle( Vec2f( x, y ), radius, mass, drag );
            mParticleSystem.addParticle( particle );
        }
    
  18. update方法中,我们需要调用ParticleSystem上的update方法。

    void MyApp::update(){
      mParticleSystem.update();
    }
    
  19. draw方法中,清除背景,定义窗口的矩阵,并在mParticleSystem上调用draw方法。

    void MyApp::draw(){
      gl::clear( Color( 0, 0, 0 ) );
      gl::setMatricesWindow( getWindowWidth(), getWindowHeight() );
      mParticleSystem.draw();
    }
    
  20. 由于我们希望在用户按下鼠标时创建弹簧,因此我们需要声明mouseDown方法。

    将以下代码片段添加到应用程序的类声明中:

      void mouseDown( MouseEvent event );
    
  21. mouseDown实现中,我们将连接两个随机粒子。

    首先声明一个Particle指针并将其定义为粒子系统中的一个随机粒子。

    Particle *particleA = mParticleSystem.particles[ randInt( mParticleSystem.particles.size() ) ];
    
  22. 现在声明第二个Particle指针并将其设置为等于第一个指针。在while循环中,我们将将其值设置为mParticleSystem中的随机粒子,直到两个粒子不同。这将避免两个指针都指向同一粒子的情形。

    Particle *particleB = particleA;
    while( particleB == particleA ){
      particleB = mParticleSystem.particles[ randInt( mParticleSystem.particles.size() ) ];
        }
    
  23. 现在我们将创建一个Spring对象,它将连接两个粒子,定义一个随机的静止距离,并将strength设置为1.0。将创建的弹簧添加到mParticleSystem中。

    以下是最终的mouseDown实现:

    void SpringsApp::mouseDown( MouseEvent event )
    {
        Particle *particleA = mParticleSystem.particles[ 
        randInt( mParticleSystem.particles.size() ) ];
        Particle *particleB = particleA;
        while( particleB == particleA ){
      particleB = mParticleSystem.particles[ randInt( mParticleSystem.particles.size() ) ];
        }
        float rest = randFloat( 100.0f, 200.0f );
        float strength = 1.0f;
        Spring *spring = new Spring
        ( particleA, particleB, rest, strength );
        mParticleSystem.addSpring( spring );
    
    }
    
  24. 构建并运行应用程序。每次按下鼠标按钮时,两个粒子将通过一条白色线条连接,并且它们的距离将保持不变。如何操作…

它是如何工作的...

Spring对象将计算两个粒子之间的差异并修正它们的位置,以便两个粒子之间的距离等于弹簧的静止值。

通过使用它们的质量,我们还将考虑每个粒子的质量,因此修正将考虑粒子的重量。

还有更多...

同样的原理也可以应用于 3D 粒子系统。

如果你正在使用 3D 粒子,如在 2D 中创建粒子系统食谱的更多内容…部分所述,Spring类只需将其计算更改为使用ci::Vec3f而不是ci::Vec2f

Spring类的update方法将类似于以下代码片段:

void Spring::update(){
    ci::Vec3f delta = particleA->position - particleB->position;
    float length = delta.length();
    float invMassA = 1.0f / particleA->mass;
    float invMassB = 1.0f / particleB->mass;
    float normDist = ( length - rest ) / ( length * ( invMassA + invMassB ) ) * strength;
    particleA->position -= delta * normDist * invMassA;
    particleB->position += delta * normDist * invMassB;
}

第六章. 渲染和纹理化粒子系统

在本章中,我们将学习以下内容:

  • 纹理化粒子

  • 为我们的粒子添加尾巴

  • 创建布料模拟

  • 纹理化布料模拟

  • 使用点精灵和着色器纹理化粒子系统

  • 连接粒子

简介

从第五章,构建粒子系统继续,我们将学习如何渲染和将纹理应用到我们的粒子上,以使它们更具吸引力。

纹理化粒子

在本食谱中,我们将使用从 PNG 文件加载的纹理来渲染上一章中引入的粒子。

开始

本食谱的代码库是第五章,构建粒子系统模拟粒子随风飘动的食谱示例。我们还需要一个单个粒子的纹理。你可以用任何图形程序轻松制作一个。对于这个例子,我们将使用存储在assets文件夹中名为particle.png的 PNG 文件。在这种情况下,它只是一个带有透明度的径向渐变。

开始

如何做…

我们将使用之前创建的纹理来渲染粒子。

  1. 包含必要的头文件:

    #include "cinder/gl/Texture.h"
    #include "cinder/ImageIo.h"
    
  2. 向应用程序主类添加一个成员:

    gl::Texture particleTexture;
    
  3. setup方法中加载particleTexture

    particleTexture=gl::Texture(loadImage(loadAsset("particle.png")));
    
  4. 我们还必须更改此示例中的粒子大小:

    float radius = Rand::randFloat(2.f, 10.f);
    
  5. draw方法结束时,我们将按照以下方式绘制我们的粒子:

    gl::enableAlphaBlending();
    particleTexture.enableAndBind();
    gl::color(ColorA::white());
    mParticleSystem.draw();
    
  6. Particle.cpp源文件内的draw方法替换为以下代码:

    void Particle::draw(){
    ci::gl::drawSolidRect(ci::Rectf(position.x-radius, position.y-radius,
    position.x+radius, position.y+radius));
    }
    

它是如何工作的…

在步骤 5 中,我们看到了两条重要的行。一条启用了 alpha 混合,另一条将存储在particleTexture属性中的纹理绑定。如果你看步骤 6,你可以看到我们以矩形的形式绘制了每个粒子,每个矩形都应用了纹理。这是一种简单的纹理化粒子的方法,但不是非常高效,但在这个例子中,它相当有效。通过在调用ParticleSystem上的draw方法之前更改颜色,可以更改绘制粒子的颜色。

如何工作…

参见

查看食谱使用点精灵和着色器纹理化粒子系统

为我们的粒子添加尾巴

在本食谱中,我们将向您展示如何为粒子动画添加尾巴。

开始

在本食谱中,我们将使用来自第五章,构建粒子系统应用排斥和吸引力的力的代码库。

如何做…

我们将使用不同的技术为粒子添加尾巴。

绘制历史

简单地替换draw方法为以下代码:

void MainApp::draw()
{   
gl::enableAlphaBlending();
gl::setViewport(getWindowBounds());
gl::setMatricesWindow(getWindowWidth(), getWindowHeight());

gl::color( ColorA(0.f,0.f,0.f, 0.05f) );
gl::drawSolidRect(getWindowBounds());
gl::color( ColorA(1.f,1.f,1.f, 1.f) );
mParticleSystem.draw();
}

尾巴作为线条

我们将添加由几条线构成的尾巴。

  1. Particle.h头文件内的Particle类中添加新的属性:

    std::vector<ci::Vec2f> positionHistory;
    int tailLength;
    
  2. Particle构造函数的末尾,在Particle.cpp源文件中,将tailLength属性的默认值设置为:

    tailLength = 10;
    
  3. Particle类的update方法末尾添加以下代码:

    position History.push_back(position);
    if(positionHistory.size() >tailLength) {
    positionHistory.erase( positionHistory.begin() );
    }
    
  4. 将您的Particle::draw方法替换为以下代码:

    void Particle::draw(){
      glBegin( GL_LINE_STRIP );
      for( int i=0; i<positionHistory.size(); i++ ){
    float alpha = (float)i/(float)positionHistory.size();
    ci::gl::color( ci::ColorA(1.f,1.f,1.f, alpha));
    ci::gl::vertex( positionHistory[i] );
      }
      glEnd();
    
    ci::gl::color( ci::ColorA(1.f,1.f,1.f, 1.f) );
    ci::gl::drawSolidCircle( position, radius );
    }
    

工作原理…

现在,我们将解释每种技术是如何工作的。

绘制历史

这种方法的背后思想非常简单,我们不是清除绘图区域,而是连续绘制半透明的矩形,这些矩形越来越多地覆盖旧的绘图状态。这种方法非常简单,但可以给粒子带来有趣的效果。您还可以通过更改矩形的 alpha 通道来操纵每个矩形的透明度,这将成为背景的颜色。

绘制历史

尾巴作为线条

要用线条绘制尾巴,我们必须存储几个粒子位置,并通过这些位置绘制具有可变透明度的线条。透明度的规则只是用较低的透明度绘制较旧的位置。您可以在步骤 4 中看到绘图代码和 alpha 通道计算。

尾巴作为线条

创建布料模拟

在这个食谱中,我们将学习如何通过创建由弹簧连接的粒子网格来模拟布料。

准备工作

在这个食谱中,我们将使用第五章中描述的粒子系统,构建粒子系统中的“在 2D 中创建粒子系统”。

我们还将使用在第五章中通过“创建弹簧”食谱创建的Springs类,构建粒子系统

因此,您需要将以下文件添加到您的项目中:

  • Particle.h

  • ParticleSystem.h

  • Spring.h

  • Spring.cpp

如何操作…

我们将创建一个由弹簧连接的粒子网格来创建布料模拟。

  1. 通过在源文件顶部添加以下代码将粒子系统文件包含到您的项目中:

    #include "ParticleSystem.h"
    
  2. 在应用程序类声明之前添加using语句,如下所示:

    using namespace ci;
    using namespace ci::app;
    using namespace std;
    
  3. 创建一个ParticleSystem对象实例和存储网格顶角的成员变量。我们还将创建存储组成我们网格的行数和列数的变量。在您的应用程序类中添加以下代码:

    ParticleSystem mParticleSystem;
      Vec2f mLeftCorner, 
    mRightCorner;
      intmNumRows, mNumLines;
    
  4. 在我们开始创建我们的粒子网格之前,让我们更新并绘制我们的应用程序中的updatedraw方法中的粒子系统。

    Void MyApp::update(){
      mParticleSystem.update();
    }
    
    void MyApp::draw(){
      gl::clear( Color( 0, 0, 0 ) ); 
      mParticleSystem.draw();
    }
    
  5. setup方法中,让我们初始化网格角落位置和行数。在setup方法的顶部添加以下代码:

    mLeftCorner = Vec2f( 50.0f, 50.0f );
    mRightCorner = Vec2f( getWindowWidth() - 50.0f, 50.0f );
    mNumRows = 20;
    mNumLines = 15;
    
  6. 计算网格上每个粒子之间的距离。

    float gap = ( mRightCorner.x - mLeftCorner.x ) / ( mNumRows-1 );
    
  7. 让我们创建一个均匀分布的粒子网格并将它们添加到ParticleSystem中。我们将通过创建一个嵌套循环来实现这一点,其中每个循环索引将用于计算粒子的位置。在setup方法中添加以下代码:

    for( int i=0; i<mNumRows; i++ ){
    for( int j=0; j<mNumLines; j++ ){
    float x = mLeftCorner.x + ( gap * i );
    float y = mLeftCorner.y + ( gap * j );
    Particle *particle = new Particle( Vec2f( x, y ), 5.0f, 5.0f, 0.95f );
    mParticleSystem.addParticle( particle );
            }
        }
    
  8. 现在粒子已经创建,我们需要用弹簧将它们连接起来。让我们首先将每个粒子与其正下方的粒子连接起来。在一个嵌套循环中,我们将计算ParticleSystem中粒子的索引以及它下面的粒子的索引。然后我们创建一个Spring类,使用它们的当前距离作为reststrength值为1.0来连接这两个粒子。将以下内容添加到setup方法的底部:

    for( int i=0; i<mNumRows; i++ ){
    for( int j=0; j<mNumLines-1; j++ ){
    int indexA = i * mNumLines + j;
    int indexB = i * mNumLines + j + 1;
                Particle *partA = mParticleSystem.particles[ indexA ];
                Particle *partB = mParticleSystem.particles[ indexB ];
    float rest = partA->position.distance( partB->position );
                Spring *spring = new Spring( partA, partB, rest, 1.0f );
    mParticleSystem.addSpring( spring );
            }
        }
    
  9. 现在我们有一个由粒子和弹簧组成的静态网格。让我们通过向每个粒子应用一个恒定的垂直力来添加一些重力。将以下代码添加到update方法的底部:

    Vec2f gravity( 0.0f, 1.0f );
    for( vector<Particle*>::iterator it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ){
            (*it)->forces += gravity;
        }
    
  10. 为了防止网格向下坠落,我们需要将顶边界的粒子在它们的初始位置(由mLeftCornermRightCorner定义)设置为静态。将以下代码添加到update方法中:

    int topLeftIndex = 0;
    int topRightIndex = ( mNumRows-1 ) * mNumLines;
    mParticleSystem.particles[ topLeftIndex ]->position = mLeftCorner;
    mParticleSystem.particles[ topRightIndex ]->position = mRightCorner;
    
  11. 构建并运行应用程序;您将看到一个带有重力下落的粒子网格,其顶部角落被锁定。如何做到这一点…

  12. 让我们通过允许用户用鼠标拖动粒子来增加一些交互性。声明一个Particle指针来存储正在拖动的粒子。

    Particle *mDragParticle;
    
  13. setup方法中初始化粒子为NULL

    mDragParticle = NULL;
    
  14. 在应用程序的类声明中声明mouseUpmouseDown方法。

    void mouseDown( MouseEvent event );
    void mouseUp( MouseEvent event );
    
  15. mouseDown事件的实现中,我们遍历所有粒子,如果粒子位于光标下,我们将mDragParticle设置为指向它。

    void MyApp::mouseDown( MouseEvent event ){
    for( vector<Particle*>::iterator it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ){
            Particle *part = *it;
            float dist = part->position.distance( event.getPos() );
    if( dist< part->radius ){
    mDragParticle = part;
    return;
            }
        }
    }
    
  16. mouseUp事件中,我们只需将mDragParticle设置为NULL

    void MyApp::mouseUp( MouseEvent event ){
    mDragParticle = NULL;
    }
    
  17. 我们需要检查mDragParticle是否是一个有效的指针并将粒子的位置设置为鼠标光标。将以下代码添加到update方法中:

    if( mDragParticle != NULL ){
    mDragParticle->position = getMousePos();
        }
    
  18. 构建并运行应用程序。按住并拖动鼠标到任何粒子上,然后将其拖动以查看布料模拟如何反应。

它是如何工作的…

布料模拟是通过创建一个二维粒子网格并使用弹簧连接它们来实现的。每个粒子将与其相邻的粒子以及其上方和下方的粒子通过弹簧连接。

更多内容…

网格的密度可以根据用户的需求进行更改。使用具有更多粒子的网格将生成更精确的模拟,但速度会较慢。

通过更改mNumLinesmNumRows来更改构成网格的粒子的数量。

布料模拟纹理化

在这个菜谱中,我们将学习如何将纹理应用到我们在当前章节的创建布料模拟菜谱中创建的布料模拟上。

准备工作

我们将使用在菜谱创建布料模拟中开发的布料模拟作为这个菜谱的基础。

您还需要一个图像作为纹理;将其放置在您的assets文件夹中。在这个菜谱中,我们将我们的图像命名为texture.jpg

如何做到这一点…

我们将计算布料模拟中每个粒子的对应纹理坐标并应用纹理。

  1. 包含必要的文件以使用纹理和读取图像。

    #include "cinder/gl/Texture.h"
    #include "cinder/ImageIo.h"
    
  2. 在你的应用程序类声明中声明一个ci::gl::Texture对象。

    gl::Texture mTexture;
    
  3. setup方法中加载图像。

    mTexture = loadImage( loadAsset( "image.jpg" ) );
    
  4. 我们将重新制作draw方法。所以我们将擦除在创建布料模拟配方中更改的所有内容,并应用clear方法。你的draw方法应该如下所示:

    void MyApp::draw(){
      gl::clear( Color( 0, 0, 0 ) );
    }
    
  5. 在调用clear方法之后,启用VERTEXTEXTURE COORD数组并绑定纹理。将以下代码添加到draw方法中:

    glEnableClientState( GL_VERTEX_ARRAY );
    glEnableClientState( GL_TEXTURE_COORD_ARRAY );
    mTexture.enableAndBind();
    
  6. 现在,我们将遍历构成布料模拟网格的所有粒子和弹簧,并在每一行及其相邻行之间绘制一个纹理三角形条带。首先创建一个for循环,循环mNumRows-1次,并创建两个std::vector<Vec2f>容器来存储顶点和纹理坐标。

    for( int i=0; i<mNumRows-1; i++ ){
      vector<Vec2f>vertexCoords, textureCoords;
    }
    
  7. 在循环内部,我们将创建一个嵌套循环,该循环将遍历布料网格中的所有行。在这个循环中,我们将计算将要绘制的粒子的索引,计算它们对应的纹理坐标,并将它们与textureCoordsvertexCoords的位置一起添加。将以下代码输入到我们在上一步创建的循环中:

    or( int j=0; j<mNumLines; j++ ){
     int indexTopLeft = i * mNumLines + j;
     int indexTopRight = ( i+1) * mNumLines + j;
     Particle *left = mParticleSystem.particles[ indexTopLeft ];
     Particle *right = mParticleSystem.particles[indexTopRight ];
     float texX = ( (float)i / (float)(mNumRows-1) ) * mTexture.getRight();
     float texY = ( (float)j / (float)(mNumLines-1) ) * mTexture.getBottom();
     textureCoords.push_back( Vec2f( texX, texY ) );
     vertexCoords.push_back( left->position );
     texX = ( (float)(i+1) / (float)(mNumRows-1) ) * mTexture.getRight();
     textureCoords.push_back( Vec2f( texX, texY ) );
     vertexCoords.push_back( right->position );
    }
    

    现在已经计算并放置了vertextexture坐标到vertexCoordstextureCoords中,我们将绘制它们。以下是完整的嵌套循环:

    for( int i=0; i<mNumRows-1; i++ ){
     vector<Vec2f> vertexCoords, textureCoords;
     for( int j=0; j<mNumLines; j++ ){
      int indexTopLeft = i * mNumLines + j;
      int indexTopRight = ( i+1) * mNumLines + j;
      Particle *left = mParticleSystem.particles[ indexTopLeft ];
      Particle *right = mParticleSystem.particles[ indexTopRight ];
      float texX = ( (float)i / (float)(mNumRows-1) ) * mTexture.getRight();
      float texY = ( (float)j / (float)(mNumLines-1) ) * mTexture.getBottom();
      textureCoords.push_back( Vec2f( texX, texY ) );
      vertexCoords.push_back( left->position );
      texX = ( (float)(i+1) / (float)(mNumRows-1) ) * mTexture.getRight();
      textureCoords.push_back( Vec2f( texX, texY ) );
      vertexCoords.push_back( right->position );
     }
     glVertexPointer 2, GL_FLOAT, 0, &vertexCoords[0] );
     glTexCoordPointer( 2, GL_FLOAT, 0, &textureCoords[0] );
     glDrawArrays( GL_TRIANGLE_STRIP, 0, vertexCoords.size() );
    }
    
  8. 最后,我们需要通过添加以下代码来解除mTexture的绑定:

    mTexture.unbind();
    

它是如何工作的…

我们根据粒子在网格上的位置计算了相应的纹理坐标。然后,我们绘制了由行上的粒子及其相邻行上的粒子形成的三角形条带纹理。

使用点精灵和着色器纹理化粒子系统

在这个配方中,我们将学习如何使用 OpenGL 点精灵和 GLSL 着色器将纹理应用到所有粒子。

此方法经过优化,允许以快速帧率绘制大量粒子。

准备工作

我们将使用在第五章中开发的 2D 粒子系统配方在 2D 中创建粒子系统,所以我们需要将以下文件添加到你的项目中:

  • Particle.h

  • ParticleSystem.h

我们还将加载一个图像作为纹理使用。图像的大小必须是 2 的幂,例如 256 x 256 或 512 x 512。将图像放置在assets文件夹中,并命名为particle.png

如何做到这一点...

我们将创建一个 GLSL 着色器,然后启用 OpenGL 点精灵来绘制纹理化的粒子。

  1. 让我们从创建 GLSL 着色器开始。创建以下文件:

    • shader.frag

    • shader.vert

    将它们添加到assets文件夹中。

  2. 在你选择的 IDE 中打开文件shader.frag并声明一个uniform sampler2D

    uniform sampler2D tex; 
    
  3. main函数中,我们使用纹理来定义片段颜色。添加以下代码:

    void main (void) {
      gl_FragColor = texture2D(tex, gl_TexCoord[0].st) * gl_Color;
    }
    
  4. 打开shader.vert文件并创建一个float attribute来存储粒子的半径。在main方法中,我们定义了位置、颜色和点大小属性。添加以下代码:

    attribute float particleRadius;
    void main(void)
    {
      gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
      gl_PointSize = particleRadius;
      gl_FrontColor = gl_Color;
    }
    
  5. 我们的着色器完成了!让我们转到我们的应用程序源文件并包含必要的文件。将以下代码添加到您的应用程序源文件中:

    #include "cinder/gl/Texture.h"
    #include "cinder/ImageIo.h"
    #include "cinder/Rand.h"
    #include "cinder/gl/GlslProg.h"
    #include "ParticleSystem.h"
    
  6. 声明创建粒子系统的成员变量以及存储粒子位置和半径的数组。还声明一个变量来存储粒子的数量。

    ParticleSystem mParticleSystem;
    int mNumParticles;
    Vec2f *mPositions;
    float *mRadiuses;
    
  7. setup方法中,我们将mNumParticles初始化为1000并分配数组。我们还将创建随机粒子。

    mNumParticles = 1000;
    mPositions = new Vec2f[ mNumParticles ];
    mRadiuses = new float[ mNumParticles ];
    
    for( int i=0; i<mNumParticles; i++ ){
     float x = randFloat( 0.0f, getWindowWidth() );
     float y = randFloat( 0.0f, getWindowHeight() );
     float radius = randFloat( 5.0f, 50.0f );
     Particle *particle = new Particle( Vec2f( x, y ), radius, 1.0f, 0.9f );
     mParticleSystem.addParticle( particle );
    }
    mParticleSystem.addParticle( particle );
    
  8. update方法中,我们将更新mParticleSystemmPositions以及mRadiuses数组。将以下代码添加到update方法中:

    mParticleSystem.update();
    for( int i=0; i<mNumParticles; i++ ){
     mPositions[i] = mParticleSystem.particles[i]->position;
     mRadiuses[i] = mParticleSystem.particles[i]->radius;
    }
    
  9. 声明着色器和粒子的纹理。

    gl::Texture mTexture;
    gl::GlslProg mShader;
    
  10. 通过在setup方法中添加以下代码来加载着色器和纹理:

    mTexture = loadImage( loadAsset( "particle.png" ) );
    mShader = gl::GlslProg( loadAsset( "shader.vert"), loadAsset( "shader.frag" ) );
    
  11. draw方法中,我们将首先用黑色清除背景,设置窗口的矩阵,启用加法混合,并绑定着色器。

    gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatricesWindow( getWindowWidth(), getWindowHeight() );
    gl::enableAdditiveBlending();
    mShader.bind();
    
  12. Vertex着色器中获取particleRadius属性的定位。启用顶点属性数组并将指针设置为mRadiuses

    GLint particleRadiusLocation = mShader.getAttribLocation( "particleRadius" );
    glEnableVertexAttribArray(particleRadiusLocation);
    glVertexAttribPointer(particleRadiusLocation, 1, GL_FLOAT, false, 0, mRadiuses);
    
  13. 启用点精灵并启用我们的着色器以写入点大小。

    glEnable(GL_POINT_SPRITE);
    glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE);
    glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);	
    
  14. 启用顶点数组和设置顶点指针为mPositions

    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2, GL_FLOAT, 0, mPositions);
    
  15. 现在启用并绑定纹理,以点形式绘制顶点数组,然后解绑纹理。

    mTexture.enableAndBind();
    glDrawArrays( GL_POINTS, 0, mNumParticles );
    mTexture.unbind();
    
  16. 现在我们需要做的就是禁用顶点数组,禁用顶点属性数组,并解绑着色器。

    glDisableClientState(GL_VERTEX_ARRAY); 
    glDisableVertexAttribArrayARB(particleRadiusLocation);
    mShader.unbind();
    
  17. 构建并运行应用程序,你将看到应用了纹理的1000个随机粒子。如何操作...

它是如何工作的...

点精灵是 OpenGL 的一个不错特性,允许将整个纹理应用于单个点。当绘制粒子系统时非常实用,并且相当优化,因为它减少了发送到图形卡的信息量,并在 GPU 上执行了大部分计算。

在配方中,我们还创建了一个 GLSL 着色器,一种高级编程语言,它允许对编程管道有更多的控制,为每个粒子定义单独的点大小。

update方法中,我们更新了PositionsRadiuses数组,因此如果粒子被动画化,数组将代表正确的值。

还有更多...

点精灵允许我们在 3D 空间中对点进行纹理化。要绘制 3D 粒子系统,请执行以下操作:

  1. 使用配方中There's more…部分描述的在 2D 中创建粒子系统Particle类,来自第五章,构建粒子系统

  2. 声明并初始化mPositions为一个ci::Vec3f数组。

  3. draw方法中,通过应用以下更改来指示顶点指针包含 3D 信息:

    glVertexPointer(2, GL_FLOAT, 0, mPositions);
    

    将之前的代码行更改为:

    glVertexPointer(3, GL_FLOAT, 0, mPositions);
    
  4. 顶点着色器需要根据粒子的深度调整点的大小。shader.vert 文件需要读取以下代码:

    attribute float particleRadius;
    
    void main(void)
    {
      vec4eyeCoord = gl_ModelViewMatrix * gl_Vertex;
      gl_Position = gl_ProjectionMatrix * eyeCoord;
      float distance = sqrt(eyeCoord.x*eyeCoord.x + eyeCoord.y*eyeCoord.y + eyeCoord.z*eyeCoord.z);
      float attenuation = 3000.0 / distance;
      gl_PointSize = particleRadius * attenuation;
      gl_FrontColor = gl_Color;
    }
    

连接点

在这个菜谱中,我们将展示如何用线条连接粒子,并介绍另一种绘制粒子的方法。

开始

本菜谱的代码库是来自菜谱 模拟粒子随风飘动 的示例(来自第五章,构建粒子系统),请参考此菜谱。

如何做到这一点…

我们将使用线条连接渲染为圆形的粒子。

  1. setup 方法中更改要创建的粒子数量:

    int numParticle = 100;
    
  2. 我们将按照以下方式计算每个粒子的 radiusmass

    float radius = Rand::randFloat(2.f, 5.f);
    float mass = radius*2.f;
    
  3. Particle.cpp 源文件内的 draw 方法替换为以下内容:

    void Particle::draw(){
     ci::gl::drawSolidCircle(position, radius);
     ci::gl::drawStrokedCircle(position, radius+2.f);
    }
    
  4. 按如下方式替换 ParticleSystem.cpp 源文件内的 draw 方法:

    void ParticleSystem::draw(){
     gl::enableAlphaBlending();
     std::vector<Particle*>::iterator it;
     for(it = particles.begin(); it != particles.end(); ++it){
      std::vector<Particle*>::iterator it2;
      for(it2=particles.begin(); it2!= particles.end(); ++it2){
       float distance = (*it)->position.distance( 
        (*it2)->position ));
       float per = 1.f - (distance / 100.f);
       ci::gl::color( ci::ColorA(1.f,1.f,1.f, per*0.8f) );
       ci::Vec2f conVec = (*it2)->position-(*it)->position;
       conVec.normalize();
       ci::gl::drawLine(
        (*it)->position+conVec * ((*it)->radius+2.f),
        (*it2)->position-conVec * ((*it2)->radius+2.f ));
      }
     }
     ci::gl::color( ci::ColorA(1.f,1.f,1.f, 0.8f) );
     std::vector<Particle*>::iterator it3;
     for(it3 = particles.begin(); it3!= particles.end(); ++it3){
      (*it3)->draw();
     }
    }
    

如何工作…

本示例中最有趣的部分在第 4 步中提到。我们正在遍历所有点,实际上是所有可能的点对,用线条连接它们并应用适当的透明度。连接两个粒子的线条透明度是根据这两个粒子之间的距离计算的;距离越长,连接线越透明。

如何工作…

看看第 3 步中粒子的绘制方式。它们是带有略微更大的外圆的实心圆。一个很好的细节是我们绘制粒子之间的连接线,这些粒子粘附在外圆的边缘,但不会穿过它。我们在第 4 步中做到了这一点,当时我们计算了连接两个粒子的向量的归一化向量,然后使用它们将连接点移动到该向量,乘以外圆半径。

如何工作…

使用样条曲线连接粒子

在这个菜谱中,我们将学习如何在 3D 中用样条曲线连接粒子。

开始

在这个菜谱中,我们将使用来自菜谱 创建粒子系统 的粒子代码库,该菜谱位于第五章,构建粒子系统。我们将使用 3D 版本。

如何做到这一点…

我们将创建连接粒子的样条曲线。

  1. ParticleSystem.h 内包含必要的头文件:

    #include "cinder/BSpline.h"
    
  2. ParticleSystem 类添加一个新属性:

    ci::BSpline3f spline;
    
  3. ParticleSystem 类实现 computeBSpline 方法:

    void ParticleSystem::computeBspline(){ 
     std::vector<ci::Vec3f> splinePoints;
     std::vector<Particle*>::iterator it;
     for(it = particles.begin(); it != particles.end(); ++it ){
      ++it;
      splinePoints.push_back( ci::Vec3f( (*it)->position ) );
     }
     spline = ci::BSpline3f( splinePoints, 3, false, false );
    }
    
  4. ParticleSystem 更新方法的末尾调用以下代码:

    computeBSpline();
    
  5. ParticleSystemdraw 方法替换为以下内容:

    void ParticleSystem::draw(){
     ci::gl::color(ci::Color::black());
     if(spline.isUniform()) {
      glBegin(GL_LINES);
      float step = 0.001f;
      for( float t = step; t <1.0f; t += step ) {
       ci::gl::vertex( spline.getPosition( t-step ) );
       ci::gl::vertex( spline.getPosition( t ) );
      } 
      glEnd();
     }
     ci::gl::color(ci::Color(0.0f,0.0f,1.0f));
     std::vector<Particle*>::iterator it;
     for(it = particles.begin(); it != particles.end(); ++it ){
      (*it)->draw();
     }
    }
    
  6. 为你的主 Cinder 应用程序类文件添加头文件:

    #include "cinder/app/AppBasic.h"
    #include "cinder/gl/Texture.h"
    #include "cinder/Rand.h"
    #include "cinder/Surface.h"
    #include "cinder/MayaCamUI.h"
    #include "cinder/BSpline.h"
    
    #include "ParticleSystem.h"
    
  7. 为你的 main 类添加成员:

    ParticleSystem mParticleSystem;
    
    float repulsionFactor;
    float maxAlignSpeed;
    
    CameraPersp        mCam;
    MayaCamUI mMayaCam;
    
    Vec3f mRepPosition;
    
    BSpline3f   spline;
    
  8. 按如下方式实现 setup 方法:

    void MainApp::setup()
    {
    repulsionFactor = -1.0f;
    maxAlignSpeed = 10.f;
    mRepPosition = Vec3f::zero();
    
    mCam.setPerspective(45.0f, getWindowAspectRatio(), 0.1, 10000);
    mCam.setEyePoint(Vec3f(7.f,7.f,7.f));
    mCam.setCenterOfInterestPoint(Vec3f::zero());
    mMayaCam.setCurrentCam(mCam);
    vector<Vec3f> splinePoints;
    float step = 0.5f;
    float width = 20.f;
    for (float t = 0.f; t < width; t += step) {
     float mass = Rand::randFloat(20.f, 25.f);
     float drag = 0.95f;
     splinePoints.push_back( Vec3f(math<float>::cos(t),
     math<float>::sin(t),
     t - width*0.5f) );
     Particle *particle;
     particle = new Particle( 
      Vec3f( math<float>::cos(t)+Rand::randFloat(-0.8f,0.8f),
       math<float>::sin(t)+Rand::randFloat(-0.8f,0.8f),
       t - width*0.5f), 
      1.f, mass, drag );
     mParticleSystem.addParticle( particle );
    }
    spline = BSpline3f( splinePoints, 3, false, false );
    }
    
  9. 为相机导航添加成员:

    void MainApp::resize( ResizeEvent event ){
      mCam = mMayaCam.getCamera();
      mCam.setAspectRatio(getWindowAspectRatio());
      mMayaCam.setCurrentCam(mCam);
    }
    
    void MainApp::mouseDown(MouseEvent event){
      mMayaCam.mouseDown( event.getPos() );
    }
    
    void MainApp::mouseDrag( MouseEvent event ){
      mMayaCam.mouseDrag( event.getPos(), event.isLeftDown(), event.isMiddleDown(), event.isRightDown() );
    }
    
  10. 按如下方式实现 update 方法:

    void MainApp::update() {
     float pos=math<float>::abs(sin(getElapsedSeconds()*0.5f));
     mRepPosition = spline.getPosition( pos );
     std::vector<Particle*>::iterator it;
     it = mParticleSystem.particles.begin();
     for(; it != mParticleSystem.particles.end(); ++it ) {
      Vec3f repulsionForce = (*it)->position - mRepPosition;
      repulsionForce = repulsionForce.normalized() *
       math<float>::max(0.f, 3.f-repulsionForce.length());
      (*it)->forces += repulsionForce;
      Vec3f alignForce = (*it)->anchor - (*it)->position;
      alignForce.limit(maxAlignSpeed);
      (*it)->forces += alignForce;
     }
     mParticleSystem.update();
    }
    
  11. 按如下方式实现 draw 方法:

    void MainApp::draw() {
     gl::enableDepthRead();
     gl::enableDepthWrite();
     gl::clear( Color::white() );
     gl::setViewport(getWindowBounds());
     gl::setMatrices(mMayaCam.getCamera());
     gl::color(Color(1.f,0.f,0.f));
     gl::drawSphere(mRepPosition, 0.25f);
     mParticleSystem.draw();
    }
    

如何工作…

B 样条使我们能够通过一些给定的点绘制一条非常平滑的曲线,在我们的情况下,是粒子位置。我们仍然可以应用一些吸引和排斥力,使得这条线的行为相当像弹簧。在 Cinder 中,你可以在 2D 和 3D 空间中使用 B 样条,并使用 BSpline 类来计算它们。

工作原理…

参考信息

关于 B 样条的更多详细信息可在 [zh.wikipedia.org/wiki/B 样条](http://zh.wikipedia.org/wiki/B 样条) 找到。

第七章。使用 2D 图形

在本章中,我们将学习如何使用 2D 图形和内置 Cinder 工具进行工作和绘图。

本章中的食谱将涵盖以下内容:

  • 绘制 2D 几何原语

  • 使用鼠标绘制任意形状

  • 实现涂鸦算法

  • 实现 2D 元球

  • 在曲线上动画文本

  • 添加模糊效果

  • 实现力导向图

绘制 2D 几何原语

在本食谱中,我们将学习如何绘制以下 2D 几何形状,作为填充和描边形状:

  • 圆形

  • 椭圆

  • 直线

  • 矩形

准备工作

包含必要的头文件以使用 Cinder 命令在 OpenGL 中进行绘制。

在你的源文件顶部添加以下代码行:

#include "cinder/gl/gl.h"

如何做到这一点…

我们将使用 Cinder 的 2D 绘图方法创建几个几何原语。执行以下步骤来完成:

  1. 让我们先声明成员变量以保存我们将要绘制的形状的信息。

    创建两个 ci::Vec2f 对象来存储线的起点和终点,一个 ci::Rectf 对象来绘制矩形,一个 ci::Vec2f 对象来定义圆的中心,以及一个 float 对象来定义其半径。最后,我们将创建 aci::Vec2f 来定义椭圆的半径,以及两个 float 对象来定义其宽度和高度。

    让我们再声明两个 ci::Color 对象来定义描边和填充颜色。

    Vec2f mLineBegin,mLineEnd;
    Rect fmRect;
    Vec2f mCircleCenter;
    float mCircleRadius;
    Vec2f mEllipseCenter;
    float mElipseWidth, mEllipseHeight;
    Color mFillColor, mStrokeColor;
    
  2. setup 方法中,让我们初始化前面的成员:

    mLineBegin = Vec2f( 10, 10 );
    mLineEnd = Vec2f( 400, 400 );
    
    mCircleCenter = Vec2f( 500, 200 );
    mCircleRadius = 100.0f;
    
    mEllipseCenter = Vec2f( 200, 300 );
    mEllipseWidth = 200.0f;
    ellipseHeight = 100.0f;
    
    mRect = Rectf( Vec2f( 40, 20 ), Vec2f( 300, 100 ) );
    
    mFillColor = Color( 1.0f, 1.0f, 1.0f );
    mStrokeColor = Color( 1.0f, 0.0f, 0.0f );
    
  3. draw 方法中,让我们首先绘制填充形状。

    让我们清除背景并将 mFillColor 设置为绘图颜色。

    gl::clear( Color( 0, 0, 0 ) );
    gl::color( mFillColor );
    
  4. 通过调用 ci::gl::drawSolidRectci::gl::drawSolidCircleci::gl::drawSolidEllipse 方法绘制填充形状。

    draw 方法中添加以下代码片段:

    gl::drawSolidRect( mRect );
    gl::drawSolidCircle( mCircleCenter, mCircleRadius );
    gl::drawSolidEllipse( mEllipseCenter, mEllipseWidth, ellipseHeight );
    
  5. 要将我们的形状作为描边图形绘制,让我们首先将 mStrokeColor 设置为绘图颜色。

    gl::color( mStrokeColor );
    
  6. 让我们再次绘制我们的形状,这次只使用描边,通过调用 ci::gl::drawLineci::gl::drawStrokeRectci::gl::drawStrokeCircleci::gl::drawStrokedEllipse 方法。

    draw 方法中添加以下代码片段:

    gl::drawLine( mLineBegin, mLineEnd );
    gl::drawStrokedRect( mRect );
    gl::drawStrokedCircle( mCircleCenter, mCircleRadius );
    gl::drawStrokedEllipse( mEllipseCenter, mEllipseWidth, ellipseHeight );
    

    这将产生以下结果:

    如何做到这一点…

它是如何工作的…

Cinder 的绘图方法使用 OpenGL 调用来提供快速且易于使用的绘图例程。

ci::gl::color 方法设置绘图颜色,以便所有形状都将使用该颜色绘制,直到再次调用 ci::gl::color 设置另一个颜色。

还有更多…

你也可以通过调用 glLineWidth 方法并传递一个 float 类型的参数来设置描边宽度。

例如,要将描边设置为 5 像素宽,你应该编写以下代码:

glLineWidth( 5.0f );

使用鼠标绘制任意形状

在本食谱中,我们将学习如何使用鼠标绘制任意形状。

每当用户按下鼠标按钮时,我们将开始一个新的轮廓,并在用户拖动鼠标时进行绘制。

该形状将使用填充和描边来绘制。

准备工作

包含必要的文件以使用 Cinder 命令绘制并创建一个 ci::Shape2d 对象。

在源文件的顶部添加以下代码片段:

#include "cinder/gl/gl.h"
#include "cinder/shape2d.h"

如何操作……

我们将创建一个 ci::Shape2d 对象,并使用鼠标坐标创建顶点。执行以下步骤来完成此操作:

  1. 声明一个 ci::Shape2d 对象来定义我们的形状,以及两个 ci::Color 对象来定义填充和描边颜色。

    Shape2d mShape;
    Color fillColor, strokeColor;
    
  2. setup 方法中初始化颜色。

    我们将使用黑色进行描边,黄色进行填充。

    mFillColor = Color( 1.0f, 1.0f, 0.0f );
    mStrokeColor = Color( 0.0f, 0.0f, 0.0f );
    
  3. 由于绘制将使用鼠标完成,因此需要使用 mouseDownmouseDrag 事件。

    声明必要的回调方法。

    void mouseDown( MouseEvent event );
    void mouseDrag( MouseEvent event );
    
  4. mouseDown 的实现中,我们将通过调用 moveTo 方法创建一个新的轮廓。

    以下代码片段显示了方法应该的样子:

    void MyApp::mouseDown( MouseEvent event ){
      mShape.moveTo( event.getpos() );
    }
    
  5. mouseDrag 方法中,我们将通过调用 lineTo 方法向我们的形状中添加一条线。

    其实现应该像以下代码片段所示:

    void MyApp::mouseDrag( MouseEvent event ){
      mShape.lineTo( event.getPos() );  
    }
    
  6. draw 方法中,我们首先需要清除背景,然后将 mFillColor 设置为绘图颜色,并绘制 mShape

    gl::clear( Color::white() );
    gl::color( mFillColor );
    gl::drawSolid( mShape );
    
  7. 剩下的只是将 mStrokeColor 设置为绘图颜色,并将 mShape 作为描边形状绘制。

    gl::color( mStrokeColor );
    gl::draw( mShape );
    
  8. 构建并运行应用程序。按下鼠标按钮开始绘制新的轮廓,并拖动以绘制。如何操作……

工作原理……

ci:Shape2d 是一个定义二维任意形状的类,允许有多个轮廓。

ci::Shape2d::moveTo 方法创建一个以参数传递的坐标为起点的新的轮廓。然后,ci::Shape2d::lineTo 方法从最后位置创建一条直线到作为参数传递的坐标。

绘制实心图形时,形状在内部被划分为三角形。

更多……

在使用 ci::Shape2d 构建形状时,也可以添加曲线。

方法 说明
quadTo (constVec2f& p1, constVec2f& p2) 使用 p1 作为控制点,从最后位置添加一个二次曲线到 p2
curveTo (constVec2f& p1, constVec2f& p2, constVec2f& p3) 使用 p1p2 作为控制点,从最后位置添加一个曲线到 p3
arcTo (constVec2f& p, constVec2f& t, float radius) 使用 t 作为切点,半径作为弧的半径,从最后位置添加一个弧到 p1

实现涂鸦算法

在这个菜谱中,我们将实现一个涂鸦算法,使用 Cinder 实现起来非常简单,但在绘制时会产生有趣的效果。您可以在 www.zefrank.com/scribbler/about.html 上了解更多关于连接相邻点的概念。您可以在 www.zefrank.com/scribbler/mrdoob.com/projects/harmony/ 上找到一个涂鸦的例子。

如何操作……

我们将实现一个展示涂鸦的应用程序。执行以下步骤来完成此操作:

  1. 包含必要的头文件:

    #include<vector>
    
  2. 向您的应用程序主类添加属性:

    vector <Vec2f> mPath;
    float mMaxDist;
    ColorA mColor;
    bool mDrawPath;
    
  3. 实现以下setup方法:

    void MainApp::setup()
    {
      mDrawPath = false;
      mMaxDist = 50.f;
      mColor = ColorA(0.3f,0.3f,0.3f, 0.05f);
      setWindowSize(800, 600);
    
      gl::enableAlphaBlending();
      gl::clear( Color::white() );
    }
    
  4. 由于绘图将使用鼠标完成,因此需要使用mouseDownmouseUp事件。实现以下方法:

    void MainApp::mouseDown( MouseEvent event )
    {
      mDrawPath = true;
    }
    
    void MainApp::mouseUp( MouseEvent event )
    {
      mDrawPath = false;
    }
    
  5. 最后,绘制方法的实现如下代码片段所示:

    void MainApp::draw(){
      if( mDrawPath ) {
      drawPoint( getMousePos() );
        }
    }
    
    void MainApp::drawPoint(Vec2f point) {
      mPath.push_back( point );
    
      gl::color(mColor);
      vector<Vec2f>::iterator it;
      for(it = mPath.begin(); it != mPath.end(); ++it) {
      if( (*it).distance(point) <mMaxDist ) {
      gl::drawLine(point, (*it));
            }
        }
    }
    

它是如何工作的…

当左鼠标按钮按下时,我们在容器中添加一个新的点,并绘制连接它和其他附近点的线条。我们正在寻找的新添加的点与其邻域中的点之间的距离必须小于mMaxDist属性的值。请注意,我们只在程序启动时,在setup方法的末尾清除一次绘图区域,因此我们不需要为每一帧重新绘制所有连接,这将非常慢。

它是如何工作的…

实现二维元球

在这个配方中,我们将学习如何实现称为元球的有机外观对象。

准备工作

在这个配方中,我们将使用来自第五章中“应用排斥和吸引力的配方”的代码库,即构建粒子系统中的构建粒子系统

如何做…

我们将使用着色器程序实现元球的渲染。执行以下步骤来完成此操作:

  1. assets文件夹中创建一个名为passThru_vert.glsl的文件,并将以下代码片段放入其中:

    void main()
    {
      gl_Position = ftransform();
      gl_TexCoord[0] = gl_MultiTexCoord0;
      gl_FrontColor = gl_Color; 
    }
    
  2. assets文件夹中创建一个名为mb_frag.glsl的文件,并将以下代码片段放入其中:

    #version 120
    
    uniform vec2 size;
    uniform int num;
    uniform vec2 positions[100];
    uniform float radius[100];
    
    void main(void)
    {
    
      // Get coordinates
      vec 2 texCoord = gl_TexCoord[0].st;
    
      vec4 color = vec4(1.0,1.0,1.0, 0.0);
      float a = 0.0;
    
      int i;  
      for(i = 0; i<num; i++) {
        color.a += (radius[i] / sqrt( ((texCoord.x*size.x)-
        positions[i].x)*((texCoord.x*size.x)-positions[i].x) + 
        ((texCoord.y*size.y)-
        positions[i].y)*((texCoord.y*size.y)-positions[i].y) ) 
        ); 
        }
    
      // Set color
      gl_FragColor = color;
    }
    
  3. 添加必要的头文件。

    #include "cinder/Utilities.h"
    #include "cinder/gl/GlslProg.h"
    
  4. 向应用程序的主类添加一个属性,即我们的 GLSL 着色器程序的GlslProg对象。

    gl::GlslProg  mMetaballsShader;
    
  5. setup方法中,更改repulsionFactornumParticle的值。

    repulsionFactor = -40.f;
    int numParticle = 10;
    
  6. setup方法的末尾,加载我们的 GLSL 着色器程序,如下所示:

    mMetaballsShader = gl::GlslProg( loadAsset("passThru_vert.glsl"), loadAsset("mb_frag.glsl") );
    
  7. 最后的主要更改是在draw方法中,如下代码片段所示:

    void MainApp::draw()
    {
      gl::enableAlphaBlending();
      gl::clear( Color::black() );
    
      int particleNum = mParticleSystem.particles.size();
    
      mMetaballsShader.bind();
      mMetaballsShader.uniform( "size", Vec2f(getWindowSize()) );
      mMetaballsShader.uniform( "num", particleNum );
    
      for (int i = 0; i<particleNum; i++) {
      mMetaballsShader.uniform( "positions[" + toString(i) + 
      "]", mParticleSystem.particles[i]->position );
      mMetaballsShader.uniform( "radius[" + toString(i) + 
        "]", mParticleSystem.particles[i]->radius );
      }
    
      gl::color(Color::white());
      gl::drawSolidRect( getWindowBounds() );
      mMetaballsShader.unbind();
    }
    

它是如何工作的…

本配方最重要的部分是步骤 2 中提到的片段着色器程序。着色器根据从我们的粒子系统传递给着色器的位置和半径生成基于渲染元球的纹理。在步骤 7 中,您可以了解如何将信息传递给着色器程序。我们使用setMatricesWindowsetViewport来设置 OpenGL 进行绘图。

它是如何工作的…

参见

在曲线上动画化文本

在这个配方中,我们将学习如何围绕用户定义的曲线动画化文本。

我们将创建LetterWord类来管理动画,一个ci::Path2d对象来定义曲线,以及一个ci::Timer对象来定义动画的持续时间。

准备工作

创建并添加以下文件到您的项目中:

  • Word.h

  • Word.cpp

  • Letter.h

  • Letter.cpp

如何做…

我们将创建一个单词并沿 ci::Path2d 对象动画化其字母。执行以下步骤来完成:

  1. Letter.h 文件中,包含必要的 textci::Vec2fci::gl::Texture 文件。

    还需添加 #pragma once

    #pragma once
    
    #include "cinder/vector.h"
    #include "cinder/text.h"
    #include "cinder/gl/Texture.h"
    
  2. 声明具有以下成员和方法的 Letter 类:

    class Letter{
    public:
        Letter( ci::Font font, conststd::string& letter );
    
        void draw();
        void setPos( const ci::Vec2f& newPos );
    
        ci::Vec2f pos;
        float rotation;
        ci::gl::Texture texture;
        float width;
    };
    
  3. 移动到 Letter.cpp 文件以实现类。

    在构造函数中,创建一个 ci::TextBox 对象,设置其参数,并将其渲染到纹理上。同时,将宽度设置为纹理宽度加上 10 的填充值:

    Letter::Letter( ci::Font font, conststd::string& letter ){
        ci::TextBoxtextBox;  
        textBox = ci::TextBox().font( font ).size( ci::Vec2i( ci::TextBox::GROW, ci::TextBox::GROW ) ).text( letter ).premultiplied();
        texture = textBox.render();
        width = texture.getWidth() + 10.0f;
    }
    
  4. draw 方法中,我们将绘制纹理并使用 OpenGL 变换将纹理移动到其位置,并根据旋转进行旋转:

    void Letter::draw(){
        glPushMatrix();
        glTranslatef( pos.x, pos.y, 0.0f );
        glRotatef( ci::toDegrees( rotation ), 0.0f, 0.0f, 1.0f );
        glTranslatef( 0.0f, -texture.getHeight(), 0.0f );
        ci::gl::draw( texture );
        glPopMatrix();
    }
    
  5. setPos 方法的实现中,我们将更新位置并计算其旋转,使字母垂直于其移动。我们通过计算其速度的反正切来实现这一点:

    void Letter::setPos( const ci::Vec2f&newPos ){
        ci::Vec2f vel = newPos - pos;
        rotation = atan2( vel.y, vel.x );
        pos = newPos;
    }
    
  6. Letter 类已准备就绪!现在移动到 Word.h 文件,添加 #pragma once 宏,并包含 Letter.h 文件:

    #pragma once
    #include "Letter.h"
    
  7. 声明具有以下成员和方法的 Word 类:

    class Word{
    public:
        Word( ci::Font font, conststd::string& text );
    
        ~Word();
    
        void update( const ci::Path2d& curve, float curveLength, float  progress );
        void draw();
    
          std::vector< Letter* > letters;
          float length;
    };
    
  8. 移动到 Word.cpp 文件并包含 Word.h 文件:

    #include "Word.h"
    
  9. 在构造函数中,我们将遍历 text 中的每个字符并添加一个新的 Letter 对象。我们还将通过计算所有字母宽度的总和来计算文本的总长度:

    Word::Word( ci::Font font, conststd::string& text ){
      length = 0.0f;
      for( int i=0; i<text.size(); i++ ){
      std::string letterText( 1, text[i] );
              Letter *letter = new Letter( font, letterText );
      letters.push_back( letter );
      length += letter->width;
        }
    }
    

    在析构函数中,我们将删除所有 Letter 对象以清理类使用的内存:

    Word::~Word(){
      for( std::vector<Letter*>::iterator it = letters.begin(); it != letters.end(); ++it ){
      delete *it;
        }
    }
    
  10. update 方法中,我们将传递 ci::Path2d 对象的引用、路径的总长度以及动画进度的归一化值(从 0.0 到 1.0)。

    我们将计算每个单独字母沿曲线的位置,考虑到 Word 的长度和当前进度:

    void Word::update( const ci::Path2d& curve, float curveLength,   float progress ){
      float maxProgress = 1.0f - ( length / curveLength );
      float currentProgress = progress * maxProgress;
      float progressOffset = 0.0f;
      for( int i=0; i<letters.size(); i++ ){
            ci::Vec2f pos = curve.getPosition
            ( currentProgress + progressOffset );
            letters[i]->setPos( pos );
            progressOffset += ( letters[i]->width / curveLength );
        }
    }
    
  11. draw 方法中,我们将遍历所有字母并调用每个字母的 draw 方法:

    void Word::draw(){
      for( std::vector< Letter* >::iterator it = letters.begin(); it != letters.end(); ++it ){
            (*it)->draw();
        }
    }
    
  12. 随着 WordLetter 类的准备就绪,现在是时候移动到我们的应用程序的类源文件了。首先,包含必要的源文件并添加有用的 using 语句:

    #include "cinder/Timer.h"
    #include "Word.h"
    
    using namespace ci;
    using namespace ci::app;
    using namespace std;
    
  13. 声明以下成员:

    Word * mWord;
    Path2d mCurve;
    float mPathLength;
    Timer mTimer;
    double mSeconds;
    
  14. setup 方法中,我们将首先创建 std::stringci::Font,并使用它们来初始化 mWord。我们还将使用我们希望动画持续的时间来初始化 mSeconds

    string text = "Some Text";
    Font font = Font( "Arial", 46 );
    mWord = new Word( font, text );
    mSeconds = 5.0;
    
  15. 我们现在需要通过创建关键点和通过调用 curveTo 连接它们来创建曲线:

    Vec2f curveBegin( 0.0f, getWindowCenter().y );
    Vec2f curveCenter = getWindowCenter();
    Vec2f curveEnd( getWindowWidth(), getWindowCenter().y );
    
    mCurve.moveTo( curveBegin );
    mCurve.curveTo( Vec2f( curveBegin.x, curveBegin.y + 200.0f ), Vec2f( curveCenter.x, curveCenter.y + 200.0f ), curveCenter );
    mCurve.curveTo( Vec2f( curveCenter.x, curveCenter.y - 200.0f ), Vec2f( curveEnd.x, curveEnd.y - 200.0f ), curveEnd );
    
  16. 让我们通过计算每个点与其相邻点之间的距离之和来计算路径的长度。在 setup 方法中添加以下代码片段:

    mPathLength = 0.0f;
    for( int i=0; i<mCurve.getNumPoints()-1; i++ ){
      mPathLength += mCurve.getPoint( i ).distance( mCurve.getPoint( i+1 ) );
        }
    
  17. 我们需要检查 mTimer 是否正在运行,并通过计算已过秒数与 mSeconds 之间的比率来计算进度。在 update 方法中添加以下代码片段:

    if( mTimer.isStopped() == false ){
      float progress;
      if( mTimer.getSeconds() >mSeconds ){
        mTimer.stop();
        progress = 1.0f;
            } else {
      progress = (float)( mTimer.getSeconds() / mSeconds );
            }
    mWord->update( mCurve, mPathLength, progress );
        }
    
  18. draw 方法中,我们需要清除背景,启用 alpha 混合,绘制 mWord,并绘制路径:

    gl::clear( Color( 0, 0, 0 ) ); 
    gl::enableAlphaBlending();
    mWord->draw(); 
    gl::draw( mCurve );
    
  19. 最后,每当用户按下任何键时,我们需要启动计时器。

    声明 keyUp 事件处理程序:

    void keyUp( KeyEvent event );
    
  20. 以下是对keyUp事件处理器的实现:

    void CurveTextApp::keyUp( KeyEvent event ){
    mTimer.start();
    }
    
  21. 构建并运行应用程序。按任意键开始动画。如何做到这一点…

添加模糊效果

在这个菜谱中,我们将学习如何在绘制纹理时应用模糊效果。

准备工作

在这个菜谱中,我们将使用 Geeks3D 提供的 Gaussian blur 着色器,请访问这里

如何做到这一点…

我们将实现一个 Cinder 应用程序示例,以说明该机制。执行以下步骤:

  1. assets文件夹中创建一个名为passThru_vert.glsl的文件,并将以下代码片段放入其中:

    void main()
    {
      gl_Position = ftransform();
      gl_TexCoord[0] = gl_MultiTexCoord0;
      gl_FrontColor = gl_Color; 
    }
    
  2. assets文件夹中创建一个名为gaussian_v_frag.glsland的文件,并将以下代码片段放入其中:

    #version 120
    
    uniform sampler2D sceneTex; // 0
    
    uniform float rt_w; // render target width
    uniform float rt_h; // render target height
    uniform float vx_offset;
    
    float offset[3] = float[]( 0.0, 1.3846153846, 3.2307692308 );
    float weight[3] = float[]( 0.2270270270, 0.3162162162, 0.0702702703 );
    
    void main() 
    { 
      vec3 tc = vec3(1.0, 0.0, 0.0);
      if (gl_TexCoord[0].x<(vx_offset-0.01)){
    vec2 uv = gl_TexCoord[0].xy;
    tc = texture2D(sceneTex, uv).rgb * weight[0];
    for (int i=1; i<3; i++) {
    tc += texture2D(sceneTex, uv + vec2(0.0, offset[i])/rt_h).rgb * weight[i];
      tc += texture2D(sceneTex, uv - vec2(0.0, offset[i])/rt_h).rgb * weight[i];
        }
      }
    else if (gl_TexCoord[0].x>=(vx_offset+0.01)){
      tc = texture2D(sceneTex, gl_TexCoord[0].xy).rgb;
      }
    gl_FragColor = vec4(tc, 1.0);
    }
    

    assets文件夹中创建一个名为gaussian_h_frag.glsl的文件,并将以下代码片段放入其中:

    #version 120
    
    uniform sampler2D sceneTex; // 0
    
    uniform float rt_w; // render target width
    uniform float rt_h; // render target height
    uniform float vx_offset;
    
    float offset[3] = float[]( 0.0, 1.3846153846, 3.2307692308 );
    float weight[3] = float[]( 0.2270270270, 0.3162162162, 0.0702702703 );
    
    void main() 
    { 
    vec3 tc = vec3(1.0, 0.0, 0.0);
    if (gl_TexCoord[0].x<(vx_offset-0.01)){
    vec2 uv = gl_TexCoord[0].xy;
    tc = texture2D(sceneTex, uv).rgb * weight[0];
    for (int i=1; i<3; i++) 
        {
        tc += texture2D(sceneTex, uv + vec2(offset[i])/rt_w, 0.0).rgb * weight[i];
        tc += texture2D(sceneTex, uv - vec2(offset[i])/rt_w, 0.0).rgb * weight[i];
        }
      }
    else if (gl_TexCoord[0].x>=(vx_offset+0.01))
      {
      tc = texture2D(sceneTex, gl_TexCoord[0].xy).rgb;
      }
    gl_FragColor = vec4(tc, 1.0);
    }
    
  3. 添加必要的头文件:

    #include "cinder/Utilities.h"
    #include "cinder/gl/GlslProg.h"
    #include "cinder/gl/Texture.h"
    #include "cinder/ImageIo.h"
    #include "cinder/gl/Fbo.h"
    
  4. 将属性添加到应用程序的主类中:

    gl::GlslProg  mGaussianVShader, mGaussianHShader;
    gl::Texture mImage, mImageBlur;
    gl::Fbo    mFboBlur1, mFboBlur2;
    float           offset, level;
    params::InterfaceGl mParams;
    
  5. 实现以下setup方法:

    void MainApp::setup(){
      setWindowSize(512, 512);
    
      level = 0.5f;
      offset = 0.6f;
    
      mGaussianVShader = gl::GlslProg( loadAsset("passThru_vert.glsl"), loadAsset("gaussian_v_frag.glsl") );
      mGaussianHShader = gl::GlslProg( loadAsset("passThru_vert.glsl"), loadAsset("gaussian_h_frag.glsl") );
      mImage = gl::Texture(loadImage(loadAsset("image.png")));
    
      mFboBlur1 = gl::Fbo
      (mImage.getWidth(), mImage.getHeight());
      mFboBlur2 = gl::Fbo
      (mImage.getWidth(), mImage.getHeight());
    
    // Setup the parameters
      mParams = params::InterfaceGl
      ( "Parameters", Vec2i( 200, 100 ) );
      mParams.addParam
      ( "level", &level, "min=0 max=1 step=0.01" );
      mParams.addParam
      ( "offset", &offset, "min=0 max=1 step=0.01");
    }
    
  6. draw方法的开头计算模糊强度:

    float rt_w = mImage.getWidth()*3.f-mImage.getWidth()*2.f*level;
    float rt_h = mImage.getHeight()*3.f-mImage.getHeight()*2.f*level;
    
  7. draw函数中,使用第一步着色器将图像渲染到mFboBlur1

    mFboBlur1.bindFramebuffer();
    gl::setViewport( mFboBlur1.getBounds() );
    mImage.bind(0);
    mGaussianVShader.bind();
    mGaussianVShader.uniform("sceneTex", 0);
    mGaussianVShader.uniform("rt_w", rt_w);
    mGaussianVShader.uniform("rt_h", rt_h);
    mGaussianVShader.uniform("vx_offset", offset);
    gl::drawSolidRect(mFboBlur1.getBounds());
    mGaussianVShader.unbind();
    mFboBlur1.unbindFramebuffer();
    
  8. draw函数中,使用第二步着色器渲染mFboBlur1中的纹理:

    mFboBlur2.bindFramebuffer();
    mFboBlur1.bindTexture(0);
    mGaussianHShader.bind();
    mGaussianHShader.uniform("sceneTex", 0);
    mGaussianHShader.uniform("rt_w", rt_w);
    mGaussianHShader.uniform("rt_h", rt_h);
    mGaussianHShader.uniform("vx_offset", offset);
    gl::drawSolidRect(mFboBlur2.getBounds());
    mGaussianHShader.unbind();
    mFboBlur2.unbindFramebuffer();
    
  9. mImageBlur设置为从mFboBlur2的结果纹理:

    mImageBlur = mFboBlur2.getTexture();
    
  10. draw方法的末尾,绘制带有结果的纹理和 GUI:

    gl::clear( Color::black() );
    gl::setMatricesWindow(getWindowSize());
    gl::setViewport(getWindowBounds());
    gl::draw(mImageBlur);
    params::InterfaceGl::draw();
    

工作原理…

由于高斯模糊着色器需要应用两次——垂直和水平处理——我们必须使用帧缓冲对象FBO),这是一种在图形卡内存中绘制到纹理的机制。在第 8 步中,我们从mImage对象中绘制原始图像,并应用存储在gaussian_v_frag.glsl文件中的着色程序,该文件已加载到mGaussianVShaderobject中。此时,所有内容都绘制到mFboBlur1中。下一步是使用mFboBlur2中的纹理,并在第 9 步中应用第二个遍历的着色器。最终处理后的纹理存储在第 10 步的mImageBlur中。在第 7 步中,我们计算模糊强度。

工作原理…

实现一个力导向图

力导向图是一种使用简单的物理,如排斥和弹簧,来绘制美观图形的方法。我们将使我们的图形交互式,以便用户可以拖动节点并看到图形如何重新组织自己。

准备工作

在本食谱中,我们将使用第五章中Building Particle Systems食谱的代码库,即在 2D 中创建粒子系统。有关如何绘制节点及其之间连接的详细信息,请参阅第六章中的Connecting particles食谱,Rendering and Texturing Particle Systems

如何做到这一点…

我们将创建一个交互式力导向图。执行以下步骤:

  1. 向你的主应用程序类添加属性。

    vector< pair<Particle*, Particle*> > mLinks;
    float mLinkLength;
    Particle*   mHandle;
    bool mIsHandle;
    
  2. setup方法中设置默认值并创建一个图。

    void MainApp::setup(){
      mLinkLength = 40.f;
      mIsHandle   = false;
    
      float drag = 0.95f;
    
      Particle *particle = newParticle(getWindowCenter(), 10.f, 10.f, drag );
      mParticleSystem.addParticle( particle );
    
      Vec2f r = Vec2f::one()*mLinkLength;
      for (int i = 1; i<= 3; i++) {
        r.rotate( M_PI * (i/3.f) );
        Particle *particle1 = newParticle( particle->position+r, 7.f,7.f, drag );
        mParticleSystem.addParticle( particle1 );
        mLinks.push_back(make_pair(mParticleSystem.particles[0], particle1));
    
        Vec2f r2 = (particle1->position-particle->position);
        r2.normalize();
        r2 *= mLinkLength;
        for (int ii = 1; ii <= 3; ii++) {
          r2.rotate( M_PI * (ii/3.f) );
          Particle *particle2 = newParticle( particle1->position+r2, 5.f, 5.f, drag );
          mParticleSystem.addParticle( particle2 );
          mLinks.push_back(make_pair(particle1, particle2));
    
          Vec2f r3 = (particle2->position-particle1->position);
          r3.normalize();
          r3 *= mLinkLength;
          for (int iii = 1; iii <= 3; iii++) {
    r3.rotate( M_PI * (iii/3.f) );
    Particle *particle3 = newParticle( particle2->position+r3, 3.f, 3.f, drag );
    mParticleSystem.addParticle( particle3 );
    mLinks.push_back(make_pair(particle2, particle3));
                }
            }
        }
    }
    
  3. 实现与鼠标的交互。

    void MainApp::mouseDown(MouseEvent event){
      mIsHandle = false;
    
      float maxDist = 20.f;
      float minDist = maxDist;
      for( std::vector<Particle*>::iterator it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ){
      float dist = (*it)->position.distance( event.getPos() );
      if(dist<maxDist&&dist<minDist) {
      mHandle = (*it);
      mIsHandle = true;
      minDist = dist;
            }
        }
    }
    
    void MainApp::mouseUp(MouseEvent event){
    mIsHandle = false;
    }
    
  4. update方法内部,计算影响粒子的所有力。

    void MainApp::update() {
      for( std::vector<Particle*>::iterator it1 = mParticleSystem.particles.begin(); it1 != mParticleSystem.particles.end(); ++it1 )
        {
        for( std::vector<Particle*>::iterator it2 = mParticleSystem.particles.begin(); it2 != mParticleSystem.particles.end(); ++it2 ){
          Vec2f conVec = (*it2)->position - (*it1)->position;
          if(conVec.length() <0.1f)continue;
    
            float distance = conVec.length();
            conVec.normalize();
            float force = (mLinkLength*2.0f - distance)* -0.1f;
            force = math<float>::min(0.f, force);
    
                (*it1)->forces +=  conVec * force*0.5f;
                (*it2)->forces += -conVec * force*0.5f;
            }
        }
    
    for( vector<pair<Particle*, Particle*> > ::iterator it = mLinks.begin(); it != mLinks.end(); ++it ){
      Vec2f conVec = it->second->position - it->first->position;
      float distance = conVec.length();
      float diff = (distance-mLinkLength)/distance;
      it->first->forces += conVec * 0.5f*diff;
      it->second->forces -= conVec * 0.5f*diff;
          }
    
      if(mIsHandle) {
        mHandle->position = getMousePos();
        mHandle->forces = Vec2f::zero();
        }
    
      mParticleSystem.update();
    }
    
  5. draw方法中实现绘制粒子和它们之间的链接。

    void MainApp::draw()
    {
      gl::enableAlphaBlending();
      gl::clear( Color::white() );
      gl::setViewport(getWindowBounds());
      gl::setMatricesWindow( getWindowWidth(), getWindowHeight() );
    
      gl::color( ColorA(0.f,0.f,0.f, 0.8f) );
      for( vector<pair<Particle*, Particle*> > ::iterator it = mLinks.begin(); it != mLinks.end(); ++it )
        {
        Vec2f conVec = it->second->position - it->first->position;
        conVec.normalize();
        gl::drawLine(it->first->position + conVec * ( it->first->radius+2.f ),
        it->second->position - conVec * ( it->second->radius+2.f ) );
        }
    
      gl::color( ci::ColorA(0.f,0.f,0.f, 0.8f) );
      mParticleSystem.draw();
    } 
    
  6. Particle.cpp源文件中,应实现每个粒子的绘制,如下所示:

    void Particle::draw(){
      ci::gl::drawSolidCircle( position, radius);
      ci::gl::drawStrokedCircle( position, radius+2.f);
    }
    

它是如何工作的…

在步骤 2 中,在setup方法中,我们为图的每个级别创建粒子,并在它们之间添加链接。在步骤 4 中的update方法中,我们计算影响所有粒子的力,这些力使每个粒子相互排斥,以及来自连接节点的弹簧的力。在排斥扩散粒子时,弹簧试图将它们保持在mLinkLength中定义的固定距离。

它是如何工作的…

参见

第八章。使用 3D 图形

在本章中,我们将学习如何使用和绘制 3D 图形。本章的配方将涵盖以下内容:

  • 绘制 3D 几何原语

  • 旋转、缩放和移动

  • 在离屏画布上绘图

  • 使用鼠标在 3D 中绘图

  • 添加灯光

  • 3D 选择

  • 从图像创建高度图

  • 使用 Perlin 噪声创建地形

  • 保存网格数据

简介

在本章中,我们将学习创建 3D 图形的基础知识。我们将使用 OpenGL 和 Cinder 包含的一些有用的包装器来处理一些高级 OpenGL 功能。

绘制 3D 几何原语

在这个配方中,我们将学习如何绘制以下 3D 几何形状:

  • 立方体

  • 球体

  • 线

  • 环面

  • 圆柱体

准备工作

包含必要的头文件以使用 Cinder 命令和语句在 OpenGL 中绘图。将以下代码添加到源文件顶部:

#include "cinder/gl/gl.h"
#include "cinder/Camera.h"

using namespace ci;

如何做…

我们将使用 Cinder 的 3D 绘图方法创建几个几何原语。

  1. 声明成员变量,包含我们的原语信息:

    Vec3f mCubePos, mCubeSize;
    Vec3f mSphereCenter;
    float mSphereRadius; 
    Vec3f mLineBegin, mLineEnd; 
    Vec3f mTorusPos;
    float mTorusOuterRadius, mTorusInnerRadius; 
    Vec3f mCylinderPos;
    float mCylinderBaseRadius, mCylinderTopRadius, mCylinderHeight;
    
  2. 使用位置和几何形状的大小初始化成员变量。在 setup 方法中添加以下代码:

    mCubePos = Vec3f( 100.0f, 300.0f, 100.0f );
    mCubeSize = Vec3f( 100.0f, 100.0f, 100.0f );
    
    mSphereCenter = Vec3f( 500, 250, 0.0f );
    mSphereRadius = 100.0f;
    
    mLineBegin = Vec3f( 200, 0, 200 );
    mLineEnd = Vec3f( 500, 500, -200 );
    
    mTorusPos = Vec3f( 300.0f, 100.0f, 0.0f );
    mTorusOuterRadius = 100.0f;
    mTorusInnerRadius = 20.0f;
    
    mCylinderPos = Vec3f( 500.0f, 0.0f, -200.0f );
    mCylinderBaseRadius = 50.0f;
    mCylinderTopRadius = 80.0f;
    mCylinderHeight = 100.0f;
    
  3. 在我们绘制形状之前,让我们也创建一个相机来围绕我们的形状旋转,以便给我们一个更好的透视感。声明一个 ci::CameraPersp 对象:

    CameraPerspmCamera;
    
  4. setup 方法中初始化它:

    mCamera = CameraPersp( getWindowWidth(), getWindowHeight(), 60.0f );
    
  5. update 方法中,我们将使相机围绕我们的场景旋转。在 update 方法中添加以下代码:

    Vec2f windowCenter = getWindowCenter();
    floatcameraAngle = getElapsedSeconds();
    floatcameraDist = 450.0f;
    float x = sinf( cameraAngle ) * cameraDist + windowCenter.x;
    float z = cosf( cameraAngle ) * cameraDist;
    mCamera.setEyePoint( Vec3f( x, windowCenter.y, z ) );
    mCamera.lookAt( Vec3f( windowCenter.x, windowCenter.y, 0.0f ) );
    
  6. draw 方法中,我们将使用黑色清除背景,并使用 mCamera 定义窗口的矩阵。我们还将启用 OpenGL 读取和写入深度缓冲区。在 draw 方法中添加以下代码:

      gl::clear( Color::black() ); 
      gl::setMatrices( mCamera );
      gl::enableDepthRead();
      gl::enableDepthWrite();
    
  7. Cinder 允许您绘制填充和描边的立方体,因此让我们绘制一个白色填充和黑色描边的立方体:

    gl::color( Color::white() );
    gl::drawCube( mCubePos, mCubeSize );
    gl::color( Color::black() );
    gl::drawStrokedCube( mCubePos, mCubeSize );
    
  8. 让我们再次将绘图颜色定义为白色,并使用 mSphereCentermSphereRadius 作为球体的位置和半径,以及段数为 30 来绘制一个球体。

    gl::color( Color::white() );
    gl::drawSphere( mSphereCenter, mSphereRadius, 30 );
    
  9. 绘制一条从 mLineBegin 开始到 mLineEnd 结束的线:

    gl::drawLine( mLineBegin, mLineEnd );
    
  10. Cinder 在原点 [0,0] 的坐标处绘制一个 Torus。因此,我们需要将其平移到 mTorusPos 处期望的位置。我们将使用 mTorusOuterRadiusmTorusInnerRadius 来定义形状的内径和外径:

    gl::pushMatrices();
    gl::translate( mTorusPos );
    gl::drawTorus( mTorusOutterRadius, mTorusInnerRadius );
    gl::popMatrices();
    
  11. 最后,Cinder 将在原点 [0,0] 处绘制一个圆柱体,因此我们需要将其平移到 mCylinderPosition 中定义的位置。我们还将使用 mCylinderBaseRadiusmCylinderTopRadius 来设置圆柱体的底部和顶部大小,以及 mCylinderHeight 来设置其高度:

    gl::pushMatrices();
    gl::translate( mCylinderPos );
    gl::drawCylinder( mCylinderBaseRadius, mCylinderTopRadius, mCylinderHeight );
    gl::popMatrices();
    

    如何做…

它是如何工作的…

Cinder 的绘图方法内部使用 OpenGL 调用来提供快速且易于使用的绘图例程。

方法 ci::gl::color 设置绘图颜色,使得所有形状都将使用该颜色进行绘制,直到再次调用 ci::gl::color 设置另一种颜色。

相关内容

要了解更多关于 OpenGL 变换(如平移、缩放和旋转)的信息,请阅读配方 旋转、缩放和平移

旋转、缩放和平移

在本配方中,我们将学习如何使用 OpenGL 变换来转换我们的图形。

我们将在 [0,0,0] 坐标处绘制一个单位立方体,然后将其平移到窗口中心,应用旋转,并将其缩放到更可见的大小。

准备工作

包含必要的文件以使用 OpenGL 绘图并添加有用的 using 语句。将以下代码添加到源文件顶部:

#include "cinder/gl/gl.h"
using namespace ci;
using namespace ci::app;
using namespace std;

如何操作…

我们将应用旋转、平移和缩放来改变我们的立方体渲染方式。我们将使用 Cinder 的 OpenGL 包装器。

  1. 让我们声明变量来存储平移、旋转和缩放变换的值:

        Vec3f mTranslation;
        Vec3f mScale;
        Vec3f mRotation;
    
  2. 为了定义平移量,让我们在 x 轴上平移窗口宽度的一半,在 y 轴上平移窗口高度的一半。这将使我们绘制的 [0,0,0] 上的任何内容移动到窗口中心。在 setup 方法中添加以下代码:

    mTranslation.x = getWindowWidth() / 2;
    mTranslation.y = getWindowHeight() / 2;
    mTranslation.z = 0.0f;
    
  3. 让我们将缩放因子设置为 x 轴上的 100,y 轴上的 200,z 轴上的 100。我们绘制的任何内容在 x 和 z 轴上将是原来的 100 倍,在 y 轴上是原来的 200 倍。在 setup 方法中添加以下代码:

    mScale.x = 100.0f;
    mScale.y = 200.0f;
    mScale.z = 100.0f;
    
  4. update 方法中,我们将通过增加 x 和 y 轴上的旋转来动画化旋转值。

    mRotation.x += 1.0f;
    mRotation.y += 1.0f;
    
  5. draw 方法中,我们首先用黑色清除背景,设置窗口矩阵以允许在 3D 中绘图,并启用 OpenGL 读取和写入深度缓冲区:

    gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatricesWindowPersp( getWindowWidth(), getWindowHeight() );
    gl::enableDepthRead();
    gl::enableDepthWrite();
    
  6. 让我们在堆栈中添加一个新的矩阵,并使用之前定义的变量进行平移、缩放和旋转:

    gl::pushMatrices();
    gl::translate( mTranslation );
    gl::scale( mScale );
    gl::rotate( mRotation );
    
  7. 在原点 [0,0,0] 处绘制一个单位四边形,填充为白色,轮廓为黑色:

    gl::color( Color::white() );
    gl::drawCube( Vec3f(), Vec3f( 1.0f, 1.0f, 1.0f ) );
    gl::color( Color::black() );
    gl::drawStrokedCube( Vec3f(), Vec3f( 1.0f, 1.0f, 1.0f ) );
    
  8. 最后,移除之前添加的矩阵:

    gl::popMatrices();
    

    如何操作…

它是如何工作的…

调用 ci::gl::enableDepthReadci::gl::enableDepthWrite 分别启用对深度缓冲区的读取和写入。深度缓冲区是存储深度信息的地方。

当启用读取和写入深度缓冲区时,OpenGL 将对对象进行排序,使较近的对象在较远对象之前绘制。当读取和写入深度缓冲区时,禁用的对象将按照它们创建的顺序绘制。

方法 ci::gl::translateci::gl::rotateci::gl::scale 是用于平移、旋转和缩放的 OpenGL 命令的包装器,允许您将 Cinder 类型作为参数传递。

在 OpenGL 中,通过将顶点坐标与变换矩阵相乘来应用变换。当我们调用 ci::gl::pushMatrices 方法时,我们将当前变换矩阵的副本添加到矩阵栈中。调用 ci::gl::translateci::gl::rotateci::gl::scale 将将相应的变换应用到栈中的最后一个矩阵,这将应用到调用变换方法之后创建的任何几何体。调用 ci::gl::popMatrix 将从栈中移除最后一个变换矩阵,这样添加到最后一个矩阵的变换将不再影响我们的几何体。

在离屏画布上绘制

在本教程中,我们将学习如何使用 OpenGL 帧缓冲对象FBO)在离屏画布上绘制。

我们将在 FBO 中绘制,并将其绘制到屏幕上,同时纹理化一个旋转的立方体。

准备工作

包含必要的文件以使用 OpenGL 和 FBO,以及有用的 include 指令。

将以下代码添加到源文件顶部:

#include "cinder/gl/gl.h"
#include "cinder/gl/Fbo.h"

using namespace ci;

如何操作…

我们将使用一个 ci::gl::Fbo 对象,它是 OpenGL FBO 的包装器,以在离屏目标上绘制。

  1. 声明一个 ci::gl::Fbo 对象以及一个 ci::Vec3f 对象来定义立方体的旋转:

    gl::FbomFbo;
    Vec3f mCubeRotation;
    
  2. setup 方法中添加以下代码以初始化 mFbo,大小为 256 x 256 像素:

    mFbo = gl::Fbo( 256, 256 );
    
  3. update 方法中动画化 mCubeRotation

    mCubeRotation.x += 1.0f;
    mCubeRotation.y += 1.0f;
    
  4. 声明一个我们将绘制到 FBO 的方法:

    void drawToFbo();
    
  5. drawToFbo 的实现中,我们首先创建一个 ci::gl::SaveFramebufferBinding 对象,然后绑定 mFbo

    gl::SaveFramebufferBinding fboBindingSave;
    mFbo.bindFramebuffer();
    
  6. 现在,我们将使用深灰色清除背景,并使用 FBO 的宽度和高度设置矩阵。

    gl::clear( Color( 0.3f, 0.3f, 0.3f ) );
    gl::setMatricesWindowPersp( mFbo.getWidth(), mFbo.getHeight() );
    
  7. 现在,我们将绘制一个大小为 100 的旋转彩色立方体,位于 FBO 的中心,并使用 mCubeRotation 旋转立方体。

    gl::pushMatrices();
    Vec3f cubeTranslate( mFbo.getWidth() / 2, mFbo.getHeight() / 2, 0.0f );
    gl::translate( cubeTranslate );
    gl::rotate( mCubeRotation );
    gl::drawColorCube( Vec3f(), Vec3f( 100, 100, 100 ) );
    gl::popMatrices();
    
  8. 让我们转向 draw 方法的实现。首先调用 drawToFbo 方法,用黑色清除背景,设置窗口的矩阵,并启用对深度缓冲区的读写。在 draw 方法中添加以下代码:

    drawToFbo();
    gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatricesWindowPersp( getWindowWidth(), getWindowHeight() );
    gl::enableDepthRead();
    gl::enableDepthWrite();
    

    让我们使用 mFbo 纹理在窗口的左上角绘制我们的 Fbo:

    gl::draw( mFbo.getTexture(), Rectf( 0.0f, 0.0f, 100.0f, 100.0f ) );
    
  9. 启用并绑定 mFbo 的纹理:

    mFbo.getTexture().enableAndBind();
    
  10. 使用 mCubeRotation 定义其旋转,在窗口中心绘制一个旋转的立方体:

    gl::pushMatrices();
    Vec3f center( getWindowWidth() / 2, getWindowHeight() / 2, 0.0f );
    gl::translate( center );
    gl::rotate( mCubeRotation );
    gl::drawCube( Vec3f(), Vec3f( 200.0f, 200.0f, 200.0f ) );
    gl::popMatrices();
    
  11. 为了完成,解绑 mFbo 的纹理:

    mFbo.unbindTexture();
    

    如何操作…

它是如何工作的…

ci::gl::Fbo 包装了一个 OpenGL FBO

帧缓冲对象是 OpenGL 对象,包含一组可以作为渲染目标的缓冲区。OpenGL 上下文提供了一个默认的帧缓冲区,渲染在其中发生。帧缓冲对象允许将渲染到替代的离屏位置。

FBO 有一个颜色纹理,图形存储在其中,它可以像常规 OpenGL 纹理一样绑定和绘制。

在第 5 步中,我们创建了一个 ci::gl::SaveFramebufferBinding 对象,这是一个辅助类,用于恢复之前的 FBO 状态。当使用 OpenGL ES 时,此对象将在销毁时恢复并绑定之前绑定的 FBO(通常是 screen FBO)。

参见

查看关于 OpenGL 变换的更多信息的配方 旋转、缩放和移动

使用鼠标在 3D 中绘制

在这个配方中,我们将使用鼠标在 3D 空间中绘制。当拖动鼠标时,我们将绘制线条;当同时拖动并按下 Shift 键时,我们将旋转 3D 场景。

准备工作

包含绘制所需的必要文件,以及使用 Cinder 的透视、Maya 相机和多边形的文件。

#include "cinder/gl/gl.h"
#include "cinder/Camera.h"
#include "cinder/MayaCamUI.h"
#include "cinder/PolyLine.h"

还要添加以下 using 语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何操作…

我们将使用 ci::CameraPerspci::Ray 类将鼠标坐标转换为我们的旋转 3D 场景。

  1. 声明一个 ci::MayaCamUI 对象和一个 ci::PolyLine<ci::Vec3f>std::vector 对象来存储绘制的线条:

    MayaCamUI mCamera;
    vector<PolyLine<Vec3f> > mLines;
    
  2. setup 方法中,我们将创建 ci::CameraPersp 并将其设置为中心点为窗口的中心。我们还将设置相机为 mCamera: 的当前相机。

    CameraPersp cameraPersp( getWindowWidth(),getWindowHeight(), 60.0f );
    Vec3f center( getWindowWidth() / 2, getWindowHeight() / 2,0.0f );
    cameraPersp.setCenterOfInterestPoint( center );
    mCamera.setCurrentCam( cameraPersp );
    
  3. draw 方法中,让我们用黑色清除背景,并使用我们的相机设置窗口的矩阵。

      gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatrices( mCamera.getCamera() );
    
  4. 现在,让我们迭代 mLines 并绘制每个 ci::PolyLine。将以下代码添加到 draw 方法中:

    for( vector<PolyLine<Vec3f> > ::iterator it = mLines.begin(); it != mLines.end(); ++it ){
    gl::draw( *it );
        }
    
  5. 在场景设置好并且线条正在绘制时,我们需要创建 3D 透视!让我们首先声明一个将坐标从屏幕位置转换为全局位置的方法。将以下方法声明添加到以下代码中:

        Vec3f screenToWorld( const Vec2f&point ) const;
    
  6. screenToWorld 的实现中,我们需要使用相机的透视从 point 生成一条射线。在 screenToWorld 中添加以下代码:

    float u = point.x / (float)getWindowWidth();
    float v = point.y / (float)getWindowHeight();
    
    const CameraPersp& cameraPersp = mCamera.getCamera();
    
    Ray ray = cameraPersp.generateRay( u, 1.0f - v, cameraPersp.getAspectRatio() );
    
  7. 现在,我们需要计算射线将在相机中心兴趣点的垂直平面上相交的位置,然后返回交点。在 screenToWorld 的实现中添加以下代码:

    float result = 0.0f;
    Vec3f planePos = cameraPersp.getCenterOfInterestPoint();
    Vec3f normal = cameraPersp.getViewDirection();
    
    ray.calcPlaneIntersection( planePos, normal, &result );
    
    Vec3f intersection= ray.calcPosition( result );
    return intersection;
    
  8. 让我们使用之前定义的方法用鼠标绘制。声明 mouseDownmouseDrag 事件处理器:

    void mouseDown( MouseEvent event );
    void mouseDrag( MouseEvent event );
    
  9. mouseDown 的实现中,我们将检查是否按下了 Shift 键。如果是,我们将调用 mCameramouseDown 方法;否则,我们将向 mLines 中添加 ci::PolyLine<ci::Vec3f>,使用 screenToWorld 计算鼠标光标的全局位置,并将其添加:

    void MyApp::mouseDown( MouseEvent event ){
      if( event.isShiftDown() ){
      mCamera.mouseDown( event.getPos() );
        }
    else {    
            mLines.push_back( PolyLine<Vec3f>() );
            Vec3f point = screenToWorld( event.getPos() );
            mLines.back().push_back( point );
        }
    }
    
  10. mouseDrag 的实现中,我们将检查是否按下了 Shift 键。如果是,我们将调用 mCameramouseDrag 方法;否则,我们将计算鼠标光标的全局位置并将其添加到 mLines 的最后一行。

    void Pick3dApp::mouseDrag( MouseEvent event ){
        if( event.isShiftDown() ){
        mCamera.mouseDrag( event.getPos(), event.isLeftDown(), event.isMiddleDown(), event.isRightDown() );
        } else {
            Vec3f point = screenToWorld( event.getPos() );
            mLines.back().push_back( point );
        }
    }
    
  11. 构建并运行应用程序。按住并拖动鼠标以绘制线条。按住 Shift 键并按住并拖动鼠标以旋转场景。

它是如何工作的…

我们使用 ci::MayaCamUI 来轻松旋转场景。

ci::Ray类是对射线的表示,包含一个起点、方向和无限长度。它提供了有用的方法来计算射线与三角形或平面的交点。

为了计算鼠标光标的全局位置,我们计算了一个从相机眼睛位置沿相机视图方向的射线。

我们然后计算了射线与场景中心的平面的交点,该平面垂直于相机。

然后将计算出的位置添加到ci::PolyLine<ci::Vec3f>对象中,以绘制线条。

参见

  • 要了解更多关于如何使用ci::MayaCamUI的信息,请参阅第二章中的配方使用 MayaCamUI准备工作

  • 要了解如何在 2D 中绘制,请阅读第七章中的配方使用鼠标绘制任意形状使用 2D 图形

添加灯光

在本章中,我们将学习如何使用 OpenGL 灯光照亮 3D 场景。

准备工作

包含必要的文件以使用 OpenGL 灯光、材质和绘制。将以下代码添加到源文件顶部:

#include "cinder/gl/gl.h"
#include "cinder/gl/Light.h"
#include "cinder/gl/Material.h"

还需添加以下using语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何操作…

我们将使用默认的 OpenGL 灯光渲染方法来照亮我们的场景。我们将使用ci::gl::Materialci::gl::Light类,这些类是 OpenGL 功能的包装器。

  1. 声明ci::gl::Material以定义正在绘制的物体的材质属性,以及ci::Vec3f以定义灯光的位置。

    gl::Material mMaterial;
    Vec3f mLightPos;
    
  2. 让我们在setup方法中添加以下代码来设置材质的AmbientDiffuseSpecularEmissionShininess属性。

    mMaterial.setAmbient( Color::black() );
    mMaterial.setDiffuse( Color( 1.0f, 0.0f, 0.0f ) );
    mMaterial.setSpecular( Color::white() );
    mMaterial.setEmission( Color::black() );
    mMaterial.setShininess( 128.0f );
    
  3. update方法中,我们将使用鼠标来定义灯光位置。在update方法中添加以下代码:

    mLightPos.x = getMousePos().x;
    mLightPos.y = getMousePos().y;
    mLightPos.z = 200.0f;
    
  4. draw方法中,我们将首先清除背景,设置窗口的矩阵,并启用读取和写入深度缓冲区。

    gl::clear( Color::black() );
    gl::setMatricesWindowPersp( getWindowWidth(), getWindowHeight() );
    gl::enableDepthWrite();
    gl::enableDepthRead();
    
  5. 让我们使用ci::gl::Light对象创建一个 OpenGL 灯光。我们将将其定义为POINT灯光,并将其 ID 设置为0。我们还将将其位置设置为mLightPos并定义其衰减。

    gl::Light light( gl::Light::POINT, 0 );
    light.setPosition( mLightPos );
    light.setAttenuation( 1.0f, 0.0f, 0.0f );
    
  6. 让我们启用 OpenGL 灯光、之前创建的灯光并应用材质。

    glEnable( GL_LIGHTING );
    light.enable();
    mMaterial.apply();
    
  7. 让我们在窗口中心绘制一个旋转的Torus,并使用经过的时间来旋转它。将以下代码添加到draw方法中:

    gl::pushMatrices();
    gl::translate( getWindowCenter() );
    float seconds = (float)getElapsedSeconds() * 100.0f;
    glRotatef( seconds, 1.0f, 0.0f, 0.0f );
    glRotatef( seconds, 0.0f, 1.0f, 0.0f );
    gl::drawTorus( 100.0f, 40.0f, 30, 30 );
    gl::popMatrices();
    
  8. 最后,禁用灯光:

    light.disable();
    
  9. 构建并运行应用程序;您将看到一个红色的旋转环面。移动鼠标以更改灯光的位置。如何操作…

的工作原理…

我们正在使用ci::gl::Materialci::gl::Light对象,这些是定义灯光和材质属性的辅助类。

setup方法中定义的材质属性按以下方式工作:

材质属性 功能
环境光 物体如何反射来自各个方向的光线。
Diffuse 物体如何反射来自特定方向或位置的光。
Specular 由于漫射光照,物体将反射的光。
Emission 物体发出的光线。
Shininess 物体反射镜面光的角度。必须是介于 1 和 128 之间的值。

材料的环境、漫射和镜面颜色将与来自光源的环境、漫射和镜面颜色相乘,默认情况下这些颜色都是白色。

可以定义三种不同类型的灯光。在先前的例子中,我们将我们的光源定义为类型 ci::gl::Light::POINT

这里是可用的灯光类型及其属性:

灯光类型 属性
ci::gl::Light::POINT 点光源是从空间中的特定位置发出的光线,向所有方向照明。
ci::gl::Light::DIRECTION 方向光模拟来自非常远的位置的光,所有光束都是平行的,并且以相同方向到达。
ci::gl::Light::SPOTLIGHT 聚光灯是从空间中的特定位置和特定方向发出的光线。

我们还定义了衰减值。OpenGL 中的灯光允许定义常量衰减、线性衰减和二次衰减的值。这些值定义了随着与光源距离的增加,光线如何变暗。

要照亮几何体,必须计算每个顶点的法线。使用 Cinder 的命令创建的所有形状都会为我们计算法线,所以我们不必担心这一点。

更多内容…

还可以定义来自光源的环境、漫射和镜面颜色。这些颜色中定义的值将与材料的相应颜色相乘。

这里是允许您定义灯光颜色的 ci::gl::Light 方法:

方法 灯光
setAmbient( const Color& color ) 环境光的颜色。
setDiffuse( const Color& color ) 漫射光的颜色。
setSpecular( const Color& color ) 镜面光的颜色。

可以创建多个光源。灯光的数量取决于显卡的实现,但至少总是 8 个。

要创建更多光源,只需创建更多的 ci::gl::Light 对象,并确保每个都获得一个唯一的 ID。

参见

请阅读 计算顶点法线 菜谱,了解如何计算用户创建的几何体的顶点法线。

3D 选择

在这个菜谱中,我们将计算鼠标光标与 3D 模型的交点。

准备工作

包含必要的文件以使用 OpenGL 绘图,使用纹理和加载图像,加载 3D 模型,定义 OpenGL 灯光和材料,并使用 Cinder 的 Maya 相机。

#include "cinder/gl/gl.h"
#include "cinder/gl/Texture.h"
#include "cinder/gl/Light.h"
#include "cinder/gl/Material.h"
#include "cinder/TriMesh.h"
#include "cinder/ImageIo.h"
#include "cinder/MayaCamUI.h"

此外,添加以下 using 语句:

using namespace ci;
using namespace ci::app;
using namespace std;

我们将使用一个 3D 模型,所以将文件及其纹理放在 assets 文件夹中。在这个例子中,我们将使用一个名为 ducky.msh 的网格文件和一个名为 ducky.png 的纹理。

如何操作…

  1. 我们将使用 ci::CameraPerspci::Ray 类将鼠标坐标转换为我们的旋转 3D 场景并计算与 3D 模型的交点。

  2. 声明成员以定义 3D 模型及其与鼠标的交点,以及一个用于轻松导航的 ci::MayaCamUI 对象和一个用于照明的 ci::gl::Material

    TriMesh mMesh;
    gl::Texture mTexture;
    MayaCamUI mCam;
    bool mIntersects;
    Vec3f mNormal, mHitPos;
    AxisAlignedBox3f mMeshBounds;
    gl::Material mMaterial;
    
  3. 声明一个方法,我们将计算 ci::Ray 类与组成 mMesh 的三角形之间的交点。

    void calcIntersectionWithMeshTriangles( const ci::Ray& ray );
    
  4. setup 方法中,让我们加载模型和纹理并计算其边界框:

    mMesh.read( loadAsset( "ducky.msh" ) );
    mTexture = loadImage( loadAsset( "ducky.png" ) );
    mMeshBounds = mMesh.calcBoundingBox();
    
  5. 让我们在 setup 方法中定义相机并使其看起来位于模型的中心。在 setup 方法中添加以下代码:

    CameraPersp cam;
    Vec3f modelCenter = mMeshBounds.getCenter();
    cam.setEyePoint( modelCenter + Vec3f( 0.0f, 0.0f, 20.0f ) );
    cam.setCenterOfInterestPoint( modelCenter );
    mCam.setCurrentCam( cam );
    
  6. 最后,设置模型的照明材质。

    mMaterial.setAmbient( Color::black() );
    mMaterial.setDiffuse( Color::white() );
    mMaterial.setEmission( Color::black() );
    
  7. 声明 mouseDownmouseDrag 事件的处理器。

    void mouseDown( MouseEvent event );
    void mouseDrag( MouseEvent event );
    
  8. 通过调用 mCam 的必要方法来实现这些方法:

    void MyApp::mouseDown( MouseEvent event ){
      mCam.mouseDown( event.getPos() );
    }
    
    void MyApp::mouseDrag( MouseEvent event ){
      mCam.mouseDrag( event.getPos(), event.isLeftDown(), event.isMiddleDown(), event.isRightDown() );
    }
    
  9. 让我们实现 update 方法并计算鼠标光标与我们的模型之间的交点。让我们先获取鼠标位置,然后计算从我们的相机发出的 ci::Ray

    Vec2f mousePos = getMousePos();
    float u = mousePos.x / (float)getWindowWidth();
    float v = mousePos.y / (float)getWindowHeight();
    CameraPersp cameraPersp = mCam.getCamera();
    Ray ray = cameraPersp.generateRay( u, 1.0f - v, cameraPersp.getAspectRatio() );
    
  10. 让我们进行快速测试,检查射线是否与模型的边界框相交。如果结果是 true,我们将调用 calcIntersectionWithMeshTriangles 方法。

        if( mMeshBounds.intersects( ray ) == false ){
      mIntersects = false;
        } else {
      calcIntersectionWithMeshTriangles( ray );
        }
    
  11. 让我们实现 calcIntersectionWithMeshTriangles 方法。我们将遍历我们模型的全部三角形,计算最近的交点并存储其索引。

    float distance = 0.0f;
    float resultDistance = 999999999.9f;
    int resultIndex = -1;
    int numTriangles = mMesh.getNumTriangles();
    for( int i=0; i<numTriangles; i++ ){
            Vec3f v1, v2, v3;
            mMesh.getTriangleVertices( i, &v1, &v2, &v3 );
            if( ray.calcTriangleIntersection( v1, v2, v3, &distance ) ){
            if( distance <resultDistance ){
            resultDistance = distance;
            resultIndex = i;
                }
            }
        }
    
  12. 让我们检查是否有任何交点并计算其位置和法向量。如果没有找到交点,我们将简单地设置 mIntersectsfalse

    if( resultIndex> -1 ){
            mHitPos = ray.calcPosition( resultDistance );
            mIntersects = true;
            Vec3f v1, v2, v3;
            mMesh.getTriangleVertices( resultIndex, &v1, &v2, &v3 );
            mNormal = ( v2 - v1 ).cross( v3 - v1 );
            mNormal.normalize();
        } else {
          mIntersects = false;
        }
    
  13. 在计算出交点后,让我们绘制模型、交点和法向量。首先用黑色清除背景,使用我们的相机设置窗口的矩阵,并启用深度缓冲区的读写。在 draw 方法中添加以下代码:

    gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatrices( mCam.getCamera() );
    gl::enableDepthRead();
    gl::enableDepthWrite();
    
  14. 现在,让我们创建一个光源并将其位置设置为相机的眼睛位置。我们还将启用光源并应用材质。

    gl::Light light( gl::Light::POINT, 0 );
    light.setPosition( mCam.getCamera().getEyePoint() );
    light.setAttenuation( 1.0f, 0.0f, 0.0f );
    glEnable( GL_LIGHTING );
    light.enable();
    mMaterial.apply();
    
  15. 现在,启用并绑定模型的纹理,绘制模型,然后禁用纹理和照明。

    mTexture.enableAndBind();
    gl::draw( mMesh );
    mTexture.unbind();
    glDisable( GL_LIGHTING ); 
    
  16. 最后,我们将检查 mIntersects 是否为 true,并在交点处绘制一个球体以及法向量。

    if( mIntersects ){
      gl::color( Color::white() );
      gl::drawSphere( mHitPos, 0.2f );
      gl::drawVector( mHitPos, mHitPos + ( mNormal * 2.0f ) );
        }
    

    如何操作…

它是如何工作的…

要计算 3D 中鼠标与模型的交点,我们从一个鼠标位置向相机的观察方向生成一个射线。

由于性能原因,我们首先计算射线是否与模型的边界框相交。如果与模型相交,我们进一步计算射线与组成模型的每个三角形的交点。对于找到的每个交点,我们检查其距离并仅计算最近交点的交点和法向量。

从图像创建高度图

在这个菜谱中,我们将学习如何根据用户选择的图像创建点云。我们将创建一个点阵,其中每个点将对应一个像素。每个点的 x 和 y 坐标将等于图像上像素的位置,而 z 坐标将基于其颜色计算。

准备工作

包含必要的文件以使用 OpenGL、图像表面、VBO 网格和加载图像。

将以下代码添加到源文件顶部:

#include "cinder/gl/gl.h"
#include "cinder/Surface.h"
#include "cinder/gl/Vbo.h"
#include "cinder/MayaCamUI.h"
#include "cinder/ImageIo.h"

还要添加以下using语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何做…

我们将学习如何从图像中读取像素值并创建点云。

  1. 声明ci::Surface32f以存储图像像素,ci::gl::VboMesh我们将用作点云,以及ci::MayaCamUI以方便地旋转我们的场景。

    Surface32f mImage;
    gl::VboMesh mPointCloud;gl::VboMesh mPointCloud;
    MayaCamUI mCam;
    
  2. setup方法中,我们首先打开一个文件加载对话框,然后让用户选择要使用的图像并检查它是否返回一个有效的路径。

    fs::path imagePath = getOpenFilePath( "", ImageIo::getLoadExtensions() );
    if( imagePath.empty() == false ){
    
  3. 接下来,让我们加载图像并初始化mPointCloud。我们将设置ci::gl::VboMesh::Layout以具有动态位置和颜色,这样我们就可以稍后更改它们。

    mImage = loadImage( imagePath );
    int numPixels = mImage.getWidth() * mImage.getHeight();
    gl::VboMesh::Layout layout;
    layout.setDynamicColorsRGB();
    layout.setDynamicPositions();
    mPointCloud = gl::VboMesh( numPixels, 0, layout, GL_POINTS );
    
  4. 接下来,我们将遍历图像的像素并更新mPointCloud中的顶点。

    Surface32f::IterpixelIt = mImage.getIter();
    gl::VboMesh::VertexItervertexIt( mPointCloud );
    while( pixelIt.line() ){
      while( pixelIt.pixel() ){
                        Color color( pixelIt.r(), pixelIt.g(), pixelIt.b() );
        float height = color.get( CM_RGB ).length();
        float x = pixelIt.x();
        float y = mImage.getHeight() - pixelIt.y();
        float z = height * 100.0f;
        vertexIt.setPosition( x,y, z );
        vertexIt.setColorRGB( color );
                        ++vertexIt;
                    }
                }
    
  5. 现在,我们将设置相机,使其围绕点云的中心旋转,并关闭我们在第二步开始时的if语句。

            Vec3f center( (float)mImage.getWidth()/2.0f, (float)mImage.getHeight()/2.0f, 50.0f );
        CameraPersp camera( getWindowWidth(), getWindowHeight(), 60.0f );
        camera.setEyePoint( Vec3f( center.x, center.y, (float)mImage.getHeight() ) );
        camera.setCenterOfInterestPoint( center );
        mCam.setCurrentCam( camera );
        }
    
  6. 让我们声明并实现必要的鼠标事件处理程序以使用mCam

    void mouseDown( MouseEvent event );	
    void mouseDrag( MouseEvent event );
    
  7. 并实现它们:

    void MyApp::mouseDown( MouseEvent event ){
      mCam.mouseDown( event.getPos() );
    }
    
    void MyApp::mouseDrag( MouseEvent event ){
      mCam.mouseDrag( event.getPos(), event.isLeft(), event.isMiddle(), event.isRight() );
    }
    
  8. draw方法中,我们将首先清除背景,设置由mCam定义的窗口矩阵,并启用读取和写入深度缓冲区。

    gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatrices( mCam.getCamera() );
    gl::enableDepthRead();
    gl::enableDepthWrite();
    
  9. 最后,我们将检查mPointCloud是否是一个有效的对象,并绘制它。

    if( mPointCloud ){
      gl::draw( mPointCloud );
        }
    
  10. 构建并运行应用程序。您将看到一个对话框提示您选择一个图像文件。选择它,您将看到图像的点云表示。拖动鼠标光标以旋转场景。如何做…

它是如何工作的…

我们首先将图像加载到ci::Surface32f中。此表面将像素存储为范围从01的浮点数。

我们创建了一个点阵,其中xy坐标代表图像上像素的位置,而z坐标是颜色向量的长度。

点云由一个ci::gl::VboMesh表示,它是一个由顶点、法线、颜色和索引组成的网格,其下是一个顶点缓冲对象。它允许优化几何图形的绘制。

使用 Perlin 噪声创建地形

在这个菜谱中,我们将学习如何使用Perlin 噪声在 3D 中构建表面,以创建类似于地形的有机构变形。

准备工作

包含必要的文件以使用 OpenGL 绘制、Perlin 噪声、Maya 相机进行导航和 Cinder 的数学工具。将以下代码添加到源文件顶部:

#include "cinder/gl/gl.h"
#include "cinder/Perlin.h"
#include "cinder/MayaCamUI.h"
#include "cinder/CinderMath.h"

还要添加以下using语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何做…

我们将创建一个 3D 点阵,并使用 Perlin 噪声计算一个平滑的表面。

  1. 在应用程序类声明之前添加以下代码来声明存储地形顶点的struct

    struct Vertice{
        Vec3f position;
        Color color;
    };
    
  2. 在应用程序类声明中添加以下成员:

    vector< vector<Vertice> > mTerrain;
    int mNumRows, mNumLines;
    MayaCamUI mCam;
    Perlin mPerlin;
    
  3. setup方法中,定义将构成地形网格的行数和列数。还要定义每个点之间的间隔距离。

    mNumRows = 50;
    mNumLines = 50;
    float gap = 5.0f;
    
  4. 通过创建一个位于xz轴上的点网格,将顶点添加到mTerrain中。我们将使用ci::Perlin生成的值来计算每个点的海拔高度。我们还将使用点的海拔高度来定义它们的颜色:

       mTerrain.resize( mNumRows );
        for( int i=0; i<mNumRows; i++ ){
            mTerrain[i].resize( mNumLines );
            for( int j=0; j<mNumLines; j++ ){
                float x = (float)i * gap;
                float z = (float)j * gap;
                float y = mPerlin.noise( x*0.01f, z*0.01 ) * 100.0f;
                mTerrain[i][j].position = Vec3f( x, y, z );
                float colorVal = lmap( y, -100.0f, 100.0f, 0.0f, 1.0f );
                mTerrain[i][j].color = Color( colorVal, colorVal, colorVal );
            }
        }
    
  5. 现在让我们定义我们的相机,使其指向地形的中心。

    float width = mNumRows * gap;
    float height = mNumLines * gap;
    Vec3f center( width/2.0f, height/2.0f, 0.0f );
    Vec3f eye( center.x, center.y, 300.0f );
    CameraPersp camera( getWindowWidth(), getWindowHeight(), 60.0f );
    camera.setEyePoint( eye );
    camera.setCenterOfInterestPoint( center );
    mCam.setCurrentCam( camera );
    
  6. 声明鼠标事件处理程序以使用mCam

    Void mouseDown( MouseEvent event );
    void mouseDrag( MouseEvent event );
            }
    
  7. 现在让我们实现鼠标处理程序。

    void MyApp::mouseDown( MouseEvent event ){
      mCam.mouseDown( event.getPos() );
    }
    void MyApp::mouseDrag( MouseEvent event ){
      mCam.mouseDrag( event.getPos(), event.isLeft(), event.isMiddle(), event.isRight() );
    }
    
  8. draw方法中,让我们首先清除背景,使用mCam设置矩阵,并启用深度缓冲区的读写。

    gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatrices( mCam.getCamera() );
    gl::enableDepthRead();
    gl::enableDepthWrite();
    
  9. 现在启用 OpenGL 以使用VERTEXCOLOR数组:

    glEnableClientState( GL_VERTEX_ARRAY );
    glEnableClientState( GL_COLOR_ARRAY );
    
  10. 我们将使用嵌套的for循环遍历地形,并将每个地形条绘制为GL_TRIANGLE_STRIP

    for( int i=0; i<mNumRows-1; i++ ){
      vector<Vec3f> vertices;
      vector<ColorA> colors;
      for( int j=0; j<mNumLines; j++ ){
    
        vertices.push_back( mTerrain[i][j].position );
        vertices.push_back( mTerrain[i+1][j].position );
        colors.push_back( mTerrain[i][j].color );
        colors.push_back( mTerrain[i+1][j].color );
    
                }
      glColor3f( 1.0f, 1.0f, 1.0f );
      glVertexPointer( 3, GL_FLOAT, 0, &vertices[0] );
      glColorPointer( 4, GL_FLOAT, 0, &colors[0] );
      glDrawArrays( GL_TRIANGLE_STRIP, 0, vertices.size() );
      }
    

    如何操作...

它是如何工作的...

Perlin 噪声是一个连续的随机数生成器,能够创建有机纹理和过渡。

我们使用ci::Perlin对象创建的值来计算构成地形的顶点的高度,并在顶点之间创建平滑的过渡。

更多内容...

我们还可以通过向用于计算 Perlin 噪声的坐标添加递增偏移来动画化我们的地形。在你的类声明中声明以下成员变量:

float offsetX, offsetZ;

setup方法中初始化它们。

offsetX = 0.0f;
offsetZ = 0.0f;

update方法中,通过添加0.01来动画化每个偏移值。

offsetX += 0.01f;
offsetZ += 0.01f;

同样,在update方法中,我们将遍历mTerrain的所有顶点。对于每个顶点,我们将使用其xz坐标,通过mPerlin noise计算Y坐标,但我们将偏移坐标。

  for( int i=0; i<mNumRows; i++ ){
  for( int j=0; j<mNumLines; j++ ){
  Vertice& vertice = mTerrain[i][j];
  float x = vertice.position.x;
  float z = vertice.position.z;
  float y = mPerlin.noise( x*0.01f + offsetX, z*0.01f + offsetZ ) * 100.0f;
            vertice.position.y = y;
        }
    }

保存网格数据

假设你正在使用TriMesh类来存储 3D 几何形状,我们将向你展示如何将其保存到文件中。

准备工作

我们假设你正在使用存储在TriMesh对象中的 3D 模型。示例应用程序加载 3D 几何形状可以在Cinder samples目录中的文件夹:OBJLoaderDemo中找到。

如何操作...

我们将实现保存 3D 网格数据。

  1. 包含必要的头文件:

    #include "cinder/ObjLoader.h"
    #include "cinder/Utilities.h"
    
  2. 按照以下方式实现你的keyDown方法:

    if( event.getChar() == 's' ) {
      fs::path path = getSaveFilePath(getDocumentsDirectory() / fs::path("mesh.trimesh") );
      if( ! path.empty() ) {
        mMesh.write( writeFile( path ) );
            }
    }
      else if( event.getChar() == 'o' ) {
      fs::path path = getSaveFilePath(getDocumentsDirectory() / fs::path("mesh.obj") );
      if( ! path.empty() ) {
      ObjLoader::write( writeFile( path ), mMesh );
        }
    }
    

它是如何工作的...

在 Cinder 中,我们使用TriMesh类来存储 3D 几何形状。使用TriMesh,我们可以存储和操作从 3D 模型文件加载的几何形状,或者通过代码添加每个顶点。

每当你按下键盘上的S键时,会弹出一个保存对话框,询问你将TriMesh对象的二进制数据保存到何处。当你按下O键时,OBJ 格式文件将被保存到你的documents文件夹中。如果你不需要与其他软件交换数据,二进制数据的保存和加载通常更快。

第九章。添加动画

在本章中,我们将学习动画化 2D 和 3D 对象的技术。我们将介绍 Cinder 在此领域的功能,例如时间轴和数学函数。

本章的食谱将涵盖以下内容:

  • 使用时间轴动画化

  • 使用时间轴创建动画序列

  • 沿路径动画化

  • 将相机运动与路径对齐

  • 动画化文本 - 文本作为电影的遮罩

  • 动画化文本 - 滚动文本行

  • 使用 Perlin 噪声创建流场

  • 在 3D 中创建图像库

  • 使用 Perlin 噪声创建球形流场

使用时间轴动画化

在本食谱中,我们将学习如何使用 Cinder 的新功能;时间轴来动画化值。

当用户按下鼠标按钮时,我们动画化背景颜色、圆的位置和半径。

准备工作

包含必要的文件以使用时间轴、生成随机数和使用 OpenGL 绘制。在源文件顶部添加以下代码片段:

#include "cinder/gl/gl.h"
#include "cinder/Timeline.h"
#include "cinder/Rand.h"

此外,添加以下有用的 using 语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何做到这一点...

我们将创建几个参数,这些参数将使用时间轴进行动画化。执行以下步骤来完成此操作:

  1. 声明以下成员以进行动画化:

    Anim<Color> mBackgroundColor;
    Anim<Vec2f> mCenter;
    Anim<float> mRadius;
    
  2. setup 方法中初始化参数。

    mBackgroundColor = Color( CM_HSV, randFloat(), 1.0f, 1.0f );
    mCenter = getWindowCenter();
    mRadius = randFloat( 20.0f, 100.0f );
    
  3. draw 方法中,我们需要使用在 mBackgroundColor 中定义的颜色清除背景,并在 mCenter 位置使用 mRadius 作为半径绘制一个圆。

    gl::clear( mBackgroundColor.value() ); 
    gl::drawSolidCircle( mCenter.value(), mRadius.value() );
    
  4. 要在用户按下鼠标按钮时动画化值,我们需要声明 mouseDown 事件处理器。

    void mouseDown( MouseEvent event );  
    
  5. 让我们实现 mouseDown 事件处理器并将动画添加到主时间轴。我们将动画化 mBackgroundColor 到一个新的随机颜色,将 mCenter 设置为鼠标光标的当前位置,并将 mRadius 设置为一个新的随机值。

    Color backgroundColor( CM_HSV, randFloat(), 1.0f, 1.0f );
    timeline().apply( &mBackgroundColor, backgroundColor, 2.0f, EaseInCubic() );
    timeline().apply( &mCenter, (Vec2f)event.getPos(), 1.0f, EaseInCirc() );
    timeline().apply( &mRadius, randFloat( 20.0f, 100.0f ), 1.0f, EaseInQuad() );
    

它是如何工作的...

时间轴是 Cinder 在 0.8.4 版本中引入的新功能。它允许用户通过将参数添加到时间轴一次来动画化参数,所有更新都在幕后进行。

动画必须是模板类 ci::Anim 的对象。此类可以使用支持 + 操作符的任何模板类型创建。

ci::Timeline 对象可以通过调用 ci::app::App::timeline() 方法访问。始终有一个主时间轴,用户也可以创建其他 ci::Timeline 对象。

ci::Timeline::apply 方法中的第四个参数是一个表示 Tween 方法的 functor 对象。Cinder 有几个可用的 Tweens 可以作为参数传递,以定义动画的类型。

还有更多...

在先前的示例中使用的 ci::Timeline::apply 方法使用了 ci::Anim 对象的初始值,但也可以创建一个动画,其中开始和结束值都传递。

例如,如果我们想将 mRadius 从起始值 10.0 动画到结束值 100.0 秒,我们将调用以下方法:

timeline().apply( &mRadius, 10.0f, 100.0f 1.0f, EaseInQuad() );

参见

使用时间轴创建动画序列

在本菜谱中,我们将学习如何使用 Cinder 强大的时间轴功能来创建动画序列。我们将绘制一个圆,并按顺序动画化半径和颜色。

准备工作

包含必要的文件以使用时间轴、在 OpenGL 中绘制以及生成随机数。

#include "cinder/gl/gl.h"
#include "cinder/Timeline.h"
#include "cinder/Rand.h"

还要添加以下有用的 using 语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何实现…

我们将使用时间轴依次动画化几个参数。执行以下步骤来完成:

  1. 声明以下成员以定义圆的位置、半径和颜色:

    Anim<float> mRadius;
    Anim<Color> mColor;
    Vec2f mPos;
    
  2. setup 方法中,初始化成员。将位置设置为窗口中心,半径为 30,并使用 HSV 颜色模式生成一个随机颜色。

    mPos = (Vec2f)getWindowCenter();
    mRadius = 30.0f;
    mColor = Color( CM_HSV, randFloat(), 1.0f, 1.0f );
    
  3. draw 方法中,我们将使用黑色清除背景,并使用之前定义的成员来绘制圆圈。

    gl::clear( Color::black() ); 
    gl::color( mColor.value() );
    gl::drawSolidCircle( mPos, mRadius.value() );
    
  4. 声明 mouseDown 事件处理器。

      void mouseDown( MouseEvent event );
    
  5. mouseDown 的实现中,我们将应用动画到主时间轴上。

    我们将首先将 mRadius 从 30 动画到 200,然后向 mRadius 附加另一个从 200 到 30 的动画。

    将以下代码片段添加到 mouseDown 方法中:

    timeline().apply( &mRadius, 30.0f, 200.0f, 2.0f, EaseInOutCubic() );
    timeline().appendTo( &mRadius, 200.0f, 30.0f, 1.0f, EaseInOutCubic() );
    
  6. 让我们使用 HSV 颜色模式创建一个随机颜色,并将其用作动画 mColor 的目标颜色,然后将此动画附加到 mRadius

    mouseDown 方法内添加以下代码片段:

        Color targetColor = Color( CM_HSV, randFloat(), 1.0f, 1.0f );
    timeline().apply( &mColor, targetColor, 1.0f, EaseInQuad() ).appendTo( &mRadius );
    

它是如何工作的…

附加动画是创建复杂动画序列的强大且简单的方法。

在第 5 步中,我们使用以下代码行将动画添加到 mRadius

timeline().appendTo( &mRadius, 200.0f, 30.0f, 1.0f, EaseInOutCubic() );

这意味着此动画将在之前的 mRadius 动画完成后发生。

在第 6 步中,我们使用以下代码行将 mColor 动画附加到 mRadius

timeline().apply( &mColor, targetColor, 1.0f, EaseInQuad() ).appendTo( &mRadius );

这意味着 mColor 动画仅在之前的 mRadius 动画完成后才会发生。

还有更多…

当附加两个不同的动画时,可以通过定义偏移秒数作为第二个参数来偏移起始时间。

因此,例如,将第 6 步中的行更改为以下内容:

timeline().apply( &mColor, targetColor, 1.0f, EaseInQuad() ).appendTo( &mRadius, -0.5f );

这意味着 mColor 动画将在 mRadius 完成后 0.5 秒开始。

沿路径动画化

在本菜谱中,我们将学习如何在 3D 空间中绘制平滑的 B 样条,并动画化对象沿计算出的 B 样条的位置。

准备工作

要在 3D 空间中导航,我们将使用在 第二章 中介绍的 使用 MayaCamUI 菜单的 MayaCamUI为开发做准备

如何实现…

我们将创建一个示例动画,展示一个对象沿着样条曲线移动。执行以下步骤来完成:

  1. 包含必要的头文件。

    #include "cinder/Rand.h"
    #include "cinder/MayaCamUI.h"
    #include "cinder/BSpline.h"
    
  2. 从声明成员变量开始,以保留 B 样条和当前对象的位置。

    Vec3f       mObjPosition;
    BSpline3f   spline;
    
  3. setup方法内部准备一个随机的样条曲线:

    mObjPosition = Vec3f::zero();
    
    vector<Vec3f> splinePoints;
    float step = 0.5f;
    float width = 20.f;
    for (float t = 0.f; t < width; t += step) {
     Vec3f pos = Vec3f(
      cos(t)*randFloat(0.f,2.f),
      sin(t)*0.3f,
      t - width*0.5f);
     splinePoints.push_back( pos );
    }
    spline = BSpline3f( splinePoints, 3, false, false );
    
  4. update方法内部,检索沿着样条移动的物体的位置。

    float dist = math<float>::abs(sin( getElapsedSeconds()*0.2f ));
    mObjPosition = spline.getPosition( dist );
    
  5. 绘制场景的代码片段看起来如下:

    gl::enableDepthRead();
    gl::enableDepthWrite();
    gl::enableAlphaBlending();
    gl::clear( Color::white() );
    gl::setViewport(getWindowBounds());
    gl::setMatrices(mMayaCam.getCamera());
    
    // draw dashed line
    gl::color( ColorA(0.f, 0.f, 0.f, 0.8f) );
    float step = 0.005f;
    glBegin(GL_LINES);
    for (float t = 0.f; t <= 1.f; t += step) {
      gl::vertex(spline.getPosition(t));
    }
    glEnd();
    
    // draw object
    gl::color(Color(1.f,0.f,0.f));
    gl::drawSphere(mObjPosition, 0.25f);
    

它是如何工作的…

首先,看看第 3 步,我们在该步骤中通过基于正弦和余弦函数以及 x 轴上的一些随机点计算 B 样条。路径存储在spline类成员中。

然后,我们可以轻松地检索路径上任何距离的 3D 空间中的位置。我们在第 4 步中这样做;使用spline成员上的getPosition方法。路径上的距离作为 0.0 到 1.0 范围内的float值传递,其中 0.0 表示路径的开始,1.0 表示路径的结束。

最后,在第 5 步中,我们绘制了一个动画,一个红色球体沿着我们的路径(用黑色虚线表示)移动,如下面的截图所示:

它是如何工作的…

参见

  • 将相机运动与路径对齐配方

  • 第七章中的在曲线上动画文本配方,使用 2D 图形

将相机运动与路径对齐

在本配方中,我们将学习如何动画化沿着路径(计算为 B 样条)的相机位置。

准备工作

在本例中,我们将使用MayaCamUI,因此请参阅第二章中的使用 MayaCamUI配方,准备开发

如何操作…

我们将创建一个演示该机制的程序。执行以下步骤:

  1. 包含必要的头文件。

    #include "cinder/Rand.h"
    #include "cinder/MayaCamUI.h"
    #include "cinder/BSpline.h"
    
  2. 从成员变量的声明开始。

    MayaCamUI mMayaCam;
    BSpline3f   spline;
    CameraPersp mMovingCam;
    Vec3f       mCamPosition;
    vector<Rectf> mBoxes;
    
  3. 设置成员的初始值。

    setWindowSize(640*2, 480);
    mCamPosition = Vec3f::zero();
    
    CameraPersp  mSceneCam;
    mSceneCam.setPerspective(45.0f, 640.f/480.f, 0.1, 10000);
    mSceneCam.setEyePoint(Vec3f(7.f,7.f,7.f));
    mSceneCam.setCenterOfInterestPoint(Vec3f::zero());
    mMayaCam.setCurrentCam(mSceneCam);
    
    mMovingCam.setPerspective(45.0f, 640.f/480.f, 0.1, 100.f);
    mMovingCam.setCenterOfInterestPoint(Vec3f::zero());
    
    vector<Vec3f> splinePoints;
    float step = 0.5f;
    float width = 20.f;
    for (float t = 0.f; t < width; t += step) {
     Vec3f pos = Vec3f( cos(t)*randFloat(0.8f,1.2f),
      0.5f+sin(t*0.5f)*0.5f,
      t - width*0.5f);
     splinePoints.push_back( pos );
    }
    spline = BSpline3f( splinePoints, 3, false, false );
    
    for(int i = 0; i<100; i++) {
     Vec2f pos = Vec2f(randFloat(-10.f,10.f), 
      randFloat(-10.f,10.f));
     float size = randFloat(0.1f,0.5f);
     mBoxes.push_back(Rectf(pos, pos+Vec2f(size,size*3.f)));
    }
    
  4. update方法内部更新相机属性。

    float step = 0.001f;
    float pos = abs(sin( getElapsedSeconds()*0.05f ));
    pos = min(0.99f, pos);
    mCamPosition = spline.getPosition( pos );
    
    mMovingCam.setEyePoint(mCamPosition);
    mMovingCam.lookAt(spline.getPosition( pos+step ));
    
  5. 现在的整个draw方法看起来如下代码片段:

    gl::enableDepthRead();
    gl::enableDepthWrite();
    gl::enableAlphaBlending();
    gl::clear( Color::white() );
    gl::setViewport(getWindowBounds());
    gl::setMatricesWindow(getWindowSize());
    
    gl::color(ColorA(0.f,0.f,0.f, 1.f));
    gl::drawLine(Vec2f(640.f,0.f), Vec2f(640.f,480.f));
    
    gl::pushMatrices();
    gl::setViewport(Area(0,0, 640,480));
    gl::setMatrices(mMayaCam.getCamera());
    
    drawScene();
    
    // draw dashed line
    gl::color( ColorA(0.f, 0.f, 0.f, 0.8f) );
    float step = 0.005f;
    glBegin(GL_LINES);
    for (float t = 0.f; t <= 1.f; t += step) {
      gl::vertex(spline.getPosition(t));
    }
    glEnd();
    
    // draw object
    gl::color(Color(0.f,0.f,1.f));
    gl::drawFrustum(mMovingCam);
    
    gl::popMatrices();
    
    // -------------
    
    gl::pushMatrices();
    gl::setViewport(Area(640,0, 640*2,480));
    gl::setMatrices(mMovingCam);
    drawScene();
    gl::popMatrices();
    
  6. 现在我们必须实现drawScene方法,它实际上绘制我们的 3D 场景。

    GLfloat light0_position[] = { 1000.f, 500.f, -500.f, 0.1f };
    GLfloat light1_position[] = { -1000.f, 100.f, 500.f, 0.1f };
    GLfloat light1_color[] = { 1.f, 1.f, 1.f };
    
    glLightfv( GL_LIGHT0, GL_POSITION, light0_position );
    glLightfv( GL_LIGHT1, GL_POSITION, light1_position );
    glLightfv( GL_LIGHT1, GL_DIFFUSE, light1_color );
    
    glEnable( GL_LIGHTING );
    glEnable( GL_LIGHT0 );
    glEnable( GL_LIGHT1 );
    
    ci::ColorA diffuseColor(0.9f, 0.2f, 0.f );
    gl::color(diffuseColor);
    glMaterialfv( GL_FRONT, GL_DIFFUSE,  diffuseColor );
    
    vector<Rectf>::iterator it;
    for(it = mBoxes.begin(); it != mBoxes.end(); ++it) {
     gl::pushMatrices();
     gl::translate(0.f, it->getHeight()*0.5f, 0.f);
     Vec2f center = it->getCenter();
     gl::drawCube(Vec3f(center.x, 0.f, center.y), 
      Vec3f(it->getWidth(),
     it->getHeight(), it->getWidth()));
     gl::popMatrices();
    }
    
    glDisable( GL_LIGHTING );
    glDisable( GL_LIGHT0 );
    glDisable( GL_LIGHT1 );
    
    // draw grid
    drawGrid(50.0f, 2.0f);
    
  7. 我们最后需要的是drawGrid方法,其实现可以在第二章中的使用 3D 空间指南配方中找到,准备开发

它是如何工作的…

在本例中,我们使用 B 样条作为路径,我们的相机沿着该路径移动。请参阅沿着路径动画配方,以查看对象在路径上动画化的基本实现。如您在第 4 步中看到的,我们通过在mMovingCam成员上调用setEyePosition方法来设置相机位置,我们必须设置相机视图方向。为此,我们获取路径上的下一个点的位置并将其传递给lookAt方法。

我们正在绘制一个分割屏幕,其中左侧是我们的场景预览,右侧我们可以看到沿着路径移动的相机视锥体内的内容。

它是如何工作的…

参见

  • 沿着路径动画配方

  • 第二章中的使用 3D 空间指南食谱,准备开发

  • 第二章中的使用 MayaCamUI食谱,准备开发

动画文本 – 文本作为电影的遮罩

在这个食谱中,我们将学习如何使用简单的着色器程序将文本用作电影的遮罩。

准备工作

在这个例子中,我们使用了一部由 NASA 提供的令人惊叹的视频,由国际空间站(ISS)机组人员拍摄,你可以在eol.jsc.nasa.gov/找到它。请下载一个并将其保存为assets文件夹内的video.mov

如何做…

我们将创建一个示例 Cinder 应用程序来展示该机制。按照以下步骤进行操作:

  1. 包含必要的头文件。

    #include "cinder/gl/Texture.h"
    #include "cinder/Text.h"
    #include "cinder/Font.h"
    #include "cinder/qtime/QuickTime.h"
    #include "cinder/gl/GlslProg.h"
    
  2. 声明成员变量。

    qtime::MovieGl mMovie;
    gl::Texture     mFrameTexture, mTextTexture;
    gl::GlslProg  mMaskingShader;
    
  3. 实现以下setup方法:

    setWindowSize(854, 480);
    
    TextLayout layout;
    layout.clear( ColorA(0.f,0.f,0.f, 0.f) );
    layout.setFont( Font("Arial Black", 96 ) );
    layout.setColor( Color( 1, 1, 1 ) );
    layout.addLine( "SPACE" );
    Surface8u rendered = layout.render( true );
    
    gl::Texture::Format format;
    format.setTargetRect();
    mTextTexture = gl::Texture( rendered, format );
    
    try {
      mMovie = qtime::MovieGl( getAssetPath("video.mov") );
      mMovie.setLoop();
      mMovie.play();
    } catch( ... ) {
      console() <<"Unable to load the movie."<<endl;
      mMovie.reset();
    }
    
    mMaskingShader = gl::GlslProg( loadAsset("passThru_vert.glsl"), loadAsset("masking_frag.glsl") );
    
  4. update方法内部,我们必须更新我们的mFrameTexture,其中我们保存当前的电影帧。

    if( mMovie ) mFrameTexture = mMovie.getTexture();
    
  5. draw方法将类似于以下代码片段:

    gl::enableAlphaBlending();
    gl::clear( Color::gray(0.05f) );
    gl::setViewport(getWindowBounds());
    gl::setMatricesWindow(getWindowSize());
    
    gl::color(ColorA::white());
    if(mFrameTexture) {
     Vec2f maskOffset = (mFrameTexture.getSize() 
      - mTextTexture.getSize() ) * 0.5f;
     mFrameTexture.bind(0);
     mTextTexture.bind(1);
     mMaskingShader.bind();
     mMaskingShader.uniform("sourceTexture", 0);
     mMaskingShader.uniform("maskTexture", 1);
     mMaskingShader.uniform("maskOffset", maskOffset);
     gl::pushMatrices();
     gl::translate(getWindowCenter()-mTextTexture.getSize()*0.5f);
     gl::drawSolidRect( mTextTexture.getBounds(), true );
     gl::popMatrices();
     mMaskingShader.unbind();
    }
    
  6. 正如你在setup方法中看到的,我们正在加载一个用于遮罩的着色器。我们必须通过assets文件夹内的一个名为passThru_vert.glsl的顶点着色器传递。你可以在第七章的实现 2D 元球食谱中找到它,使用 2D 图形

  7. 最后,片段着色器程序代码将类似于以下代码片段,并且也应该位于assets文件夹下,命名为masking_frag.glsl

    #extension GL_ARB_texture_rectangle : require
    
    uniform sampler2DRect sourceTexture;
    uniform sampler2DRect maskTexture;
    uniform vec2 maskOffset;
    
    void main() 
    { 
      vec2 texCoord = gl_TexCoord[0].st;  
    
      vec4 sourceColor = texture2DRect( sourceTexture, texCoord+maskOffset );   
      vec4 maskColor = texture2DRect( maskTexture, texCoord ); 
    
      vec4 color = sourceColor * maskColor;
    
      gl_FragColor = color;
    }
    

它是如何工作的…

在第 3 步的setup方法内部,我们将文本渲染为Surface,然后将其转换为gl::Texture,我们稍后将其用作遮罩纹理。当我们将其用作电影的遮罩时,设置遮罩纹理的矩形格式非常重要,因为qtime::MovieGl正在创建一个具有矩形帧的纹理。为此,我们定义了一个名为formatgl::Texture::Format对象,并在其上调用setTargetRect方法。在创建gl::Texture时,我们必须将format作为第二个参数传递给构造函数。

要绘制电影帧,我们使用在第 5 步中应用的遮罩着色器程序。我们必须传递三个参数,分别是电影帧作为sourceTexture、带有文本的遮罩纹理作为maskTexture以及遮罩的位置作为maskOffset

在第 7 步中,你可以看到片段着色器代码,它简单地乘以sourceTexturemaskTexture中相应像素的颜色。请注意,我们正在使用sampler2DRecttexture2DRect来处理矩形纹理。

它是如何工作的…

动画文本 – 滚动文本行

在这个食谱中,我们将学习如何逐行创建文本滚动。

如何做…

我们现在将创建一个带有滚动文本的动画。按照以下步骤进行操作:

  1. 包含必要的头文件。

    #include "cinder/gl/Texture.h"
    #include "cinder/Text.h"
    #include "cinder/Font.h"
    #include "cinder/Utilities.h"
    
  2. 添加成员值。

    vector<gl::Texture> mTextTextures;
    Vec2f   mTextSize;
    
  3. setup方法内部,我们需要为每行文本生成纹理。

    setWindowSize(854, 480);
    string font( "Times New Roman" );
    
    mTextSize = Vec2f::zero();
    į
    for(int i = 0; i<5; i++) {
       TextLayout layout;
       layout.clear( ColorA(0.f,0.f,0.f, 0.f) );
       layout.setFont( Font( font, 48 ) );
       layout.setColor( Color( 1, 1, 1 ) );
       layout.addLine( "Animating text " + toString(i) );
       Surface8u rendered = layout.render( true );
       gl::TexturetextTexture = gl::Texture( rendered );
       textTexture.setMagFilter(GL_NICEST);
       textTexture.setMinFilter(GL_NICEST);
       mTextTextures.push_back(textTexture);
       mTextSize.x = math<float>::max(mTextSize.x, 
        textTexture.getWidth());
       mTextSize.y = math<float>::max(mTextSize.y, 
        textTexture.getHeight());
    }
    
  4. 此示例的draw方法如下所示:

    gl::enableAlphaBlending();
    gl::clear( Color::black() );
    gl::setViewport(getWindowBounds());
    gl::setMatricesWindowPersp(getWindowSize());
    
    gl::color(ColorA::white());
    
    float time = getElapsedSeconds()*0.5f;
    float timeFloor = math<float>::floor( time );
    inttexIdx = 1 + ( (int)timeFloor % (mTextTextures.size()-1) );
    float step = time - timeFloor;
    
    gl::pushMatrices();
    gl::translate(getWindowCenter() - mTextSize*0.5f);
    
    float radius = 30.f;
    gl::color(ColorA(1.f,1.f,1.f, 1.f-step));
    gl::pushMatrices();
    gl::rotate( Vec3f(90.f*step,0.f,0.f) );
    gl::translate(0.f,0.f,radius);
    gl::draw(mTextTextures[texIdx-1], Vec2f(0.f, -mTextTextures[texIdx-1].getHeight()*0.5f) );
    gl::popMatrices();
    
    gl::color(ColorA(1.f,1.f,1.f, step));
    gl::pushMatrices();
    gl::rotate( Vec3f(-90.f + 90.f*step,0.f,0.f) );
    gl::translate(0.f,0.f,radius);
    gl::draw(mTextTextures[texIdx], Vec2f(0.f, -mTextTextures[texIdx].getHeight()*0.5f) );
    gl::popMatrices();
    
    gl::popMatrices();
    

它是如何工作的…

在步骤 3 中,我们在setup方法内部首先执行的操作是生成带有渲染文本的纹理,并将其推送到向量结构mTextTextures中。

在步骤 4 中,你可以找到绘制当前和先前文本的代码,以构建连续循环动画。

如何工作…

使用 Perlin 噪声创建流动场

在本食谱中,我们将学习如何使用流动场来动画化对象。我们的流动场将是一个二维速度向量网格,它将影响对象的移动方式。

我们还将使用 Perlin 噪声计算出的向量来动画化流动场。

准备工作

包含必要的文件以使用 OpenGL 图形、Perlin 噪声、随机数和 Cinder 的数学工具。

#include "cinder/gl/gl.h"
#include "cinder/Perlin.h"
#include "cinder/Rand.h"
#include "cinder/CinderMath.h"

还要添加以下有用的using语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何做…

我们将使用流动场创建一个动画。执行以下步骤以实现此目的:

  1. 我们将首先创建一个Follower类来定义将受流动场影响的对象。

    在主应用程序类之前声明以下类:

    class Follower{
    public:
     Follower( const Vec2f& pos ){
      this->pos = pos;
     }
     void update( const Vec2f& newVel ){
      vel += ( newVel - vel ) * 0.2f;
      pos += vel;
      if( pos.x < 0.0f ){
       pos.x = (float)getWindowWidth();
       vel = Vec2f();
      }
      if( pos.x > (float)getWindowWidth() ){
       pos.x = 0.0f;
       vel = Vec2f();
      }
      if( pos.y < 0.0f ){
       pos.y = (float)getWindowHeight();
       vel = Vec2f();
      }
      if( pos.y > (float)getWindowHeight() ){
       pos.y = 0.0f;
       vel = Vec2f();
      } 
     }
     void draw(){
      gl::drawSolidCircle( pos, 5.0f );
      gl::drawLine( pos, pos + ( vel * 20.0f ) );
     }
     Vec2f pos, vel;
    };
    
  2. 让我们创建流动场。声明一个二维std::vector来定义流动场,以及定义向量间隔和行数列数的变量。

    vector< vector< Vec2f> > mFlowField;
    Vec2f mGap;
    float mCounter;
    int mRows, mColumns;
    
  3. setup方法中,我们将定义行数和列数,并计算每个向量之间的间隔。

    mRows = 40;
    mColumns = 40;
    mGap.x = (float)getWindowWidth() / (mRows-1);
    mGap.y = (float)getWindowHeight() / (mColumns-1);
    
  4. 根据行数和列数,我们可以初始化mFlowField

    mFlowField.resize( mRows );
    for( int i=0; i<mRows; i++ ){
      mFlowField[i].resize( mColumns );
    
  5. 让我们使用 Perlin 噪声来动画化流动场。为此,声明以下成员:

      Perlin mPerlin;
    float mCounter;
    
  6. setup方法中,将mCounter初始化为零。

      mCounter = 0.0f;
    }
    
  7. update方法中,我们将增加mCounter并使用嵌套的for循环遍历mFlowField,并使用mPerlin来动画化向量。

    for( int i=0; i<mRows; i++ ){
     for( int j=0; j<mColumns; j++ ){
      float angle= mPerlin.noise( ((float)i)*0.01f + mCounter,
       ((float)j)*0.01f ) * M_PI * 2.0f;
      mFlowField[i][j].x = cosf( angle );
      mFlowField[i][j].y = sinf( angle );
     } 
    }
    
  8. 现在,遍历mFlowField并绘制表示向量方向的线条。

    draw方法内部添加以下代码片段:

    for( int i=0 i<mRows; i++ ){
     for( int j=0; j<mColumns; j++ ){
      float x = (float)i*mGap.x;
      float y = (float)j*mGap.y;
      Vec2f begin( x, y );
      Vec2f end = begin + ( mFlowField[i][j] * 10.0f );
      gl::drawLine( begin, end );
     }
    }
    
  9. 让我们添加一些Followers。声明以下成员:

    vector<shared_ptr<Follower>> mFollowers;
    
  10. setup方法中,我们将初始化一些跟随者并将它们随机添加到窗口中的位置。

    int numFollowers = 50;
    for( int i=0; i<numFollowers; i++ ){
     Vec2f pos( randFloat( getWindowWidth() ), 
      randFloat(getWindowHeight() ) );
     mFollowers.push_back( 
      shared_ptr<Follower>( new Follower( pos ) ) );
    }
    
  11. 在更新中,我们将遍历mFollowers并根据其位置在mFlowField中计算相应的向量。

    然后,我们将使用该向量更新Follower类。

    for( vector<shared_ptr<Follower> >::iterator it = 
     mFollowers.begin(); it != mFollowers.end(); ++it ){
     shared_ptr<Follower> follower = *it;
     int indexX= ci::math<int>::clamp(follower->pos.x / mGap.x,0,
      mRows-1 );
     int indexY= ci::math<int>::clamp(follower->pos.y / mGap.y,0, 
      mColumns-1 );
     Vec2f flow = mFlowField[ indexX ][ indexY ];
     follower->update( flow );
    }
    
  12. 最后,我们只需要绘制每个Follower类。

    draw方法内部添加以下代码片段:

    for( vector< shared_ptr<Follower> >::iterator it = 
     mFollowers.begin(); it != mFollowers.end(); ++it ){
     (*it)->draw();
    }
    

    以下结果是:

如何做…

如何工作…

Follower类代表一个将跟随流动场的代理。在Follower::update方法中,将一个新的速度向量作为参数传递。follower对象将将其速度插值到传递的值中,并使用它进行动画。Follower::update方法还负责通过在对象位于窗口外部时扭曲其位置来保持每个代理在窗口内。

在步骤 11 中,我们计算了流场中影响Follower对象的向量,使用其位置。

创建一个 3D 图片库

在这个菜谱中,我们将学习如何创建一个 3D 图片库。图片将从用户选择的文件夹中加载,并以三维圆形方式显示。使用键盘,用户可以更改选定的图片。

准备工作

当启动应用程序时,您将被要求选择一个包含图片的文件夹,所以请确保您有一个。

此外,在您的代码中包含使用 OpenGL 绘图调用、纹理、时间轴和加载图片所需的必要文件。

#include "cinder/gl/gl.h"
#include "cinder/gl/Texture.h"
#include "cinder/Timeline.h"
#include "cinder/ImageIo.h"

此外,添加以下有用的using语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何操作…

我们将在 3D 空间中显示和动画图片。执行以下步骤以实现此目的:

  1. 我们将首先创建一个Image类。在主应用程序类之前添加以下代码片段:

    class Image{
    public:
    Image( gl::Texture texture, constRectf& maxRect ){
     this->texture = texture;
     distance = 0.0f;
     angle = 0.0f;
     Vec2f size = Vec2f(texture.getWidth(), texture.getHeight());
     rect = Rectf(-size * 0.5f, size*0.5f).getCenteredFit( 
      maxRect, true );
    }
    void draw(){
     gl::pushMatrices();
     glRotatef( angle, 0.0f, 1.0f, 0.0f );
     gl::translate( 0.0f, 0.0f, distance );
     gl::draw( texture, rect );
     gl::popMatrices();
    }
    gl::Texture texture;
    float distance;
    float angle;
    Rectfrect;
    }
    
  2. 在主应用程序类中,我们将声明以下成员:

    vector<shared_ptr<Image>> mImages;
    int mSelectedImageIndex;
    Anim<float> mRotationOffset;
    
  3. setup方法中,我们将要求用户选择一个文件夹,然后尝试从文件夹中的每个文件创建一个纹理。如果纹理成功创建,我们将使用它来创建一个Image对象并将其添加到mImages中。

    fs::path imageFolder = getFolderPath( "" );
    if( imageFolder.empty() == false ){
     for( fs::directory_iterator it( imageFolder ); it !=
      fs::directory_iterator(); ++it ){
      const fs::path& file = it->path();
      gl::Texture texture;
      try {
       texture = loadImage( file );
      } catch ( ... ) { }
      if( texture ){
       Rectf maxRect(RectfmaxRect( Vec2f( -50.0f, -50.0f),
        Vec2f( 50.0f,50.0f ) );
       mImages.push_back( shared_ptr<Image>( 
        new Image( texture, maxRect) ) );
      } 
     }
    }
    
  4. 我们需要遍历mImages并定义每个图片与中心的角度和距离。

    float angle = 0.0f;
    float angleAdd = 360.0f / mImages.size();
    float radius = 300.0f;
    for( int i=0; i<mImages.size(); i++ ){
     mImages[i]->angle = angle;
     mImages[i]->distance = radius;
     angle += angleAdd;
    }
    
  5. 现在,我们可以初始化剩余的成员。

    mSelectedImageIndex = 0;
    mRotationOffset = 0.0f;
    
  6. draw方法中,我们首先清除窗口,将窗口的矩阵设置为支持 3D,并启用深度缓冲区的读写:

    gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatricesWindowPersp( getWindowWidth(), getWindowHeight() );
    gl::enableDepthRead();
    gl::enableDepthWrite();
    
  7. 接下来,我们将绘制图片。由于所有图片都已围绕原点显示,我们必须将它们平移到窗口的中心。我们还将使用mRotationOffset中的值围绕 y 轴旋转它们。所有这些都将放在一个if语句中,该语句将检查mImages是否包含任何图片,以防在设置过程中没有生成图片。

  8. draw方法内部添加以下代码片段:

    if( mImages.size() ){
     Vec2f center = (Vec2f)getWindowCenter();
     gl::pushMatrices();
     gl::translate( center.x, center.y, 0.0f );
     glRotatef( mRotationOffset, 0.0f, 1.0f, 0.0f );
     for(vector<shared_ptr<Image> >::iterator it=mImages.begin();
      it != mImages.end(); ++it ){
      (*it)->draw();
     }
     gl::popMatrices();
    }
    
  9. 由于用户可以使用键盘切换图片,我们必须声明keyUp事件处理程序。

    void keyUp( KeyEvent event );
    
  10. keyUp的实现中,我们将根据左键或右键是否释放将图片移动到左侧或右侧。

    如果选定的图片已更改,我们将mRotationOffset动画到相应的值,以便正确的图片现在面向用户。

    keyUp方法内部添加以下代码片段:

    bool imageChanged = false;
    if( event.getCode() == KeyEvent::KEY_LEFT ){
     mSelectedImageIndex--;
     if( mSelectedImageIndex< 0 ){
      mSelectedImageIndex = mImages.size()-1;
      mRotationOffset.value() += 360.0f;
     }
     imageChanged = true;
    } else if( event.getCode() == KeyEvent::KEY_RIGHT ){
     mSelectedImageIndex++;
     if( mSelectedImageIndex>mImages.size()-1 ){
      mSelectedImageIndex = 0;
      mRotationOffset.value() -= 360.0f;
     }
     imageChanged = true;
    }
    if( imageChanged ){
     timeline().apply( &mRotationOffset, 
      mImages[ mSelectedImageIndex]->angle, 1.0f, 
      EaseOutElastic() );
    }
    
  11. 构建并运行应用程序。您将被提示选择一个包含图片的文件夹,然后图片将以圆形方式显示。按键盘上的左键或右键更改选定的图片。如何操作…

工作原理…

Image类的draw方法将围绕 y 轴旋转坐标系,然后在 z 轴上平移图像绘制。这将根据给定的角度从中心向外扩展图像。这是一个简单且方便的方法,无需处理坐标变换即可实现所需效果。

Image::rect成员用于绘制纹理,并计算以适应在构造函数中传入的矩形内。

在选择要显示在前面图像时,mRotationOffset的值将是图像角度的相反数,使其成为在视图中绘制的图像。

keyUp事件中,我们检查是否按下了左键或右键,并将mRotationOffset动画化到所需值。我们还考虑到角度是否绕过,以避免动画中的故障。

使用 Perlin 噪声创建球形流场

在这个菜谱中,我们将学习如何使用球形流场与 Perlin 噪声,并以有机的方式在球体周围动画化对象。

我们将使用球形坐标来动画化我们的对象,然后将其转换为笛卡尔坐标以绘制它们。

准备工作

添加必要的文件以使用 Perlin 噪声和用 OpenGL 绘制:

#include "cinder/gl/gl.h"
#include "cinder/Perlin.h"
#include "cinder/Rand.h"

添加以下有用的using语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何做到这一点...

我们将创建在球形流场中有机移动的Follower对象。执行以下步骤来完成此操作:

  1. 我们将首先创建一个表示将跟随球形流场的对象的Follower类。

    在应用程序类的声明之前添加以下代码片段:

    class Follower{
    public:
    Follower(){
     theta = 0.0f;
     phi = 0.0f;
    }
    void moveTo( const Vec3f& target ){
     prevPos = pos;
     pos += ( target - pos ) * 0.1f;
    }
    void draw(){
     gl::drawSphere( pos, 10.0f, 20 );
     Vec3f vel = pos - prevPos;
     gl::drawLine( pos, pos + ( vel * 5.0f ) );
    }
    Vec3f pos, prevPos;
    float phi, theta;
    };
    
  2. 我们将使用球形到笛卡尔坐标,因此在应用程序的类中声明以下方法:

    Vec3f sphericalToCartesians(sphericalToCartesians( float radius, float theta, float phi );
    
  3. 此方法的实现如下:

    float x = radius * sinf( theta ) * cosf( phi );
    float y = radius * sinf( theta ) * sinf( phi );
    float z = radius * cosf( theta );
    return Vec3f( x, y, z );
    
  4. 在应用程序的类中声明以下成员:

    vector<shared_ptr< Follower > > mFollowers;
    float mRadius;
    float mCounter;
    Perlin mPerlin;
    
  5. setup方法中,我们首先初始化mRadiusmCounter

    mRadius = 200.0f;
    mCounter = 0.0f;
    
  6. 现在,让我们创建 100 个跟随者并将它们添加到mFollowers中。我们还将为Follower对象的phitheta变量分配随机值,并设置它们的初始位置:

    int numFollowers = 100;
    for( int i=0; i<numFollowers; i++ ){
     shared_ptr<Follower> follower( new Follower() );
     follower->theta = randFloat( M_PI * 2.0f );
     follower->phi = randFloat( M_PI * 2.0f );
     follower->pos = sphericalToCartesian( mRadius, 
      follower->theta, follower->phi );
     mFollowers.push_back( follower );
    }
    
  7. update方法中,我们将动画化我们的对象。让我们首先增加mCounter

    mCounter += 0.01f;
    
  8. 现在,我们将遍历mFollowers中的所有对象,并根据跟随者的位置使用 Perlin 噪声来计算它在球形坐标上应该移动多少。然后我们将计算相应的笛卡尔坐标,并移动对象。

    update方法内部添加以下代码片段:

    float resolution = 0.01f;
    for( int i=0; i<mFollowers.size(); i++ ){
     shared_ptr<Follower> follower = mFollowers[i];
     Vec3f pos = follower->pos;
     float thetaAdd = mPerlin.noise( pos.x * resolution, 
      pos.y * resolution, mCounter ) * 0.1f;
     float phiAdd = mPerlin.noise( pos.y * resolution, 
      pos.z * resolution, mCounter ) * 0.1f;
     follower->theta += thetaAdd;
     follower->phi += phiAdd;
     Vec3f targetPos = sphericalToCartesian( mRadius, 
      follower->theta, follower->phi );
     follower->moveTo( targetPos );
    }
    
  9. 让我们转到draw方法,首先清除背景,设置窗口矩阵,并启用深度缓冲区的读写。

    gl::clear( Color( 0, 0, 0 ) ); 
    gl::setMatricesWindowPersp( getWindowWidth(), getWindowHeight() );
    gl::enableDepthRead();
    gl::enableDepthWrite();
    
  10. 由于跟随者正在围绕原点移动,我们将使用深灰色将它们平移到原点进行绘制。我们还将绘制一个白色球体,以便更好地理解运动。

    gl::pushMatrices();
    Vec2f center = getWindowCenter();
    gl::translate( center );
    gl::color( Color( 0.2f, 0.2f, 0.2f ) );
    for(vector<shared_ptr<Follower> >::iterator it = 
     mFollowers.begin(); it != mFollowers.end(); ++it ){
     (*it)->draw();
    }
    gl::color( Color::white() );
    gl::drawSphere( Vec3f(), mRadius, 100 );
    gl::popMatrices();
    

它是如何工作的...

我们使用 Perlin 噪声来计算Follower对象中thetaphi成员的变化。我们使用这些值,再加上mRadius,通过标准的球坐标到笛卡尔坐标的转换来计算对象的位置。由于 Perlin 噪声根据Follower对象的当前位置使用坐标来给出连贯的值,我们得到了相当于一个流场的等效效果。mCounter变量用于在第三维度中动画化流场。

如何工作...

参见

第十章。与用户交互

在这一章中,我们将学习如何接收和响应用户的输入。本章将涵盖以下菜谱:

  • 创建一个对鼠标做出响应的交互式对象

  • 将鼠标事件添加到我们的交互式对象中

  • 创建一个滑块

  • 创建一个响应式文本框

  • 使用多点触控拖动、缩放和旋转对象

简介

在这一章中,我们将创建图形对象,它们通过鼠标和触摸交互来响应用户。我们将学习如何创建具有自己事件的简单图形界面,以获得更大的灵活性。

创建一个对鼠标做出响应的交互式对象

在这个菜谱中,我们将创建一个 InteractiveObject 类,用于制作与鼠标光标交互的图形对象,并执行以下操作:

动作 描述
按下 用户在对象上按下鼠标按钮。
在对象外按下 用户在对象外按下鼠标按钮。
释放 在对象上按下鼠标按钮后释放,鼠标仍在对象上。
在对象外释放 鼠标按钮在对象外被释放。
悬停 光标移至对象上。
移出 光标移出对象。
拖动 光标在对象上被拖动,并且在按下对象之后。

对于之前的每个操作,都会调用一个虚拟方法,这将改变绘制对象的颜色。

此对象可以用作基类,以创建具有更有趣图形的交互式对象,例如纹理。

准备工作

将以下文件创建并添加到您的项目中:

  • InteractiveObject.h

  • InteractiveObject.cpp

在包含您的应用程序类的源文件中,包含 InteractiveObject.h 文件并添加以下 using 语句:

#include "InteractiveObject.h"
using namespace ci;
using namespace ci::app;
using namespace std;

如何操作…

我们将创建一个 InteractiveObject 类,并使其对鼠标事件做出响应。

  1. 移动到文件 InteractiveObject.h 并添加 #pragma once 指令以及包含以下文件:

    #pragma once
    
    #include "cinder/Rect.h"
    #include "cinder/Color.h"
    #include "cinder/app/MouseEvent.h"
    #include "cinder/gl/gl.h"
    #include "cinder/app/App.h"
    
  2. 声明 InteractiveObject 类:

    class InteractiveObject{
    public:
    InteractiveObject( const ci::Rectf& rect );
    virtual ~InteractiveObject();
    virtual void draw();
    virtual void pressed();
    virtual void pressedOutside();
    virtual void released();
    virtual void releasedOutside();
    virtual void rolledOver();
    virtual void rolledOut();
    virtual void dragged();
    void mouseDown( ci::app::MouseEvent& event );
    void mouseUp( ci::app::MouseEvent& event );
    void mouseDrag( ci::app::MouseEvent& event );
    void mouseMove( ci::app::MouseEvent& event );
    
    ci::Rectf rect;
    ci::Color pressedColor, idleColor, overColor, strokeColor;
    
    protected:
    bool mPressed, mOver;
    };
    
  3. 继续到 InteractiveObject.cpp 文件,让我们首先包含 InteractiveObject.h 文件并添加以下 using 语句:

    #include "InteractiveObject.h"
    
    using namespace ci;
    using namespace ci::app;
    using namespace std;
    
  4. 让我们从实现 构造函数析构函数 开始。

    InteractiveObject::InteractiveObject( const Rectf& rect ){
      this->rect = rect;
      pressedColor = Color( 1.0f, 0.0f, 0.0f );
      idleColor = Color( 0.7f, 0.7f, 0.7f );
      overColor = Color( 1.0f, 1.0f, 1.0f );
      strokeColor = Color( 0.0f, 0.0f, 0.0f );
      mPressed = false;
      mOver = false;
    }
    
    InteractiveObject::~InteractiveObject(){    
    }
    
  5. InteractiveObject::draw 方法中,我们将使用适当的颜色绘制矩形:

    void InteractiveObject::draw(){
     if( mPressed ){
      gl::color( pressedColor );
     } else if( mOver ){
      gl::color( overColor );
     } else {
      gl::color( idleColor );
     }
     gl::drawSolidRect( rect );
     gl::color( strokeColor );
     gl::drawStrokedRect( rect );
    }
    
  6. pressedreleasedrolledOverrolledOutdragged 方法中,我们将简单地向控制台输出刚刚发生的动作:

    void InteractiveObject::pressed(){
      console() << "pressed" << endl;
    }
    
    void InteractiveObject::pressedOutside(){
      console() << "pressed outside" << endl;
    }
    
    void InteractiveObject::released(){
      console() << "released" << endl;
    }
    
    void InteractiveObject::releasedOutside(){
      console() << "released outside" << endl;
    }
    
    void InteractiveObject::rolledOver(){
      console() << "rolled over" << endl;
    }
    
    void InteractiveObject::rolledOut(){
      console() << "rolled out" << endl;
    }
    
    void InteractiveObject::dragged(){
      console() << "dragged" << endl;
    }
    
  7. 在鼠标事件处理程序中,我们将检查光标是否在对象内,并相应地更新 mPressedmOver 变量。每次检测到动作时,我们也会调用相应的函数。

    void InteractiveObject::mouseDown( MouseEvent& event ){
      if( rect.contains( event.getPos() ) ){
        mPressed = true;
        mOver = false;
        pressed();
      }else{
          pressedOutside();
      }
    }
    
    void InteractiveObject::mouseUp( MouseEvent& event ){
     if( rect.contains( event.getPos() ) ){
      if( mPressed ){
       mPressed = false;
       mOver = true;
       released();
      }
     } else {
      mPressed = false;
      mOver = false;
      releasedOutside();
     } 
    }
    
    void InteractiveObject::mouseDrag( MouseEvent& event ){
     if( mPressed && rect.contains( event.getPos() ) ){
      mPressed = true;
      mOver = false;
      dragged();
     } 
    }
    
    void InteractiveObject::mouseMove( MouseEvent& event ){
     if( rect.contains( event.getPos() ) ){
      if( mOver == false ){
       mPressed = false;
       mOver = true;
       rolledOver();
      }
     } else {
      if( mOver ){
       mPressed = false;
       mOver = false;
       rolledOut();
      } 
     }
    
  8. 在我们的 InteractiveObject 类准备好之后,让我们转向应用程序的类源文件。让我们首先声明一个 InteractiveObject 对象。

    shared_ptr<InteractiveObject> mObject;
    
  9. setup 方法中,我们将初始化 mObject

    Rectf rect( 100.0f, 100.0f, 300.0f, 300.0f );
    mObject = shared_ptr<InteractiveObject>( new InteractiveObject( rect ) );
    
  10. 我们需要声明鼠标事件处理程序。

    void mouseDown( MouseEvent event );	
    void mouseUp( MouseEvent event );
    void mouseDrag( MouseEvent event );
    void mouseMove( MouseEvent event );
    
  11. 在前面方法的实现中,我们将简单地调用 mObject 的相应方法。

    void MyApp::mouseDown( MouseEvent event ){
      mObject->mouseDown( event );
    }
    
    void MyApp::mouseUp( MouseEvent event ){
      mObject->mouseUp( event );
    }
    
    void MyApp::mouseDrag( MouseEvent event ){
      mObject->mouseDrag( event );
    }
    
    void MyApp::mouseMove( MouseEvent event ){
      mObject->mouseMove( event );
    }
    
  12. draw 方法的实现中,我们将用黑色清除背景并调用 mObjectdraw 方法。

    gl::clear( Color( 0, 0, 0 ) ); 
    mObject->draw();
    
  13. 现在构建并运行应用程序。使用鼠标与对象进行交互。无论何时按下、释放、悬停或移出对象,都会向控制台发送一条消息,指示行为。

它是如何工作的…

InteractiveObject 类应作为交互对象的基类使用。pressedreleasedrolledOverrolledOutdragged 方法是专门设计为可重写的。

InteractiveObject 的鼠标处理程序在检测到动作时调用前面的方法。通过重写这些方法,可以实现特定的行为。

虚拟析构函数被声明,以便扩展类可以有自己的析构函数。

将鼠标事件添加到我们的交互对象中

在这个菜谱中,我们将继续使用之前的菜谱,创建一个响应鼠标的交互对象,并将鼠标事件添加到我们的 InteractiveObject 类中,以便其他对象可以注册并在鼠标事件发生时接收通知。

准备工作

从菜谱 创建一个响应鼠标的交互对象 中获取代码并将其添加到你的项目中,因为我们将继续使用之前创建的内容。

如何操作…

我们将使 InteractiveObject 类在与其光标交互时发送自己的事件。

  1. 让我们创建一个类,用作发送事件时的参数。在 InteractiveObject.h 文件中 InteractiveObject 类声明之前添加以下代码:

    class InteractiveObject;
    class InteractiveObjectEvent: public ci::app::Event{
    public:
    enum EventType{ Pressed, PressedOutside, Released,
     ReleasedOutside, RolledOut, RolledOver, Dragged };
    InteractiveObjectEvent( InteractiveObject *sender, 
     EventType type ){
     this->sender = sender;
     this->type = type;
    }
    
    InteractiveObject *sender;
    EventType type;
    };
    
  2. InteractiveObject 类中,我们需要声明一个成员,使用 ci::CallbakcMgr 类来管理已注册的对象。将以下代码声明为受保护的成员:

    ci::CallbackMgr< void(InteractiveObjectEvent) > mEvents;
    
  3. 现在,我们需要添加一个方法,以便其他对象可以注册以接收事件。由于该方法将使用模板,我们将在 InteraciveObject.h 文件中声明和实现它。添加以下成员方法:

    template< class T >
    ci::CallbackId addListener( T* listener, 
     void (T::*callback)(InteractiveObjectEvent) ){
     return mEvents.registerCb( std::bind1st( 
      std::mem_fun( callback ), listener ) );
    }
    
  4. 让我们再创建一个方法,以便对象可以注销以停止接收进一步的事件。声明以下方法:

    void removeListener( ci::CallbackId callId );
    
  5. 让我们实现 removeListener 方法。在 InteractiveObject.cpp 文件中添加以下代码:

    void InteractiveObject::removeListener( CallbackId callbackId ){
      mEvents.unregisterCb( callbackId );
    }
    
  6. 修改 mouseDownmouseUpmouseDragmouseMove 方法,以便在发生任何事件时调用 mEvents。这些方法的实现应如下所示:

    void InteractiveObject::mouseDown( MouseEvent& event ){
     if( rect.contains( event.getPos() ) ){
      mPressed = true;
      mOver = false;
      pressed();
      mEvents.call( InteractiveObjectEvent( this,
       InteractiveObjectEvent::Pressed ) );
     } else {
      pressedOutside();
      mEvents.call( InteractiveObjectEvent( this, 
       InteractiveObjectEvent::PressedOutside ) );
     } 
    }
    
    void InteractiveObject::mouseUp( MouseEvent& event ){
     if( rect.contains( event.getPos() ) ){
      if( mPressed ){
       mPressed = false;
       mOver = true;
       released();
       mEvents.call( InteractiveObjectEvent( this, 
        InteractiveObjectEvent::Released ) );
      }
     } else {
      mPressed = false;
      mOver = false;
      releasedOutside();
      mEvents.call( InteractiveObjectEvent( this, 
       InteractiveObjectEvent::ReleasedOutside ) );
     } 
    }
    
    void InteractiveObject::mouseDrag( MouseEvent& event ){
     if( mPressed && rect.contains( event.getPos() ) ){
      mPressed = true;
      mOver = false;
    
      dragged();
      mEvents.call( InteractiveObjectEvent( this,
      InteractiveObjectEvent::Dragged ) );
     }
    }
    
    void InteractiveObject::mouseMove( MouseEvent& event ){
     if( rect.contains( event.getPos() ) ){
      if( mOver == false ){
       mPressed = false;
       mOver = true;
       rolledOver();
       mEvents.call( InteractiveObjectEvent( this, 
        InteractiveObjectEvent::RolledOver ) );
      }
     } else {
      if( mOver ){
       mPressed = false;
       mOver = false;
       rolledOut();
       mEvents.call( InteractiveObjectEvent( this, 
        InteractiveObjectEvent::RolledOut ) );
      }
     }
    }
    
  7. 在我们的 InteractiveObject 类准备好之后,我们需要将我们的应用程序类注册以接收其事件。在你的应用程序类声明中添加以下方法:

    void receivedEvent( InteractiveObjectEvent event );
    
  8. 让我们实现 receivedEvent 方法。我们将检查接收到的事件类型,并将消息打印到控制台。

    void MyApp::receivedEvent( InteractiveObjectEvent event ){
    string text;
    switch( event.type ){
    case InteractiveObjectEvent::Pressed:
    text = "Pressed event";
    break;
    case InteractiveObjectEvent::PressedOutside:
    text = "Pressed outside event";
    break;
    case InteractiveObjectEvent::Released:
    text = "Released event";
    break;
    case InteractiveObjectEvent::ReleasedOutside:
    text = "Released outside event";
    break;
    case InteractiveObjectEvent::RolledOver:
    text = "RolledOver event";
    break;
    case InteractiveObjectEvent::RolledOut:
    text = "RolledOut event";
    break;
    case InteractiveObjectEvent::Dragged:
    text = "Dragged event";
    break;
    default:
    text = "Unknown event";      
        }
    console() << "Received " + text << endl;
    }
    
  9. 剩下的只是注册事件。在 setup 方法中,在 mObject 初始化后添加以下代码:

    mObject->addListener( this, &InteractiveObjectApp::receivedEvent );
    
  10. 现在构建并运行应用程序,并使用鼠标与窗口上的矩形进行交互。每当在 mObject 上发生鼠标事件时,我们的方法 receivedEvent 将被调用。

它是如何工作的...

我们正在使用模板类 ci::CallbakMgr 来管理我们的事件监听器。这个类接受一个模板,其中包含可以注册的方法的签名。在我们的前一个代码中,我们声明 mEventsci::CallbakcMgr<void(InteractiveObjectEvent)>; 类型,这意味着只有返回 void 并接收 InteractiveObejctEvent 作为参数的方法可以被注册。

模板方法 registerEvent 将接受一个对象指针和方法指针。这些通过 std::bind1st 绑定到 std::function 并添加到 mEvents 中。该方法将返回一个 ci::CallbackId,用于标识监听器。ci::CallbackId 可以用来注销监听器。

更多内容...

InteractiveObject 类对于创建用户界面非常有用。如果我们想使用三个纹理(用于显示按下、悬停和空闲状态)创建一个 Button 类,我们可以这样做:

  1. 包含 InteractiveObject.hcinder/gl/texture.h 文件:

    #include "InteractiveObject.h"
    #include "cinder/gl/Texture.h"
    
  2. 声明以下类:

    class Button: public InteractiveObject{
    public:
    Button( const ci::Rectf& rect, ci::gl::Texture idleTex, 
     ci::gl::Texture overTex, ci::gl::Texture pressTex)
    :InteractiveObject( rect )
    {
     mIdleTex = idleTex;
     mOverTex = overTex;
     mPressTex = pressTex;
    }
    
    virtual void draw(){
     if( mPressed ){
      ci::gl::draw( mPressTex, rect );
     } else if( mOver ){
      ci::gl::draw( mOverTex, rect );
     } else {
      ci::gl::draw( mPressTex, rect );
     }
    }
    
    protected:
    ci::gl::Texture mIdleTex, mOverTex, mPressTex;
    };
    

创建滑块

在这个菜谱中,我们将学习如何通过扩展本章中提到的 创建一个响应鼠标的交互对象 菜谱中的 InteractiveObject 类来创建滑块 UI 元素。

创建滑块

准备工作

请参考 创建一个响应鼠标的交互对象 菜谱以找到 InteractiveObject 类的头文件和源代码。

如何实现...

我们将创建一个 Slider 类,并展示如何使用它。

  1. 将一个名为 Slider.h 的新头文件添加到你的项目中:

    #pragma once
    
    #include "cinder/gl/gl.h"
    #include "cinder/Color.h"
    
    #include "InteractiveObject.h"
    
    using namespace std;
    using namespace ci;
    using namespace ci::app;
    
    class Slider : publicInteractiveObject {
    public:
    Slider( ) : InteractiveObject( Rectf(0,0, 100,10) ) {
     mValue = 0.f;
    }
    Vec2f   getPosition() { return rect.getUpperLeft(); }
    void    setPosition(Vec2f position) { rect.offset(position); }
    void    setPosition(float x, float y) { setPosition(Vec2f(x,y)); }
    float   getWidth() { return getSize().x; }
    float   getHeight() { return getSize().y; }
    Vec2f   getSize() { return rect.getSize(); }
    void    setSize(Vec2f size) { 
     rect.x2 = rect.x1+size.x; rect.y2 = rect.y1+size.y; 
    }
    void    setSize(float width, float height) { 
     setSize(Vec2f(width,height)); 
    }
    virtual float getValue() { return mValue; }
    virtual void setValue(float value) {
     mValue = ci::math<float>::clamp(value);
    }
    
    virtual void pressed() {
     InteractiveObject::pressed();
     dragged();
    }
    
    virtual void dragged() {
     InteractiveObject::dragged();
     Vec2i mousePos = AppNative::get()->getMousePos();
     setValue( (mousePos.x - rect.x1) / rect.getWidth() );
    }
    
    virtual void draw() {
     gl::color(Color::gray(0.7f));
     gl::drawSolidRect(rect);
     gl::color(Color::black());
     Rectf fillRect = Rectf(rect);
     fillRect.x2 = fillRect.x1 + fillRect.getWidth() * mValue;
     gl::drawSolidRect( fillRect );
    }
    
    protected:
    float mValue;
    };
    
  2. 在你的主应用程序类的源文件中包含之前创建的头文件:

    #include "Slider.h"
    
  3. 将新属性添加到你的主类中:

    shared_ptr<Slider> mSlider1;
    shared_ptr<Slider> mSlider2;
    shared_ptr<Slider> mSlider3;
    
  4. setup 方法中初始化 slider 对象:

    mSlider1 = shared_ptr<Slider>( new Slider() );
    mSlider1->setPosition(70.f, 20.f);
    mSlider1->setSize(200.f, 10.f);
    mSlider1->setValue(0.75f);
    
    mSlider2 = shared_ptr<Slider>( new Slider() );
    mSlider2->setPosition(70.f, 35.f);
    mSlider2->setValue(0.25f);
    
    mSlider3 = shared_ptr<Slider>( new Slider() );
    mSlider3->setPosition(70.f, 50.f);
    mSlider3->setValue(0.5f);
    
  5. 在你的 draw 方法中添加以下代码来绘制滑块:

    gl::enableAlphaBlending();
    gl::clear( Color::white() );
    gl::setViewport(getWindowBounds());
    gl::setMatricesWindow(getWindowSize());
    
    mSlider1->draw();
    gl::drawStringRight("Value 1:", mSlider1->getPosition()+Vec2f(-5.f, 3.f), Color::black());
    gl::drawString(toString(mSlider1->getValue()), mSlider1->getPosition()+Vec2f(mSlider1->getWidth()+5.f, 3.f), Color::black());
    
    mSlider2->draw();
    gl::drawStringRight("Value 2:", mSlider2->getPosition()+Vec2f(-5.f, 3.f), Color::black());
    gl::drawString(toString(mSlider2->getValue()), mSlider2->getPosition()+Vec2f(mSlider2->getWidth()+5.f, 3.f), Color::black());
    
    mSlider3->draw();
    gl::drawStringRight("Value 3:", mSlider3->getPosition()+Vec2f(-5.f, 3.f), Color::black());
    gl::drawString(toString(mSlider3->getValue()), mSlider3->getPosition()+Vec2f(mSlider3->getWidth()+5.f, 3.f), Color::black());
    

它是如何工作的...

我们通过继承和重写 InteractiveObject 类的方法和属性创建了 Slider 类。在第 1 步中,我们扩展了它,添加了控制 slider 对象位置和尺寸的方法。getValuesetValue 方法可以用来检索或设置 slider 的实际状态,其值可以从 0 变化到 1

在第 4 步中,你可以找到通过设置初始位置、大小和值来初始化示例滑块的代码,这些值是在创建 Slider 对象后立即设置的。我们正在绘制示例滑块,包括标题和当前状态的信息。

相关内容

  • 菜谱 创建一个响应鼠标的交互对象

  • 菜谱 使用多点触控拖动、缩放和旋转对象

创建一个响应式文本框

在这个菜谱中,我们将学习如何创建一个响应用户按键的文本框。当鼠标悬停在其上时,它将是活动的,当鼠标在框外释放时,它将是非活动的。

准备工作

从菜谱 创建一个响应鼠标的交互对象 中获取以下文件并将其添加到你的项目中:

  • InteractiveObject.h

  • InteractiveObject.cpp

创建并添加以下文件到你的项目中:

  • InteractiveTextBox.h

  • InteractiveTextBox.cpp

如何操作…

我们将创建一个继承自 InteractiveObject 并添加文本功能的 InteractiveTextBox 类。

  1. 转到文件 InteractiveTextBox.h 并添加 #pragma once 宏,包含必要的文件。

    #pragma once
    
    #include "InteractiveObject.h"
    #include "cinder/Text.h"
    #include "cinder/gl/Texture.h"
    #include "cinder/app/KeyEvent.h"
    #include "cinder/app/AppBasic.h"
    
  2. 现在声明 InteractiveTextBox 类,使其成为 InteractiveObject 的子类,并具有以下成员和方法:

    class InteractiveTextBox: public InteractiveObject{
    public:
        InteractiveTextBox( const ci::Rectf& rect );
    
        virtual void draw();
        virtual void pressed();
        virtual void releasedOutside();
    
        void keyDown( ci::app::KeyEvent& event );
        protected:
            ci::TextBox mTextBox;
        std::string mText;
        bool mActive;
        bool mFirstText;
    };
    
  3. 现在转到 InteractiveTextBox.cpp 文件,包含 InteractiveTextBox.h 文件,并添加以下 using 语句:

    #include "InteractiveTextBox.h"
    
    using namespace std;
    using namespace ci;
    using namespace ci::app;
    
  4. 现在让我们通过初始化父类和设置内部的 ci::TextBox 来实现构造函数。

    InteractiveTextBox::InteractiveTextBox( const Rectf& rect ):
    InteractiveObject( rect )
    {
      mActive = false;
      mText = "Write some text";
      mTextBox.setText( mText );
      mTextBox.setFont( Font( "Arial", 24 ) );
      mTextBox.setPremultiplied( true );
      mTextBox.setSize( Vec2i( rect.getWidth(), rect.getHeight() ) );
      mTextBox.setBackgroundColor( Color::white() );
      mTextBox.setColor( Color::black() );
      mFirstText = true;
    }
    
  5. InteractiveTextBox::draw 方法中,我们将根据 mTextBox 是否处于活动状态来设置其背景颜色。我们还将 mTextBox 渲染到 ci::gl::Texture 中并绘制它。

    void InteractiveTextBox::draw(){
     if( mActive ){
      mTextBox.setBackgroundColor( Color( 0.7f, 0.7f, 1.0f ) );
     } else {
      mTextBox.setBackgroundColor( Color::white() );
     }
     gl::color( Color::white() );
     gl::Texture texture = mTextBox.render();
     gl::draw( texture, rect );
    }
    
  6. 现在让我们实现重写的 pressedreleasedOutside 方法来定义 mActive 的值。

    void InteractiveTextBox::pressed(){
      mActive = true;
    }
    
    void InteractiveTextBox::releasedOutside(){
      mActive = false;
    }
    
  7. 最后,我们需要实现 keyPressed 方法。

    如果 mActive 为假,此方法将直接返回。否则,如果释放的键是 退格 键,我们将从 mText 中删除最后一个字母,或者,如果按下了其他任何键,我们将添加相应的字母。

    void InteractiveTextBox::keyDown( KeyEvent& event ){
     if( mActive == false ) return;
     if( mFirstText ){
      mText = "";
      mFirstText = false;
    
     }
     if( event.getCode() == KeyEvent::KEY_BACKSPACE ){
      if( mText.size() > 0 ){
       mText = mText.substr( 0, mText.size()-1 );
      }
     } else {
      const char character = event.getChar();
      mText += string( &character, 1 );
     }
     mTextBox.setText( mText );
    }
    
  8. 现在转到你的应用程序类源文件,包含以下文件和 using 语句:

    #include "InteractiveTextBox.h"
    
    using namespace ci;
    using namespace ci::app;
    using namespace std;
    
  9. 在你的应用程序类中声明以下成员:

    shared_ptr<InteractiveTextBox> mTextBox;
    
  10. 让我们在 setup 方法中初始化 mTextBox

    Rectf rect( 100.0f, 100.0f, 300.0f, 200.0f );
    mTextBox = shared_ptr<InteractiveTextBox>( new InteractiveTextBox( rect ) );
    
  11. draw 方法中,我们将用黑色清除背景,启用 AlphaBlending,并绘制我们的 mTextBox

      gl::enableAlphaBlending();
      gl::clear( Color( 0, 0, 0 ) );
      mTextBox->draw();
    
  12. 我们现在需要声明以下鼠标事件处理器:

    void mouseDown( MouseEvent event );
    void mouseUp( MouseEvent event );
    void mouseDrag( MouseEvent event );
    void mouseMove( MouseEvent event );
    
  13. 并通过调用 mTextBox 的相应鼠标事件处理器来实现它们:

    void MyApp::mouseDown( MouseEvent event ){
      mTextBox->mouseDown( event );
    }
    
    void MyApp::mouseUp( MouseEvent event ){
      mTextBox->mouseUp( event );
    }
    
    void MyApp::mouseDrag( MouseEvent event ){
      mTextBox->mouseDrag( event );
    }
    
    void MyApp::mouseMove( MouseEvent event ){
      mTextBox->mouseMove( event );
    }
    
  14. 现在我们只需要对键释放事件处理器做同样的处理。首先声明它:

    void keyDown( KeyEvent event );
    
  15. 在其实现中,我们将调用 mTextBoxkeyUp 方法。

    void InteractiveObjectApp::keyDown( KeyEvent event ){
      mTextBox->keyDown( event );
    }
    
  16. 现在构建并运行应用程序。你会看到一个带有短语 Write some text 的白色文本框。按下文本框并输入一些文本。点击文本框外以将文本框设置为非活动状态。

它是如何工作的…

内部,我们的 InteractiveTextBox 使用一个 ci::TextBox 对象。这个类管理一个具有指定宽度和高度的框内的文本。我们利用这一点,根据用户按下的键更新文本。

使用多点触控拖动、缩放和旋转对象

在本食谱中,我们将学习如何通过扩展本章中提到的 创建一个对鼠标做出响应的交互对象 食谱中提到的 InteractiveObject 类来创建负责多指手势的对象,例如拖动、缩放或旋转。我们将构建一个使用 iOS 设备多指功能的 iOS 应用程序。

使用多指拖动、缩放和旋转对象

准备工作

请参考 创建一个对鼠标做出响应的交互对象 食谱以找到 InteractiveObject 类的头文件和源代码,以及从 第一章 中创建 iOS 触摸应用程序项目的食谱。

如何操作…

我们将创建一个带有示例对象的 iPhone 应用程序,这些对象可以被拖动、缩放或旋转。

  1. 向你的项目添加一个名为 TouchInteractiveObject.h 的新头文件:

    #pragma once
    
    #include "cinder/app/AppNative.h"
    #include "cinder/gl/gl.h"
    #include "cinder/Color.h"
    
    #include "InteractiveObject.h"
    
    using namespace std;
    using namespace ci;
    using namespace ci::app;
    
    class TouchInteractiveObject : public InteractiveObject {
    public:
    TouchInteractiveObject( const Vec2f& position, 
     const Vec2f& size );
    bool    touchesBegan(TouchEvent event);
    bool    touchesMoved(TouchEvent event);
    bool    touchesEnded(TouchEvent event);
    Vec2f   getPosition() { return position; }
    void    setPosition(Vec2f position) { this->position = position; }
    void    setPosition(float x, float y) { setPosition(Vec2f(x,y)); }
    float   getWidth() { return getSize().x; }
    float   getHeight() { return getSize().y; }
    Vec2f   getSize() { return rect.getSize(); }
    void    setSize(Vec2f size) { 
     size.x = max(30.f,size.x); 
     size.y = max(30.f,size.y); 
     rect = Rectf(getPosition()-size*0.5f,getPosition()+size*0.5f);
    }
    void    setSize(float width, float height) {
     setSize(Vec2f(width,height)); 
    }
    float   getRotation() { return rotation; }
    void    setRotation( float rotation ) { 
     this->rotation = rotation;
    }
    virtual void draw();
    
    protected:
    Vec2f   position;
    float   rotation;
    bool    scaling;
    
    unsigned int    dragTouchId;
    unsigned int    scaleTouchId;
    };
    
  2. 向你的项目添加一个名为 TouchInteractiveObject.cpp 的新源文件,并通过添加以下代码行包含之前创建的头文件:

    #include "TouchInteractiveObject.h"
    
  3. 实现 TouchInteractiveObject 的构造函数:

    TouchInteractiveObject::TouchInteractiveObject( 
     const Vec2f& position, const Vec2f& size )
      : InteractiveObject( Rectf() )
    {
     scaling = false;
     rotation = 0.f;
     setPosition(position);
     setSize(size);
     AppNative::get()->registerTouchesBegan(this, 
      &TouchInteractiveObject::touchesBegan);
     AppNative::get()->registerTouchesMoved(this, 
      &TouchInteractiveObject::touchesMoved);
     AppNative::get()->registerTouchesEnded(this, 
      &TouchInteractiveObject::touchesEnded);
    }
    
  4. 实现触摸事件的处理程序:

    bool TouchInteractiveObject::touchesBegan(TouchEvent event)
    
    {
     Vec2f bVec1 = getSize()*0.5f;
     Vec2f bVec2 = Vec2f(getWidth()*0.5f, -getHeight()*0.5f);
     bVec1.rotate((rotation) * (M_PI/180.f));
     bVec2.rotate((rotation) * (M_PI/180.f));
     Vec2f bVec;
     bVec.x = math<float>::max( abs(bVec1.x), abs(bVec2.x));
     bVec.y = math<float>::max( abs(bVec1.y), abs(bVec2.y));
     Area activeArea = Area(position-bVec, position+bVec);
     for (vector<TouchEvent::Touch>::const_iterator it 
       = event.getTouches().begin(); 
       it != event.getTouches().end(); ++it) {
      if(activeArea.contains( it->getPos() )) {
       if(mPressed) {
        scaling = true;
        scaleTouchId = it->getId();
       } else {
        mPressed = true;
        dragTouchId = it->getId();
       }
      } 
     }
     return false;
    }
    
    bool TouchInteractiveObject::touchesMoved(TouchEvent event)
    {
     if(!mPressed) return false;
     const TouchEvent::Touch* dragTouch;
     const TouchEvent::Touch* scaleTouch;
     for (vector<TouchEvent::Touch>::const_iterator it 
       = event.getTouches().begin(); 
       it != event.getTouches().end(); ++it) {
      if (scaling && scaleTouchId == it->getId()) {
       scaleTouch = &(*it);
      }
      if(dragTouchId == it->getId()) {
       dragTouch = &(*it);
      }
     }
     if(scaling) {
      Vec2f prevPos = (dragTouch->getPrevPos() 
       + scaleTouch->getPrevPos()) * 0.5f;
      Vec2f curPos = (dragTouch->getPos() 
       + scaleTouch->getPos())*0.5f;
      setPosition(getPosition() + curPos - prevPos);
      Vec2f prevVec = dragTouch->getPrevPos() 
       - scaleTouch->getPrevPos();
      Vec2f curVec = dragTouch->getPos() - scaleTouch->getPos();
    
      float scaleFactor = (curVec.length() - prevVec.length()) 
       / prevVec.length();
      float sizeFactor = prevVec.length() / getSize().length();
      setSize(getSize() + getSize() * sizeFactor * scaleFactor);
    
      float angleDif = atan2(curVec.x, curVec.y) 
       - atan2(prevVec.x, prevVec.y);
      rotation += -angleDif * (180.f/M_PI);
     } else {
      setPosition(getPosition() + dragTouch->getPos() 
       - dragTouch->getPrevPos() );
     }
     return false;
    }
    
    bool TouchInteractiveObject::touchesEnded(TouchEvent event)
    {
     if(!mPressed) return false;
     for (vector<TouchEvent::Touch>::const_iterator it 
       = event.getTouches().begin(); 
       it != event.getTouches().end(); ++it) {
      if(dragTouchId == it->getId()) {
       mPressed = false;
       scaling = false;
      }
      if(scaleTouchId == it->getId()) {
       scaling = false;
      } 
     }
     return false;
    }
    
  5. 现在,为 TouchInteractiveObjects 实现基本的 draw 方法:

    void TouchInteractiveObject::draw() {
     Rectf locRect = Rectf(Vec2f::zero(), getSize());
     gl::pushMatrices();
     gl::translate(getPosition());
     gl::rotate(getRotation());
     gl::pushMatrices();
     gl::translate(-getSize()*0.5f);
     gl::color(Color::gray( mPressed ? 0.6f : 0.9f ));
     gl::drawSolidRect(locRect);
     gl::color(Color::black());
     gl::drawStrokedRect(locRect);
     gl::popMatrices();
     gl::popMatrices();
    }
    
  6. 这里是继承 TouchInteractiveObject 所有功能并重写 draw 方法的类,在这种情况下,我们希望我们的交互对象是一个圆形。将以下类定义添加到你的主源文件中:

    class Circle : publicTouchInteractiveObject {
    public:
     Circle(const Vec2f& position, const Vec2f& size)
       : TouchInteractiv eObject(position, size) {}
    
     virtual void draw() {
      gl::color(Color::gray( mPressed ? 0.6f : 0.9f ));
      gl::drawSolidEllipse(getPosition(), 
       getSize().x*0.5f, getSize().y*0.5f);
      gl::color(Color::black());
      gl::drawStrokedEllipse(getPosition(), 
       getSize().x*0.5f, getSize().y*0.5f);
     } 
    };
    
  7. 现在看看你的主应用程序类文件。包含必要的头文件:

    #include "cinder/app/AppNative.h"
    #include "cinder/Camera.h"
    #include "cinder/Rand.h"
    
    #include "TouchInteractiveObject.h"
    
  8. 添加 typedef 声明:

    typedef shared_ptr<TouchInteractiveObject> tio_ptr;
    
  9. 向你的应用程序类添加成员以处理对象:

    tio_ptr mObj1;
    tio_ptr mCircle;
    
  10. setup 方法中初始化对象:

    mObj1 = tio_ptr( new TouchInteractiveObject(getRandPos(), Vec2f(100.f,100.f)) );
    mCircle = tio_ptr( new Circle(getRandPos(), Vec2f(100.f,100.f)) );
    
  11. draw 方法很简单,如下所示:

    gl::setMatricesWindow(getWindowSize());
    gl::clear( Color::white() );
    mObj1->draw();
    mCircle->draw();
    
  12. 正如你在 setup 方法中看到的,我们使用了 getRandPos 函数,该函数返回屏幕边界内带有一些边距的随机位置:

    Vec2f MainApp::getRandPos()
    {
      return Vec2f( randFloat(30.f, getWindowWidth()-30.f),  randFloat(30.f, getWindowHeight()-30.f));
    }
    

它是如何工作的…

我们通过继承和重写 InteractiveObject 的方法和属性创建了 TouchInteractiveObject 类。我们还通过控制位置和尺寸的方法扩展了它。

在步骤 3 中,我们初始化属性并注册触摸事件的回调。下一步是实现这些回调。在 touchesBegan 事件中,我们检查对象是否被任何新的触摸所接触,但所有关于移动和手势的计算都在 touchesMoved 事件期间发生。

在步骤 6 中,你可以看到通过重写 draw 方法,如何简单地更改外观并保留 TouchInteractiveObject 的所有交互功能。

还有更多…

你可能会注意到一个问题,即当你拖动多个对象时,它们是重叠的。为了解决这个问题,我们将添加一个简单的对象激活管理器。

  1. 在你的 Cinder 应用程序中添加一个新的类定义:

    class ObjectsManager {
    public:
        ObjectsManager() { }
    
        void addObject( tio_ptr obj) {
            objects.push_front(obj);
        }
    
        void update() {
            bool rel = false;
            deque<tio_ptr>::const_iterator it;
            for(it = objects.begin(); it != objects.end(); ++it) {
                if( rel ) 
                    (*it)->release();
                else if( (*it)->isActive() )
                    rel = true;
            }
        }
    
    protected:
        deque<tio_ptr> objects;
    };
    
  2. 向你的应用程序的主类添加一个新的成员:

    shared_ptr<ObjectsManager> mObjMgr;
    
  3. setup方法的末尾初始化mObjMgr,这是对象的管理器,并添加之前初始化的交互对象:

    mObjMgr = shared_ptr<ObjectsManager>( new ObjectsManager() );
    mObjMgr->addObject( mObj1 );
    mObjMgr->addObject( mCircle );
    
  4. 按照以下方式将update方法添加到你的主类中:

    void MainApp::update()
    {
        mObjMgr->update();
    }
    
  5. TouchInteractiveObject类添加两个新方法:

    bool    isActive() { return mPressed; }
    void    release() { mPressed = false; }
    

第十一章. 感知和跟踪来自摄像头的输入

在本章中,我们将学习如何接收和处理来自摄像头或微软 Kinect 传感器等输入设备的数据。

以下菜谱将涵盖:

  • 从摄像头捕获

  • 基于颜色跟踪对象

  • 使用光流跟踪运动

  • 对象跟踪

  • 读取 QR 码

  • 使用 Kinect 构建 UI 导航和手势识别

  • 使用 Kinect 构建增强现实

从摄像头捕获

在这个菜谱中,我们将学习如何捕获和显示来自摄像头的帧。

准备工作

包含必要的文件以从摄像头捕获图像并将它们绘制到 OpenGL 纹理中:

#include "cinder/gl/gl.h"
#include "cinder/gl/Texture.h"
#include "cinder/Capture.h"

还需要添加以下 using 语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何做…

现在,我们将从摄像头捕获并绘制帧。

  1. 在你的应用程序类中声明以下成员:

        Capture mCamera;
        gl::Texture mTexture;
    
  2. setup 方法中,我们将初始化 mCamera

        try{
            mCamera = Capture( 640, 480 );
            mCamera.start();
        } catch( ... ){
            console() << "Could not initialize the capture" << endl;
    
  3. update 方法中,我们将检查 mCamera 是否已成功初始化。如果还有新的帧可用,将摄像头的图像复制到 mTexture

        if( mCamera ){
            if( mCamera.checkNewFrame() ){
                mTexture = gl::Texture( mCamera.getSurface() );
            }
        }
    
  4. draw 方法中,我们将简单地清除背景,检查 mTexture 是否已初始化,并将其图像绘制到屏幕上:

      gl::clear( Color( 0, 0, 0 ) ); 
        if( mTexture ){
            gl::draw( mTexture, getWindowBounds() );
        }
    

它是如何工作的…

ci::Capture 是一个类,它在苹果电脑上围绕 Quicktime,在 iOS 平台上围绕 AVFoundation,在 Windows 上围绕 Directshow 进行封装。在底层,它使用这些低级框架来访问和捕获来自网络摄像头的帧。

每当找到新的帧时,它的像素将被复制到 ci::Surface 方法中。在前面的代码中,我们在每个 update 方法中通过调用 ci::Capture::checkNewFrame 方法来检查是否有新的帧,并使用其表面更新我们的纹理。

更多…

还可以获取可用捕获设备的列表,并选择你希望开始的设备。

要请求设备列表并打印它们的信息,我们可以编写以下代码:

vector<Capture::DeviceRef> devices = Capture::getDevices();
for( vector<Capture::DeviceRef>::iterator it = devices.begin(); 
 it != devices.end(); ++it ){
 Capture::DeviceRef device = *it;
 console() << "Found device:" 
  << device->getName() 
  << " with ID:" << device->getUniqueId() << endl;
}

要使用特定设备初始化 mCapture,你只需在构造函数中将 ci::Capture::DeviceRef 作为第三个参数传递。

例如,如果你想用第一个设备初始化 mCapture,你应该编写以下代码:

vector<Capture::DeviceRef> devices = Capture::getDevices();
mCapture = Capture( 640, 480 devices[0] );

基于颜色跟踪对象

在这个菜谱中,我们将展示如何使用 OpenCV 库跟踪指定颜色的对象。

准备工作

在这个菜谱中,我们将使用 OpenCV,因此请参阅第三章 与 OpenCV 集成菜谱使用图像处理技术。我们还需要 InterfaceGl,它包含在第二章 准备开发设置 GUI 以调整参数 菜谱中。

如何做…

我们将创建一个应用程序,该应用程序使用所选颜色跟踪对象。

  1. 包含必要的头文件:

    #include "cinder/gl/gl.h"
    #include "cinder/gl/Texture.h"
    #include "cinder/Surface.h"
    #include "cinder/ImageIo.h"
    #include "cinder/Capture.h"
    #include "cinder/params/Params.h"
    #include  "CinderOpenCV.h"
    
  2. 添加成员以存储原始和处理的帧:

    Surface8u   mImage;
    
  3. 添加成员以存储跟踪对象的坐标:

    vector<cv::Point2f> mCenters;
    vector<float> mRadius;
    
  4. 添加成员以存储将传递给跟踪算法的参数:

    double mApproxEps;
    int mCannyThresh;
    
    ColorA      mPickedColor;
    cv::Scalar  mColorMin;
    cv::Scalar  mColorMax;
    
  5. 添加成员以处理捕获设备和帧纹理:

    Capture     mCapture;
    gl::Texture mCaptureTex;
    
  6. setup 方法中,我们将设置窗口尺寸并初始化捕获设备:

    setWindowSize(640, 480);
    
    try {
      mCapture = Capture( 640, 480 );
      mCapture.start();
    }
    catch( ... ) {
      console() <<"Failed to initialize capture"<<std::endl;
    }
    
  7. setup 方法中,我们必须初始化变量并设置 GUI 以预览跟踪的颜色值:

    mApproxEps = 1.0;
    mCannyThresh = 200;
    
    mPickedColor = Color8u(255, 0, 0);
    setTrackingHSV();
    
    // Setup the parameters
    mParams = params::InterfaceGl( "Parameters", Vec2i( 200, 150 ) );
    mParams.addParam( "Picked Color", &mPickedColor, "readonly=1" );
    
  8. update 方法中,检查是否有任何新帧需要处理并将其转换为 cv::Mat,这对于进一步的 OpenCV 操作是必要的:

    if( mCapture&&mCapture.checkNewFrame() ) {
      mImage = mCapture.getSurface();
      mCaptureTex = gl::Texture( mImage );
    
      cv::Mat inputMat( toOcv( mImage) );
      cv::resize(inputMat, inputMat, cv::Size(320, 240) );
    
      cv::Mat inputHSVMat, frameTresh;
      cv::cvtColor(inputMat, inputHSVMat, CV_BGR2HSV);
    
  9. 处理捕获的帧:

      cv::inRange(inputHSVMat, mColorMin, mColorMax, frameTresh);
    
      cv::medianBlur(frameTresh, frameTresh, 7);
    
      cv::Mat cannyMat;
      cv::Canny(frameTresh, cannyMat, mCannyThresh, mCannyThresh*2.f, 3 );
     vector< std::vector<cv::Point> >  contours;
     cv::findContours(cannyMat, contours, CV_RETR_LIST, 
      CV_CHAIN_APPROX_SIMPLE);
     mCenters = vector<cv::Point2f>(contours.size());
     mRadius = vector<float>(contours.size());
     for( int i = 0; i < contours.size(); i++ ) {
      std::vector<cv::Point> approxCurve;
      cv::approxPolyDP(contours[i], approxCurve, 
       mApproxEps, true);
      cv::minEnclosingCircle(approxCurve, mCenters[i], 
       mRadius[i]);
     }
    
  10. 关闭 if 语句的主体。

    }
    
  11. 实现方法 setTrackingHSV,它设置跟踪颜色的值:

    void MainApp::setTrackingHSV()
    {
    void MainApp::setTrackingHSV() {
     Color8u col = Color( mPickedColor );
     Vec3f colorHSV = col.get(CM_HSV);
     colorHSV.x *= 179;
     colorHSV.y *= 255;
     colorHSV.z *= 255;
     mColorMin = cv::Scalar(colorHSV.x-5, colorHSV.y -50, 
      colorHSV.z-50);
     mColorMax = cv::Scalar(colorHSV.x+5, 255, 255);
    }
    
  12. 实现鼠标按下事件处理器:

    void MainApp::mouseDown(MouseEvent event) {
     if( mImage&&mImage.getBounds().contains( event.getPos() ) ) {
      mPickedColor = mImage.getPixel( event.getPos() );
      setTrackingHSV();
     } 
    }
    
  13. 按如下方式实现 draw 方法:

    void MainApp::draw()
    {
     gl::clear( Color( 0.1f, 0.1f, 0.1f ) );
     gl::color(Color::white());
     if(mCaptureTex) {
      gl::draw(mCaptureTex);
      gl::color(Color::white());
      for( int i = 0; i <mCenters.size(); i++ )
      {
       Vec2f center = fromOcv(mCenters[i])*2.f;
       gl::begin(GL_POINTS);
       gl::vertex( center );
       gl::end();
       gl::drawStrokedCircle(center, mRadius[i]*2.f );
      }
     }
     params::InterfaceGl::draw();
    }
    

它是如何工作的…

通过准备捕获帧以进行处理,我们将其转换为 色调、饱和度和值HSV) 颜色空间描述方法,这在这种情况下非常有用。这些是描述 HSV 颜色空间中颜色的属性,以更直观的方式用于颜色跟踪。我们可以为检测设置一个固定的色调值,而饱和度和值可以在指定的范围内变化。这可以消除由相机视图中不断变化的光线引起的噪声。看看帧图像处理的第一个步骤;我们使用 cv::inRange 函数来获取适合我们跟踪颜色范围的像素掩码。跟踪颜色的范围是从窗口内部点击选择的颜色值计算得出的,这实现在 mouseDown 处理器和 setTrackingHSV 方法中。

正如您在 setTrackingHSV 内部所看到的,我们通过简单地扩大范围来计算 mColorMinmColorMax。您可能需要根据您的相机噪声和光照条件调整这些计算。

参见

使用光流跟踪运动

在这个配方中,我们将学习如何使用 OpenCV 和流行的 Lucas Kanade 光流算法跟踪来自网络摄像头的图像中的运动。

准备工作

在这个配方中,我们需要使用 OpenCV,因此请参考第三章(第三章。使用图像处理技术)中的 与 OpenCV 集成 配方,使用图像处理技术,并将 OpenCV 和 CinderBlock 添加到您的项目中。将以下文件包含到您的源文件中:

#include "cinder/Capture.h"
#include "cinder/gl/Texture.h"
#include "CinderOpenCV.h"

添加以下 using 语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何做到这一点…

我们将读取来自相机的帧并跟踪运动。

  1. 声明 ci::gl::Textureci::Capture 对象以显示和从相机捕获。同时,声明一个 cv::Mat 对象作为前一个帧,两个 std::vector<cv::Point2f> 对象以存储当前和前一个特征,以及一个 std::vector<uint8_t> 对象以存储每个特征的状态:

        gl::Texture mTexture;
        Capture mCamera;
        cv::Mat mPreviousFrame;
        vector< cv::Point2f > mPreviousFeatures, mFeatures;
        vector< uint8_t > mFeatureStatuses;
    
  2. setup方法中,我们将初始化mCamera

    try{
            mCamera = Capture( 640, 480 );
            mCamera.start();
        } catch( ... ){
            console() << "unable to initialize device" << endl;
        }
    
  3. update方法中,我们需要检查mCamera是否已正确初始化,并且是否有新的帧可用:

        if( mCamera ){
            if( mCamera.checkNewFrame() ){
    
  4. 在那些if语句之后,我们将获取mCameraci::Surface引用,并将其复制到我们的mTexture中以进行绘制:

                Surface surface = mCamera.getSurface();
                mTexture = gl::Texture( surface );
    
  5. 现在让我们创建一个cv::Mat,包含当前相机帧。我们还将检查mPreviousFrame是否包含任何初始化的数据,计算适合跟踪的良好特征,并计算它们从先前的相机帧到当前帧的运动:

                cv::Mat frame( toOcv( Channel( surface ) ) );
                if( mPreviousFrame.data != NULL ){
                    cv::goodFeaturesToTrack( frame, mFeatures, 300, 0.005f, 3.0f );
                    vector<float> errors;
                    mPreviousFeatures = mFeatures;
                    cv::calcOpticalFlowPyrLK( mPreviousFrame, frame, mPreviousFeatures, mFeatures, mFeatureStatuses, errors );
                }
    
  6. 现在我们只需将帧复制到mPreviousFrame并关闭初始的if语句:

                mPreviousFrame = frame;
            }
        }
    
  7. draw方法中,我们将首先用黑色清除背景,并绘制mTexture

      gl::clear( Color( 0, 0, 0 ) ); 
        if( mTexture ){
            gl::color( Color::white() );
            gl::draw( mTexture, getWindowBounds() );
        }
    
  8. 接下来,我们将使用mFeatureStatus在已跟踪的特征上绘制红色线条,以绘制已匹配的特征:

        glColor4f( 1.0f, 0.0f, 0.0f, 1.0f );
        for( int i=0; i<mFeatures.size(); i++ ){
            if( (bool)mFeatureStatuses[i] == false ) continue;
            gl::drawSolidCircle( fromOcv( mFeatures[i] ), 5.0f );
        }
    
  9. 最后,我们将使用mFeatureStatus绘制一条线,将先前的特征和当前的特征连接起来,以绘制已匹配的一个特征:

        for( int i=0; i<mFeatures.size(); i++ ){
            if( (bool)mFeatureStatuses[i] == false ) continue;
            Vec2f pt1 = fromOcv( mFeatures[i] );
            Vec2f pt2 = fromOcv( mPreviousFeatures[i] );
            gl::drawLine( pt1, pt2 );
        }
    

    在以下图像中,红色点代表适合跟踪的良好特征:

    如何做…

它是如何工作的...

光流算法将对跟踪点从一个帧移动到另一个帧的距离进行估计。

还有更多...

在这个菜谱中,我们使用cv::goodFeaturesToTrack对象来计算哪些特征最适合跟踪,但也可以手动选择我们希望跟踪的点。我们只需手动将我们希望跟踪的点填充到mFeatures中,并将其传递给cv::calcOpticalFlowPyrLK对象。

对象跟踪

在这个菜谱中,我们将学习如何使用 OpenCV 及其相应的 CinderBlock 在 Webcam 中跟踪特定的平面对象。

准备工作

您需要一个描述您希望在相机中跟踪的物理对象的图像。对于这个菜谱,请将此图像放置在assets文件夹中,并命名为object.jpg

在这个菜谱中,我们将使用 OpenCV CinderBlock,请参考第三章的与 OpenCV 集成菜谱,使用图像处理技术,并将 OpenCV 及其 CinderBlock 添加到您的项目中。

如果您使用的是 Mac,您需要自己编译 OpenCV 静态库,因为 OpenCV CinderBlock 在 OSX 上缺少一些必要的库(它将在 Windows 上正常工作)。您可以从以下链接下载正确的版本:sourceforge.net/projects/opencvlibrary/files/opencv-unix/2.3/

您需要自己使用提供的CMake文件编译静态库。一旦您的库正确添加到项目中,请包含以下文件:

#include "cinder/Capture.h"
#include "cinder/gl/Texture.h"
#include "cinder/ImageIo.h"

添加以下using语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何做...

我们将根据描述该对象的图像在相机帧中跟踪一个对象

  1. 让我们首先创建一个struct方法来存储用于特征跟踪和匹配的必要对象。在你的应用程序类声明之前添加以下代码:

    struct DetectionInfo{
        vector<cv::Point2f> goodPoints;
        vector<cv::KeyPoint> keyPoints;
        cv::Mat image, descriptor;
        gl::Texture texture;
    };
    
  2. 在你的类声明中添加以下成员对象:

    DetectionInfo mObjectInfo, mCameraInfo;
        cv::Mat mHomography;
        cv::SurfFeatureDetector mFeatureDetector;
        cv::SurfDescriptorExtractor mDescriptorExtractor;
        cv::FlannBasedMatcher mMatcher;
        vector<cv::Point2f> mCorners;
    
  3. setup方法中,让我们首先初始化相机:

        try{
            mCamera = Capture( 640, 480 );
            mCamera.start();
        } catch( ... ){
            console() << "could not initialize capture" << endl;
        }
    
  4. 让我们调整mCorners的大小,加载我们的物体图像,并计算其imagekeyPointstexturedescriptor

    mCorners.resize( 4 );
        Surface objectSurface = loadImage( loadAsset( "object.jpg" ) );
        mObjectInfo.texture = gl::Texture( objectSurface );
        mObjectInfo.image = toOcv( Channel( objectSurface ) );
        mFeatureDetector.detect( mObjectInfo.image, mObjectInfo.keyPoints );
        mDescriptorExtractor.compute( mObjectInfo.image, mObjectInfo.keyPoints, mObjectInfo.descriptor );
    
  5. update方法中,我们将检查mCamera是否已初始化,并且是否有新的帧需要处理:

        if( mCamera ){
            if( mCamera.checkNewFrame() ){
    
  6. 现在,让我们获取mCamera的表面并初始化mCameraInfotextureimage对象。我们将从cameraSurface创建一个ci::Channel对象,该对象将彩色表面转换为灰度通道表面:

    Surface cameraSurface = mCamera.getSurface();
    mCameraInfo.texture = gl::Texture( cameraSurface );
    mCameraInfo.image = toOcv( Channel( cameraSurface ) );
    
  7. 让我们计算mCameraInfofeaturesdescriptor值:

    mFeatureDetector.detect( mCameraInfo.image, mCameraInfo.keyPoints);
    mDescriptorExtractor.compute( mCameraInfo.image, mCameraInfo.keyPoints, mCameraInfo.descriptor );
    
  8. 现在,让我们使用mMatcher来计算mObjectInfomCameraInfo之间的匹配:

                vector<cv::DMatch> matches;
                mMatcher.match( mObjectInfo.descriptor, mCameraInfo.descriptor, matches );
    
  9. 为了进行测试以检查错误匹配,我们将计算匹配之间的最小距离:

    double minDist = 640.0;
    for( int i=0; i<mObjectInfo.descriptor.rows; i++ ){
                    double dist = matches[i].distance;
                    if( dist < minDist ){
                        minDist = dist;
                    }
                }
    
  10. 现在,我们将添加所有距离小于minDist*3.0的点到mObjectInfo.goodPoints.clear();

    mCameraInfo.goodPoints.clear();
    for( vector<cv::DMatch>::iterator it = matches.begin(); 
     it != matches.end(); ++it ){
     if( it->distance < minDist*3.0 ){
      mObjectInfo.goodPoints.push_back( 
       mObjectInfo.keyPoints[ it->queryIdx ].pt );
      mCameraInfo.goodPoints.push_back( 
       mCameraInfo.keyPoints[ it->trainIdx ].pt );
     }
    
    
  11. }在我们所有的点都计算并匹配后,我们需要计算mObjectInfomCameraInfo之间的单应性:

    mHomography = cv::findHomography( mObjectInfo.goodPoints, mCameraInfo.goodPoints, CV_RANSAC );
    
  12. 让我们创建一个vector<cv::Point2f>,包含我们物体的角点,并执行透视变换来计算相机图像中物体的角点:

    小贴士

    不要忘记关闭我们之前打开的括号。

      vector<cv::Point2f> objCorners( 4 );
      objCorners[0] = cvPoint( 0.0f, 0.0f );
      objCorners[1] = cvPoint( mObjectInfo.image.cols, 0.0f);
      objCorners[2] = cvPoint( mObjectInfo.image.cols, 
       mObjectInfo.image.rows );
      objCorners[3] = cvPoint( 0.0f, mObjectInfo.image.rows);
      mCorners = vector< cv::Point2f >( 4 );
      cv::perspectiveTransform( objCorners, mCorners, 
       mHomography );
     } 
    }
    
  13. 让我们转到draw方法,首先清除背景并绘制相机和物体纹理:

      gl::clear( Color( 0, 0, 0 ) );
    
        gl::color( Color::white() );
        if( mCameraInfo.texture ){
            gl::draw( mCameraInfo.texture, getWindowBounds() );
        }
    
        if( mObjectInfo.texture ){
            gl::draw( mObjectInfo.texture );
        }
    
  14. 现在,让我们遍历mObjectInfomCameraInfo中的goodPoints值并绘制它们:

    for( int i=0; i<mObjectInfo.goodPoints.size(); i++ ){
     gl::drawStrokedCircle( fromOcv( mObjectInfo.goodPoints[ i ] ),
      5.0f );
     gl::drawStrokedCircle( fromOcv( mCameraInfo.goodPoints[ i ] ),
      5.0f );
     gl::drawLine( fromOcv( mObjectInfo.goodPoints[ i ] ), 
      fromOcv( mCameraInfo.goodPoints[ i ] ) );
    }
    
  15. 现在,让我们遍历mCorners并绘制找到的物体的角点:

    gl::color( Color( 1.0f, 0.0f, 0.0f ) );
        gl::begin( GL_LINE_LOOP );
        for( vector<cv::Point2f>::iterator it = mCorners.begin(); it != mCorners.end(); ++it ){
            gl::vertex( it->x, it->y );
        }
        gl::end();
    
  16. 构建并运行应用程序。拿起你在object.jpg图像中描述的物理物体,并将其放在图像前面。程序将尝试在相机图像中跟踪该物体,并在图像中绘制其角点。

它是如何工作的…

我们使用加速鲁棒特征SURF)特征检测器和描述符来识别特征。在步骤 4 中,我们计算特征和描述符。我们使用一个cv::SurfFeatureDetect对象来计算物体上的良好特征以进行跟踪。然后,cv::SurfDescriptorExtractor对象使用这些特征来创建我们物体的描述。在步骤 7 中,我们对相机图像做同样的处理。

在步骤 8 中,我们使用一个名为cv::FlannBasedMatcher快速近似最近邻库FLANN)来执行操作。这个匹配器从相机帧和我们的物体中获取描述,并计算它们之间的匹配。

在步骤 9 和 10 中,我们使用匹配之间的最小距离来消除可能的错误匹配。结果传递到mObjectInfo.goodPointsmCameraInfo.goodPoints

在步骤 11 中,我们计算图像和摄像头之间的单应性。单应性是使用射影几何从一个空间到另一个空间的投影变换。我们在步骤 12 中使用它来对 mCorners 应用透视变换,以识别摄像头图像中的对象角落。

更多内容…

要了解更多关于 SURF 是什么以及它是如何工作的信息,请参考以下网页:en.wikipedia.org/wiki/SURF.

要了解更多关于 FLANN 的信息,请参考网页 en.wikipedia.org/wiki/Nearest_neighbor_search.

要了解更多关于单应性的信息,请参考以下网页:

en.wikipedia.org/wiki/Homography.

读取 QR 码

在本例中,我们将使用 ZXing 库进行 QR 码读取。

准备工作

请从 GitHub 下载 Cinder ZXing 模块并将其解压到 blocks 文件夹:github.com/dawidgorny/Cinder-ZXing

如何操作…

现在我们将创建一个 QR 码读取器:

  1. 将头文件搜索路径添加到项目的构建设置中:

    $(CINDER_PATH)/blocks/zxing/include
    
  2. 将预编译的 ZXing 库路径添加到项目的构建设置中:$(CINDER_PATH)/blocks/zxing/lib/macosx/libzxing.a。对于调试配置,使用 $(CINDER_PATH)/blocks/zxing/lib/macosx/libzxing_d.a

  3. 按照以下方式将 Cinder ZXing 模块文件添加到项目结构中:如何操作…

  4. libiconv.dylib 库添加到 Link Binary With Libraries 列表中:如何操作…

  5. 添加必要的头文件:

    #include "cinder/gl/Texture.h"
    #include "cinder/Surface.h"
    #include "cinder/Capture.h"
    
    #include <zxing/qrcode/QRCodeReader.h>
    #include <zxing/common/GlobalHistogramBinarizer.h>
    #include <zxing/Exception.h>
    #include <zxing/DecodeHints.h>
    
    #include "CinderZXing.h"
    
  6. 将以下成员添加到您的应用程序主类中:

    Capture     mCapture;
    Surface8u   mCaptureImg;
    gl::Texture mCaptureTex;
    bool        mDetected;
    string      mData;
    
  7. setup 方法中,设置窗口尺寸并从摄像头初始化捕获:

    setWindowSize(640, 480);
    
    mDetected = false;
    
    try {
        mCapture = Capture( 640, 480 );
        mCapture.start();
    }
    catch( ... ) {
        console() <<"Failed to initialize capture"<< std::endl;
    }
    
  8. 按照以下方式实现 update 函数:

    if( mCapture && mCapture.checkNewFrame() ) {
        mCaptureImg = mCapture.getSurface();
        mCaptureTex = gl::Texture( mCaptureImg );
    
        mDetected = false;
    
    try {
            zxing::Ref<zxing::SurfaceBitmapSource> source(new zxing::SurfaceBitmapSource(mCaptureImg));
    
            zxing::Ref<zxing::Binarizer> binarizer(NULL);
            binarizer = new zxing::GlobalHistogramBinarizer(source);
    
            zxing::Ref<zxing::BinaryBitmap> image(new zxing::BinaryBitmap(binarizer));
            zxing::qrcode::QRCodeReader reader;
            zxing::DecodeHints hints(zxing::DecodeHints::BARCODEFORMAT_QR_CODE_HINT);
    
            zxing::Ref<zxing::Result> result( reader.decode(image, hints) );
    
            console() <<"READ("<< result->count() <<") : "<< result->getText()->getText() << endl;
    
    if( result->count() ) {
                mDetected = true;
                mData = result->getText()->getText();
            }
    
        } catch (zxing::Exception& e) {
            cerr <<"Error: "<< e.what() << endl;
        }
    
    }
    
  9. 按照以下方式实现 draw 函数:

    gl::clear( Color( 0.1f, 0.1f, 0.1f ) );
    
    gl::color(Color::white());
    
    if(mCaptureTex) {
        gl::draw(mCaptureTex);
    
    }
    
    if(mDetected) {
        Vec2f pos = Vec2f( getWindowWidth()*0.5f, getWindowHeight()-100.f );
        gl::drawStringCentered(mData, pos);
    }
    

工作原理…

我们正在使用常规的 ZXing 库方法。由 Cinder ZXing 模块提供的 SurfaceBitmapSource 类实现了与 Cinder Surface 类型对象的集成。当 QR 码被检测并读取时,mDetected 标志被设置为 true,读取的数据存储在 mData 成员中。

工作原理…

使用 Kinect 构建 UI 导航和手势识别

在本食谱中,我们将创建由 Kinect 传感器控制的交互式 GUI。

小贴士

由于 Kinect for Windows SDK 仅适用于 Windows,因此本食谱仅适用于 Windows 用户。

使用 Kinect 构建 UI 导航和手势识别

准备工作

在本例中,我们使用的是我们在第十章“与用户交互”中的“创建对鼠标响应的交互对象”食谱中介绍的 InteractiveObject 类。

www.microsoft.com/en-us/kinectforwindows/ 下载并安装 Kinect for Windows SDK。

从 GitHub 下载 KinectSDK CinderBlock github.com/BanTheRewind/Cinder-KinectSdk,并将其解压到blocks目录。

如何做到这一点…

我们现在将创建一个通过手势控制的手势 Cinder 应用程序。

  1. 包含必要的头文件:

    #include "cinder/Rand.h"
    #include "cinder/gl/Texture.h"
    #include "cinder/Utilities.h"
    
    #include "Kinect.h"
    #include "InteractiveObject.h";
    
  2. 使用以下语句添加 Kinect SDK:

    using namespace KinectSdk;
    
  3. 按如下方式实现挥手手势识别的类:

    class WaveHandGesture {
    public:
      enum GestureCheckResult { Fail, Pausing, Suceed };
    
    private:
      GestureCheckResult checkStateLeft( const Skeleton & skeleton ) {
        // hand above elbow
        if (skeleton.at(JointName::NUI_SKELETON_POSITION_HAND_RIGHT).y > skeleton.at(JointName::NUI_SKELETON_POSITION_ELBOW_RIGHT).y)
        {
          // hand right of elbow
          if (skeleton.at(JointName::NUI_SKELETON_POSITION_HAND_RIGHT).x > skeleton.at(JointName::NUI_SKELETON_POSITION_ELBOW_RIGHT).x)
          {
            return Suceed;
          }
          return Pausing;
        }
        return Fail;
      }
      GestureCheckResult checkStateRight( const Skeleton & skeleton ) {
        // hand above elbow
        if (skeleton.at(JointName::NUI_SKELETON_POSITION_HAND_RIGHT).y > skeleton.at(JointName::NUI_SKELETON_POSITION_ELBOW_RIGHT).y)
        {
          // hand left of elbow
          if (skeleton.at(JointName::NUI_SKELETON_POSITION_HAND_RIGHT).x < skeleton.at(JointName::NUI_SKELETON_POSITION_ELBOW_RIGHT).x)
          {
            return Suceed;
          }
          return Pausing;
        }
        return Fail;
      }
    
      int currentPhase;
    
    public:
      WaveHandGesture() {
        currentPhase = 0;
      }
    
      GestureCheckResult check( const Skeleton & skeleton )
      {
        GestureCheckResult res;
        switch(currentPhase) {
        case0: // start on left
        case2: 
          res = checkStateLeft(skeleton);
          if( res == Suceed ) { currentPhase++; }
          elseif( res == Fail ) { currentPhase = 0; return Fail; }
          return Pausing;
          break;
        case1: // to the right
        case3: 
          res = checkStateRight(skeleton);
          if( res == Suceed ) { currentPhase++; }
          elseif( res == Fail ) { currentPhase = 0; return Fail; }
          return Pausing;
          break;
        case4: // to the left
          res = checkStateLeft(skeleton);
          if( res == Suceed ) { currentPhase = 0; return Suceed; }
          elseif( res == Fail ) { currentPhase = 0; return Fail; }
          return Pausing;
          break;
        }
    
        return Fail;
      }
    };
    
  4. 实现NuiInteractiveObject类,该类扩展了InteractiveObject类:

    class NuiInteractiveObject: public InteractiveObject {
    public:
      NuiInteractiveObject(const Rectf & rect) : InteractiveObject(rect) {
        mHilight = 0.0f;
      }
    
      void update(bool activated, const Vec2f & cursorPos) {
        if(activated && rect.contains(cursorPos)) {
          mHilight += 0.08f;
        } else {
          mHilight -= 0.005f;
        }
        mHilight = math<float>::clamp(mHilight);
      }
    
      virtualvoid draw() {
        gl::color(0.f, 0.f, 1.f, 0.3f+0.7f*mHilight);
        gl::drawSolidRect(rect);
      }
    
      float mHilight;
    };
    
  5. 实现NuiController类,该类管理活动对象:

    class NuiController {
    public:
      NuiController() {}
    
      void registerObject(NuiInteractiveObject *object) {
        objects.push_back( object );
      }
    
      void unregisterObject(NuiInteractiveObject *object) {
        vector<NuiInteractiveObject*>::iterator it = find(objects.begin(), objects.end(), object);
        objects.erase( it );
      }
    
      void clear() { objects.clear(); }
    
      void update(bool activated, const Vec2f & cursorPos) {
        vector<NuiInteractiveObject*>::iterator it;
        for(it = objects.begin(); it != objects.end(); ++it) {
          (*it)->update(activated, cursorPos);
        }
      }
    
      void draw() {
        vector<NuiInteractiveObject*>::iterator it;
        for(it = objects.begin(); it != objects.end(); ++it) {
          (*it)->draw();
        }
      }
    
      vector<NuiInteractiveObject*> objects;
    };
    
  6. 将成员添加到主应用程序类中,用于处理 Kinect 设备和数据:

    KinectSdk::KinectRef          mKinect;
    vector<KinectSdk::Skeleton>   mSkeletons;
    gl::Texture                   mVideoTexture;
    
  7. 添加成员以存储计算出的光标位置:

    Rectf  mPIZ;
    Vec2f  mCursorPos;
    
  8. 添加我们将用于手势识别和用户激活的成员:

    vector<WaveHandGesture*>    mGestureControllers;
    bool  mUserActivated;
    int  mActiveUser;
    
  9. 添加一个处理NuiController的成员:

    NuiController* mNuiController;
    
  10. 通过实现prepareSettings设置窗口设置:

    void MainApp::prepareSettings(Settings* settings)
    {
      settings->setWindowSize(800, 600);
    }
    
  11. setup方法中,为成员设置默认值:

    mPIZ = Rectf(0.f,0.f, 0.85f,0.5f);
    mCursorPos = Vec2f::zero();
    
    mUserActivated = false;
    mActiveUser = 0;
    
  12. setup方法中,为10个用户初始化 Kinect 和手势识别:

    mKinect = Kinect::create();
    mKinect->enableDepth( false );
    mKinect->enableVideo( false );
    mKinect->start();
    
    for(int i = 0; i <10; i++) {
        mGestureControllers.push_back( new WaveHandGesture() );
    }
    
  13. setup方法中,初始化由NuiInterativeObject类型对象组成用户界面:

    mNuiController = new NuiController();
    
    float cols = 10.f;
    float rows = 10.f;
    
    Rectf rect = Rectf(0.f,0.f, getWindowWidth()/cols - 1.f, getWindowHeight()/rows - 1.f);
    
    or(int ir = 0; ir < rows; ir++) {
     for(int ic = 0; ic < cols; ic++) {
      Vec2f offset = (rect.getSize()+Vec2f::one()) 
       * Vec2f(ic,ir);
      Rectf r = Rectf( offset, offset+rect.getSize() );
      mNuiController->registerObject( 
       new NuiInteractiveObject® );
     } 
    }
    
  14. update方法中,我们正在检查 Kinect 设备是否正在捕获、获取跟踪骨骼以及迭代:

    if ( mKinect->isCapturing() ) {
     if ( mKinect->checkNewSkeletons() ) {
      mSkeletons = mKinect->getSkeletons();
     }
     uint32_t i = 0;
     vector<Skeleton>::const_iterator skeletonIt;
     for (skeletonIt = mSkeletons.cbegin(); 
       skeletonIt != mSkeletons.cend(); ++skeletonIt, i++ ) {
    
  15. 在循环内部,我们正在检查骨骼是否完整,如果不完整,则停用光标控制:

      if(mUserActivated && i == mActiveUser 
       && skeletonIt->size() != 
       JointName::NUI_SKELETON_POSITION_COUNT ) {
       mUserActivated = false;
      }
    
  16. 在循环内部检查骨骼是否有效。注意我们只处理 10 个骨骼。你可以修改这个数字,但请记住在mGestureControllers中提供足够的动作控制器数量:

    if ( skeletonIt->size() == JointName::NUI_SKELETON_POSITION_COUNT && i <10 ) {
    
  17. 在循环和if语句内部,检查完成的激活手势。当骨骼被激活时,我们正在计算人机交互区域:

    if( !mUserActivated || ( mUserActivated && i != mActiveUser ) ) {
        WaveHandGesture::GestureCheckResult res;
        res = mGestureControllers[i]->check( *skeletonIt );
    
        if( res == WaveHandGesture::Suceed && ( !mUserActivated || i != mActiveUser ) ) {
            mActiveUser = i;
    
         float armLen = 0;
            Vec3f handRight = skeletonIt->at(JointName::NUI_SKELETON_POSITION_HAND_RIGHT);
            Vec3f elbowRight = skeletonIt->at(JointName::NUI_SKELETON_POSITION_ELBOW_RIGHT);
            Vec3f shoulderRight = skeletonIt->at(JointName::NUI_SKELETON_POSITION_SHOULDER_RIGHT);
    
            armLen += handRight.distance( elbowRight );
            armLen += elbowRight.distance( shoulderRight );
    
            mPIZ.x2 = armLen;
            mPIZ.y2 = mPIZ.getWidth() / getWindowAspectRatio();
    
            mUserActivated = true;
        }
    }
    
  18. 在循环和if语句内部,我们正在计算活动用户的鼠标位置:

    if(mUserActivated && i == mActiveUser) {
        Vec3f handPos = skeletonIt->at(JointName::NUI_SKELETON_POSITION_HAND_RIGHT);
    
        Rectf piz = Rectf(mPIZ);
        piz.offset( skeletonIt->at(JointName::NUI_SKELETON_POSITION_SPINE).xy() );
    
        mCursorPos = handPos.xy() - piz.getUpperLeft();
        mCursorPos /= piz.getSize();
        mCursorPos.y = (1.f - mCursorPos.y);
        mCursorPos *= getWindowSize();
    }
    
  19. 关闭打开的if语句和for循环:

            }
        }
    }
    
  20. update方法的末尾,更新NuiController对象:

    mNuiController->update(mUserActivated, mCursorPos);
    
  21. 按如下方式实现draw方法:

    void MainApp::draw()
    {
      // Clear window
      gl::setViewport( getWindowBounds() );
      gl::clear( Color::white() );
      gl::setMatricesWindow( getWindowSize() );
      gl::enableAlphaBlending();
    
      mNuiController->draw();
    
      if(mUserActivated) {
        gl::color(1.f,0.f,0.5f, 1.f);
        glLineWidth(10.f);
        gl::drawStrokedCircle(mCursorPos, 25.f);
      }
    }
    

它是如何工作的…

该应用程序正在使用 Kinect SDK 跟踪用户。活动用户的骨骼数据用于根据 Microsoft 提供的 Kinect SDK 文档中的指南计算鼠标位置。激活是通过挥手手势触发的。

这是一个用户通过手势控制的鼠标控制的 UI 响应示例。光标下的网格元素会亮起,并在移出时淡出。

使用 Kinect 构建增强现实

在本食谱中,我们将学习如何结合 Kinect 的深度和图像帧来创建增强现实应用程序。

小贴士

由于 Kinect for Windows SDK 仅适用于 Windows,因此本食谱仅适用于 Windows 用户。

准备工作

www.microsoft.com/en-us/kinectforwindows/下载并安装 Kinect for Windows SDK。

从 GitHub 下载 KinectSDK CinderBlock,网址为github.com/BanTheRewind/Cinder-KinectSdk,并将其解压到blocks目录中。

在此示例中,我们使用 Cinder 包中提供的示例程序之一中的资源。请将cinder_0.8.4_mac/samples/Picking3D/resources/中的ducky.mshducky.pngphong_vert.glslphong_frag.glsl文件复制到您的assets文件夹中。

如何做到这一点…

我们现在将创建一个使用示例 3D 模型的增强现实应用程序。

  1. 包含必要的头文件:

    #include "cinder/app/AppNative.h"
    #include "cinder/gl/Texture.h"
    #include "cinder/gl/GlslProg.h"
    #include "cinder/TriMesh.h"
    #include "cinder/ImageIo.h"
    #include "cinder/MayaCamUI.h"
    #include "cinder/params/Params.h"
    #include "cinder/Utilities.h"
    
    #include "Kinect.h"
    
  2. 添加 Kinect SDK 的using语句:

    using namespace KinectSdk;
    
  3. 向主应用程序类中添加用于处理 Kinect 设备和数据的成员:

    KinectSdk::KinectRef            mKinect;
    vector<KinectSdk::Skeleton>     mSkeletons;
    gl::Texture                     mVideoTexture;
    
  4. 向存储 3D 摄像机场景属性的成员中添加成员:

    CameraPersp        mCam;
    Vec3f              mCamEyePoint;
    float              mCamFov;
    
  5. 向存储校准设置的成员中添加成员:

    Vec3f    mPositionScale;
    float    mActivationDist;
    
  6. 添加将存储几何形状、纹理和着色器程序的 3D 对象的成员:

    gl::GlslProg  mShader;
    gl::Texture   mTexture;
    TriMesh       mMesh;
    
  7. setup方法内部,设置窗口尺寸和初始值:

    setWindowSize(800, 600);
    
    mCamEyePoint = Vec3f(0.f,0.f,1.f);
    mCamFov = 33.f;
    
    mPositionScale = Vec3f(1.f,1.f,-1.f);
    mActivationDist = 0.6f;
    
  8. setup方法内部加载 3D 对象的几何形状、纹理和着色器程序:

    mMesh.read( loadFile( getAssetPath("ducky.msh") ) );
    
    gl::Texture::Format format;
    format.enableMipmapping(true);
    ImageSourceRef img = loadImage( getAssetPath("ducky.png") );
    if(img) mTexture = gl::Texture( img, format );
    
    mShader = gl::GlslProg( loadFile(getAssetPath("phong_vert.glsl")), loadFile(getAssetPath("phong_frag.glsl")) );
    
  9. setup方法内部,初始化 Kinect 设备并开始捕获:

    mKinect = Kinect::create();
    mKinect->enableDepth( false );
    mKinect->start();
    
  10. setup方法的末尾,创建用于参数调整的 GUI:

    mParams = params::InterfaceGl( "parameters", Vec2i( 200, 500 ) );
    mParams.addParam("Eye Point", &mCamEyePoint);
    mParams.addParam("Camera FOV", &mCamFov);
    mParams.addParam("Position Scale", &mPositionScale);
    mParams.addParam("Activation Distance", &mActivationDist);
    
  11. 按如下方式实现update方法:

    void MainApp::update()
    {
      mCam.setPerspective( mCamFov, getWindowAspectRatio(), 0.1, 10000 );
      mCam.setEyePoint(mCamEyePoint);
      mCam.setViewDirection(Vec3f(0.f,0.f, -1.f*mCamEyePoint.z));
    
      if ( mKinect->isCapturing() ) {
        if ( mKinect->checkNewVideoFrame() ) {
          mVideoTexture = gl::Texture( mKinect->getVideo() );
        }
        if ( mKinect->checkNewSkeletons() ) {
          mSkeletons = mKinect->getSkeletons();
        }
      }
    }
    
  12. 实现将使用纹理和着色应用来绘制我们的 3D 模型的drawObject方法:

    void MainApp::drawObject()
    {
    
      mTexture.bind();
      mShader.bind();
      mShader.uniform("tex0", 0);
    
      gl::color( Color::white() );
      gl::pushModelView();
      gl::scale(0.05f,0.05f,0.05f);
      gl::rotate(Vec3f(0.f,-30.f,0.f));
      gl::draw( mMesh );
      gl::popModelView();
    
      mShader.unbind();
      mTexture.unbind();
    }
    
  13. 按如下方式实现draw方法:

    void MainApp::draw()
    {
      gl::setViewport( getWindowBounds() );
      gl::clear( Colorf( 0.1f, 0.1f, 0.1f ) );
      gl::setMatricesWindow( getWindowSize() );
    
      if ( mKinect->isCapturing() && mVideoTexture ) {
        gl::color( ColorAf::white() );
        gl::draw( mVideoTexture, getWindowBounds() );
        draw3DScene();
      }
    
      params::InterfaceGl::draw();
    }
    
  14. 最后缺少的是在draw方法内部调用的draw3DScene方法。按如下方式实现draw3DScene方法:

    gl::enableDepthRead();
    gl::enableDepthWrite();
    
    Vec3f mLightDirection = Vec3f( 0, 0, -1 );
    ColorA mColor = ColorA( 0.25f, 0.5f, 1.0f, 1.0f );
    
    gl::pushMatrices();
    gl::setMatrices( mCam );
    
    vector<KinectSdk::Skeleton>::const_iterator skelIt;
    for ( skelIt = mSkeletons.cbegin(); skelIt != mSkeletons.cend(); ++skelIt ) {
    
    if ( skelIt->size() == JointName::NUI_SKELETON_POSITION_COUNT ) {
            KinectSdk::Skeleton skel = *skelIt;
    
            Vec3f pos, dV;
    float armLen = 0;
            Vec3f handRight = skeletonIt->at(JointName::NUI_SKELETON_POSITION_HAND_RIGHT);
            Vec3f elbowRight = skeletonIt->at(JointName::NUI_SKELETON_POSITION_ELBOW_RIGHT);
            Vec3f shoulderRight = skeletonIt->at(JointName::NUI_SKELETON_POSITION_SHOULDER_RIGHT);
    
            armLen += handRight.distance( elbowRight );
            armLen += elbowRight.distance( shoulderRight );
    
            pos = skel[JointName::NUI_SKELETON_POSITION_HAND_RIGHT];
            dV = pos - skel[JointName::NUI_SKELETON_POSITION_SHOULDER_RIGHT];
    if( dV.z < -armLen*mActivationDist ) {
                gl::pushMatrices();
                gl::translate(pos*mPositionScale);
                drawObject();
                gl::popMatrices();
            }
        }
    }
    
    gl::popMatrices();
    
    gl::enableDepthRead(false);
    gl::enableDepthWrite(false);
    
  15. 实现用于在程序终止时停止从 Kinect 捕获的shutdown方法:

    void MainApp::shutdown()
    {
      mKinect->stop();
    }
    

它是如何工作的…

应用程序正在使用 Kinect SDK 跟踪用户。用户的骨骼数据用于计算从 Cinder 示例程序中获取的 3D 鸭模型的坐标。当用户的手在用户面前时,3D 模型将渲染在用户的右手上方。激活距离是通过mActivationDist成员值计算得出的。

它是如何工作的…

要正确地将 3D 场景叠加到视频帧上,您必须根据 Kinect 视频摄像机设置相机 FOV。为此,我们使用Camera FOV属性。

第十二章。使用音频输入和输出

在本章中,我们将通过物理模拟驱动声音生成的示例来学习如何生成声音。我们还将展示使用音频反应动画可视化声音的示例。

以下食谱将涵盖:

  • 生成正弦振荡器

  • 使用频率调制生成声音

  • 添加延迟效果

  • 在物体碰撞时生成声音

  • 可视化 FFT

  • 制作声音反应粒子

生成正弦振荡器

在本食谱中,我们将学习如何通过操纵声卡的 PCM脉冲编码调制)音频缓冲区来生成正弦波振荡器。正弦波的频率将由鼠标的 y 坐标定义。

我们还将绘制正弦波以进行可视化表示。

准备工作

包含以下文件:

#include "cinder/audio/Output.h"
#include "cinder/audio/Callback.h"
#include "cinder/Rand.h"
#include "cinder/CinderMath.h"

并添加以下有用的 using 语句:

using namespace ci;
using namespace ci::app;
using namespace std;

如何操作…

我们将按照以下步骤创建正弦波振荡器:

  1. 声明以下成员变量和回调方法:

    void audioCallback( uint64_t inSampleOffset, uint32_t ioSampleCount, audio::Buffer32f *buffer );
    float mFrequency;
    float mPhase, mPhaseAdd;
    vector<float> mOutput;
    
  2. setup 模块中,我们将初始化变量并使用以下代码创建音频回调:

    mFrequency = 0.0f;
    mPhase = 0.0f;
    mPhaseAdd = 0.0f;
    audio::Output::play( audio::createCallback( this, &SineApp::audioCallback ) );
    
  3. update 模块中,我们将根据鼠标的 y 位置更新 mFrequency。鼠标的位置将被映射并夹在 05000 之间的频率值:

    float maxFrequency = 5000.0f;
    float targetFrequency = ( getMousePos().y / (float)getWindowHeight() ) * maxFrequency;
    mFrequency = math<float>::clamp( targetFrequency, 0.0f, maxFrequency );
    

    让我们实现音频回调。我们首先根据需要调整 mOutput 的大小。然后我们将计算并插值 mPhaseAdd,接着遍历音频缓冲区中的所有值,并根据 mPhase 的正弦值计算它们的值,并将 mPhaseAdd 添加到 mPhase

    if( mOutput.size() != ioSampleCount ){
     mOutput.resize( ioSampleCount );
    }
    int numChannels = buffer->mNumberChannels;
    mPhaseAdd += ( ( mFrequency / 44100.0f ) - mPhaseAdd ) * 0.1f;
    for( int i=0; i<ioSampleCount; i++ ){
     mPhase += mPhaseAdd;
     float output = math<float>::sin( mPhase * 2.0f * M_PI );
     for( int j=0; j<numChannels; j++ ){
      buffer->mData[ i*numChannels + j ] = output;
     }
     mOutput[i] = output;
    }
    
  4. 最后,我们需要绘制正弦波。在 draw 方法中,我们将用黑色清除背景,并使用存储在 mOutput 中的值绘制放大后的正弦波,使用线条条带:

    gl::clear( Color( 0, 0, 0 ) );
    if( mOutput.size() > 0 ){
     Vec2f scale;
     scale.x = (float)getWindowWidth() / (float)mOutput.size();
     scale.y = 100.0f;
     float centerY= getWindowHeight() / 2.0f;
     gl::begin( GL_LINE_STRIP );
     for( int i=0; i<mOutput.size(); i++ ){
      float x = (float)i * scale.x;
      float y = mOutput[i] * scale.y + centerY;
      gl::vertex( x,  y );
     } 
     gl::end();
    }
    
  5. 构建并运行应用程序。垂直移动鼠标以更改频率。以下屏幕截图显示了表示生成的正弦波的线条:如何操作…

它是如何工作的…

我们正在操作 PCM 缓冲区。PCM 是一种通过在固定间隔内对值进行采样来表示音频的方法。通过访问 PCM 缓冲区,我们可以直接操纵声卡将输出的音频信号。

每当调用 audioCallback 方法时,我们都会收到 PCM 缓冲区的一个样本,其中我们计算生成连续正弦波所需的值。

update 模块中,我们通过映射鼠标的 y 位置来计算频率。

audioCallback 实现的以下行中,我们根据 44100 的采样率计算 mPhase 需要增加多少以生成频率为 mFrequency 的波:

mPhaseAdd += ( ( mFrequency / 44100.0f ) - mPhaseAdd ) * 0.1f;

使用频率调制生成声音

在本食谱中,我们将学习如何使用另一个低频正弦波来调制正弦波振荡器。

我们将基于前一个菜谱,其中鼠标的 y 位置控制正弦波的频率;在这个菜谱中,我们将使用鼠标的 x 位置来控制调制频率。

准备工作

我们将使用前一个菜谱中的代码,生成正弦振荡器

如何做…

我们将使用前一个菜谱中创建的正弦波与另一个低频正弦波相乘。

  1. 添加以下成员变量:

    float mModFrequency;
    float mModPhase, mModPhaseAdd;
    
  2. setup 模块中添加以下内容以初始化之前创建的变量:

    mModFrequency = 0.0f;
    mModPhase = 0.0f;
    mModPhaseAdd = 0.0f;
    
  3. update 模块中,添加以下代码以根据鼠标光标的 x 位置计算调制频率:

    float maxModFrequency= 30.0f;
    float targetModFrequency= ( getMousePos().x / (float)getWindowWidth() ) * maxModFrequency;
    mModFrequency = math<float>::clamp( targetModFrequency, 0.0f, maxModFrequency );
    
  4. 我们需要使用 mModFrequencymModPhasemModPhaseAdd 计算另一个正弦波,并使用它来调制我们的第一个正弦波。

    以下是对 audioCallback 的实现:

    if( mOutput.size() != ioSampleCount ){
     mOutput.resize( ioSampleCount );
    }
    mPhaseAdd += ( ( mFrequency / 44100.0f ) - mPhaseAdd ) * 0.1f;
    mModPhaseAdd += ( ( mModFrequency / 44100.0f ) - mModPhaseAdd )
      * 0.1f;
    int numChannels= buffer->mNumberChannels;
    for( int i=0; i<ioSampleCount; i++ ){
     mPhase += mPhaseAdd;
     mModPhase += mModPhaseAdd;
     float output = math<float>::sin( mPhase * 2.0f * M_PI ) 
       * math<float>::sin( mModPhase * 2.0f * M_PI );
     for( int j=0; j<numChannels; j++ ){
      buffer->mData[ i*numChannels + j ] = output;
     }
     mOutput[i] = output;
    }
    
  5. 构建并运行应用程序。将鼠标光标移至 y 轴以确定频率,移至 x 轴以确定调制频率。

我们可以在前一个菜谱中看到正弦波的变化,它在乘以另一个低频正弦波时,幅度会发生变化。

如何做…

它是如何工作的…

我们使用低频振荡(LFO)计算第二个正弦波,并使用它来调制第一个正弦波。为了调制波,我们将它们相乘。

添加延迟效果

在这个菜谱中,我们将学习如何将延迟效果添加到前一个菜谱中生成的频率调制音频。

准备工作

我们将使用前一个菜谱,使用频率调制生成声音 的源代码。

如何做…

我们将通过以下步骤存储我们的音频值并在一段时间后播放它们,以实现延迟效果:

  1. 添加以下成员变量:

    int mDelay;
    float mMix, mFeedback;
    vector<float> mDelayLine;
    int mDelayIndex;
    int mDelaySize;
    

    让我们初始化上面创建的变量,并用零初始化我们的延迟行。

    然后在 setup 方法中添加以下内容:

    mDelay = 200;
    mMix = 0.2f;
    mFeedback = 0.3f;
    mDelaySize = mDelay * 44.1f;
    for( int i=0; i<mDelaySize; i++ ){
     mDelayLine.push_back( 0.0f );
    }
    
  2. 在我们 audioCallback 方法的实现中,我们将从缓冲区中读取在频率调制中生成的值,并计算延迟。

    最终值再次传递到缓冲区以输出。

    audioCallback 方法中添加以下代码:

    for( int i=0; i<ioSampleCount; i++ ){
     float output = buffer->mData[ i*numChannels ];
     int readIndex= mDelayIndex - mDelaySize + 1;
     if( readIndex< 0 ) readIndex += mDelaySize;
     float delay = mDelayLine[ readIndex * numChannels ];
     mDelayLine[ mDelayIndex ] = output + delay * mFeedback;
     if( ++mDelayIndex == mDelaySize ){
      mDelayIndex = 0;
     }
     output = math<float>::clamp(output+mMix*delay,-1.0f,1.0f);
     mOutput[i] = output;
     for( int j=0; j<numChannels; j++ ){
      buffer->mData[ i*numChannels + j ] = output;
     } 
    }
    
  3. 构建并运行应用程序。通过在 x 轴上移动鼠标,你控制振荡器频率,通过在 y 轴上移动鼠标,你控制调制频率。输出将包含以下截图所示的延迟效果:如何做…

它是如何工作的…

延迟是一种音频效果,其中输入被存储,然后在确定的时间后播放。我们通过创建一个大小为 mDelay 乘以频率率的缓冲区来实现这一点。每次 audioCallback 被调用时,我们从延迟行中读取,并使用当前输出值更新延迟行。然后我们将延迟值添加到输出中,并前进 mDelayIndex

在对象碰撞时生成声音

在本配方中,我们将学习如何将简单的物理应用到对象粒子,并在两个对象碰撞时生成声音。

准备工作

在本例中,我们使用本章中描述的“生成正弦振荡器”配方中的代码,请参阅该配方。

如何操作…

我们将创建一个 Cinder 应用程序来展示该机制:

  1. 包含以下必要的头文件:

    #include "cinder/audio/Output.h"
    #include "cinder/audio/Callback.h"
    #include "cinder/Rand.h"
    #include "cinder/CinderMath.h"
    #include "ParticleSystem.h"
    
  2. 向应用程序的main类添加成员以进行粒子模拟:

    ParticleSystem mParticleSystem;
    Vec2fattrPosition;
    float attrFactor;
    float attrRadius;
    
  3. 向应用程序的main类添加成员以使粒子交互式:

    bool    dragging;
    Particle *dragParticle;
    
  4. 添加生成声音的成员:

    void audioCallback( uint64_t inSampleOffset, uint32_t ioSampleCount,audio::Buffer32f *buffer );
    float mSndFrequency;
    float mPhase, mPhaseAdd;
    vector<float> mOutput;
    
  5. setup方法中初始化粒子系统:

    mRunning= true;
    dragging = false;
    attrPosition = getWindowCenter();
    attrRadius = 75.f;
    attrFactor = 0.02f;
    int numParticle= 10;
    for( int i=0; i<numParticle; i++ ){
     float x = Rand::randFloat( 0.0f, getWindowWidth() );
     float y = Rand::randFloat( 0.0f, getWindowHeight() );
     float radius = Rand::randFloat(2.f, 40.f);
     Rand::randomize();
     float mass = radius;
     float drag = 0.95f;
     Particle *particle = new Particle( Vec2f( x, y ), radius,
      mass, drag );
     mParticleSystem.addParticle( particle );
    }
    
  6. setup方法中初始化生成声音的成员并注册音频回调:

    mSndFrequency = 0.0f;
    mPhase = 0.0f;
    mPhaseAdd = 0.0f;
    audio::Output::play( audio::createCallback( this, &MainApp::audioCallback ) );
    
  7. 实现一个resize方法,以便在应用程序窗口调整大小时更新吸引子位置:

    void MainApp::resize(ResizeEvent event)
    {
      attrPosition = getWindowCenter();
    }
    
  8. 实现鼠标事件处理程序以与粒子进行交互:

    void MainApp::mouseDown(MouseEvent event)
    
    {
     dragging = false;
     std::vector<Particle*>::iterator it;
     for( it = mParticleSystem.particles.begin(); 
       it != mParticleSystem.particles.end(); ++it ) {
      if( (*it)->position.distance(event.getPos()) 
        < (*it)->radius ) {
       dragging = true;
       dragParticle = (*it);
      }
     }
    }
    
    void MainApp::mouseUp(MouseEvent event) {
     dragging = false;
    }
    
  9. update方法中,添加以下代码进行声音频率计算:

    float maxFrequency = 15000.0f;
    float targetFrequency = ( getMousePos().y / (float)getWindowHeight() ) * maxFrequency;
    targetFrequency = mSndFrequency - 10000.f;
    mSndFrequency = math<float>::clamp( targetFrequency, 0.0f, maxFrequency );
    
  10. update方法中,添加以下代码进行粒子运动计算。在此阶段,我们正在检测碰撞并计算声音频率:

    std::vector<Particle*>::iterator it;
    for( it = mParticleSystem.particles.begin(); 
      it != mParticleSystem.particles.end(); ++it ) {
     std::vector<Particle*>::iterator it2;
     for( it2 = mParticleSystem.particles.begin(); 
       it2 != mParticleSystem.particles.end(); ++it2 ) {
      float d = (*it)->position.distance( (*it2)->position );
      float d2 = (*it)->radius + (*it2)->radius;
      if(d >0.f&& d <= d2 ) {
       (*it)->forces += -1.1f * ( (*it2)->position 
         - (*it)->position );
       (*it2)->forces += -1.1f * ( (*it)->position 
         - (*it2)->position );
       mSndFrequency = 2000.f;
       mSndFrequency+= 10000.f
         * (1.f - ((*it)->radius / 40.f));
       mSndFrequency+= 10000.f 
         * (1.f - ((*it2)->radius / 40.f));
      }
     }
     Vec2f attrForce = attrPosition - (*it)->position;
     attrForce *= attrFactor;
     (*it)->forces += attrForce;
    }
    mSndFrequency = math<float>::clamp( mSndFrequency, 
     0.0f, maxFrequency );maxFrequency );
    
  11. 更新拖拽粒子的位置(如果有),并更新粒子系统:

    if(dragging) {
      dragParticle->forces = Vec2f::zero();
      dragParticle->position = getMousePos();
    }
    
    mParticleSystem.update();
    
  12. 通过以下方式实现draw方法来绘制粒子:

    gl::clear( Color::white() );
    gl::setViewport(getWindowBounds());
    gl::setMatricesWindow( getWindowWidth(), getWindowHeight() );
    gl::color( Color::black() );
    mParticleSystem.draw();
    
  13. 实现与“生成正弦振荡器”配方中所述的音频回调处理程序。

它是如何工作的…

我们正在生成应用了物理和碰撞检测的随机粒子。当检测到碰撞时,根据粒子的半径计算正弦波的频率。

它是如何工作的…

update方法中,我们正在遍历粒子并检查它们之间的距离以检测碰撞,如果发生碰撞。从碰撞粒子的半径计算出一个频率,半径越大,声音的频率越低。

可视化 FFT

在本配方中,我们将展示一个示例,说明如何在圆形布局上使用FFT快速傅里叶变换)数据可视化,并添加一些平滑动画。

准备工作

将您最喜欢的音乐作品以music.mp3的名称保存在资产文件夹中。

如何操作…

我们将根据以下步骤创建基于示例 FFT 分析的可视化:

  1. 包含以下必要的头文件:

    #include "cinder/gl/gl.h"
    #include "cinder/audio/Io.h"
    #include "cinder/audio/Output.h"
    #include "cinder/audio/FftProcessor.h"
    #include "cinder/audio/PcmBuffer.h"
    
  2. 向您的应用程序主类添加以下成员:

    void drawFft();
    audio::TrackRef mTrack;
    audio::PcmBuffer32fRef mPcmBuffer;
    uint16_t bandCount;
    float levels[32];
    float levelsPts[32];
    
  3. setup方法中,初始化成员并从资产文件夹中加载声音文件。我们使用 FFT 将信号分解成 32 个频率:

    bandCount = 32;
    std::fill(boost::begin(levels), boost::end(levels), 0.f);
    std::fill(boost::begin(levelsPts), boost::end(levelsPts), 0.f);
    mTrack = audio::Output::addTrack( audio::load( getAssetPath("music.mp3").c_str() ) );
    mTrack->enablePcmBuffering( true );
    
  4. 按照以下方式实现update方法:

    mPcmBuffer = mTrack->getPcmBuffer();
    for( int i = 0; i< ( bandCount ); i++ ) {
      levels[i] = max(0.f, levels[i] - 1.f );
      levelsPts[i] = max(0.f, levelsPts[i] - 0.95f );
    }
    
  5. 按照以下方式实现draw方法:

    gl::enableAlphaBlending();
    gl::clear( Color( 1.0f, 1.0f, 1.0f ) );
    gl::color( Color::black() );
    gl::pushMatrices();
    gl::translate(getWindowCenter());
    gl::rotate( getElapsedSeconds() * 10.f );
    drawFft();
    gl::popMatrices();
    
  6. 按照以下方式实现drawFft方法:

    float centerMargin= 25.0f;
    if( !mPcmBuffer ) return;
    std::shared_ptr<float> fftRef = audio::calculateFft( 
     mPcmBuffer->getChannelData( audio::CHANNEL_FRONT_LEFT ), 
     bandCount );
    if( !fftRef ) {
    return;
    }
    float *fftBuffer = fftRef.get();
    gl::color( Color::black() );
    gl::drawSolidCircle(Vec2f::zero(), 5.f);
    glLineWidth(3.f);
    float avgLvl= 0.f;
    for( int i= 0; i<bandCount; i++ ) {
      Vec2f p = Vec2f(0.f, 500.f);
      p.rotate( 2.f * M_PI * (i/(float)bandCount) );
      float lvl = fftBuffer[i] / bandCount * p.length();
      lvl = min(lvl, p.length());
      levels[i] = max(levels[i], lvl);
      levelsPts[i] = max(levelsPts[i], levels[i]);
      p.limit(1.f + centerMargin + levels[i]);
      gl::drawLine(p.limited(centerMargin), p);
      glPointSize(2.f);
      glBegin(GL_POINTS);
      gl::vertex(p+p.normalized()*levelsPts[i]);
      glEnd();
      glPointSize(1.f);
      avgLvl += lvl;
    }
    avgLvl /= (float)bandCount;
glLineWidth(1.f);
    gl::color( ColorA(0.f,0.f,0.f, 0.1f) );
    gl::drawSolidCircle(Vec2f::zero(), 5.f+avgLvl);
    

它是如何工作的…

我们可以将可视化分为频段,中心带有 alpha 值的灰色圆圈。频段是audio::calculateFft函数计算出的数据的直接表示,并通过向中心回退进行一些平滑动画。以下屏幕截图所示的灰色圆圈代表所有频段的平均电平:

FFT 是一种计算DFT离散傅里叶变换)的算法,它将信号分解为不同频率的列表。

如何工作…

制作声音响应的粒子

在这个菜谱中,我们将展示一个基于音频响应粒子的音频可视化示例。

准备工作

将您最喜欢的音乐作品以music.mp3的名称保存在资产文件夹中:

请参阅第六章,了解如何使用瓦片绘制粒子的说明:

如何做到这一点…

我们将按照以下步骤创建一个示例音频响应可视化:

  1. 添加以下必要的头文件:

    #include "cinder/Rand.h"
    #include "cinder/MayaCamUI.h"
    #include "cinder/audio/Io.h"
    #include "cinder/audio/Output.h"
    #include "cinder/audio/FftProcessor.h"
    #include "cinder/audio/PcmBuffer.h"
    #include "ParticleSystem.h"
    
  2. 添加以下用于音频播放和分析的成员:

    audio::TrackRef mTrack;
    audio::PcmBuffer32fRef mPcmBuffer;
    float beatForce;
    float beatSensitivity;
    float avgLvlOld;
    float randAngle;
    
  3. 添加以下用于粒子模拟的成员:

    ParticleSystem mParticleSystem;
    Vec3f   attrPosition;
    float attrFactor;
    CameraPersp mCam;
    
  4. setup方法内部,初始化成员和粒子的模拟:

    beatForce = 150.f;
    beatSensitivity = 0.03f;
    avgLvlOld = 0.f;
    randAngle = 15.f;
    attrPosition = Vec3f::zero();
    attrFactor = 0.05f;
    int numParticle = 450;
    for( int i=0; i<numParticle; i++ ){
     float x = Rand::randFloat( 0.0f, getWindowWidth() );
     float y = Rand::randFloat( 0.0f, getWindowHeight() );
     float z = Rand::randFloat( 0.0f, getWindowHeight() );
     float radius = Rand::randFloat(2.f, 5.f);
     float mass = radius;
     if(i>300) {
      radius = 1.f;
      mass = 1.0f; 
     }
     float drag = 0.95f;
     Particle *particle = new Particle( Vec3f( x, y, z ), radius,
      mass, drag );
     mParticleSystem.addParticle( particle );
    }
    
  5. setup方法内部,初始化相机和音频播放:

    mCam.setPerspective(45.0f, 640.f/480.f, 0.1, 10000);
    mCam.setEyePoint(Vec3f(0.f,0.f,500.f));
    mCam.setCenterOfInterestPoint(Vec3f::zero());
    mTrack = audio::Output::addTrack( audio::load( getAssetPath("music.mp3").c_str() ) );
    mTrack->enablePcmBuffering( true );
    
  6. 实现用于更新相机属性以适应窗口大小的resize方法:

    void MainApp::resize(ResizeEvent event)
    {
    mCam.setPerspective(45.0f, getWindowAspectRatio(), 0.1, 10000);
    }
    
  7. update方法内部,实现简单的节拍检测。我们使用 FFT 将信号分解为 32 个频率:

    float beatValue = 0.f;
    mPcmBuffer = mTrack->getPcmBuffer();
    if( mPcmBuffer ) {
     int bandCount= 32;
     std::shared_ptr<float> fftRef = audio::calculateFft( 
      mPcmBuffer->getChannelData( audio::CHANNEL_FRONT_LEFT ), 
      bandCount );
     if( fftRef ) {
      float * fftBuffer = fftRef.get();
      float avgLvl= 0.f;
      for( int i= 0; i<bandCount; i++ ) {
       avgLvl += fftBuffer[i] / (float)bandCount;
      }
      avgLvl /= (float)bandCount;
      if(avgLvl>avgLvlOld+beatSensitivity) {
       beatValue = avgLvl - beatSensitivity;
      }
      avgLvlOld = avgLvl;
     }
    }
    
  8. 此外,在update方法内部,计算粒子模拟:

    std::vector<Particle*>::iterator it;
    for( it = mParticleSystem.particles.begin(); it != mParticleSystem.particles.end(); ++it ) {
        Vec3f attrForce = attrPosition - (*it)->position;
    attrForce *= attrFactor;
    if( attrPosition.distance( (*it)->position ) <100.f ) {
      attrForce = (*it)->position - attrPosition;
      attrForce *= 0.02f;
        }
        (*it)->forces += attrForce;
        Vec3f bearForceVec = (*it)->position - attrPosition;
        bearForceVec.normalize();
        bearForceVec.rotate(randVec3f(), randAngle);
        bearForceVec *= beatValue*randFloat(beatForce*0.5f, beatForce);
        (*it)->forces += bearForceVec;
        std::vector<Particle*>::iterator it2;
        for( it2 = mParticleSystem.particles.begin(); it2 != mParticleSystem.particles.end(); ++it2 ) {
            (*it)->forces +=  ( (*it)->position - (*it2)->position ) *0.5f * 0.0001f;
        }
    }
    mParticleSystem.update();
    
  9. 按照以下方式实现draw方法:

    gl::enableAlphaBlending();
    gl::clear( ColorA::white() );
    gl::setViewport(getWindowBounds());
    gl::setModelView(mCam);
    float r = getElapsedSeconds()*10.f;
    gl::rotate(Vec3f::one()*r);
    mParticleSystem.draw();
    

如何工作…

粒子被绘制为黑点,或者更准确地说是一个球体和一个尾巴。由于特定的频率差异,施加了从吸引子中心排斥粒子的力,并添加了一个随机向量到这些力中。

如何工作…

更多内容…

您可能想要为特定的音乐作品自定义可视化:

添加 GUI 调整参数

我们将按照以下步骤添加影响粒子行为的 GUI:

  1. 添加以下必要的头文件:

    #include "cinder/params/Params.h"
    
  2. 将以下成员添加到您的应用程序的main类中:

    params::InterfaceGl    mParams;
    
  3. setup方法结束时,使用以下代码初始化 GUI:

    mParams = params::InterfaceGl( "Parameters", Vec2i( 200, 100 ) );
    mParams.addParam( "beatForce", &beatForce, "step=0.01" );
    mParams.addParam( "beatSensitivity", &beatSensitivity, "step=0.01" );
    mParams.addParam( "randAngle", &randAngle, "step=0.01" );
    
  4. draw方法结束时,添加以下代码:

    params::InterfaceGl::draw();
    
posted @ 2025-10-02 09:33  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报