其他

无限与有限 - 希尔伯特曲线的实现

2018-04-08 Wenzy InsLab

前段时间看了 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

感谢作者和译者带来的精彩视频 !


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存