用莫比乌斯带巧解内接矩形问题:拓扑学的妙用
推荐一个昨天看到的视频。深入浅出,相当精彩。即使你已经阔别学校多年,也能从中领略数学特有的美感。
https://v.qq.com/txp/iframe/player.html?width=500&height=375&auto=0&vid=n0346maboeq
视频作者:3Blue1Brown 译制作者:昨梦电羊
整个视频试图阐述拓扑学能解决什么实际问题,并从证明一个命题开始 - 闭合环路中是否能找到四个点组成一个长方形。
贯穿整个视频,其实都在不断类比。最终为了寻找一个更合理,更直观的模型来解答这一问题。先从闭合环路开始,对应到二维平面。再把二维平面扭曲成环面(甜甜圈),最后到莫比乌斯环。
当然,未必所有人都能完全读懂每个细节。但其中有一步,是将闭合环路转成曲面。这个部分比较通俗,用简单而巧妙的方法就可以将平面曲线转化成立体图形,很值得用代码模拟一番。
下面将使用 Openframeworks 实现这一过程。
最终效果
绘制到生成
立体图形(显示模式1)
立体图形(显示模式3)
实现思路:
若不清楚如何确定曲面坐标,可以再回顾视频。
代码实现思路:
1.记录轨迹,绘制曲线
2.确定细分精度。计算闭合环路坐标点,两两组合的所有情况。根据中点与距离,推出曲面的空间坐标
3.提取曲面坐标,进行绘制与渲染
源代码:
— ofApp.h 内
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
...
ofEasyCam cam;
ofPolyline poly; // 记录绘制轨迹
int displayMode; // 显示模式
bool startDrawing; // 是否开始绘制
int divideNum; // 曲线细分数量
vector<ofPoint> bodyPos; // 记录曲面上的关键点
};
— ofApp.cpp 内
void ofApp::setup(){
// 数量越多,精度越高
divideNum = 80;
// 默认线框模式
displayMode = 0;
// 进入开始绘制
startDrawing = true;
}
void ofApp::update(){
if(!startDrawing){
// divideNum = mouseX/(float)ofGetWidth() * 80;
}
}
void ofApp::draw(){
ofBackground(0);
cam.begin();
// 闭合图形,等价于 poly.close();
poly.setClosed(true);
// 绘制原始路径
ofNoFill();
ofSetColor(255,200);
ofSetLineWidth(3);
poly.draw();
ofSetLineWidth(1);
if(!startDrawing){
bodyPos.clear();
for(int i = 0;i < divideNum;i++){
if(displayMode == 1){
ofNoFill();
ofSetColor(255,200);
ofEnableBlendMode(OF_BLENDMODE_ALPHA);
}
if(displayMode == 2){
ofNoFill();
ofSetColor(ofColor::fromHsb(i / (float)divideNum * 255,255,255));
ofEnableBlendMode(OF_BLENDMODE_ALPHA);
}
if(displayMode == 3){
ofEnableBlendMode(OF_BLENDMODE_ADD);
ofFill();
ofSetColor(ofColor::fromHsb((ofGetFrameNum() + i * 3) % 255,255,255),20);
}
ofBeginShape();
for(int j = 0;j < divideNum;j++){
float ratio1 = i/(float)divideNum;
float ratio2 = j/(float)divideNum;
ofPoint newPos,startPos,middlePos;
// 根据百分比获取曲线路径上的关键点
startPos = poly.getPointAtPercent(ratio2);
newPos = poly.getPointAtPercent(ratio1);
middlePos = (startPos + newPos)/2;
// 计算两点间长度
float length = startPos.distance(newPos)/2;
// 将长度设为中点的高度
middlePos.z = length;
bodyPos.push_back(middlePos);
ofVertex(middlePos);
}
ofEndShape(TRUE);
}
ofFill();
ofSetSphereResolution(5);
ofSetColor(255,150);
for(int i = 0;i < bodyPos.size();i++){
ofDrawSphere(bodyPos[i], 2);
}
}
cam.end();
if(startDrawing && ofGetFrameNum() > 1){
// 绘制时,鼠标不影响镜头角度
cam.disableMouseInput();
}else{
cam.enableMouseInput();
}
}
void ofApp::keyReleased(int key){
if(key == 'c'){
// 按 c 键清除
poly.clear();
}
if(key == ' '){
if(!startDrawing){
startDrawing = true;
// 恢复镜头默认视角
cam.setOrientation(ofPoint(0,0,0));
cam.setPosition(ofPoint(0,0,600));
}else{
startDrawing = false;
}
}
if(key == '1'){
displayMode = 1;
}
if(key == '2'){
displayMode = 2;
}
if(key == '3'){
displayMode = 3;
}
}
void ofApp::mouseDragged(int x, int y, int button){
if(startDrawing){
poly.curveTo(x - ofGetWidth()/2,-(y - ofGetHeight()/2));
}
}
浅析:
使用 ofPolyline 来保存坐标点,是因为其中有一个成员函数 getPointAtPercent,可以很方便地根据百分比来获取坐标
为了让整个图形比例更美观,做了些修改。曲面的高度仅为两点距离的一半
通过开启 update 中的语句,可以通过移动鼠标实时改变细分精度
可以尝试用 ofNoise 函数动态生成基础曲线,相应的立体图形也会实时变化
源码与可执行程序下载(mac):
( https://pan.baidu.com/s/1nv4jeHj )
使用说明:
点击鼠标开始绘制
‘c’ 键清除轨迹
空格键切换绘制模式与 3D 模式
在 3D 模式下拖拽鼠标可缩放或旋转视角
数字键 1,2,3 切换显示模式