无限与有限 - 希尔伯特曲线的实现
前段时间看了 3Blue1Brown 的一段关于希尔伯特曲线的科普视频。深入浅出,引人入胜。
【希尔伯特曲线:无限数学怎样应用于有限世界】
作者:3Blue1Brown@Youtube 译制:昨梦电羊
https://v.qq.com/txp/iframe/player.html?vid=f0195hdbv6z&width=500&height=375&auto=0
里面以一个声音视觉应用为例,巧妙地阐明了希尔伯特曲线的定义以及可能的应用。
在 4:00 ,作者用可视化的方式讲述了各阶“伪希尔伯特曲线”是如何迭代而来的。这个过程非常有趣,下面尝试用 C++ 去实现一番,使用的图形框架为 Openframeworks。
源码:
—- ofApp.h 内 —-
#include "WenzyHilbertCurve.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
WenzyHilbertCurve curve;
ofEasyCam cam;
};
—- ofApp.cpp 内 —-
void ofApp::setup(){
ofSetWindowShape(2000,2000);
curve = WenzyHilbertCurve(4);
}
void ofApp::update(){
}
void ofApp::draw(){
ofBackground(0);
cam.begin();
curve.draw();
cam.end();
}
—- 自定义类 —-
class WenzyHilbertCurve{
public:
int level;
vector<ofVec2f> curvePos;
WenzyHilbertCurve(){
}
WenzyHilbertCurve(int level_){
level = level_;
curvePos.push_back(ofVec2f(-0.5,-0.5));
curvePos.push_back(ofVec2f(-0.5,0.5));
curvePos.push_back(ofVec2f(0.5,0.5));
curvePos.push_back(ofVec2f(0.5,-0.5));
for(int i = 0;i < level - 1;i++){
curvePos = divide(curvePos);
}
}
vector<ofVec2f> divide(vector<ofVec2f> orginPos){
vector<ofVec2f> newPos; // 保存新的顶点
// (设置'左下方的区间' : 将 xy 的坐标反转)
ofVec2f tempPos; // 坐标的临时变量
float tempVal; // 用作交换 x,y 的值
for(int i = 0;i < orginPos.size();i++){
tempPos = orginPos[i];
tempVal = tempPos.y;
tempPos.y = tempPos.x;
tempPos.x = tempVal;
// 整体缩小,再移动到左下方
tempPos *= 0.5;
tempPos += ofVec2f(-0.5,-0.5);
newPos.push_back(tempPos);
}
// (设置'左上方的区间与右上方的区间' : 相对位置不用改变)
for(int i = 0;i < orginPos.size();i++){
tempPos = orginPos[i];
tempPos *= 0.5;
tempPos += ofVec2f(-0.5,0.5);
newPos.push_back(tempPos);
}
for(int i = 0;i < orginPos.size();i++){
tempPos = orginPos[i];
tempPos *= 0.5;
tempPos += ofVec2f(0.5,0.5);
newPos.push_back(tempPos);
}
// (设置'右下方的区间' : 将 xy 的坐标反转)
for(int i = 0;i < orginPos.size();i++){
tempPos = orginPos[i];
tempVal = tempPos.y;
// 符号需要取反
tempPos.y = -tempPos.x;
tempPos.x = -tempVal;
// 整体缩小,再移动到右下方
tempPos *= 0.5;
tempPos += ofVec2f(0.5,-0.5);
newPos.push_back(tempPos);
}
return newPos;
}
void draw(){
ofSetLineWidth(2);
ofPolyline polyline;
float scale = 900;
for(int i = 0;i < curvePos.size();i++){
polyline.addVertex(curvePos[i].x * scale,curvePos[i].y * scale);
}
polyline.draw();
ofDrawSphere(polyline.getPointAtPercent(fmod(ofGetElapsedTimef() / 30.0,1)),15);
}
};
运行结果:
思路浅析
代码实现思路与视频中的讲解并无二致。curvePos 用作储存曲线顶点,level 表示‘阶数’。这里将曲线的绘制区间定义在(-1,1)内,并且在构造函数中初始化时,就依序加入 4 个点,以此对应下图的‘一阶伪希尔伯特曲线’
当阶数越高,需要进行的系列操作就越多。由于对顶点的操作规律是固定的。所以定义了函数 divide 来实现这一过程。先‘复制’四份,再‘缩小’‘平移’到四个象限,最后将左下右下的象限曲线进行‘反转’操作。
最终生成的 curvePos 顶点列表。与曲线的先后顺序是一一对应的。之所以采用 ofPolyline 来绘制。是因为内置的函数 getPointAtPercent 可以直接根据百分比去获得插值坐标。从而绘制出平滑的小球运动动画。
其他实验
基于上面代码。这里还做了一些实验
形变动画
形变算法在这篇有提及([源代码]- Test 96 变换动画)。只要将各阶曲线提前生成一份顶点,再将顶点数较低的层级往最高层级的顶点数进行统一,就可以实现自由变换
层叠对比
上图的各阶曲线递进比率是相同的。不难发现,随着阶数的增多,对应的点坐标越趋于稳定
拓展
关于曲线的代码实现就到这里。如果不满足于此,不妨再看下面的视频,去挑战更多新模式~
https://v.qq.com/txp/iframe/player.html?vid=v0195vbejp9&width=500&height=375&auto=0
感谢作者和译者带来的精彩视频 !