openFrameworks periodicSignalsExample 详解:用正弦、噪声、随机与取模绘制周期信号
2026/9/24 14:21:30 网站建设 项目流程
  • 图形学
  • 音视频

【免费下载链接】openFrameworks

openFrameworks is a community-developed cross platform toolkit for creative coding in C++.

项目地址:https://gitcode.com/gh_mirrors/op/openFrameworks
点击查看免费下载

导读

periodicSignalsExample是 openFrameworks 官方示例库中位于examples/math/目录下的一个教学示例,它用 8 条水平移动的轨迹,直观对比了四类周期信号生成方式:三角函数(sin)、柏林噪声(ofNoise/ofSignedNoise)、真随机(ofRandom)以及浮点取模(fmod)。通过阅读本文,你将掌握这些信号函数在 openFrameworks 中的底层实现与取值范围差异,理解“随机 ≠ 噪声”“伪随机噪声带缓动”的核心区别,并学会用滑块实时调整振幅、速度与频率来观察每种信号的形态特征。这对于制作粒子系统、运动动画、程序化生成等创意编码场景都极具参考价值。

示例概述与运行方式

本示例的完整源码位于 examples/math/periodicSignalsExample,核心文件为 ofApp.cpp 与 ofApp.h,另附带一个自定义的 ofxSimpleSlider 滑块控件(由 Golan Levin 于 2012 年编写)。窗口尺寸在 main.cpp 中设置为1024 x 600,标准窗口模式(OF_WINDOW)。

应用启动后,画面上会出现8 条水平移动的轨迹,每条轨迹对应一种不同的周期信号函数,右侧标注有函数名称。底部的时间轴刻度以秒(s)或像素(p)为单位标记当前 x 位置,右上角实时显示帧率(fps)、运行时间(t)与当前 x 坐标,便于对照观察。

八种周期信号的核心实现

在 ofApp.cpp 的update()中,示例以运行时间t(通过ofGetElapsedTimef()获取并减去初始时间得到)作为输入,为每条轨迹计算y值。时间变量t的求法为:

t = ofGetElapsedTimef(); if(trail[0].size()==0){ initTime = t; } t = t - initTime;

而水平位置x由速度滑块的值与时间共同决定,并取模限制在窗口宽度内:

x = int(t*speed.getValue())%(ofGetWidth()-rightMargin);

1. constant —— 常量基线

y[0] = 60*2;setup()中直接赋值为常量,代表一条水平的直线基线,用于与其他波动信号形成参照。

2. ofNoise(t) —— 柏林噪声(0 ~ 1)

y[1] = 60*3+ofNoise(t*freq.getValue())*yFactor.getValue();

ofNoise()基于柏林噪声(Perlin noise)算法,返回0 到 1.0 之间的连续伪随机值。查看 ofMath.cpp 的底层实现可以看到:

float ofNoise(float x) { return _slang_library_noise1(x) * 0.5f + 0.5f; }

它先调用底层的_slang_library_noise1()(值域约为 -1 ~ 1),再通过* 0.5 + 0.5映射到 [0, 1] 区间。所谓“伪随机带缓动”,是指噪声函数在相邻输入之间会产生平滑过渡的渐变值,而非毫无关联的跳变,因此适合模拟自然、有机的运动。

3. ofSignedNoise(t) —— 带符号噪声(-1 ~ 1)

y[2] = 60*4+ofSignedNoise(t*freq.getValue())*yFactor.getValue();

ofSignedNoise()ofNoise()共享同一个噪声内核,但不做 0.5 偏移,直接返回原始噪声值,值域为-1.0 到 1.0

float ofSignedNoise(float x) { return _slang_library_noise1(x); }

两者对比如下:

函数底层调用值域用途特点
ofNoise(x)_slang_library_noise1(x) * 0.5f + 0.5f[0, 1]适合 alpha、比例等非负参数
ofSignedNoise(x)_slang_library_noise1(x)[-1, 1]适合位移、旋转等对称变化

在 ofMath.h 中,两者都提供了 1~4 维重载,分别接受floatglm::vec2glm::vec3glm::vec4参数。

4. ofRandom(-1,1) —— 真随机(-1 ~ 1)

y[3] = 60*5+ofRandom(-1,1)*yFactor.getValue(); // random can't be dependent on time or frequency

ofRandom(min, max)返回 min 与 max 之间完全随机的浮点数,每次调用结果彼此独立、没有连续性。其实现见 ofMath.cpp:

float ofRandom(float min, float max) { return of::random::uniform<float>(min, max); }

ofRandom与噪声函数有着本质区别:噪声是伪随机但连续(相邻值平滑过渡),而ofRandom完全不连续的跳跃值。因此示例代码特意注释:“random can't be dependent on time or frequency”——由于随机值不受时间与频率影响,这条轨迹看起来会不断跳变,无法形成平滑的波形。

