Processing临摹平面动图及实现四维立方体动图(作业用博文)

前言

做了两个动图的临摹。第一个是临摹得比较像,但是过于简单。第二个是临摹得不是特别像,但大方向上是相似的,且难度一般。

其中第二个临摹作品为四维立方体,参考了B站的视频教程av30230939

略微吐槽一句,教程英文生肉无字幕且全程写bug改bug很打断思路,但思路讲得还意外地挺清楚的。观看顺序为P3-P2-P1,up主为了吸引眼球,把最后部分提到了P1,直接看一头雾水。

所临摹的原图及对比

原图1:
很简单的一个图
临摹作品。为示区别,反向旋转:
我的临摹1
原图2:四维立方体
在这里插入图片描述
临摹作品。截图软件问题,调低了帧持续时间,否则图片卡顿,所以略快:
临摹四维立方体

代码实现

图一:平面图形

思路分析

观察图形,不难发现它的运动规律。
1、共有“两圈”点。
2、同一圈上的点,每个点都在做周期相同初相位不同直线运动,或者说,是在一定范围内沿直线振荡
3、每圈都有两片“叶子”。事实上,这与初相位的倍数有关。
4、同一径向上的两点形成连线。
这基本就是这张图的规律所在了。

代码步骤

首先,两圈点的运动规律几乎相同。所以我们先一圈圈来。

结合老师上课时的画图步骤,我们先画一个静态的、含有64个圆点的圆。
利用圆的参数方程,求得所有圆点的坐标。

先设定一个参数r1来作为标准半径。之后小点将在标准半径附近振荡。

再设定一个参数theta来作为方向角,theta的每一个值代表一个从原点向外发出的一条射线的方向。

其余参数见注释。

float dutoPI=(2*PI)/360;	//角度转为弧度时应当乘以该数
float dotnum;				//点的数量,64个
float deltatheta;			//角度递增量,为2PI/dotnum,为弧度制
float theta;				//方向角
float r1,r2;				//内、外圈的标准半径
float x1,y1,x2,y2;			//内、外圈圆点坐标
float offr1=0;				//内圈振荡值
float offr2=0;
float phy=0;				//应该是没用到
float ye1=2;				//内圈的“叶子”数目
float ye2=2;
void setup(){
  size(900,700);
  background(0);
  stroke(255);
  dotnum=64;
  deltatheta=(2/dotnum)*PI;
  r1=180;
  r2=250;
  theta=0;
}

void draw() {
  translate(width/2, height/2);				//坐标中心移到画布中央
  background(0);							//背景填充黑色
  for (int i=0; i<dotnum; i++) {			//循环64次,画出64个点
    fill(255, 255, 255);
    x1=r1*cos(theta);			//根据圆的参数方程来的,其中cos(theta)和sin(theta)
    y1=r1*sin(theta);			//唯一确定该方向,前面的系数即确定该点离原点有多远
    theta+=deltatheta;			//方向角递增,用于绘制下一个圆点
    ellipse(x1, y1, 8, 8);		//画一个小圆
    if (i==dotnum-1) {			//由于精度问题,画完一圈后,方向角会出现偏差
      theta=0;					//用于校正方向角,使其归零
    }
  }
}

这样将得到一个静态的圆点图案:
静态的圆
现在要使其振荡起来,即沿半径方向扩张、收缩。

代码中,x1,y1计算时,注释中有写,其三角函数值表示方向,系数则表示距离原点的距离。

那么,只要固定方向不动(即三角函数内的方向角不动,否则将会进行旋转。如果变化过大,还会有跳帧感。也可以故意修改方向角,使其达到转动效果),修改距离原点的距离,使其周期性变长、变短即可。可以使用的周期函数很多,甚至可以自己写一个周期函数来达到自己满意的效果。这里使用的是经典的三角函数作为周期函数。

