Cinder 创造性编程入门指南(全)
原文:
zh.annas-archive.org/md5/ff242635ffa8fae765566ebbc873aed4
译者:飞龙
协议:CC BY-NC-SA 4.0
前言
Cinder 是目前互联网上最强大的开源创意编程框架之一。它基于最受欢迎和最强大的中级编程语言 C++,并依赖于最少的第三方库。它是低级和高级语言特性的结合,这使得对于那些有高级编程语言背景的人来说相对容易掌握。
Cinder 可以被认为是那些熟悉 Processing、ActionScript 或其他类似高级编程语言或框架的人的下一级创意编程框架。Cinder 可能看起来与 openFrameworks 相似,因为它基于 C++,但框架背后的哲学略有不同。
openFrameworks 基于许多第三方库来完成工作,有时可能会使一个简单的应用程序变得很大。Cinder 在某种程度上是不同的,即它试图利用它所运行的操作系统的功能。这并不意味着它比 openFrameworks 更好,但在某些情况下,它可能比另一个更有效率。
编程是一项迅速成为每个人都应该掌握的技能。爱沙尼亚刚刚为所有上学的儿童引入了计算机编程学习,这意味着从一年级到十二年级的所有学生都在学习如何编码。相信未来会有更多国家效仿这一点并不困难。这意味着在未来,计算机技术将比现在无处不在,并且这种情况将需要大量的人来照顾它。另一个事实是,我们很可能会看到编程在更多以前与它几乎没有或完全没有联系的领域中出现。
正是在这里,创意编程发挥了作用。创意编程在当今越来越受欢迎,因为越来越多的人开始欣赏编程能为他们的工作带来的逻辑和交互维度。可以创造出一幅自己绘画的画作,一个活生生的雕塑,或者一个不允许任何人坐上去的互动椅子——可能性是无限的。这仅仅取决于个人的创造力和编程知识。
在当今的创意编程框架中,一个重要的事情是性能和灵活性。通常,一个框架越强大、越灵活,学习它就越困难。由于 Cinder 既灵活又强大,理解如何使用它并不是一件容易的事情。
本书试图使 Cinder 的学习曲线不那么陡峭。通过一系列简单的引导示例,我们将尝试涵盖其他创意编程工具通常提供的基本功能。
本书涵盖内容
第一章, 学习 Cinder 基础知识 – 现在就学!,提供了关于创意编码和 Cinder 的简要介绍。这将帮助您在 Mac OS X 和 Windows 机器上设置和测试 Cinder。
第二章, 了解可能实现的内容 – Cinder 工具集,介绍了通过编译、运行和讨论一些 Cinder 示例应用程序,可以使用 Cinder 执行的各种基本任务。
第三章, 初始设置 – 创建 BaseApp,解释了如何使用 Cinder 的集成工具 TinderBox 创建基础项目,以及从头开始创建。
第四章, 准备画笔 – 绘制基本形状,深入介绍了 Cinder 内置的基本形状绘制函数,这些函数通常在其他创意编码框架中也有。
第五章, 利用图像 – 加载和显示,解释了图像的加载和显示,这是每个创意编码语言的重要部分。在本章中,我们将学习如何使用 Cinder 从本地和网络存储加载和显示图像。
第六章, 加速 – 创建生成动画,教授使用 Cinder 进行生成或过程动画的基础知识。在整个章节中,我们将创建一个无限循环的动画应用程序。
第七章, 处理图像 – 实时后处理和效果,解释了 Cinder 提供的几个功能,让您能够操纵图像,并将移动图像帧降低到像素级别,而不会失去速度。在本章中,我们将学习如何使用 Cinder 为静态图像和实时应用应用和创建基本效果。
第八章, 增加深度 – Cinder 3D 基础知识,介绍了基本 3D 方面和实用方法,以及可以用 Cinder 绘制的 3D 原语。
第九章, 进入声音 – 添加声音和音频,解释了如何加载、修改、播放和使用声音文件,以及如何使用实时音频输入进行绘制和动画。
第十章, 与用户交流 – 添加交互性和 UI 事件,解释了如何在 Cinder 中处理鼠标、键盘和其他事件。
附录 A, 基本 Cinder 功能参考,帮助您找到本书中使用的某些基本 Cinder 功能,以便日后参考。
附加章节,逃离孤独 – 与其他应用程序通信,本书中未包含,但可在以下链接下载:www.packtpub.com/sites/default/files/downloads/Escaping_Singleness.pdf
为本书所需条件
你只需要一台 Mac OS X 或 Windows 计算机。本书中的代码示例已在 Mac OS X 10.6+和 Windows 7 机器上测试。每个操作系统的开发软件列表将在以下章节中讨论。在这本书中,我们将使用以下软件:
Mac OS X
-
Xcode 3+或 Xcode 4+
-
MadMapper
Windows
-
Microsoft Visual C++ 10+
-
Windows 平台 SDK
-
DirectX SDK
-
QuickTime SDK
两者
- Pure Data
本书面向对象
这本书是为那些想要从低级创意编码框架或其他类型的编码转向使用 Cinder 进行创意编码的人准备的。在这本书中,你不会找到关于编程的一般性介绍章节,你应该已经了解一些相关知识。
如果你之前有使用基于文本的编程语言(如 Processing、ActionScript、JavaScript)或视觉编程语言(如 Pure Data、Max MSP、VVVV 或 Quartz Composer)的经验,这本书就是为你准备的。
约定
在这本书中,你会发现许多不同风格的文本,用于区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。
文本中的代码单词如下所示:“我们可以通过使用include
指令来包含其他上下文。”
代码块设置如下:
void TextBoxApp::render()
{string txt = "Here is some text that is larger than can fit naturally inside of 100 pixels.\nAnd here is another line after a hard break."; [default]
当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
void TextBoxApp::render()
{string txt = "Here is some text that is larger than can fit naturally inside of 100 pixels.\nAnd here is another line after a hard break."; [default]
新术语和重要词汇将以粗体显示。你会在屏幕上看到,例如在菜单或对话框中的单词,将以如下方式显示:“点击下一个按钮将你带到下一屏幕”。
注意
警告或重要提示将以这样的框显示。
小贴士
小技巧和技巧如下所示。
读者反馈
我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们开发你真正能从中获得最大价值的标题非常重要。
要向我们发送一般反馈,只需发送电子邮件到 <feedback@packtpub.com>
,并在邮件主题中提及书名。如果你在某个领域有专业知识,并且对撰写或参与一本书感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲拥有者,我们有一些可以帮助你从购买中获得最大价值的事情。
下载示例代码
您可以从您在www.packtpub.com
的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
下载本书彩色图像
我们还为您提供了一个包含本书中使用的截图/图表的彩色图像的 PDF 文件。这些彩色图像将帮助您更好地理解输出中的变化。
您可以从以下链接下载此文件:
www.packtpub.com/sites/default/files/downloads/9564OS_ColoredImages.pdf
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata
,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站,或添加到该标题的错误清单部分。您可以通过从www.packtpub.com/support
选择您的标题来查看任何现有的错误清单。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过以下链接联系我们 <copyright@packtpub.com>
,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者以及为我们提供有价值内容方面的帮助。
咨询
如果您在本书的任何方面遇到问题,可以通过以下链接 <questions@packtpub.com>
联系我们,我们将尽力解决。
第一章. 现在开始学习 Cinder 基础
在本章中,我们将学习:
-
什么是创意编程
-
什么是 Cinder
-
如何在 Mac OS X 和 Windows 上设置
-
如何测试设置是否实际有效
什么是创意编程
这是对创意编程的简要介绍,我相信您可以在互联网上找到更多关于这个主题的信息。尽管如此,我将尝试从我的角度解释它是什么样子。
创意编程是一个相对较新的术语,用于描述一个结合编码和设计的领域。这个术语的核心可能是“编码”——要成为一名创意编码者,您需要知道如何编写代码以及一些关于编程的一般知识。另一个部分——“创意”——包含设计和所有可以与编码结合的其他事物。
同时精通编码和设计让您能够将您的想法解释为界面设计、艺术装置、手机应用和其他领域的可工作原型。它可以节省您在向他人解释您的想法以获得帮助时所花费的时间和精力。创意编程方法可能在大项目中效果不佳,除非涉及多个创意编码。
在过去几年中,出现了许多使编程更容易接触的新工具。它们都易于使用,但通常一个工具越简单,它的功能就越弱,反之亦然。
Cinder 是创意编程领域中最强大的工具之一。在本章中,我们将学习 Cinder 是什么以及如何在两个最流行的操作系统——Mac OS X 和 Windows 上设置它。如果您可以访问两者,并且希望尽可能快地完成,请选择 Mac OS X。如果您有一个好的互联网连接,在 Windows 上设置它不会花费太多时间。
关于 Cinder 的几句话
因此,我们来进行一些 Cinder 编程!Cinder 是您可以在互联网上免费获得的最多专业和强大的创意编程框架之一。如果您正在创建一些非常复杂的交互式实时音视频作品,它可以帮助您,因为它使用的是最流行和功能强大的底层编程语言之一——C++,并且依赖于最少的第三方代码库。Cinder 的创造者也试图使用所有最新的 C++ 语言特性,甚至包括那些尚未标准化(但很快将会)的特性,通过使用所谓的 Boost 库。
这本书不是关于 Cinder、C++ 编程语言或涉及的数学领域的 A 到 Z 指南。这是一本简短的介绍,面向那些已经使用过类似框架或工具并且已经了解一些编程的人。由于 Cinder 依赖于 C++,我们了解得越多越好。对 ActionScript、Java 或甚至 JavaScript 的了解将帮助您理解这里发生的事情。
下载 Cinder
好吧,少说多做!将您的浏览器指向 Cinder 网站 (libcinder.org
)。
点击主菜单中的 下载 链接,并根据您选择的操作系统选择以下两个版本之一:
-
Cinder for Mac OS X (Mac OS X)
-
Cinder for Visual C++ 2010 (Windows)
注意
我们将在整本书中使用打包的 Cinder 版本 0.8.4,但会指出它和最新版本的 Cinder 0.8.5 之间的某些差异。
现在我们将 Cinder 设置分为两组:Mac OS X 和 Windows。这是因为配置过程,对于每个平台都有所不同。如果您使用 Mac OS X,您将更快地上手,但如果您似乎使用 Windows,这也不是什么过错——只需确保您有一个良好的互联网连接,您就可以开始了!
在 Mac OS X 上设置 Cinder
如果您之前做过一些编码,那么在 Mac OS X 上设置 Cinder 相对简单——这意味着,如果您已经安装了 Xcode。如果没有,您必须下载并安装它。为此,您可以从 developer.apple.com/programs/register/
获取 Apple 开发者账户。
一旦您有了 Apple 开发者 ID 和密码,请访问 developer.apple.com/xcode/
的 Xcode 下载部分,或者打开 App Store 应用程序并在其中搜索 Xcode 应用程序。
安装 Xcode 的过程不是本书将要涵盖的主题,但它应该相当直观,因为苹果公司一直致力于使他们的应用程序尽可能用户友好。如果您在安装 Xcode 时遇到问题,请尝试 Google 或访问位于 developer.apple.com/support/xcode/
的官方 Apple 开发者 Xcode 常见问题解答。
因此,现在您已经在您的计算机上安装了 Xcode!让我们继续,并将下载的 Cinder 包解压缩到安全的位置,例如,/Users/Your_User-name/cinder
。在我的情况下,路径是 /Users/kr15h/cinder
。
接下来,我们将通过打开一个示例应用程序来测试我们新创建的设置是否正常工作。我们将尝试 QuickTime 示例应用程序。为此,请在 cinder/samples/QuickTime/xcode/
目录(在 Cinder 0.8.5 中为 cinder/samples/QuickTimeBasic/xcode/
)中打开文件 quickTimeSample.xcodeproj
(在 Cinder 0.8.5 中为 QuickTimeBasic.xcodeproj
)。在 Xcode 中,选择 构建 | 构建并运行(在 Xcode 4+ 中为 产品 | 运行)。您也可以通过按 Cmd + R 或在工具栏中点击 构建并运行(在 Xcode 4+ 中为 运行)按钮来实现相同的效果(您可以通过一个放置在灰度锤子或 Xcode 4+ 中的圆形灰色播放按钮上的圆形绿色播放按钮来识别它)。
应该会弹出一个 打开文件 对话框。从您的硬盘驱动器中选择一个电影文件!
如果你看到电影在窗口中播放,那就成功了!你已经准备好进入下一章。如果没有,请仔细遵循本节中提到的步骤——也许你没有注意到某些东西。
在 Windows 上设置 Cinder
在 Windows 上设置 Cinder 可能会比在 Mac OS X 上花费更长的时间。但这并不意味着它会运行得更慢。Windows PC 的好处在于,你通常可以更灵活地配置硬件。所以,如果你有强烈的意愿使用你选择的合适硬件来创建一些令人印象深刻的生成内容,那么选择 Windows,并配备一些四核 CPU、SSD 硬盘和适当的 GPU。这不会有害。
在 Windows 上配置 Cinder 花费更多时间的主要原因是你将需要下载更多内容。除了 Cinder 软件包外,你还需要下载和安装以下内容。
Microsoft Visual C++ Express 2010
选择这个 IDE 的原因是所有官方 Cinder 示例应用程序项目都是为它创建的。在这里,我们将使用 Microsoft Visual C++,因为它是 Cinder 创建者选择的默认 Windows IDE。
要下载 Microsoft Visual C++,请将浏览器指向 www.microsoft.com/visualstudio/eng/downloads#d-2010-express
。
点击 Visual C++ 2010 Express 选项卡,选择要下载的语言,然后点击 安装。应该开始下载,下载完成后,打开 *.exe
文件。
我们不会涵盖整个安装过程,因为这不是本书的范围。只需为所有内容选择默认选项,就会一切顺利。让我们继续到我们还需要下一个组件。
Windows 平台 SDK
我们需要这个 软件开发工具包(SDK)来与 Windows 系统进行通信。Cinder 框架会为我们完成这个任务,所以我们不会看到任何特定于 Windows 的内容——只有多平台 Cinder 特定的内容。不要害怕,从 bit.ly/IL35OV
下载 Windows 平台 SDK。
下载完成后,一定要运行它!再次选择默认选项,一切应该都会好。
DirectX SDK
我们需要这个,因为 Cinder 音频核心依赖于它。所以,让我们从 bit.ly/OSe24s.
获取它。
使用默认设置安装,然后就可以继续了。
QuickTime SDK
如果你使用 Cinder 0.8.5,则不需要下载 QuickTime SDK。
你将需要获取一个 Apple 开发者账户来下载它。从 developer.apple.com/programs/register/
获取它。
然后转到 developer.apple.com/quicktime/
。
安装时请小心。Cinder 预期 QuickTime SDK 与 Cinder 本身位于同一目录级别(并且目录名为 QuickTimeSDK-7.3
)。因此,如果您将 Cinder 保存在 C:\cinder
,则 QuickTime SDK 应位于 C:\QuickTimeSDK-7.3
——在安装 SDK 时请更改此路径。
当您到达可以选择目标文件夹的安装点时,请点击 更改 按钮。
现在将 目标文件夹 路径更改为 C:\QuickTimeSDK-7.3
。
如果您熟悉重新链接库,则无需担心此警告。
启动示例应用程序
在启动您的第一个示例应用程序之前,请确保您的系统上已安装 QuickTime Player。如果没有,请访问 www.apple.com/quicktime/download/
,下载并安装它。
前往 Cinder 示例目录,例如,C:/cinder/samples/
,然后找到一个名为 QuickTime 的文件夹。打开 C:/cinder/samples/QuickTime/vc10/quickTimeSample.sln
。
小贴士
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。这些彩色图像将帮助您更好地理解输出中的变化。
您可以从以下链接下载此文件:
www.packtpub.com/sites/default/files/downloads/9564OS_ColoredImages.pdf
欢迎使用 Microsoft Visual C++ 2010!按 F5 键构建项目并运行。您应该会收到一个提示,要求选择文件——从您的硬盘驱动器中选择一个与 QuickTime 兼容的电影文件,看看会发生什么。您应该看到电影正在播放,并且有一个半透明的矩形覆盖在电影文件上,显示不同的统计数据。
如果您看不到电影,请检查是否已安装 QuickTime Player。我第一次尝试这个示例应用程序时没有成功。我花了好几个小时试图弄清楚问题在哪里?什么都没有!您只是没有 QuickTime Player。从 www.apple.com/quicktime/download/
下载它。
摘要
总结来说,这些是最简单、最快的方法来启动并运行 Cinder。当然,还有其他可能的方法,但它们超出了本书的范围。但如果您要查找 Cinder 的最新版本,请访问 Cinder 官方网站上的 CINDER + GIT 部分 (libcinder.org/docs/welcome/GitSetup.html
)。
在下一章中,我们将通过在 Cinder 示例包中启动不同的示例来尝试了解 Cinder 的可能性。
第二章:了解可能实现的内容 – Cinder 工具集
本章介绍了可以通过编译、运行、更改和讨论一些示例应用程序来使用 Cinder 完成的各种基本任务。
我们已经通过编译和运行 QuickTime 示例应用程序来测试了我们的设置。现在,我们将通过编译其他示例并讨论它们的特别之处来了解 Cinder 的可能性。
我们将浏览展示本书将讨论的大部分功能的示例。将会有很多部分目前还不清楚且难以解释,但不用担心,我们将在接下来的章节中尝试理解它们。
这里有一些我们将要讨论的示例列表。如果你遵循了上一章中的教程,请前往你的 Cinder 示例文件夹(Mac OS X 上为 (/Users/You/cinder/samples/)
,Windows 上为 C:\cinder\samples\
)。
-
BasicApp (
samples/basicApp
) -
BezierPath (
samples/bezierPath
) -
CairoBasic (
samples/CairoBasic
) -
CaptureTest (
samples/captureTest
) -
EaseGallery (
samples/EaseGallery
) -
TextBox (
samples/TextBox
) -
ArcballDemo (
samples/ArcballDemo
) -
Earthquake (
samples/Earthquake
) -
AudioGenerative (
samples/AudioGenerative
)
BasicApp
前往你的 Cinder 示例文件夹(如果遵循了上一章中的教程,MAC 上为 /Users/You/cinder/samples/
,Windows 上为 C:\cinder\samples\
)。
将会看到一个名为 BasicApp
的文件夹。看看里面有什么。如果你是 Mac OS X 用户,请打开位于 xcode/basicApp.xcodeproj
的项目文件。Windows 用户应从 vc10\basicApp.sln
打开项目文件。编译并运行项目。
如我们所见,一个黑色背景的窗口出现了。看起来那里除了纯粹的黑色和无限的虚空之外什么都没有。但事实并非如此!拿起你的鼠标,尝试通过在应用程序窗口的黑色表面上指向和点击-拖动来填充这个虚空。出现了一条橙色线条。是的,这是一个非常基础的绘图程序,允许你绘制连续的线条。线条是 Cinder 可以创建的基本 2D 几何形状之一。
让我们尝试改变线条的颜色。关闭窗口,点击位于所选 IDE 的项目导航器或项目文件树浏览器中示例项目源目录下的 basicApp.cpp
文件。导航到代码中可以查看以下内容:
void BasicApp::draw()
{
这是在程序中定义实际绘图过程的地点。查看以下代码行:
// We'll set the color to orange
glColor3f( 1.0f, 0.5f, 0.25f );
这是改变线条颜色的地方。这是通过使用名为glColor3f
的函数来完成的。从函数的名称中可以推断出它使用 OpenGL、改变颜色,并使用三个float
值来完成。已知 RGB 颜色值由三个分量组成,分别是 R(红色)、G(绿色)和 B(蓝色)。在这个函数中,0.0
是每个分量的最小可能值,而1.0
是最大值。
让我们改变线的颜色为红色。我们需要将颜色的红色分量设置为最大(1.0
)并将所有其他分量设置为最小(0.0
)。所以它看起来会像以下这样:
glColor3f( 1.0f, 0.0f, 0.0f );
我们使用*dot-zero-f*
记号的原因是我们想告诉编译器我们正在将浮点常数传递给函数。这里我不会深入探讨这个问题,因为互联网上有大量关于这个问题的在线资源。
现在保存、编译、运行并绘制。做得好!你已经用 Cinder 编写了第一条自定义代码!让我们立即继续另一个例子!
BezierPath
现在,我们已经准备好了一个稍微复杂一点的例子,这个例子将允许你绘制一条连续的贝塞尔曲线。贝塞尔曲线之所以被称为贝塞尔曲线,是因为一位法国工程师皮埃尔·贝塞尔(Pierre Bézier),他实际上通过在设计中应用它们来申请专利并使它们流行起来。贝塞尔曲线在计算机图形、动画和其他领域得到广泛应用。贝塞尔曲线的概念使我们能够通过使用笛卡尔坐标系和数字来创建参数曲线。
前往你的 Cinder 样本文件夹(在 Mac OS X 上为/Users/You/cinder/samples/
,如果你使用 Windows 则为C:\cinder\samples\
)。
前往名为bezierPath
的文件夹。如果你是 Mac OS X 用户,请打开xcode/bezierPath.xcodeproj
(如果你使用 Windows,则为vc10\bezierPath.sln
)。编译并运行项目。
点击并拖动以在窗口表面上放置点。你将在第二次点击后开始看到线条。当你按下鼠标并移动它时,你可以控制正在绘制的线段的曲率。玩一会儿,以更好地理解贝塞尔曲线的工作原理。
为了给这个过程增加一些额外的技术乐趣,让我们改变一下之前例子中做的事情。关闭应用程序窗口,并在编辑器中打开文件bezierPathApp.cpp
。让我们假设我们不喜欢屏幕上用来表示路径形成点的圆圈——我们想用矩形代替。导航到一个看起来像以下代码片段的代码块:
// draw the control points
gl::color( Color( 1.0f, 1.0f, 0.0f ) );
for( size_t p = 0; p < mPath.getNumPoints(); ++p )
gl::drawSolidCircle( mPath.getPoint( p ), 2.5f );
如前一个代码块中的注释所示,这个代码块负责绘制控制点。它通过以下行设置下一个要绘制的东西(或在这个例子中的圆圈)的颜色:
gl::color( Color( 1.0f, 1.0f, 0.0f ) );
这使用一个for
循环来遍历mPath
对象中的所有数据。圆圈是通过以下行绘制的:
gl::drawSolidCircle( mPath.getPoint( p ), 2.5f );
只需通过查看函数名,我们就可以知道它绘制圆形。gl::
告诉我们这个函数位于 Cinder OpenGL 命名空间中(Cinder 确实使用了大量的命名空间,这使得其代码确实更易于阅读),draw
告诉我们它绘制了某个东西,solid
表示这个函数绘制了实心物体,而circle
通过明确指出使用 OpenGL 将绘制一个实心圆来总结这一点。
函数传递了两个参数。第一个是一个包含 x 和 y 坐标值的点对象,另一个定义了正在绘制的圆的半径(以像素为单位)。
让我们先改变圆的半径。将2.5f
改为10.0f
,使其看起来如下:
gl::drawSolidCircle( mPath.getPoint( p ), 10.0f );
构建并运行项目以查看更改!嗯,很有趣!确实如此!但让我们不要过早庆祝,记住,我们想要将圆形改为矩形。我们将使用以下定义的函数:
gl::drawSolidRect( const Rectf &rect, bool textureRectangle=false );
正如函数的名称所告诉我们的,这个函数绘制一个实心矩形。尽管如此,我们仍然需要向函数提供一个参数。只需要一个参数(另一个是可选的,默认设置为false
),并且该参数必须是Rectf
类型。一个Rectf
由四个值组成。前两个值定义矩形的右上角,其他两个定义矩形的右下角位置。在大多数绘图 API 中,这种对象通常是通过传递矩形的左上角的 x 和 y 坐标以及矩形的宽度和高度来定义的。但这次不是这样。因此,我们必须传递类似以下内容的东西:
Rectf( float x1, float y1, float x2, float y2 )
其中x1
是左上角的 x 坐标,y1
是左上角的 y 坐标,而x2
和y2
定义了屏幕上应该放置右下角的位置。
注意
这只是 Cinder 中五个Rectf
(或RectT
)构造函数中的一个。如果你想查看其他几个,请查看 Cinder 在线参考libcinder.org/docs/v0.8.4/classcinder_1_1_rect_t.html
。
通过分析点绘制函数(gl::drawSolidCircle( mPath.getPoint( p ), 10.0f );
),我们得到了一个表示当前控件点 x 和 y 坐标的对象的代码片段:
mPath.getPoint(p)
我们可以通过编写以下内容来访问 x 和 y 坐标:
mPath.getPoint(p).x
并且
mPath.getPoint(p).y
让我们构建我们的矩形:
Rectf( mPath.getPoint(p).x, mPath.getPoint(p).y,mPath.getPoint(p).x+10.0f, mPath.getPoint(p).y+10.0f )
前一段代码中的数字常量10.0f
代表矩形的宽度和高度。由于我们希望宽度和高度相等,所以我们在这里使用相同的值。以下是将要使用的最终代码:
// draw the control points
gl::color( Color( 1.0f, 1.0f, 0.0f ) );
for( size_t p = 0; p < mPath.getNumPoints(); ++p ) {gl::drawSolidRect( Rectf( mPath.getPoint(p).x,mPath.getPoint(p).y, mPath.getPoint(p).x+10.0f,mPath.getPoint(p).y+10.0f ) );
}
构建并运行项目。你将看到矩形被绘制在实际的控件点旁边,而不是像应该的那样绘制在它们上面。让我们修复这个问题:
for( size_t p = 0; p < mPath.getNumPoints(); ++p ) {gl::drawSolidRect( Rectf( mPath.getPoint(p).x-5.0f,mPath.getPoint(p).y-5.0f, mPath.getPoint(p).x+5.0f,mPath.getPoint(p).y+5.0f ) );
}
正如你所见,我们将所有的 10.0f
值改为 5.0f
,并从矩形构造函数的前两个参数中减去 5.0f
。通过这样做,我们将矩形的左上角向左移动了它宽度的一半,向上移动了它高度的一半(5 个像素),同时保持了相同的宽度和高度(10 个像素)。
构建并运行项目,正如你所见,现在矩形已经到了正确的位置。
也许一开始这有点复杂,但随着你做这类调整的次数越多,你学到的也就越多。在本章的剩余部分,我们不会再做任何更改。
CairoBasic
你曾经梦想过创建可以无失真打印的生成式打印艺术品吗?来认识一下 Cairo。
Cairo 是一个矢量图形软件库,它允许你绘制所有可以在软件中完成的疯狂矢量图形,例如 Illustrator,它还允许我们将它保存为单独的矢量图形文件。如果你想了解更多关于 Cairo 本身的信息,请访问它的网站 (cairographics.org/
)。
让我们试试看!在 samples
目录中搜索 CairoBasic
文件夹。打开 xcode/CairoBasic.xcodeproj
(如果你在 Windows 上,则为 vc10\CairoBasic.sln
)。构建并运行项目。看起来那里没有什么特别之处,除了一个径向渐变背景。但试着点击它!一朵花。再点击一次!随机出现的花朵。试着用它们填满整个屏幕——你能闻到花香吗?
好了,这就足够了。关闭应用程序窗口。那么,刚才发生了什么?为了理解这一点,请在你代码编辑器中打开文件 CairoBasicApp.cpp
。我现在不会解释它,但你可以看到里面的代码并不多。转到一段看起来像以下的代码块:
void CairoBasicApp::keyDown( KeyEvent event )
{
这是一个主要应用类的方法,当我们在键盘上按下键时,它会执行一些操作。我们可以看到,如果我们按下 F 键,就可以使应用程序全屏显示,然后为字母 S、E、P 和 D 定义了某些功能——那是什么呢?如果你按下一个这些按钮,你将保存准备好的图像到一个文件中,但这个文件的格式将是一个矢量格式。正如你所见,如果你按 S,你会得到一个 SVG 文件,对于 E 你会得到一个 EPS 文件,对于 P 你会得到一个 PS 或 PostScript 文件,而对于 D 你会得到一个 PDF 文件。
试试看!再次构建并运行应用程序,创建你的艺术品,然后依次按下 S、E、P 和 D。然后转到你的家目录(MAC 上为 /Users/You/
,Windows 上为 C:\Users\You
),你会看到那里有四个新文件:
-
CairoBasicShot.svg
-
CairoBasicShot.eps
-
CairoBasicShot.ps
-
CairoBasicShot.pdf
尝试使用你喜欢的矢量图形软件打开这些文件之一。
注意
你也可以用你的网络浏览器打开CairoBasicShot.svg
文件。这意味着如果你想在网络上使用它,你不需要将矢量图形转换为 JPG 或 PNG 等格式——你可以直接使用 SVG 矢量文件格式。
是的,这是真的,你可以无限放大,细节仍然清晰。这就是矢量图形的力量!现在你可以以 300 dpi 的分辨率后处理并打印出 1 米 x 1 米的生成花卉海报,而不会丢失任何细节。酷吧?而且你不仅可以打印你的艺术品——你还可以将其用作激光切割机或 CNC 铣床等物理对象的基础。
CaptureTest
在第一章中,我们看到了 Cinder 能够加载和播放视频文件。那么,使用实时视频怎么样呢?这个示例应用程序表明,可以从连接到你的计算机的摄像头访问视频。
去搜索samples
目录中名为captureTest
(在 Cinder 0.8.5 中为CaptureBasic
)的文件夹。打开xcode/captureTest.xcodeproj
(如果你使用 Windows,则为vc10\captureTest.sln
)。构建并运行项目。稍等片刻,你应该能看到来自你摄像头的移动图像。
关于这个示例没有太多可以说的,它只是捕获并显示摄像头的图像。你可以在以后以许多不同的方式使用这些实时数据。
EaseGallery
由于我们打算用 Cinder 做一些动画,所以看到 Cinder 为我们提供的动画可能性会很好。继续打开samples
目录中名为EaseGallery
的文件夹。打开xcode/EaseGallery.xcodeproj
(如果你是 Windows 用户,则为vc10\EaseGallery.sln
)。构建并运行项目。
如果你熟悉某种动画软件或 TweenLite ActionScript 库,你可能记得一个术语叫做缓动。这就是 EaseGallery 示例的内容。在这里,你可以看到 Cinder 提供的所有不同的动画缓动算法。我们稍后会深入探讨这一点,但你现在只需要记住有这样的函数,你不需要在互联网的最黑暗角落里搜索它们,也不需要自己实现它们。
TextBox
Cinder 对文本的支持非常好——即使是那些以非英语字母字符为母语的人。
让我们在samples
目录中搜索一个名为TextBox
的文件夹。打开xcode/TextBox.xcodeproj
(如果你是 Windows 用户,则为vc10\TextBox.sln
)。构建并运行项目。在窗口中点击任何地方。当你点击并拖动时,文本框的宽度会改变。甚至更棒的是,里面的文本会相应地换行。一个代表文本占据的空间的完整宽度和高度的矩形正在它后面绘制。
让我们关闭窗口,尝试对应用程序进行一些更改(是的,我骗了你们,我们又要进行更改了)。打开TextBoxApp.cpp
并找到一个看起来像以下内容的代码块:
void TextBoxApp::render()
{string txt = "Here is some text that is larger than can fit naturally inside of 100 pixels.\nAnd here is another line after a hard break.";
尝试更改引号内的文本(不要害怕使用 Unicode 字符):
string txt = "Hi, that's me, Ratman!\nWhere is Mickey Mouse?";
保存文件,然后再次构建并运行项目。如果你使用了 Unicode 字符,你将会非常惊喜——所有的字符都在那里!
ArcballDemo
这个示例演示了 Cinder 的基本 3D 功能。前往samples
文件夹中的ArcballDemo
目录,并打开xcode/ArcballDemo.xcodeproj
(如果你使用的是 Windows,则为vc10\ArcBallDemo.sln
)。
应用程序窗口中会出现一个奇怪的图像。不用担心,只需点击并拖动。你会注意到它是一个 3D 立方体。我不会解释这个演示中旨在展示的其他内容,但重要的是要知道,Cinder 中的渲染(默认的 Open GL 渲染模式)是通过使用 Open GL 完成的,这意味着你可以渲染 2D 图像,以及 3D 空间。
地震
如果你打开这个示例,你会看到在 Cinder 中使用实时 3D 图形可以获得多少细节。在samples
文件夹中找到Earthquake
目录,打开xcode/Earthquake.xcodeproj
(如果你使用的是 Windows,则为vc10\Earthquake.sln
)。构建并运行应用程序。
你应该看到的是一个地球的 3D 模型,上面有带数字的红锥体。你可以通过移动鼠标来旋转它,通过使用鼠标滚轮来放大和缩小。
实时交互式 3D 应用程序,这不是很酷吗?
AudioGenerative
欢迎来到生成音频的世界!这个应用程序示例将向你展示实时音频-视觉世界的可能性。
在 Cinder 的samples
目录中找到名为AudioGenerative
的文件夹。打开项目文件xcode/AudioGenerative.xcodeproj
(如果你使用的是 Windows,则为vc10\AudioGenerative.sln
)。构建并运行应用程序。调高音量,但不必害怕!它只是一个正弦波。尝试将鼠标在应用程序窗口上方上下移动——这样做会改变声音的频率。
摘要
因此,我们只浏览了 Cinder 样本包中包含的一些示例。还有很多,你可以通过浏览所有示例,尝试更改它们的一些部分,并尝试理解它是如何工作的,这是了解 Cinder 的最佳方式之一。
这里关于示例的讨论旨在作为对下一章将要讨论的 Cinder 功能的介绍。我建议尝试编译并运行所有样本,以便尽可能广泛地了解 Cinder 功能。
第三章。初始设置——创建 BaseApp
在本章中,我们将从头开始创建一个 Hello World 应用程序。该应用程序将被命名为 BaseApp,我们将在后续章节中使用其结构作为起点。
本章将涵盖以下内容:
-
使用 TinderBox 设置项目
-
在 Mac OS X(Xcode 3 和 4+)上从头开始设置项目
-
在 Windows 上从头开始设置项目
它与上一章的 BasicApp 没有多少共同之处,除了基本结构,这将是所有 Cinder 项目的共同点。
TinderBox
创建 Cinder 项目的最简单方法是使用其集成工具 TinderBox。你可以在 cinder/tools/
目录中找到它。这将与 Mac OS X 和 Windows 一起工作。
让我们试一试。打开 TinderBox 应用程序。如果你是第一次做,你将需要指向你的 Cinder 目录。
完成这些后,TinderBox 项目设置窗口将出现。从 Target 列表中选择 Basic App,从 Template 下拉列表中选择 OpenGL。在 Project Name 字段中输入 BaseApp
。确保你在 Naming Prefix 字段中有 Base。
注意
对于使用 Cinder 0.8.5 的人来说,在新的 TinderBox 中没有 Target 列表。你必须选择 Basic OpenGL 模板。此外,在新的 TinderBox 中没有 Naming Prefix 字段,所以请忽略它。
在 Location 字段中选择一个目录,用于存储你的 Cinder 项目。我的目录是 /Users/kr15h/Documents/CinderProjects/
(在 Windows 上为 C:\Users\kr15h\Documents\CinderProjects\
)。
在 Cinder Version 字段中保留 HEAD,并在 Compilers 部分选择 Xcode。如果你使用 Windows 或计划在两个操作系统上开发,请选择 Visual C++ 2010。
点击 Create!
你的项目已经准备好了!导航到你在 Location 字段中指定的 Cinder 项目文件夹,你将找到一个名为 BaseApp
的文件夹。你将在 xcode
目录中找到一个 Xcode 项目文件(在 Windows 的 vc10
目录中为 Visual C++ 2010 项目文件)。
打开 BaseApp.xcodeproj
(在 Windows 上为 BaseApp.sln
)。
尝试构建并运行它。你应该看到以下截图所示的结果:
窗口中还没有绘制出任何有趣的内容。我们稍后会做一些更改,但如果你能够编译、运行并看到这个,你就是大师了!
在 Mac OS X 上从头创建项目
我们中的一些人只想用困难的方式来做,实际上并不难,但涉及一些必须记住和理解的操作步骤。如果没有 TinderBox 会怎样?没问题!
在这里我们将再次分成两组——这次是根据我们使用的 Xcode 版本来分。有旧的方式(Xcode 3)和新方式(Xcode 4+)。你可以通过打开 Xcode 并从 Xcode 菜单中选择 AboutXcode 来检查版本。
注意
如果你正在使用 Cinder 0.8.5,请注意最低要求的 Mac OS X 版本是 10.7,并且你需要 Xcode 4.4+。
基本项目设置(Xcode 3)
前往你的 Cinder 项目目录,在我的例子中是/Users/kr15h/Documents/CinderProjects/
。为我们的新项目创建一个空目录并命名为BaseApp
。在其内部创建另一个目录并命名为src
。它应该看起来像以下截图所示:
打开 Xcode 并从文件菜单中选择新建项目。选择Mac OS X 应用程序类别并选择Cocoa 应用程序。点击选择,浏览到我们之前创建的目录(CinderProjects/BaseApp
),然后点击保存。
现在建议关闭 Xcode,浏览到你的新创建的项目目录,并将 Xcode 项目文件夹(BaseApp 中的 BaseApp——即.xcodeproj
文件的父文件夹)重命名为xcode
。
现在打开BaseApp.xcodeproj
文件。我们必须删除我们不需要的一些文件。这些文件包括main.m
、InfoPlist.strings
、MainMenu.xib
以及BaseAppAppDelegate.m
和BaseAppAppDelegate.h
。当被提示时,点击也移动到废纸篓。
让我们创建我们的第一个源文件。在Xcode 项目导航器面板中右键点击Classes文件夹,然后选择添加 | 新建文件。从C 和 C++类别中选择C++文件。将其命名为BaseApp
并取消选择也创建"BaseApp.h"选项。对于位置,选择我们之前创建的BaseApp/src
文件夹,然后点击完成。
基本项目设置(Xcode 4+)
打开 Xcode 并从文件菜单中选择新建 | 新建项目。在Mac OS X类别下选择应用程序项并选择Cocoa 应用程序。点击下一步。将产品名称输入为BaseApp
,例如,将公司标识符输入为com.mycompany
。再次点击下一步,你将被提示选择项目位置。浏览到我们之前创建的目录(CinderProjects/BaseApp
)并点击创建。
关闭 Xcode(通过按Cmd + Q完全关闭)并导航到 BaseApp 项目目录。你可以看到那里有两个文件夹,BaseApp
和src
。将BaseApp
重命名为xcode
——所有与 Xcode 相关的文件都将存储在那里,而所有存储在src
目录中的代码都打算在其他任何 IDE 中使用。这不是法律,这只是 Cinder 示例项目的组织方式,这也是保持跨平台代码项目良好组织的好方法。这样做是为了避免为每个平台和 IDE 创建源文件的单独版本。
打开 BaseApp.xcodeproj
Xcode 项目文件。我们将移除一些我们不需要的文件。在项目导航器中,在 BaseApp 文件夹下,选择并删除 AppDelegate.h
、AppDelegate.m
和 MainMenu.xib
。当提示删除实际文件时,点击 删除,而不是仅仅删除引用。在 支持文件 文件夹下,选择并删除 main.m
和 BaseApp-Prefix.pch
。当提示时,再次点击 删除。
让我们创建我们的第一个 C++ 源文件。在项目导航器中的 BaseApp 文件夹上右键单击,并选择 新建文件 选项。在 Mac OS X 和 C 和 C++ 类别下选择 C++ 文件。将出现一个 另存为 对话框。导航到我们 BaseApp
项目的 src
目录 (BaseApp/src
)。将文件命名为 BaseApp
并点击 创建。
添加代码 (Xcode 3 和 4+)
在编辑器中打开我们刚刚创建的文件 (BaseApp.cpp
) 并输入以下代码:
#include "cinder/app/AppBasic.h"
#include "cinder/gl/gl.h"using namespace ci;
using namespace ci::app;
using namespace std;class BaseApp : public AppBasic {
public:void setup();void update();void draw();
};void BaseApp::setup(){}
void BaseApp::update(){}void BaseApp::draw()
{// clear out the window with blackgl::clear( Color( 0, 0, 0 ) );
}CINDER_APP_BASIC( BaseApp, RendererGl )
如果您尝试编译和运行应用程序,您将看到这是不可能的。还有一些东西仍然缺失,那就是与 Cinder 库和头文件的连接。
连接到 Cinder (Xcode 3)
在 项目 菜单下,选择 编辑项目设置。转到 构建 选项卡,在 配置 选择字段中,选择 所有配置。让我们添加一个用户定义的构建设置,这将很快证明其有用。这个用户定义的设置将存储 Cinder 目录相对于我们项目的位置。点击 项目设置 窗口左下角的齿轮按钮,并选择 添加用户定义设置。
将其命名为 CINDER_PATH
并将其值设置为 Cinder 目录的位置 /Users/You/cinder
(将 You
替换为您的用户名)。
现在我们必须利用这个新创建的变量。滚动到 搜索路径 部分,并修改 用户头文件搜索路径 设置。将值设置为 $(CINDER_PATH)/include
。完成此操作后,转到 头文件搜索路径(注意其前面没有用户),并将其设置为 $(CINDER_PATH)/boost
。
然后转到 架构 部分,将 架构 变量设置为 i386
。
接下来,我们必须告诉 Xcode Cinder 库的位置以便链接。找到 链接 部分,并将 其他链接器标志 下的 调试 字段更改为 $(CINDER_PATH)/lib/libcinder_d.a
,并将 发布 字段更改为 $(CINDER_PATH)/lib/libcinder.a
。
现在是最后一件事。我们需要添加 Mac OS X 框架,这些框架对于我们的 Cinder 应用程序是必需的。关闭 项目设置 面板,在 Xcode 项目导航器中的 框架 文件夹上右键单击,选择 添加 | 现有框架。然后,选择以下内容:
-
Accelerate.framework
-
AudioToolbox.framework
-
AudioUnit.framework
-
CoreAudio.framework
-
CoreVideo.framework
-
OpenGL.framework
-
QTKit.framework
-
QuickTime.framework
编译并运行项目。成功!现在,你可以前往本章的最终调整部分。
连接到 Cinder(Xcode 4+)
按照以下步骤创建一个带有我们主 Cinder 位置路径的用户定义变量:
-
在 Xcode 项目导航器面板中选择BaseApp项目图标。
-
在目标类别下选择BaseApp目标。
-
确保我们对所有配置都进行了更改(选择所有和组合)。
-
点击添加构建设置按钮,然后从菜单中选择添加用户定义设置。
-
呼叫它
CINDER_PATH
,并输入你复制 Cinder 文件的位置的路径。
现在我们必须使用这个新创建的变量。滚动到搜索路径部分,并修改用户头文件搜索路径设置。将值设置为$(CINDER_PATH)/include
。完成此操作后,转到头文件搜索路径(注意其前面没有用户),并将其设置为$(CINDER_PATH)/boost
。
然后转到架构部分,并将架构变量设置为i386
。
接下来,我们必须告诉 Xcode 在哪里可以找到用于链接的 Cinder 库。找到链接部分,并将其他链接器标志下的调试字段更改为$(CINDER_PATH)/lib/libcinder_d.a
,将发布字段更改为$(CINDER_PATH)/lib/libcinder.a
。
Xcode 还为我们自动添加了另一件事,一个链接到我们之前删除的前缀头文件。这将在编译时引发错误,因为编译器将无法找到前缀头文件。转到Apple LLVM 编译器 3.0 – 语言选项卡,并为调试和发布清除前缀头字段。
最后,我们必须将我们的应用程序链接到 Mac OS X 框架库。为此,请按照以下步骤操作:
-
转到构建阶段选项卡。
-
展开链接二进制与库部分。
-
点击添加项目按钮以选择框架。
选择以下框架,然后点击添加:
-
Accelerate.framework
-
AudioToolbox.framework
-
AudioUnit.framework
-
CoreAudio.framework
-
CoreVideo.framework
-
OpenGL.framework
-
QTKit.framework
-
QuickTime.framework
就这样!编译并运行项目。成功!现在,你可以前往本章的最终调整部分。
从头创建项目(Windows)
所以你是在 Windows 系统上,对吧?让我们在 Visual C++中创建一个空项目。我们将将其链接到所有必要的库,并确保在继续之前可以编译该项目。
前往你的 Cinder 项目目录(在我的情况下是C:\Users\kr15h\Documents\CinderProjects\
),并在其中创建一个新的目录BaseApp
。
打开 Microsoft Visual C++ 2010,前往文件 | 新建 | 项目。从Win32类别中选择Win32 项目。在名称字段中输入BaseApp
,在位置字段中输入我们刚刚创建的 BaseApp 项目目录。确保创建解决方案目录复选框未选中,并且解决方案名称为BaseApp
——与项目名称相同。
在应用程序设置对话框中,选中空项目复选框,并确保您选择Windows 应用程序作为应用程序类型。点击完成。
关闭 Visual C++并转到BaseApp
项目目录。您将看到一个名为BaseApp
的文件夹——将其重命名为vc10
。
在与vc10
文件夹同一级别的目录中创建一个新的目录,并将其重命名为src
。在vc10
文件夹中打开BaseApp.sln
文件。创建一个新的 C++文件。选择文件 | 新建文件。在对话框中,选择Visual C++模板类别,并选择C++文件。点击打开。编辑器中将打开一个空白文件。在那里输入以下代码片段:
#include "cinder/app/AppBasic.h"
#include "cinder/gl/gl.h"using namespace ci;
using namespace ci::app;
using namespace std;class BaseApp : public AppBasic {
public:void setup();void update();void draw();
};void BaseApp::setup(){}
void BaseApp::update(){}void BaseApp::draw()
{// clear out the window with blackgl::clear( Color( 0, 0, 0 ) );
}CINDER_APP_BASIC( BaseApp, RendererGl )
前往文件 | 另存为,导航到BaseApp\src
目录,并将文件命名为BaseApp.cpp
。为了保持整洁,导航到资源管理器中的BaseApp文件夹,并将其拖放到 Visual C++ 2010 解决方案资源管理器中的源文件目录。
您现在还不能编译和运行它。还有一些其他事情要做。
前往项目 | BaseApp 属性。在配置选择字段中选择所有配置。
在左侧列中点击C/C++类别,然后在右侧列中找到附加包含目录字段。将以下路径添加到那里:
-
C:\cinder\include
-
C:\cinder\boost
在左侧列中点击链接器,并编辑附加库目录字段的值。您必须输入以下两个路径:
-
C:\cinder\lib
-
C:\cinder\lib\msw
点击确定。现在选择调试配置,并在项目属性窗口的左侧列中点击链接器下的输入。将cinder_d.lib
添加到附加依赖项字段,并将LIBCMT
添加到忽略特定默认库字段。
完成这些操作后,选择发布配置,并将cinder.lib
添加到附加依赖项字段。
点击确定。构建并运行应用程序。一个黑色背景的窗口应该会出现。如果是这样,你就是大师,我们可以继续下一部分。
最终调整
无论您选择哪种设置,您都必须编辑一个位于项目目录相同位置且在所有配置中名称相同的单个文件,即src/BaseApp.cpp
。打开它。我们将尝试理解不同代码行的含义。
#include "cinder/app/AppBasic.h"
#include "cinder/gl/gl.h"
这些是初始的 include
语句,负责在我们开始编写实际的 Cinder 代码之前包含所有必要的 Cinder 内容。
using namespace ci;
using namespace ci::app;
using namespace std;
在这里,我们告诉编译器我们将要使用一些命名空间。我们这样做是为了避免在我们的代码中在定义在这些命名空间中的变量和方法之前写上命名空间名称。因此,这些行只是为了让我们的代码更易于阅读,让我们的创意生活更轻松。我们在这里没有添加 gl
命名空间,因为我们希望将 OpenGL 绘图代码与文件中的所有其他代码区分开来。
class BaseApp : public AppBasic {
这是我们的基础应用程序类的定义。它扩展了 Cinder AppBasic——这是一个包含所有我们需要自己编写的必要功能的高级类。
void setup();void update();void draw();
这些是我们将在该类中重写的函数。这些函数可以被认为是 Cinder 中最重要的,因为在这里你可以定义程序主运行循环中将要执行的操作。
setup()
用于设置你的程序,并且它只在程序开始时执行一次。
update()
在 draw()
函数(默认 Cinder 帧率为 30 fps)之前为每一帧调用,并且你应该在这里放置所有的计算、数据检索和分析代码。
draw()
用于在屏幕上绘制东西。由于 update()
负责所有计算,你可以在这里使用结果来在屏幕上绘制东西。可以是 2D 或 3D 文本、2D 或 3D 形状——电影文件中的某个点。选择你想要的,但尽量只使用这个方法进行绘图。
然后是方法的实现(在以 BaseApp : public AppBasic {
开始的类声明之后和以关闭括号 }
结束的类声明之间)以及负责启动你的程序的一行代码如下:
CINDER_APP_BASIC( BaseApp, RendererGl )
这告诉 Cinder 使用 RendererGl
来运行你的程序(BaseApp
)。RendererGl
是 Cinder 中可用的渲染器之一。它使用 OpenGL 进行渲染,这让你可以使用 gl::
命名空间中定义的类和函数,以及纯 OpenGL 函数。
Cinder 中还有一些其他可用的渲染器,例如 Renderer2d
,它可以与 Cairo 向量图形 Cinder 命名空间结合使用。它使用 Cairo::createWindowSurface()
函数进行绘图,可能不如使用 OpenGL 那样快。
好的,现在我们准备给我们的程序添加一些视觉反馈。我们在 draw()
方法的实现中这样做。
让我们画一些线条。按照以下方式编辑 draw()
方法的实现:
void BaseApp::draw()
{// clear out the window with blackgl::clear( Color( 0.0f, 0.0f, 0.0f ) );
在我们开始编写实际的绘图代码之前,让我向你解释一下 Cinder 和 OpenGL 坐标空间之间的区别。
在 Cinder(以及许多其他创意编码框架和 2D 图形软件中),我们利用了所谓的二维笛卡尔坐标系中的第四象限。这意味着点(0,0)位于屏幕的左上角。你可能还记得在学校数学课上学到的这一点——从(0,0)点左侧,x 值变为负,右侧变为正。从(0,0)点向下,y 值变为负,向上变为正。
与 OpenGL 不同。所有四个象限都被使用,点(0,0)位于屏幕中心。你可能还记得这一点是从学校的数学课上学到的——从(0,0)点左侧,x 值变为负,右侧变为正。从(0,0)点向下,y 值变为负,向上变为正。
Cinder 的gl
命名空间方法为我们完成了从“左上角居中”坐标系到“实际居中”坐标系的转换。所以让我们来画一下:
// draw a x over the whole window areagl::drawLine( Vec2f(0.0f, 0.0f),Vec2f(getWindowWidth(), getWindowHeight()) );gl::drawLine( Vec2f(0.0f, getWindowHeight()),Vec2f(getWindowWidth(), 0.0f) );
}
编译并运行项目。你应该会看到以下输出:
就这样!我们已经为我们的未来应用程序创建了一个很好的基础!在接下来的章节中,我们将重用它的结构。
摘要
在本章中,我们学习了如何创建一个基础应用程序。我们使用了两种不同的方法——一种简单的方法(通过使用 TinderBox)和一种困难的方法(从头创建项目)。前者让你可以快速完成,但后者让你对事物的工作原理有更深入的了解。
第四章:准备你的画笔 - 绘制基本形状
在本章中,我们将学习如何使用 Cinder 绘制不同的基本形状。这些形状是基本的,但通过组合这些形状,你可以创建相当复杂的图像。我们将逐一介绍可用的绘图方法,并尝试使用它们。
准备你的工作空间
打开 TinderBox (yourCinderPath/tools/TinderBox
) 并创建一个名为 BasicShapes
的新项目。这次将 BasicShapes
作为 命名前缀 输入。在安全的地方创建它,并打开 xcode/BasicShapes.xcodeproj
。Windows 用户应从 vc10\BasicShapes.sln
打开项目文件。在编辑器中打开 BasicShapesApp.cpp
并导航到类似于以下代码片段的位置:
void BasicShapesApp::draw()
{// clear out the window with blackgl::clear( Color( 0, 0, 0 ) );
}
这是应用程序主要绘图方法的实现。这里只执行了一行代码(gl::clear( Color( 0, 0, 0 ) );
),正如它前面的注释所说,它使用黑色清除颜色缓冲区,这本质上意味着在之前的 draw()
循环中绘制的一切都被黑色替换。
draw()
方法每帧执行一次。Cinder 的默认帧率为每秒 30 帧。因此,背景每秒清除 30 次。
让我们尝试改变背景的颜色!正如你所见,你必须向 gl::clear()
函数传递一个 Color
参数。在这种情况下,Color
对象由三个参数组成,这些参数定义了 RGB 颜色系统中的颜色。在 Color( 0, 0, 0 )
参数中,括号内有三个零。第一个定义了红色量,第二个是绿色,第三个是蓝色。假设我们想让背景是红色,那么我们必须将颜色的第一个参数传递为 1
。它应该看起来类似于以下内容:
gl::clear( Color( 1, 0, 0 ) );
编译并运行项目以查看我们是否成功。你应该会看到一个背景为鲜红色的窗口。在 Cinder 中,颜色被定义为从 0 到 1 的数字。所以如果你不想背景这么亮,试试这个:
gl::clear( Color( 0.5f, 0, 0 ) );
再次编译并运行项目。现在你应该看到一个亮度较低的鲜红色背景。太好了!现在让我们将背景颜色改为黄色。为此,我们需要更改两个 Color
参数,因为在 RGB 颜色系统中没有单独的黄色量参数。当我们看色轮时,我们可以看到黄色位于红色和绿色之间。当我们仔细检查这两种颜色之间的空间时,你会看到,红色变成橙色,橙色变成黄色,黄色变成绿色。
因此,我们必须以相同的比例混合这两种颜色以得到黄色。在代码中,它看起来类似于以下内容:
gl::clear( Color( 1, 1, 0 ) );
编译并运行项目。做得好!你现在将看到一个漂亮、明亮的黄色背景。让我们对其进行最后一次调整,将其改为白色。要做到这一点,我们必须将所有 RGB 组件设置为最大值。正如我们从物理学世界中知道的那样,白色颜色由所有可见光波长组成,当我们通过玻璃棱镜射出白光束时,我们得到彩虹——完整的可见光光谱。当我们以相同的数量将光谱中的所有颜色组合在一起时,我们得到白色:
gl::clear( Color( 1, 1, 1 ) );
现在,我们有一个漂亮的白色背景。让我们在上面画些东西!
绘制线条
让我们把背景再次改为黑色,并添加以下代码行:
// draw a line
gl::drawLine( Vec2f(0,0), Vec2f(100,100) );
编译并运行项目以查看发生了什么。你应该在屏幕上看到一条相对较短的白色线条。这正是函数gl::drawLine()
刚刚执行的操作。正如我们所见,必须向其传递两个参数。第一个参数代表线的起始坐标,第二个参数定义了线的结束坐标。这两个点被定义为Vec2f
对象。从这些对象的名字中我们可以看出,它们是存储两个浮点值的向量。这些向量可以在二维空间中使用。Vec2f(0,0)
代表位于屏幕左上角的两维空间中的点。让我们在整个窗口上画两条对角线。将以下代码替换为gl::drawLine( Vec2f(0,0), Vec2f(100,100) );
:
gl::drawLine( Vec2f(0,0),
Vec2f(getWindowWidth(),getWindowHeight()) );
gl::drawLine( Vec2f(0,getWindowHeight()),
Vec2f(getWindowWidth(),0) );
如您所见,我们在这里使用了两种新的方法,getWindowWidth()
和getWindowHeight()
,它们旨在确定应用程序运行时的窗口大小。这是一种非常方便的绘制图形的方法,可以独立于窗口大小。我们不必每次决定更改我们的应用程序窗口大小时手动更改参数。我们只需将getWindowWidth()
和getWindowHeight()
方法放在参数的位置,图形将在应用程序窗口被调整大小时自动调整。
编译并运行应用程序,并尝试通过拖动其右下角来改变窗口的大小——线条应该调整到新的窗口大小。
做这件事很好,但如果我们需要线条不是白色,而是青色呢?没问题!在drawLine()
调用之前添加以下代码行:
gl::color( 0, 1, 1 );
小贴士
下载示例代码
您可以从您在www.packtpub.com
的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/
支持并注册,以便将文件直接通过电子邮件发送给您。
这并不难。我们只需要记住在调用一个应该绘制东西的函数之前改变颜色(如果我们想的话)。所以如果我们想用红色画一条线,用青色画另一条线,我们就这样做:
gl::color( 0, 1, 1 ); // set color to cyan
gl::drawLine( Vec2f(0,0),
Vec2f(getWindowWidth(),getWindowHeight()) );
gl::color( 1, 0, 0 ); // set color to red
gl::drawLine( Vec2f(0,getWindowHeight()),
Vec2f(getWindowWidth(),0) );
所以现在我们知道了如何改变线条的颜色。那么改变粗细呢?没问题!在绘制线条的调用之前放置以下代码行:
glLineWidth( 10.0f );
因此现在我们的线条正在以 10
像素的粗细绘制。编译并运行项目以查看效果。现在你应该看到如下截图所示的线条:
以下是完全的绘制方法,如果你之前没有理解某些内容(我在以下代码中将线宽更改为 2
):
void BasicShapesApp::draw(){// clear out the window with blackgl::clear( Color( 0, 0, 0 ) );// draw some linesglLineWidth( 2 ); // set line width to 2gl::color( 0, 1, 1 ); // set color to cyangl::drawLine( Vec2f(0,0),Vec2f(getWindowWidth(),getWindowHeight()) );gl::color( 1, 0, 0 ); // set color to redgl::drawLine( Vec2f(0,getWindowHeight()),Vec2f(getWindowWidth(),0) );
}
绘制圆圈
那么,在作品中添加一些圆怎么样?在最后一个 drawLine
调用之后添加以下行:
gl::drawSolidCircle( Vec2f(getWindowWidth()/2,getWindowHeight()/2), 50 );
在编译并运行应用程序后,你应该在屏幕中间看到一个圆圈。drawSolidCircle()
函数接受两个参数。第一个是圆心的位置。第二个是圆的半径。我们再次使用 Vec2f
对象来定义位置。正如你所见,我们再次使用了 getWindowWidth()
和 getWindowHeight()
。这次我们需要它们来获取屏幕的中心坐标。我们通过将窗口的宽度和高度除以 2
来得到它。再次,通过使用这些方法,我们确保无论窗口的大小如何,我们的圆都会在屏幕中间绘制。
虽然有一种更简单的方法来做这件事,那就是使用 getWindowCenter()
方法。如果我们使用它,我们会得到相同的结果,但代码看起来更清晰一些:
gl::drawSolidCircle( getWindowCenter(), 50 );
让我们改变圆的颜色。你可以选择你自己的颜色,但这次我将使用洋红色。在 drawSolidCircle()
函数调用之前添加以下代码行:
gl::color( 1, 0, 1 ); // set color to magenta
尝试实验圆的位置、半径和颜色。尝试绘制多个形状,并尝试给它们不同的颜色。
如果我们只想绘制圆的轮廓呢?Cinder 有一个专门用于此的函数,称为 drawStrokedCircle()
。在 drawSolidCircle()
函数之后添加另一行代码,如下所示:
gl::drawStrokedCircle( getWindowCenter(), 100 );
与 drawSolidCircle()
函数类似,drawStrokedCircle()
也接受两个参数——位置和半径。区别仅在于它只绘制轮廓。轮廓的粗细与我们之前通过 glLineWidth()
函数定义的相同。让我们通过在 drawStrokedCircle()
行之前添加以下代码行来将其更改为其他颜色:
glLineWidth( 4 );
编译并运行项目,看看会发生什么。你应该看到如下截图所示的屏幕:
对于 drawSolidCircle()
和 drawStrokedCircle()
函数,有一个隐藏的第三个参数。由于圆是通过三角形绘制的,每次绘制圆时,都必须决定使用多少个三角形。Cinder 会自动完成这项工作,但我们也可以自己定义三角形数量。所以让我们改变三角形段的数量:
gl::color( 1, 0, 1 ); // set color to magenta
gl::drawSolidCircle( getWindowCenter(), 50, 5 );
glLineWidth( 4 );
gl::drawStrokedCircle( getWindowCenter(), 100, 5 );
注意代码中高亮的部分。我们告诉圆形绘制函数只使用五个三角形来绘制圆形。通过这样做,我们得到了五边形而不是圆形。编译并运行项目,亲自看看吧!
尝试调整所有属性以充分利用它。你可以使用这个函数绘制几乎任何类型的规则多边形形状。
绘制矩形
现在我们知道了如何绘制线条和圆形,让我们转向另一个我们可以使用的几何原始形状——矩形。
就像圆形的情况一样,有一个函数用于绘制填充矩形,还有一个函数用于仅绘制轮廓。它们分别是 drawSolidRect()
和 drawStrokedRect()
。
gl::color( 0, 0, 1 ); // set color to blue
gl::drawSolidRect( Rectf( getWindowWidth()/2-20.0f,getWindowHeight()/2-20.0f,getWindowWidth()/2+20.0f,getWindowHeight()/2+20.0f ) );
gl::drawStrokedRect( Rectf( getWindowWidth()/2-120.0f,getWindowHeight()/2-120.0f,getWindowWidth()/2+120.0f,getWindowHeight()/2+120.0f ) );
在这里,我们绘制了两个矩形,一个填充(或实心)和一个轮廓。请注意,我们向两个函数传递了类型为 Rectf
的单个参数。Rectf
的最简单版本是通过使用矩形的左上角和右下角的坐标来构建的。大多数绘图 API 使用左上角的 x 和 y 坐标以及矩形的宽度和高度,所以这可能有点难以适应。
其他有用的绘图函数
这些是非常基本的绘图函数,实际上如果你以创造性的方式使用它们,你可以做很多事情。但这里还有一些其他值得检查的函数。它们在以下代码片段中给出:
gl::color( 1, 1, 0 );
gl::drawSolidEllipse( Vec2f(150,100), 100, 50, 10);
gl::drawStrokedEllipse( Vec2f(400,300), 100, 175, 15);
gl::drawSolidRoundedRect(
Rectf(550,150,600,300), 20, 4 );
gl::drawStrokedRoundedRect(
Rectf(20,300,200,400), 10, 10 );
尝试通过调整这些函数的属性来完全理解它们的含义。编译并运行项目,你应该会看到一个类似于以下截图的图像:
尝试通过拖动右下角来调整窗口的大小。你会看到相对于窗口大小绘制的图形和不是相对于窗口大小绘制的图形之间的区别。在创建自己的应用程序时,请记住这一点。
以下是为绘制所有形状的完整代码,以防你不知道哪一行代码对应哪个位置:
void BasicShapesApp::draw(){// clear out the window with blackgl::clear( Color( 0, 0, 0 ) );// draw some linesglLineWidth( 2 ); // set line width to 2gl::color( 0, 1, 1 ); // set color to cyangl::drawLine( Vec2f(0,0),Vec2f(getWindowWidth(),getWindowHeight()) );gl::color( 1, 0, 0 ); // set color to redgl::drawLine( Vec2f(0,getWindowHeight()),Vec2f(getWindowWidth(),0) );// draw some circlesgl::color( 1, 0, 1 ); // set color to magentagl::drawSolidCircle( getWindowCenter(), 50, 5 );glLineWidth( 4 );gl::drawStrokedCircle( getWindowCenter(), 100, 5 );// draw rectanglesgl::color( 0, 0, 1 ); // set color to bluegl::drawSolidRect( Rectf( getWindowWidth()/2-20.0f,getWindowHeight()/2-20.0f,getWindowWidth()/2+20.0f,getWindowHeight()/2+20.0f ) );gl::drawStrokedRect( Rectf( getWindowWidth()/2-120.0f,getWindowHeight()/2-120.0f,getWindowWidth()/2+120.0f,getWindowHeight()/2+120.0f ) );// draw restgl::color( 1, 1, 0 );gl::drawSolidEllipse( Vec2f(150,100), 100, 50, 10);gl::drawStrokedEllipse( Vec2f(400,300), 100, 175, 15);gl::drawSolidRoundedRect(Rectf(550,150,600,300), 20, 4 );gl::drawStrokedRoundedRect(Rectf(20,300,200,400), 10, 10 );
}
就这些!这是用 Cinder 绘制基本形状最简单的方法之一。
概述
在本章中,我们介绍了 Cinder 中最关键的绘图函数。你可以用这些函数绘制很多东西。然而,它们在可以完成的事情方面有一些限制。如果你需要绘制一些非常复杂的东西,并且它必须表现良好或非常好,考虑获取一些 OpenGL 知识。
你可以在 libcinder.org/docs/v0.8.4/namespacecinder_1_1gl.html
找到 Cinder 当前可用的完整绘图函数列表。在网页上,滚动到函数部分,查看以 "draw" 开头的函数。
我们将在下一章中使用这个列表中的某些函数,所以这并不是我们使用绘制的唯一地方。
第五章:利用图片 – 加载和显示
在本章中,我们将学习以下内容:
-
如何加载图片
-
如何在屏幕上显示它
-
关于 Cinder 中的通用资产处理
你可以在你的应用程序中使用照片、插图或不同的图像数据库。为此,你需要一种将图片加载到你的应用程序中的方法,最重要的是,在屏幕上显示它。
在本章中,我们将学习 Cinder 文件加载功能的基础,并学习从网络和本地存储加载图片。
加载图片
打开 TinderBox 并创建一个名为 BasicImages
的新项目。打开 xcode/BasicImages.xcodeproj
项目文件。Windows 用户应打开项目文件 vc10\BasicImages.sln
。在编辑器中打开 BasicImagesApp.cpp
文件。
首先,我们将包含一些头文件,这些头文件声明了执行我们想要完成的事情所需的功能。
导航到代码中包含 #include
语句的位置,并在 #include "cinder/gl/gl.h"
之后添加以下代码行:
#include "cinder/ImageIo.h"
#include "cinder/gl/Texture.h"
第一个包含语句加载了负责 Cinder 图像输入/输出功能的代码头文件,第二个代码头文件包含语句允许我们使用 OpenGL 纹理在屏幕上绘制图片。
接下来,我们需要声明一个类型为 gl::Texture
的变量,它将包含实际的图像数据。你可以为它选择自己的名字,但我会称它为 imgTexture
:
gl::Texture imgTexture;
在 BasicImagesApp
类声明中 void draw();
之后添加高亮显示的代码行:
class BasicImagesApp : public AppBasic {
public:void setup();void update();void draw();gl::Texture imgTexture;
};
接下来,我们需要选择一个要加载到应用程序中的图片。因此,我们将使用互联网上的某些随机图片,因为如果你有网络连接,这是在屏幕上快速获取可见图片的最快方式。
注意
在编译和运行以下示例之前,请尝试在你的浏览器中打开此 URL rijnieks.lv/projects/cinder_begin_creative_coding/images/image.png
。如果你看不到图片,请在互联网上找到任何其他 *.png
或 *.jpg
图片。
在设置方法声明中添加以下高亮显示的代码行:
void BasicImagesApp::setup() {imgTexture = gl::Texture( loadImage( loadUrl( Url("http://rijnieks.lv/projects/cinder_begin_creative_coding/images/image.png") ) ) );
}
我们在 setup()
中加载它的原因是因为我们只需要加载一次图片。由于从网络上加载图片需要一些时间,我们不希望因为图片还在加载而延迟我们的 draw()
或 update()
操作。Cinder 专注于速度,我们不想牺牲它。
现在,我们需要添加一些代码来处理图片的显示。转到 draw()
方法的实现,并在 gl::clear( Color( 0, 0, 0 ) );
函数之后添加以下代码:
gl::draw( imgTexture, getWindowBounds() );
在这里,我们告诉 Cinder 我们想在应用程序窗口边界内绘制我们的imgTexture
(图片将被拉伸,使其宽度等于窗口的宽度,高度等于窗口的高度)。尝试编译并运行应用程序。经过短暂的延迟后,应该会出现一个图片。如果你尝试调整窗口大小,你可以看到图片也在调整大小。
成员函数getWindowBounds()
返回一个Area
对象,我们不需要使用这个函数,可以自己定义绘图区域。尝试以下操作:
gl::draw( imgTexture, Area(100,100,540,380) );
如果你现在尝试调整窗口大小,你会看到它不会影响图片,因为现在它的显示位置和大小是硬编码在Area(100,100,540,380)
中的。
处理资产
现在让我们看看我们需要创建一个将在没有互联网连接的计算机上使用的应用程序需要什么。我们必须在计算机的某个地方存储我们的图片。
Cinder 有预定义的处理图片和类似资产的方式。如果你使用 TinderBox 创建项目,你可能已经注意到在你的项目文件夹中有一个资产目录。如果没有,创建一个。
现在将你的图片文件复制到资产目录中,并将setup()
实现中的loadImage
代码更改为以下内容:
imgTexture = gl::Texture( loadImage( loadAsset( "MyImage.png" ) ) );
如你所见,我们将loadUrl()
更改为loadAsset()
,实际上它似乎比loadUrl(Url("http://..."))
方法简单。只是我们确实需要自己处理我们的资产。
现在尝试将资产目录在文件系统中向上移动一个级别,使其紧邻BasicImages
文件夹,如下截图所示:
尝试编译并运行我们的项目,你会惊讶地发现图片仍然可以加载!有些人可能会认为这是缓存的魔法或类似的东西,但不是,Cinder 只是自动搜索可执行文件上方最多五级的assets
目录。因此,你可以选择在哪个级别存储你的资产,但以某种方式,默认方式可能是最好的,因为它允许你为 Mac OS X 和 Windows 项目使用相同的资产。如果你在多个使用相同资产的项目上工作,那么你可以将你的assets
目录移动到项目文件夹级别,就像我们刚才做的那样。
其中有一点需要注意——如果你的图片使用 alpha 透明度,你可能会得到奇怪的结果。要绘制具有 alpha 透明度的图片,我们必须在绘制图片之前启用 alpha 混合,并在绘制后通过添加以下代码片段来禁用它:
gl::enableAlphaBlending();
gl::draw( imgTexture, Rectf(100,100,540,380) );
gl::disableAlphaBlending();
如果你有多个项目组使用相同的资产,也有一种方法添加额外的资产目录。以下代码行负责处理这一点:
addAssetDirectory( "/Users/You/myOtherAssets/" );
注意,这里你必须使用绝对路径,所以这对跨平台项目来说不是很好。在 Windows 上,你可能写成以下类似的内容:
addAssetDirectory( "C:\Users\You\myOtherAssets\" );
如果你使用资产,你必须记得将资产目录与应用程序一起部署。
有一种方法可以告诉你如何在应用程序中包含资源,但这个主题略超出了本书的范围。如果你想了解更多关于这个话题的信息,你应该在互联网上搜索“Cinder 资源管理”。
以下是我们在本章中制作的应用程序的完整代码:
#include "cinder/app/AppBasic.h"
#include "cinder/gl/gl.h"
#include "cinder/ImageIo.h"
#include "cinder/gl/Texture.h"using namespace ci;
using namespace ci::app;
using namespace std;class BasicImagesApp : public AppBasic {
public:void setup();void draw();gl::Texture imgTexture;
};void BasicImagesApp::setup() {addAssetDirectory( "/Users/kr15h/myOtherAssets/" );imgTexture = gl::Texture( loadImage( loadAsset( "MyImage.png" ) ) );
}void BasicImagesApp::draw() {gl::clear( Color( 0, 0, 0 ) );gl::enableAlphaBlending();gl::draw( imgTexture, Rectf(100,100,540,380) );gl::disableAlphaBlending();
}
CINDER_APP_BASIC( BasicImagesApp, RendererGl )
代码并不多!
摘要
在本章中,我们学习了如何从互联网以及本地存储加载图像,将其分配给 OpenGL 纹理,并在屏幕上绘制它。
我们对 Cinder 中资源管理的工作原理有了基本的了解,并且只要assets
目录位于我们的应用程序上方不超过五级,我们就不必担心其位置。
我们还了解到,在 Mac OS X 和 Windows 上,我们可以以相同的方式使用loadUrl()
和loadAsset()
函数。
第六章. 加速 - 创建生成式动画
在本章中,我们将学习以下内容:
-
程序化动画的基础
-
同时动画多个对象
-
如何利用随机性
-
如何从内置动画缓动函数中受益
我们将通过使用基本形状创建一个相对简单的绘图,然后以参数方式对这些形状进行动画处理。我们还将学习如何利用 Cinder 的内置缓动函数,这可以使我们的动画润色变得更加容易。
准备舞台
在我们开始之前,打开 TinderBox 并创建一个名为BasicAnimation
的新项目。打开xcode/BasicAnimation.xcodeproj
(在 Windows 上为vc10\BasicAnimation.sln
)。在编辑器中打开BasicAnimationApp.cpp
,以便我们可以开始进行更改。
这次我们将更改窗口大小,因为 640 x 480 像素可能对于我们想要创建的大多数构图来说太小了。为了做到这一点,我们需要覆盖另一个 Cinder 的AppBasic
方法——prepareSettings()
。在draw()
方法声明之后添加一个新的声明,如下所示:
class BasicAnimationApp : public AppBasic {
public:void setup();void update();void draw();void prepareSettings( Settings *settings );
};
现在在我们刚刚声明的setup()
方法实现之前添加方法的实现:
void BasicAnimationApp::prepareSettings( Settings *settings ){}
让我们更改窗口大小。为此,在prepareSettings
方法实现中添加以下代码行:
settings->setWindowSize( 800, 600 );
我们还将更改帧率,以更合适于 Cinder 的值——每秒 60 帧是一个不错的选择:
settings->setFrameRate( 60 );
因此,整体看起来如下所示:
void BasicAnimationApp::prepareSettings( Settings *settings ){settings->setWindowSize( 800, 600 );settings->setFrameRate( 60 );
}
现在编译并运行你的应用程序以测试它是否工作。
接下来我们将绘制一个实心圆。正如你可能从前面的章节中记得的那样,为了做到这一点,我们必须使用drawSolidCircle()
函数。
在BasicAnimationApp.cpp
文件中导航到draw()
成员函数的实现,并在gl::clear()
函数调用之后添加以下代码行:
gl::drawSolidCircle( getWindowCenter(), 30 );
这将在窗口中心绘制一个半径为 30 像素的白色圆圈。由于本章是关于动画的,让我们考虑一些移动这个圆圈的方法。
添加动画
如我们所知,在每一帧绘制任何东西之前,我们使用黑色清除前帧留下的所有内容。我们使用gl::clear()
来做到这一点。为了创建移动某物的效果,我们需要在每一帧中改变对象的位置。
为了做到这一点,我们需要将drawSolidCircle
位置参数的值替换为变量。让我们声明一个新的变量,它将保存圆在屏幕上的位置:
void prepareSettings( Settings *settings );Vec2f currentPosition;
};
这个变量将保存我们在draw()
方法中绘制的圆的x
和y
位置。
现在我们将在setup()
方法中设置变量的初始值:
void BasicAnimationApp::setup() {currentPosition = getWindowCenter();
}
如你所见,前面代码片段的一部分与我们用于drawSolidCircle()
函数调用的代码相同。我们只是让它变得可变。
最后,我们必须将drawSolidCircle()
函数中的值替换为新创建的变量:
void BasicAnimationApp::draw() {gl::clear( Color( 0, 0, 0 ) );gl::drawSolidCircle( currentPosition, 30 );
}
再次编译并运行我们的应用程序。没有什么太大的变化。别担心,在drawSolidCircle()
函数调用之前添加以下代码行:
currentPosition.x++;
再次编译并运行应用程序,那里就有了一个移动的圆。酷!我们有了第一个动画!现在让我们对圆的半径做些处理。要对其应用动画,我们必须再次声明一个新变量:
void prepareSettings( Settings *settings );Vec2f currentPosition;float circleRadius;
};
现在,设置其起始值:
void BasicAnimationApp::setup() {currentPosition = getWindowCenter();circleRadius = 100;
}
然后在draw()
方法实现中添加一个动画规则,并替换drawSolidCircle()
函数调用中圆半径参数的常量值:
currentPosition.x++;circleRadius--;gl::drawSolidCircle( currentPosition, circleRadius );
}
编译并运行我们的应用程序。你应该会看到眼前有一个大白色点消失。可能看起来你做错了什么,但别担心,一切正常。诀窍在于我们每帧减少一个像素的圆半径。这是以每秒 60 次的速率发生的,这意味着圆的半径将在大约 1.5 秒内达到0
。因此,如果半径是0
,圆就变得不可见,因为没有半径的圆是不存在的。
到目前为止,一切顺利。让我们尝试随着时间的推移将我们的圆移动到某个固定的位置。比如说,我们想从屏幕的左上角移动到底右角。要做到这一点,我们需要将圆的初始位置设置为0
,并且让我们也将初始circleRadius
改为更小的值:
void BasicAnimationApp::setup() {currentPosition = Vec2f(0,0);circleRadius = 100;
}
让我们在类声明中声明另一个Vec2f
变量,该变量将保存圆的目标位置:
Vec2f currentPosition;Vec2f targetPosition;float circleRadius;
};
我们需要将初始目标位置设置在某个地方,所以我们必须在setup()
方法实现中添加一行新代码:
void BasicAnimationApp::setup() {currentPosition = Vec2f(0,0);targetPosition = Vec2f(800,600);circleRadius = 100;
}
最后,我们需要编写一些代码来在currentPosition
和targetPosition
之间创建平滑的过渡。让我们在update()
方法实现中这样做,因为它适合这类计算。记住,尽量只使用draw()
方法进行绘图,并将所有计算放在update()
方法中。对于像这样的小型应用程序来说,这并不重要,但随着你的代码越来越大,如果不符合这个简单规则,应用程序可能表现不佳,甚至可能崩溃。
void BasicAnimationApp::update() {Vec2f difference = targetPosition - currentPosition;difference *= 0.95f;currentPosition = targetPosition - difference;
}
这三行代码计算当前圆位置和目标圆位置之间的差异。然后,我们通过乘以一个小于0
的浮点数来减小这些位置之间的差异。最后,我们通过从圆的目标位置减去新的差异来计算新的当前位置。
如果我们没有利用 Cinder 的集成矢量代数功能,这将需要更长的代码。由于Vec2f
对象包含两个值(x 和 y 坐标),当我们用一个单一值乘以它时,Vec2f
对象内的两个值都会乘以这个值。此外,如果我们用一个Vec2f
对象乘以另一个Vec2f
对象,第一个向量的第一个元素与第二个向量的第一个元素相乘,第一个向量的第二个元素与第二个向量的第二个元素相乘,依此类推。
现在编译并运行我们的应用程序。您应该看到一个圆从屏幕的左上角移动到右下角。
添加随机性
让我们通过使用强大的随机函数来增加一些不可预测性。要在 Cinder 中使用它们,您必须包含一个包含必要代码的头文件:
#include "cinder/Rand.h"
在BasicAnimationApp.cpp
文件的开始处添加此代码。接下来,我们需要在setup()
方法实现中计算随机目标位置:
void BasicAnimationApp::setup() {currentPosition = Vec2f(0,0);targetPosition.x = Rand::randFloat(0, getWindowWidth());targetPosition.y = Rand::randFloat(0, getWindowHeight());circleRadius = 100;
}
现在每次运行应用程序时,都会计算一个新的结束位置,圆将飞到屏幕上的不同位置。
让我们将圆的当前位置也改为随机值:
void BasicAnimationApp::setup() {currentPosition.x = Rand::randFloat(0, getWindowWidth());currentPosition.y = Rand::randFloat(0, getWindowHeight());targetPosition.x = Rand::randFloat(0, getWindowWidth());targetPosition.y = Rand::randFloat(0, getWindowHeight());circleRadius = 100;
}
编译并运行应用程序以查看更改。
在打开应用程序后只看到一次随机动画可能有点无聊。人们通常期望更多。那么,当圆达到当前结束位置时,我们是否可以计算一个新的随机结束位置?好的,让我们这么做!在update()
方法实现中,在currentPosition = targetPosition - difference;
之后添加以下代码片段:
if ( currentPosition.distance(targetPosition) < 1.0f ) {targetPosition.x = Rand::randFloat(0, getWindowWidth());targetPosition.y = Rand::randFloat(0, getWindowHeight());
}
从draw()
方法中注释掉或删除以下突出显示的行:
gl::clear( Color( 0, 0, 0 ) );
//currentPosition.x++;
//circleRadius--;
gl::drawSolidCircle( currentPosition, circleRadius );
编译并运行我们的应用程序。这有点更有趣,但仍然需要更多才能完整。
我们是否可以尝试在屏幕上处理多个圆?解释如何创建一个作为单独类的粒子系统是值得的,但这超出了本书的范围,所以我们将在同一文件中继续进行一些更改。
更多圆圈
让我们定义应用程序要处理的圆圈数量。在#include
语句之后添加以下代码行:
#define CIRCLE_COUNT 100
现在,让我们转到类声明部分,并将我们变量的声明更改为数组:
Vec2f currentPosition[CIRCLE_COUNT];
Vec2f targetPosition[CIRCLE_COUNT];
float circleRadius[CIRCLE_COUNT];
如您所见,我们使用了之前定义的常量作为我们数组的大小。通过这样做,我们可以轻松地稍后更改圆的数量。
接下来,我们不得不在setup()
方法实现中更改一些代码:
void BasicAnimationApp::setup() {for(int i=0; i<CIRCLE_COUNT; i++) {currentPosition[i].x=Rand::randFloat(0,getWindowWidth());currentPosition[i].y=Rand::randFloat(0,getWindowHeight());targetPosition[i].x=Rand::randFloat(0,getWindowWidth());targetPosition[i].y=Rand::randFloat(0,getWindowHeight());circleRadius[i] = Rand::randFloat(1, 10);}
}
基本上,我们将之前拥有的相同代码封装在一个for
循环中,该循环遍历所有参数数组并为它们中的每一个设置初始值。
还不要编译,因为我们仍然需要以类似的方式更改update()
和draw()
方法。按照以下方式更改我们的update()
方法:
void BasicAnimationApp::update() {Vec2f difference;for (int i=0; i<CIRCLE_COUNT; i++) {difference = targetPosition[i] - currentPosition[i];difference *= 0.95f;currentPosition[i] = targetPosition[i] - difference;if (currentPosition[i].distance(targetPosition[i]) < 1.0f) {targetPosition[i].x =Rand::randFloat(0,getWindowWidth());targetPosition[i].y =Rand::randFloat(0,getWindowHeight());}}
}
最后,按照以下方式更改我们的draw()
方法实现:
void BasicAnimationApp::draw() {gl::clear( Color( 0, 0, 0 ) );for (int i=0; i<CIRCLE_COUNT; i++) {gl::drawSolidCircle( currentPosition[i], circleRadius[i] );}
}
现在编译并运行我们的应用程序。这看起来更有趣了!
似乎 100 个圆圈还不够,那么我们是否可以将CIRCLE_COUNT
常量设置为1000
?
#define CIRCLE_COUNT 1000
没问题!
但如果我们不想关注数量,而是关注运动的质量呢?这就是动画缓动加入游戏的地方。
使用内置缓动
现在,假设我们想利用我们在EaseGallery
示例中看到的缓动算法。为此,我们必须按照某些步骤更改代码。
要使用缓动函数,我们必须包含Easing.h
头文件:
#include "cinder/Easing.h"
首先,我们将添加两个额外的变量,startPostition
和circleTimeBase
:
Vec2f startPosition[CIRCLE_COUNT];
Vec2f currentPosition[CIRCLE_COUNT];
Vec2f targetPosition[CIRCLE_COUNT];
float circleRadius[CIRCLE_COUNT];
float circleTimeBase[CIRCLE_COUNT];
然后,在setup()
方法实现中,我们必须将currentPosition
部分更改为startPosition
,并给circleTimeBase
数组成员添加一个初始值:
startPosition[i].x = Rand::randFloat(0, getWindowWidth());
startPosition[i].y = Rand::randFloat(0, getWindowHeight());
circleTimeBase[i] = 0;
接下来,我们必须更改update()
方法,使其可以与缓动函数一起使用。它们基于时间,并返回一个介于0
和1
之间的浮点值,该值定义了抽象的0
到1
时间轴上的playhead
位置:
void BasicAnimationApp::update() {Vec2f difference;for (int i=0; i<CIRCLE_COUNT; i++) {difference = targetPosition[i] - startPosition[i];currentPosition[i] = easeOutExpo(getElapsedSeconds()-circleTimeBase[i]) *difference + startPosition[i];if ( currentPosition[i].distance(targetPosition[i])< 1.0f ){targetPosition[i].x =Rand::randFloat(0, getWindowWidth());targetPosition[i].y =Rand::randFloat(0, getWindowHeight());startPosition[i] = currentPosition[i];circleTimeBase[i] = getElapsedSeconds();}}
}
上一段代码中突出显示的部分是已经更改的部分。其中最重要的部分是currentPosition[i]
计算部分。我们取时间轴起点和终点之间的距离,并将其与我们的缓动函数返回的位置浮点数相乘,在这个例子中是easeOutExpo()
。再次,它返回一个介于0
和1
之间的浮点变量,表示在抽象的0
到1
时间轴上的位置。如果我们用任何数字乘以,比如说0.33f
,我们得到该数字的三分之一,0.5f
,我们得到该数字的一半,以此类推。因此,我们将这个距离加到圆的起始位置,我们就得到了它的当前位置!
现在编译并运行我们的应用程序。你应该看到以下内容:
几乎就像暴风雪一样!不过,我们将在代码中添加一个小修改。我将在代码顶部添加一个TWEEN_SPEED
定义,并将传递给缓动函数的time
参数与之相乘,这样我们就可以控制圆的速度:
#define TWEEN_SPEED 0.2
在update()
方法实现中更改以下行:
currentPosition[i] = easeOutExpo((getElapsedSeconds()-circleTimeBase[i])*TWEEN_SPEED) *difference + startPosition[i];
我这样做是因为每个缓动的默认时间基是 1 秒。这意味着每个过渡正好持续 1 秒,这对于我们当前的情况来说有点快。我们希望它更慢,所以我们将传递给缓动函数的时间与一个小于1.0f
且大于0.0f
的浮点数相乘。通过这样做,我们确保时间被缩放,而不是 1 秒,我们的过渡时间变成了 5 秒。
因此,尝试编译并运行它,看看结果如何!以下是我们的圆创建源代码的完整代码:
#include "cinder/app/AppBasic.h"
#include "cinder/gl/gl.h"
#include "cinder/Rand.h"
#include "cinder/Easing.h"#define CIRCLE_COUNT 100
#define TWEEN_SPEED 0.2using namespace ci;
using namespace ci::app;
using namespace std;class BasicAnimationApp : public AppBasic {public:void setup();void update();void draw();void prepareSettings( Settings *settings );Vec2f startPosition[CIRCLE_COUNT];Vec2f currentPosition[CIRCLE_COUNT];Vec2f targetPosition[CIRCLE_COUNT];float circleRadius[CIRCLE_COUNT];float circleTimeBase[CIRCLE_COUNT];
};void BasicAnimationApp::prepareSettings( Settings *settings ) {settings->setWindowSize(800,600);settings->setFrameRate(60);
}void BasicAnimationApp::setup() {for(int i=0; i<CIRCLE_COUNT; i++) {currentPosition[i].x=Rand::randFloat(0, getWindowWidth());currentPosition[i].y=Rand::randFloat(0, getWindowHeight());targetPosition[i].x=Rand::randFloat(0, getWindowWidth());targetPosition[i].y=Rand::randFloat(0, getWindowHeight());circleRadius[i] = Rand::randFloat(1, 10);startPosition[i].x = Rand::randFloat(0, getWindowWidth());startPosition[i].y = Rand::randFloat(0, getWindowHeight());circleTimeBase[i] = 0;}
}void BasicAnimationApp::update() {Vec2f difference;for (int i=0; i<CIRCLE_COUNT; i++) {difference = targetPosition[i] - startPosition[i];currentPosition[i] = easeOutExpo((getElapsedSeconds()-circleTimeBase[i]) *TWEEN_SPEED) *difference + startPosition[i];if ( currentPosition[i].distance(targetPosition[i]) < 1.0f ){targetPosition[i].x =Rand::randFloat(0, getWindowWidth());targetPosition[i].y =Rand::randFloat(0, getWindowHeight());startPosition[i] = currentPosition[i];circleTimeBase[i] = getElapsedSeconds();}}
}void BasicAnimationApp::draw() {gl::clear( Color( 0, 0, 0 ) );for (int i=0; i<CIRCLE_COUNT; i++) {gl::drawSolidCircle( currentPosition[i], circleRadius[i] );}
}CINDER_APP_BASIC( BasicAnimationApp, RendererGl )
尝试调整属性并尝试更改缓动效果。并非所有这些效果都适用于此示例,但至少你会了解如何使用 Cinder 创建平滑的动画。
摘要
在本章中,我们创建了我们的第一个生成动画应用程序。我们学习了如何使用实例变量来创建平滑的运动,以及使用随机函数在可预测的范围内生成不可预测的结果。我们还使用了静态数组来改变模拟中圆的数量,并利用了 Cinder 缓动函数,这些函数在创建类似 Flash 的应用程序时非常有用(如果你有一些 Flash 编码背景,你可能知道 TweenLite 缓动库)。
最后,我们使用相对大量的对象测试了应用程序。这部分应该向你证明 Cinder 的真正实力,因为你可能知道,使用基于非 C++ 平台的框架创建类似的应用程序并不像这个一样流畅。
在下一章中,我们将讨论使用 Cinder 进行实时后处理以及应用图像和视频效果的基本方法。
第七章. 与图像一起工作 – 实时后处理和效果
在本章中,我们将讨论使用内置 Cinder 类的基本方法来对静态和动态图像应用效果。
在本章中,我们将学习以下内容:
-
理解 CPU 和 GPU 图像处理之间的区别
-
对图像应用基本效果
-
遍历图像像素以及电影帧
-
利用像素级访问
介绍纹理、表面和通道
在第五章中,利用图像 – 加载和显示,我们已经学习了如何将图像加载到 Cinder 中。以下是我们当时使用的代码的精华:
gl::Texture texture = loadImage( loadAsset( "image.jpg" ) );
使用此行代码,我们将image.jpg
图像文件中的像素加载到 GPU 内存中。Texture
用于存储图像数据,但不用于操作或显示。要在屏幕上显示图像,我们使用以下行代码:
gl::draw( texture );
假设我们想在加载和绘制阶段之间进行一些图像处理。要在 GPU 上处理图像(图像数据由Texture
对象存储),我们会使用着色器。着色器使用 OpenGL 着色语言,我们不会在这里详细介绍,因为这远远超出了本书的范围。
另一种处理图像的方法是在 CPU 上加载图像。为了使用 CPU,我们必须使用Surface
类。它与Texture
类类似,但主要区别在于它在 CPU 上存储图像数据。通过在 CPU 上加载图像,我们可以使用 C++代码进行图像处理。
要绘制一个表面,我们需要将其转换为Texture
实例。可以将Surface
数据转换为Texture
类实例以绘制类似以下内容:
Surface surface;
gl::Texture texture = gl::Texture( surface );
通过执行前面的代码,我们创建了一个适合在屏幕上显示的 GPU 友好纹理。
Cinder 中存在第三种图像数据类型,即Channel
类。如果Surface
类能够在单个对象内部的不同通道中存储红色、绿色、蓝色和 alpha 值,那么Channel
仅使用一个通道(可以存储上述提到的任何一个通道)并可用于存储灰度图像。
可以通过添加以下行从Channel
类创建一个Surface
实例:
Surface surface( channel );
上一行代码使得通过直接传递Surface
实例到Channel
类,可以创建一个高质量的灰度图像作为Channel
类实例成为可能。
Channel channel( surface );
应用灰度效果
在上一节中,我们已经讨论了几乎可以在所有图像处理软件中应用的基本效果之一,即灰度。让我们创建一个简单的应用程序,通过使用我们刚刚讨论的方法加载图像并将其转换为灰度。
打开TinderBox,创建一个名为BasicEffects
的新应用程序。将一个图像(让我们称它为OurImage.png
,我将使用从我自己的档案中提取的一个简单、数字化增强的手绘草图)放入项目的assets
目录中,并打开xcode/BasicEffects.xcodeproj
项目文件。Windows 用户可以打开vc10/BasicEffects.sln
文件。
在你的编辑器中打开BasicEfectsApp.cpp
,并包含我们稍后需要的几个头文件:
#include "cinder/ImageIo.h"
#include "cinder/gl/Texture.h"
#include "cinder/Surface.h"
现在声明Texture
和Surface
类型的变量/对象如下:
Surface surface;
gl::Texture texture;
此外,将图像加载到surface
对象中,并将其传递给Texture
构造函数,以便我们之后可以绘制它(这将在setup()
方法实现中完成):
surface = Surface( loadImage( loadAsset("OurImage.png") ) );
texture = gl::Texture(surface);
最后,进入draw()
方法实现,并添加以下代码行以绘制纹理:
if ( texture ) gl::draw( texture );
在我们绘制纹理之前,我们需要确保它确实存在。
当你编译并运行项目时,我们的图像应该出现在屏幕上。如果它没有出现,请确保图像确实存在于我们项目的assets
目录中,并且loadAsset("OurImage.png")
中的图像文件名参数是正确的。
我们已经在 CPU 上为图像处理创建了非常基本的结构。为了将图像转换为灰度,我们将使用Channel
类。首先,我们将按照以下方式包含Channel.h
文件:
#include "cinder/Channel.h"
接下来,在setup()
方法实现中,我们加载图像,从Surface
实例创建一个Channel
实例,并使用新创建的通道来构建纹理:
surface = Surface( loadImage( loadAsset("image.png") ) );
Channel channel( surface );
texture = gl::Texture( channel );
编译并运行项目。以下截图是对比图像,显示了当我们使用Surface
对象构建纹理和Channel
之间的差异:
一旦我们将图像传递给Channel
构造函数,图像就会自动转换为灰度,我们使用这个结果来创建纹理。当你编译并运行程序时,你应该自己看到这一点。
使用阈值
现在我们将使用一些逐像素操作。我们将选择一个介于 0 到 255 之间的特定阈值值,并将每个像素的每个通道与该值进行比较。如果值高于阈值,我们将通道值更改为最大值(255)。如果值较低,我们将将其更改为最小值(0)。
注意
之前我们使用 0 到 1 之间的浮点数来描述 R、G 和 B 通道。当使用Surface
对象和图像时,你可能想知道Surface (Surface8u)
对象的每个通道的每个像素由 8 位组成,可以存储 256 个值(从 0 到 255),除非未定义。
由于我们处理的是静态图像,我们只需要在setup()
方法中在surface
和texture
对象初始化之间执行此过程一次。我们将使用一个非常实用的辅助类Surface::Iter
,它将允许我们无缝地遍历每一行(行)和每一行的单个像素。
因此,这是我们必须添加到setup()
方法实现中surface
和texture
变量初始化行之间的代码,如下所示:
surface = Surface( loadImage( loadAsset("OurImage.png") ) );int threshold = 200;
Area area = surface.getBounds();
Surface::Iter iter = surface.getIter( area );
while( iter.line() ) {while( iter.pixel() ) {iter.r() = iter.r() > threshold ? 255 : 0;iter.g() = iter.g() > threshold ? 255 : 0;iter.b() = iter.b() > threshold ? 255 : 0;}
}texture = gl::Texture( surface );
首先,我们定义阈值,所有高于该阈值的值将更改为 255,所有低于或等于该阈值的值将更改为 0。再见了,平滑的渐变。接下来,我们获取我们将要更改的图像区域。为了更改整个图像,我们必须获取整个表面的边界。我们必须获取一个迭代器(iter
),以便之后构建一个漂亮的嵌套循环。最后,我们使用迭代器遍历表面的所有行(行)和像素(行内的列),逐个更改像素。
不要忘记更改Texture
的初始化(您必须再次使用我们的Surface
实例而不是Channel
):
texture = gl::Texture( surface );
注意,我们在更改表面的像素后初始化纹理。每次您更改将用作Texture
的Surface
对象时,您都必须从更改后的Surface
对象重新初始化texture
变量。
编译并运行项目,看看会发生什么!以下屏幕截图显示了应用了我们的阈值滤镜的源图像与目标图像的比较:
在我们继续下一部分之前,尝试通过改变阈值值和像素值变化的方式做一些实验。
动画效果
让我们通过使用我们刚刚学到的技巧和update()
方法制作一个简单的效果动画。我们必须修改setup()
方法实现中的代码,使其看起来类似于以下内容:
surface = Surface( loadImage( loadAsset("OurImage.png") ) );// int threshold = 200; // comment or remove this line
Area area = surface.getBounds();
Surface::Iter iter = surface.getIter( area );
while( iter.line() ) {while( iter.pixel() ) {iter.r() += 1;iter.g() += 2;iter.b() += 3;}
}texture = gl::Texture( surface );
接下来,我们必须将除了surface
初始化之外的所有代码剪切并粘贴到update()
方法实现中,如下所示:
void BasicEffectsApp::setup() {surface = Surface( loadImage( loadAsset("OurImage.png") ) );
}void BasicEffectsApp::update() {Area area = surface.getBounds();Surface::Iter iter = surface.getIter( area );while( iter.line() ) {while( iter.pixel() ) {iter.r() += 1;iter.g() += 2;iter.b() += 3;}}texture = gl::Texture( surface );
}
完成这些后,编译并运行项目,看看会发生什么:
在前面的屏幕截图中,您可以看到一种类似酸液侵蚀的动画,它将图像的所有表面都转换了。
将效果应用于移动图像
我们刚刚看到了如何获得动态效果,现在让我们尝试将效果添加到移动图像上。
在做之前,制作我们项目(BasicEffects
)的副本是个好主意。将新项目的文件夹重命名为BasicEffectsMotion
。打开项目文件(在 Mac OS X 上为xcode/BasicEffects.xcodeproj
,在 Windows 上为vc10\BasicEffects.sln
)。
注意
对于 Windows 用户,您需要在项目属性中链接 | 输入 | 附加依赖项下添加QTMLClient.lib
和CVClient.lib
。您还必须在项目属性中链接 | 常规 | 附加库目录下添加 QuickTime SDK 的路径(C:\QuickTimeSDK-7.3\Libraries
)。您可以通过菜单栏点击项目 | 基本效果属性来访问项目属性。
首先,我们需要找到一个我们可以使用的电影文件。vimeo.com/groups/freehd
似乎是一个可以找到免费视频片段的好地方。我找到了一个工业场地的片段。
将电影文件放在项目的assets
文件夹中,打开代码编辑器,并从包含QuickTime.h
文件开始:
#include "cinder/qtime/QuickTime.h"
注意
如果你使用的是 Cinder 的 AppRewrite 版本,你应该知道 QuickTime 已经被作为一个单独的块。块是 Cinder 的扩展,当你需要特定的额外功能时,你可以将其添加到你的项目中。
接下来,我们必须声明一些新的变量。由于我们已经有了一个表面和一个纹理,我们只需要声明一个用于电影的变量:
qtime::MovieSurface movie;
完成这些后,让我们转到setup()
方法部分,通过添加以下高亮代码来加载电影:
// comment out or remove this line
// surface = Surface( loadImage( loadAsset("OurImage.png") ) );// add these lines
movie = qtime::MovieSurface( getAssetPath("OurMovie.mp4") );
movie.setLoop();
movie.play();
接下来,我们必须将每一帧复制到一个表面,并将该表面转换为纹理,这样我们就可以在屏幕上绘制它。让我们转到update()
方法实现,进行以下操作,删除所有之前的代码,并添加以下内容:
if ( movie.checkNewFrame() ) {surface = movie.getSurface();// add effects hereif ( surface ) texture = gl::Texture( surface );
}
由于加载电影是一个异步过程,在我们将其传递给纹理之前,我们必须检查是否存在一个表面。同样,在绘制之前,我们必须检查是否有纹理。将draw()
方法中的gl::draw
部分更改为以下内容:
if ( texture ) gl::draw( texture, getWindowBounds() );
我们需要确保电影在应用程序窗口的边界内绘制。我们通过将getWindowBounds()
函数的结果作为gl::draw()
函数的第二个参数来确保这一点。
现在编译并运行项目。你应该能看到电影正在播放。现在我们将结合之前制作的效果。在update()
方法中找到surface
和texture
初始化之间的位置(我在那里留下了一个注释// add effects here
)。在那里添加以下代码:
if ( surface ) {Area area = surface.getBounds();Surface::Iter iter = surface.getIter( area );while( iter.line() ) {while( iter.pixel() ) {iter.r() += addR;iter.g() += addG;iter.b() += addB;}}addR += 1;addG += 2;addB += 3;
}
如你可能已经猜到的,现在我们必须声明addR
、addG
和addB
变量,并按照以下方式设置它们的初始值:
// add this in the class declaration
uint8_t addR, addG, addB;// add this in the setup() implementation
addR = addG = addB = 0;
编译并运行项目。你应该能看到电影的颜色变化,如下面的截图所示:
通过调整addR
、addG
和addB
的值来实验,看看你能得到什么样的不同效果。
摘要
在本章中,我们学习了将效果应用于静态和动态图像的基本方法。通过研究原始图像处理算法并将它们作为按像素操作应用,可以做更多的事情。所以如果你那样做,并且能够重现大多数在 Photoshop 或 Gimp 中可以找到的图像效果——恭喜!你也许还希望通过学习有关着色器和 OpenGL 着色语言的知识,将你的知识提升到下一个层次。
第八章. 深入学习 - Cinder 3D 基础
本章将向您介绍 Cinder 中可以绘制的基本 3D 方面和实用方法,以及可以用 Cinder 绘制的 3D 原语。我们将从二维坐标系过渡到三维,并继续探讨我们在三维图形编程世界中将要面临的基本问题和解决方案。在演示过程中,我们还将添加基本动画,以展示向原本静态对象添加运动的基本方法。
介绍 3D 空间
要使用 Cinder 进行 3D 绘图,我们需要了解一些关于 3D 计算机图形学的基础知识。首先,我们需要知道的是,3D 图形是在计算机中某个地方存在的三维空间中创建的,之后被转换成可以在我们的计算机屏幕上显示的二维图像。
通常有一个投影(视锥体)具有不同的属性,这些属性与我们在现实世界中拥有的相机的属性相似。视锥体负责渲染视锥体内可见的所有 3D 对象。它负责创建我们在屏幕上看到的 2D 图像。
如前图所示,所有在视锥体内的对象都在屏幕上被渲染。视锥体外的对象被忽略。
OpenGL(在 Cinder 中用于绘图)依赖于所谓的渲染管线来将对象的 3D 坐标映射到 2D 屏幕坐标。此过程使用了三种类型的矩阵:模型矩阵、视图矩阵和投影矩阵。模型矩阵将 3D 对象的局部坐标映射到世界(或全局)空间,视图矩阵将其映射到相机空间,最后投影矩阵负责将其映射到 2D 屏幕空间。较旧的 OpenGL 版本将模型和视图矩阵合并为一个——模型视图矩阵。
在前面的章节中,我们使用了二维坐标(x 和 y 坐标轴)来在屏幕上放置不同种类的对象和图形。现在我们将利用第三个维度——z 轴。
Cinder 中的坐标系从屏幕的左上角开始。放置在那里的任何对象的坐标都是 0, 0, 0(这些分别是 x、y 和 z 的值)。x 轴向右延伸,y 轴向下延伸,但 z 轴向观察者(我们)延伸,如图所示:
如前图所示,Cinder 中的坐标系与 OpenGL 中的坐标系略有不同。Cinder 内置的 OpenGL 绘图函数负责进行映射。
3D 绘图
让我们尝试考虑存在第三个维度来绘制一些东西。
使用 TinderBox 创建另一个项目,并将其命名为 Basic3D
。在 Mac 上打开项目文件(xcode/Basic3D.xcodeproj
),在 Windows 上打开(vc10\Basic3D.sln
)。在编辑器中打开 Basic3DApp.cpp
并导航到 draw()
方法实现。
在 gl::clear()
方法之后,添加以下行以绘制一个立方体:
gl::drawCube( Vec3f(0,0,0), Vec3f(100,100,100) );
第一个参数定义了立方体中心的位子,第二个定义了它的大小。注意,我们使用 Vec3f()
变量在三个(x、y 和 z)维度内定义位置和大小。
编译并运行项目。这将绘制一个位于屏幕左上角的实心立方体。我们只能看到它的四分之一,因为立方体的中心是参考点。让我们通过以下方式转换前面的行,将其移动到屏幕中心:
gl::drawCube(Vec3f(getWindowWidth()/2,getWindowHeight()/2,0),Vec3f(100,100,100) );
现在我们将立方体定位在屏幕中间,无论窗口的宽度和高度如何,因为我们传递了窗口宽度的一半 (getWindowWidth()/2
) 和窗口高度的一半 (getWindowHeight()/2
) 作为立方体位置的 x 和 y 坐标值。编译并运行项目以查看结果。通过调整大小参数来理解其背后的逻辑。
我们可能想要稍微旋转一下这个立方体。有一个内置的 rotate()
函数我们可以使用。不过,我们必须要记住的一件事是,我们必须在绘制对象之前使用它。所以,在 gl::drawCube()
之前添加以下行:
gl::rotate( Vec3f(0,1,0) );
编译并运行项目。你应该会看到一个围绕 y 轴的奇怪旋转动画。这里的问题是 rotate()
函数旋转了整个 3D 世界以及其中的对象,并且它是通过考虑场景坐标来做到这一点的。由于 3D 世界的中心(所有轴交叉为零的点)在屏幕的左上角,所有的旋转都是围绕这个点进行的。
要改变这一点,我们必须使用 translate()
函数。它在 rotate()
或 drawCube()
之前用来移动场景(或画布)。为了使我们的立方体围绕屏幕中心旋转,我们必须执行以下步骤:
-
使用
translate()
函数将 3D 世界转换到屏幕中心。 -
使用
rotate()
函数旋转 3D 世界。 -
绘制对象(
drawCube()
)。 -
使用
translate()
函数将场景转换回原位。
我们必须使用 translate()
函数将场景转换回原位置,因为每次我们调用 translate()
时,值会被添加而不是替换。在代码中应该看起来像以下这样:
gl::translate( Vec3f(getWindowWidth()/2,getWindowHeight()/2,0) );
gl::rotate( Vec3f::yAxis()*1 );
gl::drawCube( Vec3f::zero(), Vec3f(100,100,100) );
gl::translate( Vec3f(-getWindowWidth()/2,-getWindowHeight()/2,0) );
因此,现在我们得到了立方体围绕 y 轴的平滑旋转。围绕 y 轴的旋转角度在每一帧中增加 1 度,因为我们把 Vec3f::yAxis()*1
的值传递给 rotate()
函数。通过调整旋转值来更深入地理解这一点。
如果我们想让立方体保持在恒定的旋转位置,我们必须记住 rotate()
函数的工作方式类似于 translate
函数。它向场景的旋转中添加值而不是替换它们。而不是将对象旋转回来,我们将使用 pushMatrices()
和 popMatrices()
函数。
旋转和平移是变换。每次调用 translate()
或 rotate()
,你都在修改模型视图矩阵。如果进行了某些操作,有时可能不容易撤销。每次变换某个对象时,都会基于当前状态中所有之前的变换进行更改。
那么,这个状态是什么?每个状态都包含当前变换矩阵的一个副本。通过调用 pushModelView()
,我们通过复制当前模型视图矩阵并将其存储到堆栈中来进入一个新状态。现在我们将进行一些疯狂的变换,而不用担心如何撤销它们。要返回,我们调用 popModelView()
,它从堆栈中弹出(或删除)当前模型视图矩阵,并返回到具有先前模型视图矩阵的状态。
因此,让我们在 gl::clear()
调用之后添加以下代码进行尝试:
gl::pushModelView();
gl::translate( Vec3f(getWindowWidth()/2,getWindowHeight()/2,0) );
gl::rotate( Vec3f(35,20,0) );
gl::drawCube( Vec3f::zero(), Vec3f(100,100,100) );
gl::popModelView();
现在编译并运行我们的程序,你应该会看到以下截图类似的内容:
如我们所见,在执行任何操作之前,我们使用 pushModelView()
创建当前状态的副本。然后我们像之前一样,将场景平移到屏幕中间,旋转它(这次是围绕 x 轴 35
度和围绕 y 轴 20
度),最后绘制立方体!为了将舞台重置到之前的状态,我们只需要一行代码,popModelView()
。
理解嵌套状态
可以通过使用之前讨论过的函数来创建嵌套状态。通过连续两次调用 pushModelView()
,我们存储了两个不同的状态。通过随后调用一个 popModelView()
,我们只弹出最后推入的模型视图矩阵。
让我们改变我们的 draw()
方法实现,如下所示:
void Basic3DApp::draw() {gl::clear( Color( 0, 0, 0 ) );// make a copy of the current modelview matrixgl::pushModelView();// translate the origin of the world to the center of the screengl::translate( Vec3f(getWindowWidth()/2,getWindowHeight()/2,0) );// draw a ring of cubesint i; // iteratorint numCubes = 10; // number of cubes in the ringfor ( i=0; i < numCubes; i++ ){// make another copy of the current modelview matrixgl::pushModelView();// rotate the world around z axisgl::rotate( Vec3f::zAxis() * (360.f / numCubes * i) );// draw a relatively small cube// 200 pixels to the right from the center of the worldgl::drawCube( Vec3f(200,0,0), Vec3f(25,25,25) );// return to the previous stategl::popModelView();}// rotate the world around the origingl::rotate( Vec3f(35,20,0) );// draw a bigger cube in the center of the worldgl::drawCube( Vec3f::zero(), Vec3f(100,100,100) );// return to the initial stategl::popModelView();
}
注意 gl::pushModelView()
和 gl::popModelView()
的使用。编译并运行项目,你应该会看到围绕我们之前看到的立方体的一圈较小的立方体。
现在尝试将 gl::rotate()
调用移动到 for
循环之前和 gl::translate()
调用之后。编译并运行项目。你应该看到与之前看到的不同的一些图像:
这是对嵌套状态的简单演示。尝试以我们刚才的方式实验,通过添加额外的变换并改变它们的顺序。此外,OpenGL 并不仅限于两层嵌套状态。你可以尝试通过在另一个 for
循环内部添加更多的 pushModelView()
和 popModelView()
函数,在每个小正方形周围添加环绕的对象,例如。
处理深度排序
让我们看看 Cinder 内置的 3D 原始形状绘制函数。虽然它们并不多,但鉴于 Cinder 比其他类似工具更底层,您之后应该继续学习一些 OpenGL,以便充分利用 Cinder 和 3D 空间中的绘图。
让我们从将单色立方体变为更加多彩,并添加一些与之前相同的恒定旋转开始。为此,我们需要将 drawCube()
函数调用替换为 drawColorCube()
:
gl::drawColorCube( Vec3f::zero(), Vec3f(100,100,100) );
当您运行并编译应用程序时,您会看到立方体以某种奇怪的方式绘制。这是不适当的深度排序的效果。一个 3D 模型由放置在当前投影中不同深度的顶点组成。这些顶点形成面,它们也有不同的深度信息,这些信息在将它们转换为不同深度的像素时被考虑。如果这些顶点没有按其深度信息排序并按适当顺序绘制,我们就会得到一个图像,其中场景后面的面和对象被绘制在前面,反之亦然。为了避免这种情况,我们必须启用 OpenGL 的深度读取功能。在 setup()
方法实现中添加以下代码行:
gl::enableDepthRead();
编译并运行项目后,立方体应该看起来相当不错:
完成这些后,让我们添加一些旋转动画。为此,我们需要声明一些变量来存储旋转变量。然后,我们需要在 setup()
方法中为它们分配初始值,在 update()
方法中更改它们,并在 draw()
方法实现中最终绘制。让我们从声明变量开始。在 setup()
、update()
和 draw()
方法声明之后添加以下行:
Vec3f currentRotation;
Vec3f rotationIncrement;
我们不是使用一个 float
变量来存储围绕每个轴的当前和增量旋转角度,而是使用 Vec3f
数据类型来能够在一个变量中存储旋转值。
我们现在必须将这些变量的初始值分配。转到 setup()
方法实现并添加以下行:
currentRotation = Vec3f( 0, 0, 0 );
rotationIncrement = Vec3f( 1.1f, 1.2f, 1.3f );
完成这些后,转到 update()
方法实现并添加以下行:
currentRotation += rotationIncrement;
这将在每一帧中增加围绕所有三个轴的旋转。
最后,我们必须在绘制时使用 currentRotation
变量。在 gl::drawColorCube()
调用之前更改 gl::rotate()
函数的参数为 currentRotation
,如下所示:
…
// rotate the world around the origin
gl::rotate( currentRotation );// draw a bigger cube in the center
gl::drawColorCube( Vec3f::zero(), Vec3f(100,100,100) );
现在,旋转值将在每一帧中增加和更新。编译并运行项目,亲自看看!您应该会看到一个不错的旋转动画。尝试用立方体的位置和大小来做同样的操作,以更好地理解这是如何工作的。
探索 Cinder 3D 原始形状的其他功能
现在,让我们尝试使用 Cinder 提供的函数的不同 3D 原始形状。我们刚刚尝试绘制了一种不同类型的立方体,所以让我们继续一个同样经典的形状,一个球体。将 gl::drawColorCube()
函数调用替换为以下行:
gl::drawSphere( Vec3f::zero(), 100 );
这个函数的第一个参数定义了球体的中心,第二个定义了半径。还有一个第三个(可选)参数 int
,它控制球体由多少个段组成。默认值是 12,但你可能想将其更改为更高的值以增加球体的平滑度。
编译并运行项目。你应该会看到一个类似于以下截图的图像:
这里的问题是它看起来并不完全是 3D 的。正如你可能已经猜到的,问题是没有灯光。为了向场景添加灯光,在 setup()
方法实现中添加以下行:
glEnable( GL_LIGHTING );
glEnable( GL_LIGHT0 );
这些是负责打开灯光的 OpenGL 函数。第一个函数启用灯光,第二个是打开第一个灯光或 GL_LIGHT0
。有方法可以定位和更改其参数,但我们将不会深入探讨,因为这超出了本书的范围。
编译并运行项目以查看开启灯光的球体。你应该得到一个类似于以下截图的结果:
让我们尝试绘制一个球体以外的其他东西。一个圆柱体怎么样?将 drawSphere()
函数调用替换为以下行:
gl::drawCylinder(50, 50, 100);
这将在屏幕上绘制一个开口的圆柱体。第一个参数定义了圆柱体底部的宽度,第二个参数定义了圆柱体顶部的宽度。第三个参数定义了圆柱体的高度。
在我们编译并运行应用程序后,你会注意到圆柱体不是像我们之前绘制的立方体或球体那样围绕其中心旋转。这是因为它的绘制方式——从下往上而不是上下两边。要改变这一点,我们可以绘制另一个圆柱体,具有负高度或更改 translate()
值。为了简化,让我们再画一个圆柱体。将圆柱体绘制代码替换为以下内容:
gl::drawCylinder(50, 50, 50);
gl::drawCylinder(50, 50, -50);
我们可以看到相同的圆柱体,但现在它围绕其中心旋转。我们可能还想添加圆柱体的封闭端。我们必须再次使用 2D 形状来完成这个任务。在 drawCylinder()
函数调用之后直接添加以下代码行:
gl::rotate( Vec3f(-90,0,0) );
gl::translate( Vec3f(0,0,50) );
gl::drawSolidCircle(Vec2f(0,0), 50, 12);
gl::translate( Vec3f(0,0,-50) );
gl::rotate( Vec3f(180,0,0) );
gl::translate( Vec3f(0,0,50) );
gl::drawSolidCircle(Vec2f(0,0), 50, 12);
这并不太容易,对吧?还没有一个 drawSolidCircle()
函数可以根据 3D 坐标绘制圆,但无论如何这是一种简写,所以我们不必编写纯 OpenGL。这些代码行所做的是将场景或画布移动到适当的相对位置和旋转,以便相对于已经绘制的圆柱体绘制其顶部和底部。
让我们稍微修改一下代码,看看drawCylinder()
函数还能绘制圆锥甚至金字塔:
gl::drawCylinder(50, 0, 50, 4);
gl::drawCylinder(50, 0, -50, 4);
删除或注释掉drawSolidCircle()
部分,并将drawCylinder()
函数的顶部值改为0
,并添加一个第四个值(如果你想绘制圆锥,可以将其设置为4
或更多),这个值代表圆柱的切片数。
编译并运行应用程序后,我们看到的是一个金字塔,而不是我们预期的。让我们称它为钻石,以更准确地描述形状。在下面的屏幕截图中,你可以看到我们刚刚对gl::drawCylinder()
所做的调整的图像条:
另一个值得介绍的基本形状是环面。Cinder 内置了一个用于绘制环面的函数,函数看起来是这样的:
gl::drawTorus(100, 20);
在圆柱绘制函数调用之后添加前面的代码行。第一个参数定义了外半径(负责环面底部形状的圆),第二个参数定义了内半径或用于创建环面体积的圆的半径。
为了让东西看起来更加整齐,让我们添加第三个和第四个参数,它们负责环面的经度和纬度切片数:
gl::drawTorus(100, 20, 8, 8);
如果我们现在编译并运行应用程序,我们应该看到类似于以下图像的图像:
摘要
总结来说,在本章中,我们通过 Cinder 可以做的非常基础的 3D 内容进行了自我介绍。我们了解了一些关于 3D 空间的概念,以及如何在空间中绘制和动画化对象。我们学习了如何打开灯光,并使用原语构建更复杂的形状。
还有更多,但要真正发挥 Cinder 的图形能力,你必须学习一些 OpenGL 和OpenGL 着色语言(GLSL)的更多知识。
在下一章中,我们将更多地讨论 Cinder 在音频方面的可能性,而不是图形。
第九章。进入声音 - 添加声音和音频
在本章中,我们将简要讨论创意编程中的声音概念。
在本章中,我们将学习以下内容:
-
如何在 Cinder 中加载和播放声音
-
如何实时修改声音
-
如何使用音频数据来绘制和动画
-
如何利用实时声音输入
加载和播放声音文件
在创意编程中,使用声音有几种方式。一种方式是使用音频样本,另一种方式是使用实时输入,然后还有从头生成声音的可能性。然而,当你开始组合所有这些方法时,会出现更多可能性。
在本章中,我们将学习如何加载、播放和可视化音频文件以及捕获实时输入。要开始,打开 TinderBox 并创建一个名为BasicAudio
的新项目。打开编辑器中的xcode/BasicAudio.xcodeproj
(在 Windows 上是vc10\BasicAudio.sln
)。在编辑器中打开BasicAudioApp.cpp
。
为了使用 Cinder 的基本音频功能,我们必须导入适当的库:
#include "cinder/audio/Output.h"
这个特定的例子包含了加载和回放音频文件所需的所有代码。去找你自己的音频文件(mp3
或wav
都行),并将其放置在项目的assets
文件夹中。
我们需要一个audio::SourceRef
对象来存储我们将要在代码中使用的音频源的引用。将以下行添加到你的类声明中:
audio::SourceRef src;
将以下代码片段添加到setup()
方法实现中:
src = audio::load(loadAsset("sample.wav"));
audio::Output::play(src);
这将把音频文件加载到计算机内存中作为audio::Source
对象,并将引用保存在src
变量中。然后,我们可以通过使用audio::Output::play()
类方法来播放声音。
编译并运行我们的应用程序。你应该能听到声音。以这种方式回放声音有一些限制。无法以任何其他方式(除了设置和获取音量)应用循环控制。
使用轨道
Cinder 的Track
类提供了对声音文件更多的控制。让我们回顾一下Track
类的一些我们可能最常使用的功能。
要循环播放加载的音频文件,我们需要向Output
对象添加一个Track
对象,并保存返回的轨道引用,以便我们可以用它来控制声音。在类声明中添加一个用于存储轨道引用的变量:
audio::TrackRef trackRef;
将setup()
方法实现中的代码替换为以下内容:
src = audio::load(loadAsset("sample.wav"));
trackRef = audio::Output::addTrack(src);
目前,这基本上与之前相同,只是它将audio::Source
指针作为单独的轨道存储在audio::Output
对象中。addTrack()
函数还有一个参数,autoplay
,如果我们不提供其他值,它将自动设置为true
。让我们尝试将其设置为false
:
trackRef = audio::Output::addTrack(src, false);
由于我们将autoplay
参数设置为false
,音频文件在加载后不会播放。如果我们想在应用程序流程的某个其他点播放音频文件,可以使用以下代码:
trackRef->play();
我们在这里使用箭头语法,因为音轨引用只是指向实际Track
对象的 C++指针。所有其他音轨属性都以相同的方式访问。
我们可以通过创建一个新的音频源对象并将其添加到audio::Output
指针中,向Output
对象添加多个音轨。我们现在不会这样做,但请记住,如果我们想创建某种多层级音频应用程序,这是可能的。
更改音轨参数
接下来我们想要做的是在音频文件播放完毕后再次播放。这可以通过将音轨的循环设置为true
来实现:
trackRef->setLooping(true);
这将使音轨循环无限。
我们可能还想做的一件事是更改跟踪音量。你可以把Output
音量看作是主音量,而Track
音量则看作是混音器中各个音轨的独立音量。
要更改音量,请使用以下代码:
trackRef->setVolume(0.5f); // set track volume
audio::Output::setVolume(0.5f); // set master volume
我们可以使用变量动态地更改声音的音量。让我们为主音量和音轨音量都这样做。
将以下行添加到应用程序的类声明中:
float masterVol, trackVol;
void mouseMove(MouseEvent event);
使用这些代码行,我们声明了两个浮点数,用于存储主音量和跟踪音量,以及AppBasic
类的mouseMove
方法重写,这将使我们能够响应鼠标移动。
让我们转到setup()
方法实现,初始化masterVol
和trackVol
变量,此外更改trackRef->setVolume()
和audio::Output::setVolume()
的原始值:
void BasicAudioApp::setup() {masterVol = trackVol = 0.5f;src = audio::load(loadAsset("sample.wav"));trackRef = audio::Output::addTrack(src, false);trackRef->setLooping(true);trackRef->setVolume(trackVol); // set track volumeaudio::Output::setVolume(masterVol); // set master volumetrackRef->play();
}
在文件末尾,在CINDER_APP_BASIC()
调用之前添加mouseMove
方法的实现:
void BasicAudioApp::mouseMove(MouseEvent event) {masterVol = (float)event.getX()/getWindowWidth();trackVol = (float)event.getY()/getWindowHeight();audio::Output::setVolume(masterVol);trackRef->setVolume(trackVol);
}
一个类型为MouseEvent
的事件对象传递给此方法。我们可以获取事件发生时鼠标光标的当前位置以及事件对象持有的许多其他参数。我们现在将坚持使用鼠标x
和y
坐标,并使用它们来更改音量。
对于更改主音量,我们使用x
坐标。我们使用y
坐标来更改音轨音量。
由于 Cinder 中的音量定义在0.0f
到1.0f
的范围内,我们需要将鼠标位置转换为这种形式。为此,我们需要将实际鼠标位置除以其最大范围,即窗口的宽度和高度。因此,如果鼠标x
位置是0
且窗口宽度是100
,在将坐标除以窗口宽度(0/100)后,我们得到0
。如果鼠标x
位置是100
,我们得到1
。另外,如果鼠标x
位置是50
,我们得到0.5
(50/100)。
由于鼠标位置和窗口尺寸以int
值返回,我们需要将其中一个值转换为float
以进行成功的浮点数运算。这就是为什么在代码的event.getX()/getWindowWidth()
和event.getY()/getWindowHeight()
部分前面有(float)
,它将event.getX()
返回的int
值转换为float
。最后,我们使用audio::Output::setVolume(masterVol);
和trackRef->setVolume(trackVol);
设置主音量和轨道音量。
编译并运行我们的应用程序。移动鼠标,你应该能听到声音的音量是如何变化的。
接下来,我们可能想要学习的一个有用功能是跳转到轨道的某个特定位置。为此,我们将使用AppBasic
类的mouseDrag
方法。让我们通过在类声明中添加以下行来声明它的重写:
void mouseDrag(MouseEvent event);
让我们实现它。在文件的末尾,在CINDER_APP_BASIC()
调用之前添加以下代码:
void BasicAudioApp::mouseDrag(MouseEvent event) {double time =(double)event.getX()/getWindowWidth()*src->getDuration();trackRef->setTime( time );
}
在这个例子中,应用程序窗口的宽度被映射到轨道的持续时间。鼠标的x
位置被转换成音频轨道中的特定时间。代码中的trackRef->setTime(time)
部分设置了轨道播放头的位置。
编译并运行我们的应用程序。你现在应该能够进行实时搜索了。点击并拖动来尝试一下。
可视化音频
在我们的创意声音应用程序中,缺少一个重要的东西——它的视觉方面。让我们创建一个简单的均衡器。
首先,我们需要启用音频轨道的 PCM 缓冲。
PCM代表脉冲编码调制,它是一种数字表示采样模拟信号的方法。模拟音频信号是导体内部电压的波动。它的数字表示是样本或字节,通常具有从-1
到1
的浮点值。
在音频采样世界中,有一个术语,采样率,它表示每秒被采样的样本或值的数量。音频文件的 PCM 值和采样率决定了其播放速度。
要启用 PCM 缓冲并在应用程序运行时读取 PCM 值,请在setup()
方法声明中trackRef->play()
之前添加以下代码:
trackRef->enablePcmBuffering( true );
第二,我们需要一个变量,它将作为轨道 PCM 缓冲区的指针。
PCM 缓冲区是计算机内存中的一个秘密地方,其中存储着从我们的扬声器输出的原始声音波形的值。在任何声音发送到音频输出之前,它被保存在缓冲区中一段时间。当播放声音时,缓冲区被清除并再次用新的音频数据填充。
我们需要访问 PCM 缓冲区以从中读取原始波形值。将以下行添加到类声明中,以添加一个作为其引用的变量:
audio::PcmBuffer32fRef pcmBuffer;
第三,我们需要在每个帧中获取最新缓冲区的副本。将以下代码添加到update()
方法实现中:
pcmBuffer = trackRef->getPcmBuffer();
最后,将 draw()
方法实现修改如下:
void BasicAudioApp::draw() {// clear the screen by drawing// a semi-transparent black rectangle all over the screengl::enableAlphaBlending();gl::color( 0.f, 0.f, 0.f, 0.1f );gl::drawSolidRect(getWindowBounds());if( !pcmBuffer ) {gl::disableAlphaBlending();return; // stop here if the buffer is empty}// get copy of the left channel data from the pcmBufferaudio::Buffer32fRef buffer =pcmBuffer->getChannelData( audio::CHANNEL_FRONT_LEFT );// get buffer lengthuint32_t bufferLength = pcmBuffer->getSampleCount();// calculate scale for mapping the buffer data on the screenfloat scale = getWindowWidth() / (float)bufferLength;// set color to cyangl::color( 0.f, 1.f ,1.f ,0.8f );// loop through current buffer data// in steps of 10 and construct waveformfor( int i=0; i<bufferLength; i+=10 ) {// map current x position of buffer value to window widthfloat x = i * scale;// buffer data fluctuates from -1 to +1,// map it to window heightfloat y = ( (buffer->mData[i]+1) * getWindowHeight()/2 );// draw a circlegl::drawStrokedCircle( Vec2f(x, y),( abs(buffer->mData[i])*getWindowHeight()/2 ) );}gl::disableAlphaBlending();
}
编译并运行我们的应用程序,你应该会看到一个如图所示的图像:
使用音频输入
使用麦克风或线路输入作为音频源怎么样?没问题。为了实现这一点,我们需要导入音频 Input
库:
#include "cinder/audio/Input.h"
我们将扩展我们的当前应用程序,以便可以在实时输入和加载的文件播放之间切换。为此,我们需要声明两个新变量:
audio::Input input;
bool useInput;
第一个变量代表我们的声音输入,我们将使用第二个作为开关。
接下来,我们必须初始化 input
变量,因此前往 setup()
方法实现并添加以下行:
input = audio::Input();
这将默认音频输入分配给 input
变量。
让我们使用 mouseDown
方法重写并填充以下代码:
useInput = !useInput;if ( useInput ) {input.start();trackRef->stop();
} else {input.stop();trackRef->play();
}
这几行代码处理实时和加载输入之间的切换。每次鼠标点击时,useInput
变量从 false
切换到 true
或相反。根据我们得到的值,我们启用实时或加载输入。
最后,将 update()
方法实现中的代码替换如下:
if ( useInput ) pcmBuffer = input.getPcmBuffer();
else pcmBuffer = trackRef->getPcmBuffer();
这几行代码获取当前的 PCM 缓冲区。如果 useInput
变量设置为 true
,我们使用实时输入的 PCM 缓冲区,如果不是,我们使用加载轨道的 PCM 缓冲区。
编译并运行应用程序。点击窗口以启用实时输入。再次点击以返回到加载的声音。
概述
在本章中,我们学习了 Cinder 中音频的基础知识。我们学习了如何加载和播放音频文件,如何更改其音量,以及如何搜索和可视化 PCM 缓冲区。我们还学习了如何使用实时输入音频数据。
基于这些知识,现在我们能够创建音频反应式应用程序,能够实时分析几乎任何类型的音频并提供高性能的视觉反馈。音频分析是一个广泛的话题,你可能想进一步研究。例如,尝试搜索“FFT”、“分频分析”或“节拍跟踪”。如果你对 Cinder 提供的音频处理功能不满意,也可以使用某种第三方音频处理库。
第十章. 与用户对话 – 添加交互性和 UI 事件
在本章中,我们将学习以下主题:
-
检测键盘按键
-
检测鼠标移动和点击
-
创建一个使用基本输入进行实时控制的应用程序
我们在前面几章中使用了一些这些功能,但在这里我们将尝试获得一个更系统的概述,了解 Cinder 可以提供哪些基本交互性。
处理事件
在本书中,我们编写的代码基本上扩展了cinder::app::AppBasic
类及其继承的基类App
的功能。我们声明和实现的方法基本上是覆盖了AppBasic
和App
类中构建的虚拟函数,并在某些事件中被调用。其中一些可以被称为事件处理器,它们基本上是对应用程序流程中发生的某些事件做出响应。
有三种基本方法构成了 Cinder 应用程序的核心:
-
setup()
-
update()
-
draw()
这些方法处理应用程序核心内部发生的事件,用户无法控制这些函数是否被调用(在函数实现的开头使用return
或类似方式可以停止方法的执行)。
然后有一些方法可以在某些事件上执行代码,例如移动鼠标、按键盘上的键、滚动鼠标滚轮等。这是我们将在本章中关注的内容。所以这里有一个方法(或事件处理器)列表,我们将覆盖它们:
-
keyDown()
-
keyUp()
-
fileDrop()
-
mouseDown()
-
mouseUp()
-
mouseMove()
-
mouseDrag()
让我们创建一个简单的绘图应用程序,它将使用所有这些事件。为此,我们需要创建一个新的项目。打开TinderBox,创建一个名为BasicEvents
的新项目。打开项目文件(在 Mac OS X 上为xcode/BasicEvents.xcodeproj
,在 Windows 上为vc10\BasicEvents.sln
)。在编辑器中打开BasicEventsApp.cpp
,然后开始添加一些代码。
使用mouseMove()
首先,我们将添加一个自定义鼠标光标,当鼠标不移动时,它会慢慢下落,当鼠标移动时,它会返回到当前鼠标位置。为此,我们需要声明将保存光标 x 和 y 位置的变量。将以下代码行添加到文件声明部分:
Vec2i cursorPos;
这个变量将保存我们的光标x
和y
位置的int
值。Vec2i
中的i
部分告诉我们它是一个由整数值组成的二维向量。
接下来,我们需要通过将值设置为应用程序启动时的当前鼠标位置来初始化该值。将以下代码行添加到setup()
方法实现中:
cursorPos = getMousePos();
这将获取当前鼠标位置并将其分配给我们的光标位置值。
接下来,我们想在cursorPos
坐标处绘制一个圆圈。让我们导航到应用程序的draw()
方法实现,并在gl::clear()
函数调用之后添加以下行:
gl::drawSolidCircle( cursorPos, 10 );
我们之前已经使用过这个函数。这将使用cursorPos
变量定义的位置绘制一个半径为 10 像素的圆圈。编译并运行项目,亲自看看吧!
接下来,我们想要给圆圈添加下落动作。为此,我们需要在每一帧更新圆圈的y
位置。换句话说,我们将每帧增加圆圈的y
坐标。让我们导航到update()
方法实现,并添加以下简单的一行代码:
cursorPos.y++;
这将使光标下落。最后,我们需要在mouseMove
事件中使其粘附到鼠标上。我们需要在应用程序的类声明中声明mouseMove()
方法重写。在类声明的末尾添加以下行:
void mouseMove(MouseEvent event);
然后将方法实现添加到类实现中,如下所示:
void BasicEventsApp::mouseMove(MouseEvent event) {cursorPos = event.getPos();
}
获取当前鼠标位置的方法不止一种,而不是使用event.getPos()
。我们可以使用getMousePos()
,它将执行与鼠标位置分配给cursorPos
变量相同的事情。
编译并运行我们的应用程序,查看以下截图所示的结果:
你应该看到一个跟随鼠标移动的下落圆圈。
使用 mouseDown()
我们接下来要实现的事件处理程序是mouseDown()
处理程序。每次我们点击鼠标的任意按钮时,它都会执行代码。我们将编写代码,每次我们点击左鼠标按钮时,都会在屏幕上添加一个静态圆圈。当我们点击右鼠标按钮时,它将移除第一个圆圈。
首先,我们需要声明一个新的变量,该变量能够存储我们生成的圆圈的多对坐标。我们可以使用Vec2i
对象的数组,但由于我们不知道将要创建的圆圈的确切数量,我们将使用 C++的vector
。
向量是一个动态数组,能够存储std::vector::max_size
数量的特定类型的对象。vector
数组在元素添加(或推入)和移除(或弹出)时动态地改变其大小(或长度)。
在我们的类声明的末尾添加以下代码行:
vector<Vec2i> circlePositions;
void mouseDown(MouseEvent event);
可能的mouseDown()
方法已经为我们声明了。如果是这样,不要提及代码的第二行。如果mouseDown()
之前没有声明,请继续将事件处理程序方法添加到类实现中:
void BasicEventsApp::mouseDown(MouseEvent event)
{// check if the left mouse button was pressedif ( event.isLeft() ) {// it wasVec2i cp = event.getPos(); // save current mouse positioncirclePositions.push_back(cp); // and add it to the vector}// check if the right mouse button was pressedif ( event.isRight() ) {// it was// check if the vector has at least one elementif ( !circlePositions.empty() ) { // it has, erase the first elementcirclePositions.erase(circlePositions.begin());}}
}
如您从前面的代码注释中可以看出,它检查哪个鼠标按钮已被按下,然后决定接下来要做什么。正如我们之前所述,当您点击左鼠标按钮时,必须创建一个圆,当您点击右鼠标按钮时,必须移除一个圆。实际上,我们在这里并没有创建圆,我们只是保存它们的位置。在draw()
方法中,我们将能够选择是绘制圆还是其他完全不同的东西。
所以让我们导航到draw()
方法实现,并添加以下代码片段:
// declare an iterator for this specific kind of vector
vector<Vec2i>::iterator i;// loop through circle positions
for ( i=circlePositions.begin(); i!=circlePositions.end(); ++i ) {// and draw circles at these positions one by onegl::drawSolidCircle( *i, 20 );
}
要遍历一个向量,我们必须在这种情况下使用迭代器。向量迭代器是设计用来遍历向量的对象。在这种情况下,迭代器对象就像一个指针,一个vector<Vec2i>
类型的迭代器将指向它内部的Vec2i
对象。通过增加和减少迭代器的位置,我们可以访问向量中的下一个或上一个项目。可以从向量中获取begin()
和end()
迭代器,它们分别指向向量的第一个和超出结束元素。
要通过迭代器(类似于指针)访问元素,我们必须使用解引用的概念。要解引用指针,我们必须在指针变量前使用一个星号(*
)。如果i
是指向一个圆的实际位置的指针,要访问存储坐标的实际Vec2i
对象,我们必须使用*i
。要访问对象的属性,我们写(*i).x
或i->x
。
编译并运行项目。您应该能够通过点击鼠标的左右按钮来添加和移除圆。
您也可以通过使用mouseUp()
事件处理器而不是mouseDown()
来实现相同的功能。唯一的区别是,当您释放鼠标按钮时,代码将被执行。
使用 mouseDrag()
接下来,我们将利用mouseDrag()
事件处理器在屏幕上绘制一个折线。我们需要另一个vector
来存储实际形成折线的点的坐标。让我们在类声明中声明这个vector
和mouseDrag()
事件处理器。在它的末尾添加以下代码行:
PolyLine<Vec2f> line;
void mouseDrag(MouseEvent event);
我们在这里使用PolyLine<Vec2f>
,因为PolyLine
是 Cinder 类,用于存储线的控制点值。我们使用Vec2f
而不是int
,因为 Cinder 中没有接受由int
值组成的PolyLine
类的draw
函数。
让我们进行下一步,并将mouseDrag()
方法的实现添加到类实现中:
void BasicEventsApp::mouseDrag(MouseEvent event) {// create new position from current mouse positionVec2f cp = event.getPos();// copy it to the PolyLineline.push_back(cp);
}
这将在检测到鼠标位置变化时,将当前鼠标坐标的新位置添加到PolyLine
中。
最后,我们必须绘制PolyLine
。所以让我们导航到draw()
方法实现,并添加以下代码行:
if ( line.size() ) {gl::drawSolid(line);
}
gl::drawSolid
函数基本上会绘制一个填充的多边形。PolyLine
本身定义了多边形的轮廓。编译并运行项目以查看我的意思。您应该得到一个类似于以下截图所示的图像:
如果您想画一条线,请使用gl::draw(line)
代替。
使用keyDown()
如果在应用程序运行时能够清除屏幕而不是关闭和重新打开它来重新开始,那将很棒。让我们使用keyDown()
事件处理器来检测按键。我们想要做的是,当按下C键时,擦除所有圆圈和线条。为此,我们需要在类声明中声明keyDown()
方法:
void keyDown(KeyEvent event);
接下来,我们需要实现这一点,所以请在文件末尾添加以下代码片段:
void BasicEventsApp::keyDown(KeyEvent event) {if ( event.getCode() == KeyEvent::KEY_c ) {circlePositions.clear();line.getPoints().clear();}
}
keyDown()
方法接受一个KeyEvent
参数,该参数包含被按下的键的代码。在这里,我们检查键码是否代表键盘上的字母 C,如果是true
,我们就清除circlePositions
vector
和vector
值,在PolyLine
对象中以与vector
circlePositions
相同的方式存储控制点。
您可以使用keyUp()
事件处理器做同样的事情。现在我们不会为它制作一个单独的示例,因为它在按键释放时工作方式完全相同。
使用fileDrop()
但是,我们将使用fileDrop()
事件处理器来在背景中放置一张图片。它接受一个FileDropEvent
对象作为参数。它包含被拖放到应用程序窗口上的文件的路径。为了使用该路径,我们需要在类文件顶部添加以下行:
#include "cinder/gl/Texture.h"
#include "cinder/ImageIo.h"
第一个包含文件是必需的,因为它包含我们将需要的gl::Texture
类,以便存储背景图像并使用gl::draw()
函数绘制它。ImageIo.h
文件在这里是因为我们需要加载实际图像到Texture
实例中的图像加载函数。
接下来,我们需要声明一个变量来存储背景图像和fileDrop()
事件方法本身。请在类声明末尾添加以下代码行:
gl::Texture background;
void fileDrop(FileDropEvent);
现在我们需要实现fileDrop()
方法。请在类实现中添加以下代码行:
void BasicEventsApp::fileDrop(FileDropEvent event) {try {background = gl::Texture( loadImage( event.getFile(0) ) );} catch( ... ) {console() << "unable to load file" << endl;};
}
在这里,我们使用try
和catch
语句。通过这样做,我们只是确保如果放置了错误类型的文件,我们的应用程序不会崩溃。如果我们幸运的话,我们将图像加载到background
变量中,如果不幸运,则将错误消息打印到控制台。
仔细看看console()
函数调用。console()
函数指的是标准输出或控制台。这是世界上最好的调试工具之一,如果您还没有使用它,您应该考虑使用它。
还有一件事需要完成,我们必须绘制背景
。前往draw()
方法实现,在gl::clear()
函数调用之后和我们在本章中添加到该方法的所有代码之前添加以下代码片段。我们这样做是因为背景是我们需要在每一帧中首先绘制的:
if ( background ) {gl::draw( background, getWindowBounds() );
}
在我们绘制纹理之前,我们必须确保它确实存在。这就是为什么我们使用了一个额外的 if 语句。只有在这种情况下,我们才能在由getWindowBounds()
方法调用返回的应用程序窗口范围内绘制背景纹理。
编译并运行我们的应用程序。将一个图像文件拖放到我们应用程序的窗口上,看看会发生什么。你应该会看到一个类似于以下屏幕截图的图像:
摘要
在本章中,我们获得了对任何类型的应用程序都可能使用的内置和最常用事件的基本理解。我们学习了如何利用鼠标点击、鼠标拖动、键盘点击甚至文件拖放事件。我们还利用了一些在之前章节中未解释的新绘图方法。
在下一章中,我们将讨论使用 Syphon 和 Open Sound Control 消息系统,在 Cinder 内置的应用程序与其他同一或不同网络计算机上的应用程序之间的通信。
附录 A. Cinder 基本功能参考
本书的这一部分将帮助您找到本书中使用的 Cinder 基本功能,以便日后参考。这个参考非常基础,所以如果您是一位经验丰富的开发者,正在寻找详细和深入的功能参考,您应该查看 Cinder 网站上的参考(libcinder.org
),或者由于 Cinder 是开源的,查看 Cinder 的实际源代码。
基本类型
这些是许多 Cinder 函数所消耗的基本类型。这假设您已经熟悉 C++ 中的 int
、float
和其他基本数据类型。
cinder::Vec2f( float x, float y )
上述代码表示一个由 float
值组成的二维向量(x
和 y
)。这通常用于表示二维空间中的位置或大小。
cinder::Vec2i( int x, int y )
上述代码表示一个由 int
值组成的二维向量(x
和 y
)。这通常用于表示二维空间中的位置或大小。
cinder::Vec3f( float x, float y, float z )
上述代码表示一个由 float
值组成的三个维度的向量(x
、y
和 z
)。这通常用于表示三维空间中的位置或大小。
cinder::Rectf( float x1, float y1, float x2, float y2 )
上述代码表示一个由左上角和右下角坐标定义的抽象矩形。
cinder::Color( float r, float g, float b, float a )
在上述代码中,颜色对象或类代表 Cinder 环境中的颜色——r
代表红色,g
代表绿色,b
代表蓝色,a
代表 alpha,范围从 0.0f
到 1.0f
。
应用程序
以下函数构成了您的 Cinder 应用程序的基础:
-
setup()
-
update()
-
draw()
我们使用 setup()
进行准备,update()
在运行循环中进行计算,draw()
用于在屏幕上绘制。您可以使用 shutdown()
方法在我们应用程序关闭之前做一些事情(清除内存、存储数据或与远程服务器通信)。请记住,您的 Cinder 主应用程序类应该从 BaseApp
类派生,并且您应该首先在类声明中实现这些方法。
void YourApp::setup() {// setup goes here
}void YourApp::update() {// prepare data
}void YourApp::draw() {// draw
}
void YourApp::shutdown() {// do something before the app closes
}
如果您想更改应用程序的初始窗口大小或其他初始参数,请使用 prepareSettings()
方法(不要忘记首先声明此方法):
void YourApp::prepareSettings( Settings * settings ) {settings->setWindowSize( 800, 600 ); // set window to 800x600 px
}
然后,有一些有用的函数,您可以在设置和运行时使用。
cinder::app::getWindowWidth()
上述函数返回应用程序窗口宽度作为一个 int
值。
cinder::app::getWindowHeight()
上述函数返回应用程序窗口高度作为一个 int
值。
cinder::app::getWindowSize()
上述函数返回应用程序窗口大小作为一个 Vec2i
值。
cinder::app::getFrameRate()
有时,有必要知道应用程序的当前帧率。上述函数以 float
值返回它。
cinder::app::getElapsedSeconds()
上述函数返回自应用程序开始以来经过的秒数,作为一个 double
值。
cinder::app::getElapsedFrames()
上述函数返回自应用程序开始以来经过的帧数,作为一个 int
值。
console()
使用上述函数进行调试,例如,console() << "debug text" << std::endl;
。
isFullScreen()
前面的函数如果应用程序处于 fullscreen
模式则返回 true
,否则返回 false
。
setFullScreen( bool fullScreen )
前面的代码通过传递 true
或 false
设置 fullscreen
状态。
要使这些函数正常工作,你必须按照以下方式导入 AppBasic
头文件:
#include "cinder/app/AppBasic.h"
基本图形
这些函数通常在 draw()
方法实现中使用。
cinder::gl::clear( Color color )
前面的代码使用在 Color
中指定的颜色清除屏幕。
cinder::gl::drawLine( Vec2f from, Vec2f to )
前面的代码从 from
定义的点绘制到 to
定义的点绘制一条线。
cinder::gl::color( float r, float g, float b, float a )
前面的代码设置了用于绘制形状和线条的颜色。颜色定义为单独的 r
、g
、b
和 a
(可选)浮点值。
cinder::glLineWidth( float width )
前面的代码设置了 Cinder 线绘制函数的线宽。
cinder::gl::drawSolidCircle( Vec2f center, float radius, int numSegments = 0 )
前面的代码在 center
定位处绘制了一个填充的圆,半径由 radius
定义。
cinder::gl::drawStrokedCircle( Vec2f center, float radius, int numSegments = 0 )
前面的代码仅绘制了圆的轮廓,其位置由 center
定义,半径由 radius
定义。
cinder::gl::drawSolidRect( Rectf rect )
前面的代码绘制了一个在 rect
中定义的实心填充矩形。
cinder::gl::drawStrokedRect( Rectf rect )
前面的代码绘制了 rect
中定义的矩形轮廓。
cinder::gl::drawSolidEllipse( Vec2f position, float radiusX, float radiusY, int numSegments = 0 )
前面的代码在 position
定位处绘制了一个实心填充的椭圆。x 轴上的半径由 radiusX
定义,y 轴上的半径由 radiusY
定义。numSegments
是可选的;它定义了用于绘制椭圆的三角形数量,因为此形状使用 OpenGL 三角形扇形绘制。如果为 0
,扇形的数量将自动决定。
cinder::gl::drawStrokedEllipse( Vec2f position, float radiusX, float radiusY, int numSegments = 0 )
前面的代码仅绘制了椭圆的轮廓,其位置由 position
定义,x 轴上的半径由 radiusX
定义,y 轴上的半径由 radiusY
定义。并且可选地,在 numSegments
中定义段数。
cinder::gl::drawSolidRoundedRect( Rectf rect, float cornerRadius, int numSegmentsPerCorner = 0 )
前面的代码绘制了一个具有圆角的实心填充矩形。矩形由 rect
定义,其圆角半径由 cornerRadius
定义,并且可以在 numSegmentsPerCorner
中可选地定义用于绘制每个角落的段数。如果设置为 0
,段数将自动决定。
cinder::gl::drawStrokedRoundedRect( Rectf, float cornerRadius, int numSegmentsPerCorner = 0 )
前面的代码仅绘制了具有圆角的矩形轮廓。矩形由 rect
定义,其圆角半径由 cornerRadius
定义,并且可以在 numSegmentsPerCorner
中可选地定义用于绘制每个角落的段数。如果设置为 0
,段数将自动决定。
要使这些函数正常工作,你必须按照以下方式导入 OpenGL 头文件:
#include "cinder/gl/gl.h"
图片
以下是将图像加载到纹理中的基本代码:
gl::Texture myTexture = gl::Texture( loadImage( loadAsset( "image.png" ) ) );
此纹理随后在 draw()
函数中使用以下代码进行绘制:
gl::draw( myTexture, Rectf(100,100,540,380) );
第一个参数是包含加载图像的纹理,第二个参数是在纹理中绘制的矩形。
要使此代码示例正常工作,你必须导入以下头文件:
#include "cinder/gl/gl.h"
#include "cinder/ImageIo.h"
#include "cinder/gl/Texture.h"
其他函数
如果你对 3D、视频、声音或其他类型的函数参考感兴趣,请参阅本书的所有章节,因为这类功能使用类,这些函数的参考和解释将占用与章节相同的空间。
如果你想要更多关于 Cinder 的信息,但在这本书中找不到你想要的内容,请参考在 libcinder.org
可用的原始 Cinder 函数参考,或者许多 C++ 或 OpenGL 语言参考之一。另一个寻找帮助的好地方是友好的 libcinder.org 论坛。
由于 Cinder 是开源的,如果你遇到麻烦并且觉得自己经验足够,能够找到你想要的东西,或者修复挡在你道路上的问题,查看 Cinder 的源代码是个不错的选择。
这本书只是对 Cinder 实际能做什么的一个简要介绍。