5. sin(t) —— 标准正弦波(-1 ~ 1)

y[4] = 60*6 + std::sin(t*freq.getValue()*glm::two_pi<float>())*yFactor.getValue();

这是最经典的周期信号。glm::two_pi<float>()即 2π,将时间乘以 2π 后,每经过 1 秒恰好完成一个完整的正弦周期,频率滑块的值(单位 Hz)直接决定每秒的振荡次数。返回值在-1 到 1 之间平滑往返。

6. (sin(t)+1)/2 —— 正弦波归一化(0 ~ 1)

y[5] = 60*7+(std::sin(t*freq.getValue()*glm::two_pi<float>())+1)*.5*yFactor.getValue();

将标准正弦结果+1后除以 2,把值域从 [-1, 1] 线性映射到[0, 1],方便驱动透明度、颜色分量等非负参数。从源码结构看,这与ofNoise对底层噪声做* 0.5 + 0.5映射的思路如出一辙。

7. fmod(t, 1) —— 浮点取模(0 ~ 1)

y[6] = 60*8+std::fmod(t*freq.getValue(),1)*yFactor.getValue();

std::fmod(a, b)返回 a 除以 b 的浮点余数fmod(t, 1)的输出始终落在[0, 1),波形呈锯齿状:每经过 1 个时间单位归零并重新攀升。它与整数取模运算符%语义相同,但专门用于浮点数——这正是 README 中强调fmod“is the same as using %; however, it is for floats”的原因。

8. fmod(t, 1)*2-1 —— 双锯齿波(-1 ~ 1)

y[7] = 60*9+(std::fmod(t*freq.getValue(),1)*2-1)*yFactor.getValue();

fmod(t, 1)的基础上乘以 2 再减 1,将值域映射到[-1, 1],形成上下对称的双向锯齿波。

滑块实时控制:振幅、速度与频率

三个滑块的配置

setup()中创建了三个 ofxSimpleSlider 实例,分别控制垂直振幅(y amnt)、水平速度(x speed)与频率(frequency):

yFactor.setup(130, 20, 100, 20, 0, 100, 20, false, false); yFactor.setLabelString("y amnt"); speed.setup(130, 45, 100, 20, 1, 200, 30, false, false); speed.setLabelString("x speed (px/s)"); freq.setup(130, 70, 100, 20, 0, 10, 1, false, false); freq.setLabelString("frequency (hz)");

setup()的完整参数签名为setup(float inx, float iny, float inw, float inh, float loVal, float hiVal, float initialPercent, bool bVert, bool bDrawNum):前 4 个参数是滑块的位置与尺寸,loVal/hiVal是取值范围,initialPercent为初始值(内部通过ofMap映射为百分比并ofClamp到 [0,1]),bVert表示是否垂直放置,bDrawNum表示是否绘制数值。本例三个滑块均水平放置、不显示数值。

各滑块参数汇总:

滑块标签取值范围默认值作用
yFactory amnt0 ~ 10020信号的振幅(垂直幅度)
speedx speed (px/s)1 ~ 20030轨迹的水平滚动速度(像素/秒)
freqfrequency (hz)0 ~ 101信号的频率(每秒周期数)

滑块的底层实现

ofxSimpleSlider是一个不依赖任何 addon 的轻量滑块类,其核心机制是:setup()时通过ofAddListener把自己注册到ofEvents()drawmouseMovedmousePressedmouseReleasedmouseDragged事件上(见 ofxSimpleSlider.cpp),此后无需在ofApp中手动转发事件。

拖拽时,updatePercentFromMouse()把鼠标坐标映射为 0~1 的百分比:

void ofxSimpleSlider::updatePercentFromMouse (int mx, int my){ if (bVertical){ percent = ofMap(my, y, y+height, 1,0, true); } else { percent = ofMap(mx, x, x+width, 0,1, true); } }

读取当前值时,再由百分比反向映射到loValhiVal之间:

float ofxSimpleSlider::getValue(){ // THIS IS THE MAIN WAY YOU GET THE VALUE FROM THE SLIDER! float out = ofMap(percent, 0,1, lowValue,highValue, true); return out; }

速度变化的轨迹清空机制

update()中对速度变化做了特殊处理:由于不能直接比较浮点数的相等性speed.getValue() != preSpeed不可靠),示例改用“变化量大于极小阈值”的方式检测:

if( fabs(speed.getValue() - preSpeed) > 0.00001f ){ speedChanged(); } preSpeed = speed.getValue();

一旦速度改变,speedChanged()会清空所有轨迹折线,避免不同速度下的轨迹在同一个 x 轴上错位重叠:

void ofApp::speedChanged(){ for(unsigned int i=0;i<trail.size();i++){ trail[i].clear(); } }

此外,由于x = int(t*speed) % (ofGetWidth()-rightMargin)存在取模回绕,当x小于上一帧的prevX时(即发生回绕),也会主动清空轨迹,形成“换行重画”的效果:

for(int i=0;i<(int)trail.size();i++){ if(x<prevX){ trail[i].clear(); }else{ trail[i].addVertex(x,y[i]); } } prevX = x;

绘制细节:轨迹、网格与标签

draw()(见 ofApp.cpp)完成了以下绘制工作:

  • 渐变背景:使用ofBackgroundGradient(ofColor(240), ofColor(210), OF_GRADIENT_BAR)生成从浅灰到中灰的垂直渐变;
  • 时间刻度线:在左侧区域按speed值绘制垂直虚线,底部标注以秒(s)或像素(p)为单位的刻度,showPos布尔值控制刻度单位显示;
  • 8 条轨迹:每条轨迹用一个ofPolyline存储顶点序列并调用trail[i].draw()绘制;轨迹头部绘制一个半径为 3 的圆点(ofDrawCircle(x, y[i], radius)),绘制时启用ofEnableSmoothing()抗锯齿,绘制后关闭;
  • 分隔线:每条轨迹之间以水平直线分隔,每条轨迹 60 像素的垂直间距;
  • 右侧标签:在右侧预留的 190 像素边距(rightMargin = 190)内,用ofDrawBitmapString标注每条轨迹对应的函数名,例如constantofNoise(t)ofSignedNoise(t)ofRandom(-1,1)sin(glm::two_pi<float>()*t)(sin(glm::two_pi<float>()*t)+1)/2modulo(t,1)modulo(t,1)*2-1
  • 状态信息:右上角实时显示fps、运行时间t与当前位置x
  • 振幅指示:每个轨迹右侧绘制一条从分隔线到当前y值的小矩形,直观表示振幅大小。

操作指南与关键注意事项

交互方式

应用运行后,用鼠标直接拖拽右侧三个滑块即可实时调节:

  • y amnt(振幅):控制所有信号轨迹的垂直摆动幅度,范围为 0 ~ 100;
  • x speed (px/s)(速度):控制轨迹水平滚动的速度,范围为 1 ~ 200 像素/秒;改变速度会触发轨迹清空重绘;
  • frequency (hz)(频率):控制每种周期信号的振荡频率,范围为 0 ~ 10 Hz。

需要特别注意的点

  1. ofRandom不依赖时间与频率:由于随机值每次调用都是独立跳变,改变频率滑块对它没有平滑效果,这正是随机信号与噪声、三角函数最直观的差异。
  2. 噪声 ≠ 随机ofNoise/ofSignedNoise是连续平滑的伪随机(缓动),适合有机运动;ofRandom是离散跳变,适合无规律的抖动。
  3. 浮点比较陷阱:不要用==/!=直接比较浮点滑块值,应像示例那样使用阈值判断(fabs(a - b) > 0.00001f)。
  4. 值域决定用途:0~1 的信号(ofNoise(sin+1)/2fmod(t,1))适合驱动比例类参数;-1~1 的信号(ofSignedNoisesinofRandom(-1,1)fmod(t,1)*2-1)适合驱动位移、旋转等双向变化。

延伸阅读

  • 三角函数专题:examples/math/trigonometryExampleexamples/math/trigonometricMotionExample(后者同样位于 examples/math,README 中对三角函数的推荐深入阅读);
  • 随机与噪声的更多组合玩法:noise1dExample、noise1dOctaveExample、noiseField2dExample 与 randomExample;
  • 运动应用实战:particlesExample 展示了如何将周期与噪声信号用于粒子系统;
  • 底层数学实现:ofNoise/ofSignedNoise/ofRandom等函数的源码位于 libs/openFrameworks/math/ofMath.cpp 与 libs/openFrameworks/math/ofMath.h。

通过运行本示例并拖动三个滑块,你可以直观地感受到“平滑周期”“平滑伪随机”“离散随机”“锯齿周期”四类信号的形态差异——这是理解 openFrameworks 运动与生成式创作的重要基础。

  • 图形学
  • 音视频

【免费下载链接】openFrameworks

openFrameworks is a community-developed cross platform toolkit for creative coding in C++.

项目地址:https://gitcode.com/gh_mirrors/op/openFrameworks
点击查看免费下载

相关推荐

上一篇:VnCoreNLP二次开发指南:自定义annotator扩展越南语NLP流水线
下一篇:超强大屏体验:iCarousel打造iOS/MacOS双平台3D轮播组件

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询