offr1即表示振荡值,它的值为三角函数,将其加到标准半径之上,即可实现距离圆点距离在标准值附近周期伸缩。

  //将上一个循环更改为该循环:
  for (int i=0; i<dotnum; i++) {
    offr1=80*sin(0.05*millis()*dutoPI*3);	//利用了秒数作为角度值,必须转弧度
    										//系数0.05和3则控制伸缩快慢
    										//系数80控制振荡的幅度
    										//也可以另外设置变量angle,使其递增
    fill(255, 255, 255);
    x1=(offr1+r1)*cos(theta);
    y1=(offr1+r1)*sin(theta);
    theta+=deltatheta;
    ellipse(x1, y1, 8, 8);
    if (i==dotnum-1) {
      theta=0;
    }
  }

这样即可得到径向伸缩的圆:
径向伸缩圆
此时,小圆点实现了做周期相同的直线运动。若想曲线运动,则修改方向角,使其缓慢变化即可。可以试试将校正方向角的那个判断语句注释掉,看看有什么变化——伸缩同时将缓慢转动,达到一定临界值后开始快速逆转。

接下来距离成功仅一步之遥:修改相位。
相位其实我也说不清楚,只知道初相位类似于起点,影响的是这个点到达某一固定位置(比如最大值)的时间点。要修改每一个点的相位,使其相邻两点的相位差值成相等。把每个点和其相位关联起来的最简单的方法就是,相位中乘以循环变量i。

另外,关于“叶子”,它其实就是在这一圈中出现了几次周期

具体的每个i对应的点的初相位计算值为:(2 × PI × 叶子数/点数目) × i
可以画函数周期图来帮助理解。
在这里插入图片描述

  for (int i=0; i<dotnum; i++) {
  	//初相位加在sin内部
    offr1=80*sin(0.05*millis()*dutoPI*3+(i*2*PI*ye2)/dotnum);	
    fill(255, 255, 255);
    x1=(offr1+r1)*cos(theta);
    y1=(offr1+r1)*sin(theta);
    theta+=deltatheta;
    ellipse(x1, y1, 8, 8);
    if (i==dotnum-1) {
      theta=0;
    }
  }

可得:在这里插入图片描述
以此类推,画出第二个逆转的、更大的外圈,并将同方向的两点连接,即可得到临摹出来的图形。最终版for循环如下:

  for(int i=0;i<dotnum;i++){
      offr1=80*sin(0.05*millis()*dutoPI*3+(i*2*PI*ye1)/dotnum);
      offr2=150*sin(-0.05*millis()*dutoPI*3+(i*2*PI*ye2)/dotnum);
      fill(255,255,255);
      x1=(offr1+r1)*cos(theta);
      y1=(offr1+r1)*sin(theta);
      x2=(offr2+r2)*cos(theta);
      y2=(offr2+r2)*sin(theta);
      theta+=deltatheta;
      ellipse(x1,y1,8,8);
      ellipse(x2,y2,8,8);
      line(x1,y1,x2,y2);
      if(i==dotnum-1){
        theta=0;
      }
  }

图二:四维立方

思路分析

完全没有思路。

但是参考了b站的教程,获取了其思路:利用投影矩阵,使四维点映射到三维画布上。

整体思路为:
1、设置四维立方体的16个顶点,注意是四维数,需要另写类。
2、对这16个顶点进行四维旋转,四维旋转将涉及两个轴(或者说,一个面。二位旋转绕的是一维的点,三维旋转绕的是二维的轴,那么有理由可以猜想,四维旋转将绕的是三维的面),动用到第四维的轴时将出现内外翻转效果。旋转结果仍然是四维数点。
3、旋转完毕后,对这16个四维点进行投影,利用投影矩阵变换,得到可以显示的三维点。
4、顺次连接这些三维点,加粗这些三维点即可。

一点疑惑

虽然实现了出来,但我到现在仍然没能理清其第四维度到底影响了什么,且代码中有我自己也看不懂它在干嘛的成分。该部分我会用注释标出,为照抄的b站视频代码。

