- 图形学
- 音视频
【免费下载链接】openFrameworks
openFrameworks is a community-developed cross platform toolkit for creative coding in C++.
导读
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 维重载,分别接受float、glm::vec2、glm::vec3、glm::vec4参数。
4. ofRandom(-1,1) —— 真随机(-1 ~ 1)
y[3] = 60*5+ofRandom(-1,1)*yFactor.getValue(); // random can't be dependent on time or frequencyofRandom(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表示是否绘制数值。本例三个滑块均水平放置、不显示数值。
各滑块参数汇总:
| 滑块 | 标签 | 取值范围 | 默认值 | 作用 |
|---|---|---|---|---|
| yFactor | y amnt | 0 ~ 100 | 20 | 信号的振幅(垂直幅度) |
| speed | x speed (px/s) | 1 ~ 200 | 30 | 轨迹的水平滚动速度(像素/秒) |
| freq | frequency (hz) | 0 ~ 10 | 1 | 信号的频率(每秒周期数) |
滑块的底层实现
ofxSimpleSlider是一个不依赖任何 addon 的轻量滑块类,其核心机制是:setup()时通过ofAddListener把自己注册到ofEvents()的draw、mouseMoved、mousePressed、mouseReleased、mouseDragged事件上(见 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); } }读取当前值时,再由百分比反向映射到loVal与hiVal之间:
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标注每条轨迹对应的函数名,例如constant、ofNoise(t)、ofSignedNoise(t)、ofRandom(-1,1)、sin(glm::two_pi<float>()*t)、(sin(glm::two_pi<float>()*t)+1)/2、modulo(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。
需要特别注意的点
ofRandom不依赖时间与频率:由于随机值每次调用都是独立跳变,改变频率滑块对它没有平滑效果,这正是随机信号与噪声、三角函数最直观的差异。- 噪声 ≠ 随机:
ofNoise/ofSignedNoise是连续平滑的伪随机(缓动),适合有机运动;ofRandom是离散跳变,适合无规律的抖动。 - 浮点比较陷阱:不要用
==/!=直接比较浮点滑块值,应像示例那样使用阈值判断(fabs(a - b) > 0.00001f)。 - 值域决定用途:0~1 的信号(
ofNoise、(sin+1)/2、fmod(t,1))适合驱动比例类参数;-1~1 的信号(ofSignedNoise、sin、ofRandom(-1,1)、fmod(t,1)*2-1)适合驱动位移、旋转等双向变化。
延伸阅读
- 三角函数专题:
examples/math/trigonometryExample与examples/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++.
相关推荐
GNU Radio信号源模块终极指南:如何快速生成随机噪声、正弦波和方波信号
GNU Radio信号源模块终极指南:如何快速生成随机噪声、正弦波和方波信号 想要在GNU Radio中快速生成各种信号吗?GNU Radio作为免费开源的软件
通信科研Easy Effects 测试信号(Test Signal)完全指南:用正弦波与噪声信号测量音箱频响
Easy Effects 测试信号(Test Signal)完全指南:用正弦波与噪声信号测量音箱频响 Easy Effects 内置的 Test Signal(
桌面应用音频处理Agent OS 信号处理机制详解:用 POSIX 风格信号实现 AI Agent 生命周期治理
Agent OS 信号处理机制详解:用 POSIX 风格信号实现 AI Agent 生命周期治理 Agent OS(agent governance toolk
人工智能AI AgentAI 安全治理策略引擎Agent 沙箱认证鉴权
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考