代码实现
前期准备

需要:新写一个类,四维点类。结构很简单,只需要一个构造方法和表示四个维度的成员变量。

//my class for a 4D point
class Pvector4d{
  float x,y,z,w;
  
  Pvector4d(){
    this.x=0;
    this.y=0;
    this.z=0;
    this.w=0;
  }
  Pvector4d(float x, float y, float z, float w){
    this.x=x;
    this.y=y;
    this.z=z;
    this.w=w;
  }
}

矩阵方法集:该集合主攻矩阵运算,包括将四维点转为矩阵用与运算、将运算结果转为四维点用于输出等方法。投影也在这里写了。没有注释应该也能看得懂。

//some functions of matrix rigging
float[][] pv4dToMatrix(Pvector4d point){
  float[][] mat=new float[4][1];
  mat[0][0]=point.x;
  mat[1][0]=point.y;
  mat[2][0]=point.z;
  mat[3][0]=point.w;
  return mat;
}

Pvector4d matToPv4d(float[][] mat){
  Pvector4d point=new Pvector4d();
  if(mat.length!=4||mat[0].length!=1){
    println("This mat cannot be tranlated to a 4dPoint.");
    return point;
  }
  point.x=mat[0][0];
  point.y=mat[1][0];
  point.z=mat[2][0];
  point.w=mat[3][0];
  return point;
}

Pvector4d matMultPoint(float[][] mat, Pvector4d point){	//矩阵乘四维点
  float[][] pointMat;
  pointMat=pv4dToMatrix(point);
  if(mat[0].length!=pointMat.length){
    println("The mat's cols must equals to the point's rows");
    return point;
  }
  float[][] result=new float[4][1];
  for(int i=0;i<mat.length;i++){
    for(int j=0;j<pointMat[0].length;j++){
      float sum=0;
      for(int k=0;k<pointMat.length;k++){
        sum+=mat[i][k]*pointMat[k][j];
      }
      result[i][j]=sum;
    }
  }
  Pvector4d ret=matToPv4d(result);
  return ret;
}


//touying
PVector touying(Pvector4d p4d, float w){	//四维点投影到三维点
  float[][] tyMat=new float[][]{		//w参数有点特殊,为照抄
    {w,0,0,0},
    {0,w,0,0},
    {0,0,w,0}
  };
  float[][] p4dPoint=pv4dToMatrix(p4d);
  float[][] result=new float[3][1];
  for(int i=0;i<3;i++){
    for(int j=0;j<1;j++){
      float sum=0;
      for(int k=0;k<4;k++){
        sum+=tyMat[i][k]*p4dPoint[k][j];
      }
      result[i][j]=sum;
    }
  }
  
  PVector typ=new PVector();
  typ.x=result[0][0];
  typ.y=result[1][0];
  typ.z=result[2][0];
  return typ;
}

主函数中的变量准备,准备好存储顶点位置的四维点数组,并赋值:

Pvector4d[] v=new Pvector4d[16];
float angle=0;
void setup() {
  size(800, 800, P3D);
  v[0]=new Pvector4d(-1, 1, 1, 1);			//第四维数为照抄。
  v[1]=new Pvector4d(-1, -1, 1, 1);			//可以断定,第一方块共有相同的第四维
  v[2]=new Pvector4d(1, -1, 1, 1);			//第二方块为相反的第四维
  v[3]=new Pvector4d(1, 1, 1, 1);			//修改此第四维值会产生变形
  v[4]=new Pvector4d(-1, 1, -1, 1);
  v[5]=new Pvector4d(-1, -1, -1, 1);
  v[6]=new Pvector4d(1, -1, -1, 1);
  v[7]=new Pvector4d(1, 1, -1, 1);

  v[8]=new Pvector4d(-1, 1, 1, -1);
  v[9]=new Pvector4d(-1, -1, 1, -1);
  v[10]=new Pvector4d(1, -1, 1, -1);
  v[11]=new Pvector4d(1, 1, 1, -1);
  v[12]=new Pvector4d(-1, 1, -1, -1);
  v[13]=new Pvector4d(-1, -1, -1, -1);
  v[14]=new Pvector4d(1, -1, -1, -1);
  v[15]=new Pvector4d(1, 1, -1, -1);
}
主体代码

结合注释看吧。

void draw() {
  background(255);
  translate(width/2, height/2);			//平移到中心
  rotateY(-PI/6);			//为了更好的视觉效果,旋转
  rotateX(-PI/12);			//同上
  float distance=2;			//不理解处。照抄
  float w=0;				//不理解处。照抄
  //rotate
  Pvector4d[] rotated=new Pvector4d[16];	//用于存放旋转过的四维点
  float[][] rotateZW=new float[][]{		//四维旋转矩阵,动用到两个轴
    {1, 0, 0, 0}, 						//类似三位旋转矩阵,其余轴为1
    {0, 1, 0, 0}, 
    {0, 0, cos(angle), -sin(angle)}, 	//此矩阵为ZW轴旋转,故旋转主体
    {0, 0, sin(angle), cos(angle)}		//放在Z、W轴上
  };
  float[][] rotateYW=new float[][]{		//类似于ZW旋转阵,这里是YW阵
    {1, 0, 0, 0}, 
    {0,cos(angle),0,-sin(angle)}, 
    {0, 0, 1, 0}, 
    {0,sin(angle),0,cos(angle)}
  };
  float[][] rotateXW=new float[][]{		//XW阵
    {cos(angle), 0, 0, -sin(angle)}, 
    {0, 1, 0, 0}, 
    {0, 0, 1, 0}, 
    {sin(angle), 0, 0, cos(angle)}
  };
  //for(int i=0;i<16;i++){
  //  rotated[i]=matMultPoint(rotateXY,v[i]);
  //}
  for (int i=0; i<16; i++) {			//这里采取了ZW轴旋转
    rotated[i]=matMultPoint(rotateZW, v[i]);
  }
  //for(int i=0;i<16;i++){
  //  rotated[i]=matMultPoint(rotateXW,rotated[i]);
  //}
  PVector[] toDrawPoints=new PVector[16];		//用于存放投影后的点
  for (int i=0; i<16; i++) {
    w=1/(distance-rotated[i].w);			//不理解处。照抄
    toDrawPoints[i]=touying(rotated[i], w);	//投影矩阵中的w也为不理解处
    toDrawPoints[i].mult(150);				//数乘。放大向量
  }

  //connect points
  connect(toDrawPoints);				//将点连线的、自己写的函数

  for (int i=0; i<16; i++) {			//在对应位置画球
    stroke(0);
    strokeWeight(20);
    //noFill();
    sphere3d(toDrawPoints[i]);			//画球函数,自己写的
    //point(toDrawPoints[i].x,toDrawPoints[i].y,toDrawPoints[i].z);
    //也可以用上面这句画点的自带的函数。但球表现效果好一点
  }
  angle+=2*PI*(sin(8*frameCount*PI/360)+1)/360;	//角度递增
  //增量为慢慢调试出来的结果
}

void connect(PVector[] p) {		//连接函数
  //first cube
  line3d(p[0], p[1]);			//自己写的函数,在两点间画线
  line3d(p[1], p[2]);
  line3d(p[2], p[3]);
  line3d(p[3], p[0]);

  line3d(p[4], p[5]);
  line3d(p[5], p[6]);
  line3d(p[6], p[7]);
  line3d(p[7], p[4]);

  line3d(p[0], p[4]);
  line3d(p[1], p[5]);
  line3d(p[2], p[6]);
  line3d(p[3], p[7]);

  //line3d(p[0],p[5]);	//用于产生花里胡哨的图片,下面的注释部分相同
  //line3d(p[1],p[6]);
  //line3d(p[2],p[7]);
  //line3d(p[3],p[4]);

  //second cube
  line3d(p[8], p[9]);
  line3d(p[9], p[10]);
  line3d(p[10], p[11]);
  line3d(p[11], p[8]);

  line3d(p[12], p[13]);
  line3d(p[13], p[14]);
  line3d(p[14], p[15]);
  line3d(p[15], p[12]);

  line3d(p[8], p[12]);
  line3d(p[9], p[13]);
  line3d(p[10], p[14]);
  line3d(p[11], p[15]);

  //line3d(p[8],p[13]);
  //line3d(p[9],p[14]);
  //line3d(p[10],p[15]);
  //line3d(p[11],p[12]);
  
  //connect two cubes
  line3d(p[0], p[8]);
  line3d(p[1], p[9]);
  line3d(p[2], p[10]);
  line3d(p[3], p[11]);
  line3d(p[4], p[12]);
  line3d(p[5], p[13]);
  line3d(p[6], p[14]);
  line3d(p[7], p[15]);

  //line3d(p[0],p[9]);
  //line3d(p[1],p[10]);
  //line3d(p[2],p[11]);
  //line3d(p[3],p[8]);
  //line3d(p[4],p[13]);
  //line3d(p[5],p[14]);
  //line3d(p[6],p[15]);
  //line3d(p[7],p[12]);
}

void line3d(PVector p1, PVector p2) {		//连接两点的函数
  stroke(13, 233, 40, p1.z+200);		//调整透明度,增强空间感
  strokeWeight(10);					//粗细
  line(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z);
}

void sphere3d(PVector p) {			//在指定位置画球的函数
  pushMatrix();				//将变换临时压栈
  translate(p.x, p.y, p.z);	//平移到对应位置
  sphere(8);				//画半径(也可能是直径)8的球
  popMatrix();				//将变换出栈
}

这里我写了三个四维旋转矩阵。之前说过,四维旋转涉及两个轴,其旋转矩阵也是将三维旋转矩阵向更高维度推论过来的。

其实代码中矩阵命名有点小瑕疵,ZW阵其实应该叫做XY阵,因为X、Y轴对应的矩阵值为1,即不变。即绕X、Y轴旋转。这样可以与三维旋转矩阵的解释统一起来。

但这里就按照代码中的命名来了。ZW阵最终的结果如临摹图所示。经过最开始的两个调整视角的旋转之后可能看不出来,但其实方块发生了内外翻转,且翻转方向为向Z轴方向推进

类似的,YW阵也内外翻转,方向为向Y轴推进;XW阵则是向X轴方向推进。而不涉及W的旋转矩阵将不会发生内外翻转

顺次进行两个涉及W阵的旋转矩阵变换,其结果将会导致有一些点的位置发生突变,如从屏幕右上角跳转到屏幕左下。而三个涉W阵全都进行旋转的话,突变现象会消失,但翻转方向变化较复杂,观赏性不高。

额外拓展

对于第一幅图,我做的拓展如下:
在这里插入图片描述
修改了线条连接方式和内外圈的叶子数,从而达到了图中的“连续连线”效果。

float x0,y0;
void setup(){
	//...
  x0=0;
  y0=0;
}
void draw(){
  translate(width/2,height/2);
  background(0);
  for(int i=0;i<dotnum;i++)
		//...
      line(x1,y1,x2,y2);
      line(x0,y0,x2,y2);
      line(x0,y0,x1,y1);
      x0=x2;
      y0=y1;
      if(i==dotnum-1){
        theta=0;
      }
  }
}

对于第二幅图,我不想破坏四维体特有的内外翻转效果,所以就只简单地改了下连接顺序。修改代码即主要代码中“连接”函数被注掉的部分,将之替换上方的等量代码即可。效果如图:
在这里插入图片描述
以上。

  • 26
    点赞
  • 82
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值