白问

  • 首页

  • 归档

【第六期(中)】太长不看版

发表于 2019-05-22 | 更新于 2019-05-23

为什么要重复造轮子
直接用库才对嘛

  • 下载Toxi库
  • 复制以下代码
  • 完成
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    import toxi.geom.*;
    import toxi.physics2d.*;
    import toxi.physics2d.behaviors.*;
    VerletPhysics2D physics;
    AttractionBehavior2D mouseAttractor;
    Vec2D mouse;

    void setup() {
    size(500, 500, FX2D);
    noFill();
    physics = new VerletPhysics2D();
    physics.setDrag(0.30f);
    addParticle();
    mouse = new Vec2D(mouseX, mouseY);
    mouseAttractor = new AttractionBehavior2D(mouse, 500, 0.2);
    physics.addBehavior(mouseAttractor);
    }
    void draw() {
    background(255);
    mouse.set(mouseX, mouseY);
    physics.particles.get(0).x = mouseX;
    physics.particles.get(0).y = mouseY;
    if (mousePressed) {
    addParticle();
    }
    physics.update();
    for (VerletParticle2D p : physics.particles) {
    ellipse(p.x, p.y, 20, 20);
    }
    }
    void addParticle() {
    VerletParticle2D p = new VerletParticle2D(Vec2D.randomVector().scale(5).addSelf(mouseX, mouseY));
    physics.addParticle(p);
    physics.addBehavior(new AttractionBehavior2D(p, 20, -1.2f, 0.01f));
    }

【第六期(中)】吸引与排斥

发表于 2019-04-21 | 更新于 2019-05-23

本期知识点密度:★★★✩✩

本期要做的事:
做出吸引和排斥共存的效果

如果觉得太长太麻烦
可以直接移步【太长不看版】

本期主要内容:

  • 一:Processing中的向量(PVector)
  • 二:求解物体之间的排斥
  • 三:同时施加吸引与排斥

Processing中的向量(PVector)

向量(Vector)是制作动态图形必不可少的工具之一
它可以方便的表示物体的位置、速度、加速度等状态,也可以求解物体之间的空间关系;
Processing为我们提供了原生的向量数据类型:PVector
这样的话,我们就可以使用PVector内置的函数来方便的实现物体间力的施加。
下面是一个简单的使用向量的例子:

1
2
3
4
5
6
7
8
9
10
11
PVector v1, v2;

void setup() {
v1 = new PVector(40, 20);
v2 = new PVector(25, 50);

line(0, 0, v1.x, v1.y);
line(0, 0, v2.x, v2.y);
v2.add(v1);
line(0, 0, v2.x, v2.y);
}

这段代码定义了两个不同的向量,并分别将他们绘制;
之后再将两个向量相加,绘制出相加后的向量;
向量的更多具体用法,大家可以查看File——Examples——Topics——Vectors里的几个例子:

求解物体之间的排斥

对于求解排斥的问题,最容易产生关联的的就是求解“流体”之间的作用力,在计算机图形学界,求解流体有两大方向,分为欧式方法(Eulerial Methods)和拉式方法(Lagrangian Methods),在拉式方法中,流体是由粒子的集合所刻画,其中最为常用的是 SPH(Smoothed Particle Hydrodynamics) 平滑粒子动力学 和它的一些改进算法这个方法的确可以很好的求解由粒子刻画的的无边界流体的运动状态;
然而
我们是一个新手向的入门级教程
教SPH太反人类了

所以我们接下来会用最简单的向量操作
借用拉格朗日方法的思想
来初步模拟粒子排斥的问题:

假设有两个粒子:A、B
他们的位置为 \(\vec A\)、\(\vec B\),半径分别为 \(\vec RA\)、\(\vec RB\)
那么,两个粒子的间距用向量表示为 \(\vec B- \vec A\),两个粒子的半径之和为 \(\vec RA + \vec RB\)
根据示意图,两个半径相加相当于把 重叠部分计算了两次,所以可以用半径之和减去间距,计算出重叠部分的向量。
也就是:\((\vec B - \vec A) - (\vec RA + \vec RB)\)
求出了粒子碰撞后重叠部分的距离之后
接下来需要根据重叠的程度来施加排斥
因为两个粒子只需要移动重叠部分一半的距离即可分离开
那么实际(给A粒子)施加的排斥向量是:\(\frac{(\vec B - \vec A) - (\vec RA + \vec RB)}{2}\)
同理,给B粒子施加相反的向量即可:\(-\frac{(\vec B - \vec A) - (\vec RA + \vec RB)}{2}\)
用代码表示为:

1
2
3
4
5
6
7
8
9
10
void repel(PVector A, PVector B) {
PVector BsubA = PVector.sub(B, A); //向量B-A
PVector RAaddRB = BsubA.copy(); //由于粒子每次碰撞的方向不同,所以半径向量需要根据碰撞方向来确定,故复制了向量B-A
RAaddRB.normalize(); //归一化RA+RB向量
RAaddRB.mult(Radians*2); //将RA+RB扩大到两个半径之和
PVector RepelA = PVector.sub(BsubA, RAaddRB);//计算重叠部分的向量
RepelA.mult(0.5); //重叠部分减半
A.add(RepelA); //对A粒子施加排斥
B.add(RepelA.mult(-1)); //对B粒子施加排斥
}

完整代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
float Radians = 10;
ArrayList<PVector> points = new ArrayList<PVector>();
PVector mouse;

void setup() {
size(500, 500, FX2D);
noFill();
points.add(PVector.random2D());
}
void draw() {
background(255);
mouse = new PVector(mouseX, mouseY);
points.get(0).x = mouseX;
points.get(0).y = mouseY;
if (mousePressed) {
mouse.add(PVector.random2D());
points.add(mouse);
}
for (int i=0; i<points.size(); i++) {
for (int j=0; j<points.size(); j++) {
if (i!=j && PVector.dist(points.get(i), points.get(j)) <= Radians*2) {
repel(points.get(i), points.get(j));
}
}
}
for (PVector p : points) {
PVector relative = PVector.sub(mouse, p);
relative.normalize();
p.add(relative);
ellipse(p.x, p.y, Radians*2, Radians*2);
}
}
void repel(PVector A, PVector B) {
PVector BsubA = PVector.sub(B, A);
PVector RAaddRB = BsubA.copy();
RAaddRB.normalize();
RAaddRB.mult(Radians*2);
PVector RepelA = PVector.sub(BsubA, RAaddRB);
RepelA.mult(0.5);
A.add(RepelA);
B.add(RepelA.mult(-1));
}

【第六期(上)】窗口多开

发表于 2019-04-17 | 更新于 2019-04-21

本期知识点密度:★★★✩✩

本期要做的事:
用一个sketch生成 好多 好多 好多 好多 好多 个窗口

本期主要内容:

  • 一:玩转Processing窗口属性
  • 二:Processing的本质
  • 三:如何生成好多窗口

太长不看版:

1
2
3
4
5
● 一个普通的类不能生成窗口
● 一个类继承了PApplet,它就可以生成一个窗口
● 想要好多窗口,就写好多继承PApplet的类
● 通过surface.setLocation();改变窗口位置即可
● 前往文章末尾拷贝代码

下面开始逐个讲解:

玩转Processing窗口属性

在 第一期 中,我们讲过7种绘制模式,本期需要用的绘制模式,是JAVA2D;
在这种绘制模式中,窗口载体使用的是 java.awt.Frame(在2.x中可以直接访问该frame对象),也就意味着,我们可以根据Frame的特性来自定义窗口的各项参数。

在Processing 3.x中,官方为了跨平台兼容,将frame进一步封装为一个PSurface对象:surface.
所以我们需要调用surface,对Processing的窗口,进行魔改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PImage icon;

void setup() {
icon = loadImage("bai-32.png");//载入图标文件
size(300, 200);
background(0);

surface.setSize(500, 500); //重设窗口大小
surface.setLocation(0, 0); //设置窗口位置
surface.setTitle("Ohhhh!!!!");//设置窗口标题
surface.setIcon(icon); //设置窗口图标
surface.setAlwaysOnTop(true); //设置是否窗口保持置顶
surface.setResizable(true); //设置窗口可变
surface.setCursor(HAND); //设置鼠标样式 ARROW, CROSS, HAND, MOVE, TEXT, WAIT
}

留意窗口的变化:

  • 窗口并非是size中定义的300×200,而是被重设为了500×500
  • 窗口自身的位置被调整到左上角(0, 0)点
  • 窗口的标题变成了“Ohhhh!!!!”
  • 窗口图标变成了我们自定义的图标
  • 窗口始终置顶
  • 窗口大小可随意拖拽
  • 鼠标样式变成了小手 这个例子中,我们只选择了一些常用的函数进行演示,更多相关功能请参考 Processing源码

Processing的本质

————以下是硬核技术时间————
在Processing中,我们会经常使用到class,也就是“类”
但Processing的“类”与其他编程语言中常见的“类”并不相同
它们的本质是Java的 内部类
这个特性也造就了Processing的设计理念:Wrapper Facade
意味着用户无需考虑复杂的内部实现原理,直接通过清晰简单的接口来实现功能。(这种设计哲学也是我喜欢Processing的原因之一)

再聊聊Processing的代码封装
用户在PDE中写的所有代码,都会被二次加工成Java代码,再丢进Java虚拟机中运行。
比如用户只写了一段简单的代码:但最终运行的代码被加工成了这样:
可以发现:

  • 最前面import了很多东西
  • 用户写的所有的代码被放在了一个更大的class里面,class名与PDE工程名相同,并继承了PApplet
  • size()函数被移动到了一个新的函数void settings()里面
  • void setup()中的内容除了size之外直接照搬
  • void draw()里的内容直接照搬
  • 后面加上了主函数,也就是程序入口

——————硬核时间结束——————
这些都是Processing为了降低编程复杂度而做的功课,目的就是为了把初学者和繁琐的步骤隔离开,把写代码变成一种乐趣。
但是现在,我们要离开这个精致的温室,去探究它的本源了


生成好多窗口

我们在上面提到过,加工后的代码被放进了一个大类里面,而且继承了PApplet
原因是每一个继承了PApplet的类都可以成为一个完整的Processing程序
它有自己的窗口,有自己的void setup()和void draw()…

如果我们想要生成新的窗口,就需要再写一个继承PApplet的类,就像这样:

1
2
3
4
5
6
7
8
9
10
11
void setup() {
Inner in1 = new Inner();
Inner in2 = new Inner();
Inner in3 = new Inner();
}
class Inner extends PApplet { //继承了PApplet
Inner() { //构造函数
super(); //先执行父类的构造函数
PApplet.runSketch(new String[]{"Inner"}, this);//然后运行自己
}
}

这段代码可以直接生成四个窗口(一个原窗口+三个新窗口)
又由于我们离开了精致的温室,不能再在void setup()里写size了,所以如果要设置窗体大小,void settings()函数也需要老老实实加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Inner[] inners = new Inner[50]; //创建一个数组存储这些窗口

void settings() {
size(300, 200);
}
void setup() {
background(255);
for (int i=0; i<inners.length; i++) {
inners[i] = new Inner(); //初始化每个窗口
}
}
class Inner extends PApplet {
Inner() {
super();
PApplet.runSketch(new String[]{"Inner"}, this);
}
void settings() {
size(200, 200);
}
void setup() {
surface.setLocation((int)random(1920), (int)random(1080));//随机设置每个窗口的位置,并设置背景色
background(0);
}
}

  • 【第六期(上)】,已经讲解完毕,我们已经实现了创建多个窗口,并且知道了如何改变每个窗口的位置。
  • 【第六期(中)】,我们会暂时搁置本期内容,用最基础的(画圆圈的)方式,讲一讲吸引和排斥的实现原理
  • 【第六期(下)】,我们会将前两期的内容结合起来,实现窗口之间互相吸引与排斥的效果

【第五期】制作PDE启动画面

发表于 2019-02-24 | 更新于 2019-04-21

本期知识点密度:★★★★✩

上一期留下的问题是:
一:如何实现Processing启动界面的渐变线条
二:如何复刻完整的启动界面

仔细分辨一下哪个是原版

渐变线条

首先说说如何绘制渐变色的线条
如果去查Processing的官网,官网会给出这样的建议:https://processing.org/examples/lineargradient.html
这种方法从本质上阐述了渐变是如何实现的(把所有的颜色一点一点画出来)
但是,这种方法的计算效率很低
而且,用起来很麻烦
你可能会想:诺大一个Processing,居然连渐变函数都没有内置吗?!
答案是:确实没有
但是,我们可以用一种巧妙的方式
借助内置功能实现渐变绘制

即:使用OpenGL特性

在原生OpenGL中,渐变线条可以这样绘制:

1
2
3
4
5
6
glBegin( GL_LINE_STRIP );
glColor3f( 设置为起点颜色 );
glVertex3f( 起点 );
glColor3f( 设置为终点颜色 );
glVertex3f( 终点 );
glEnd();

Processing虽然没有直接公开OpenGL的函数
但是也做了相应的封装
写法类似上文:

1
2
3
4
5
6
beginShape(LINES);   //括号里声明的是我们需要绘制的元素类型,这里写LINES即可,除此之外还有 POINTS TRIANGLES TRIANGLE_STRIP TRIANGLE_FAN QUADS QUAD_STRIP 六种绘制方法
stroke( 设置为起点颜色 );
vertex( 起点 );
stroke( 设置为终点颜色 );
vertex( 终点 );
endShape();

如果按照这个写法把代码写出来,就会出现这样的情况:

1
2
3
4
5
6
7
8
9
10
11
12
void setup() {
size(400, 400);
}
void draw() {
background(0);
beginShape(LINES);
stroke( 255, 0, 0 );
vertex( 100, 100 );
stroke( 0, 255, 0 );
vertex( 300, 300 );
endShape();
}

线条并没有渐变
原因是绘制模式是Java2D,而非OpenGL
这一点在 【白问】第一期 中有提及
只有当初始化的时候启用OpenGL相关模式
才能使用OpenGL的相关的特性
稍加修改:

1
size(400, 400, P2D);

即可生效:


Plexus效果

实现了线条渐变
再来谈谈如何实现网状结构
更确切一点应该叫做 Plexus 效果
熟悉AE的朋友肯定会想起著名的 Plexus插件
其实Plexus的英文释义就是: n. (血管、淋巴管、神经等的)[解剖] 丛
是不是非常形象~

Plexus 的实现原理也非常简单
1:循环遍历所有点,计算每两个点间的距离
2:如果两点间距小于一定程度
3:就在两点间画一条线

用程序表达就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ArrayList<PVector> points = new ArrayList<PVector>();

void setup() {
size(400, 400, P2D);
for (int i=0; i<100; i++) {
points.add(new PVector(random(width), random(height)));
} //随机生成100个点
}
void draw() {
background(255);
for (int i=0; i<points.size(); i++) {
for (int j=0; j<points.size(); j++) {
float dist = PVector.dist(points.get(i), points.get(j)); //计算两点间的距离
if (i!=j && dist < 60) { //如果距离小于60
line(points.get(i).x, points.get(i).y, points.get(j).x, points.get(j).y);
} //在两点之间画线
}
}
}

稍做美化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ArrayList<PVector> points = new ArrayList<PVector>();

void setup() {
size(400, 400, P2D);
for (int i=0; i<100; i++) {
points.add(new PVector(random(width), random(height)));
} //随机生成100个点
}
void draw() {
background(0);
for (int i=0; i<points.size(); i++) {
for (int j=0; j<points.size(); j++) {
float dist = PVector.dist(points.get(i), points.get(j)); //计算两点间的距离
if (i!=j && dist < 60) { //如果距离小于60
stroke(255, map(dist, 0, 60, 60, 0)); //将距离映射为线条透明度
line(points.get(i).x, points.get(i).y, points.get(j).x, points.get(j).y);
} //在两点之间画线
}
}
}

用上面提到的渐变色绘制方法替代line函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ArrayList<PVector> points = new ArrayList<PVector>();

void setup() {
size(400, 400, P2D);
colorMode(HSB); //颜色模式调整为HSB模式
strokeWeight(2); //线条粗细调整为2
for (int i=0; i<100; i++) {
points.add(new PVector(random(width), random(height)));
}
}
void draw() {
background(0);
for (int i=0; i<points.size(); i++) {
for (int j=0; j<points.size(); j++) {
float dist = PVector.dist(points.get(i), points.get(j));
if (i!=j && dist < 80) {
beginShape(LINES);
stroke(random(255), random(255), random(255), map(dist, 0, 80, 100, 0)); //随机生成颜色
vertex(points.get(i).x, points.get(i).y);
stroke(random(255), random(255), random(255), map(dist, 0, 80, 100, 0)); //随机生成颜色
vertex(points.get(j).x, points.get(j).y);
endShape();
} //用渐变色绘制方法替代line函数
}
}
noLoop(); //暂停画面,避免每帧刷新随机颜色
}


Json数据读写

由于粒子的位置是完全随机的
所以并不是每次随机的结果都会很好看
有时候会分布的比较均匀也有时候会聚在一坨
所以为了和原图保持一致
就需要精确记录每个点的位置

首先在原图的上标记每个点的位置
然后使用Json文件来存储这些位置

标记工具代码如下:

相关下载(右键 - 另存为)
工具代码
PDE原图
前景粒子Json文件
背景粒子Json文件

将所有点绘制完毕后,按任意键即另存为json文件
(由于工具涉及篇幅过长,暂时不展开讲解,如需单独讲解可以联系我)

此时,只需把 points 从随机修改为读Json文件数据即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ArrayList<PVector> points = new ArrayList<PVector>();

void setup() {
size(375, 450, P2D); //画面分辨率和原图保持一致
colorMode(HSB);
strokeWeight(1.5);
JSONArray poses = loadJSONArray("poses.json");

for (int i=0; i<poses.size(); i++) {
points.add(new PVector(poses.getJSONArray(i).getFloat(0), poses.getJSONArray(i).getFloat(1)));
} //把随机生成位置 修改为从文件中获取位置
}
void draw() {
background(0);
for (int i=0; i<points.size(); i++) {
for (int j=0; j<points.size(); j++) {
float dist = PVector.dist(points.get(i), points.get(j));
if (i!=j && dist < 100) { //最大距离调整为100
beginShape(LINES);
stroke(random(255), 255, 255, map(dist, 0, 100, 200, 20));
vertex(points.get(i).x, points.get(i).y);
stroke(random(255), 255, 255, map(dist, 0, 100, 200, 20));
vertex(points.get(j).x, points.get(j).y);
endShape();
}
}
}
noLoop();
}

美化与文字渲染

调整背景色,调整粒子颜色,添加背景粒子,添加半透明渐变图层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
ArrayList<PVector> points = new ArrayList<PVector>();
ArrayList<PVector> bgpoints = new ArrayList<PVector>();

void setup() {
size(375, 450, P2D);
smooth(10);
colorMode(HSB);

JSONArray poses = loadJSONArray("poses.json");
JSONArray bgposes = loadJSONArray("bgposes.json");

for (int i=0; i<poses.size(); i++) {
points.add(new PVector(poses.getJSONArray(i).getFloat(0), poses.getJSONArray(i).getFloat(1)));
}
for (int i=0; i<bgposes.size(); i++) {
bgpoints.add(new PVector(bgposes.getJSONArray(i).getFloat(0), bgposes.getJSONArray(i).getFloat(1)));
}
}
void draw() {
background(#101F2D); // 调整背景色

// 添加背景粒子
for (int i=0; i<bgpoints.size(); i++) {
for (int j=0; j<bgpoints.size(); j++) {
float dist = PVector.dist(bgpoints.get(i), bgpoints.get(j));
if (i!=j && dist < 100) {
beginShape(LINES);
stroke(random(145, 150), random(170, 190), random(140, 170), random(20, 130)); //调整粒子颜色
strokeWeight(0.2);
vertex(bgpoints.get(i).x, bgpoints.get(i).y);
stroke(random(145, 150), random(170, 190), random(140, 170), random(20, 130)); //调整粒子颜色
strokeWeight(1);
vertex(bgpoints.get(j).x, bgpoints.get(j).y);
endShape();
}
}
}

for (int i=0; i<points.size(); i++) {
for (int j=0; j<points.size(); j++) {
float dist = PVector.dist(points.get(i), points.get(j));
if (i!=j && dist < 100) {
beginShape(LINES);
stroke(random(40, 160), random(20, 110), random(180, 240), random(40, 150));
strokeWeight(0.5);
vertex(points.get(i).x, points.get(i).y);
stroke(random(40, 160), random(20, 110), random(180, 240), random(40, 150));
strokeWeight(1.5);
vertex(points.get(j).x, points.get(j).y);
endShape();
}
}
}

// 添加半透明渐变图层
beginShape();
noStroke();
fill(0, 0);
vertex(0, 0);
vertex(375, 0);
fill(140, 150, 5, 210);
vertex(375, 450);
vertex(0, 450);
endShape(CLOSE);
}

最后,创建字体,绘制文字
Processing使用的封面字体名为:TheSerif HP5 Plain
字体文件可由此 下载
下载后安装到系统字库中即可调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PFont font;
font = createFont("TheSerif HP5 Plain", 50);

fill(255);
textFont(font, 60);
text("Processing 3", 25, 216);
fill(#B0C4D0, 180);
textFont(font, 13);
text("An Open project initiated by Ben Fry and Casey Reas.", 26, 260);
text("Supported by programmers like you and the nonprofit", 26, 278);
text("Processing Foundation, 501(c)(3).", 26, 296);
textFont(font, 10);
text("© 2012-2018 The Processing Foundation", 26, 398);
text("© 2004-2012 Ben Fry and Casey Reas", 26, 411);
text("© 2001-2004 Massachusetts Institude of Technology", 26, 424);

【第四期】矩形的旋转相切

发表于 2019-01-03 | 更新于 2019-02-24

本期知识点密度:★★✩✩✩

大家嚎!我又回来啦~
我们先解答本期的问题:如何实现精确的旋转矩形相切
就像这样:


问题剖析:

想在大方块内绘制一个稍微小一点的方块,需要知道它的旋转角度和边长

旋转角度直截了当,只需使用rotate()函数每次旋转一个固定值即可,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
void setup() {
float l=300;
size(500, 500);
rectMode(CENTER);

translate(width/2, height/2); //将画布原点移动到屏幕中央
while (l>5) //当边长大于5像素
{
rotate(15*DEG_TO_RAD); //每次旋转15度
rect(0, 0, l, l); //绘制方形
l-=5; //方形边长每次减小5像素
}
}

这段代码能很好的处理旋转和绘制,但并没有把角度和正方形边长的变化联系起来,我们接下来就要关联这两个变量。


首先,如果有内外两个相切的正方形,我们可以假设,大小正方形的边长分别为L和l,如图:
接下来,可以将图中的 α 角视为旋转角,可得到 α 角的对边长\(l×sinα\) 和邻边长\(l×cosα\)
同时,另一组边的关系也可以轻易得出:$$L=(L-a)+a$$ 其中 $$(L-a)=l×cosα$$ $$a = l×sinα$$
那么,在得知角α的大小后,我们可以列出如下等式:$$L=l×cosα+l×sinα$$ 经变换后得到:$$L=l×(sinα+cosα)$$ $$l=\frac{L}{sinα+cosα}$$

1
2
3
4
5
6
7
8
9
10
11
12
13
void setup() {
float l = 500;
float rotation = 15*DEG_TO_RAD;
size(500, 500);
rectMode(CENTER);

translate(width/2, height/2);
while (l>5) {
rect(0, 0, l, l);
rotate(rotation);
l = l / (sin(rotation)+cos(rotation));
}
}

稍加修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void setup() {
float l = 500;
float rotation = 15*DEG_TO_RAD;
int count = 0;
size(500, 500);
noStroke();
rectMode(CENTER);

translate(width/2, height/2);
while (l>5) {
if (count%2==0) fill(51);
else fill(200);
rect(0, 0, l, l);
rotate(rotation);
l = l / (sin(rotation)+cos(rotation));
count ++;
}
}

下期预告:

一:如何实现Processing启动界面的渐变线条
二:如何复刻完整的启动界面

【第三期】圆与方块的接触

发表于 2018-12-23 | 更新于 2019-05-23

本期知识点密度:★★★✩✩

点和圆的接触

初中数学曾经学过:圆的判定公式

$$x^2+y^2=r^2$$

所以想要求解一点是否在圆内,只需判断 \(x^2+y^2\) 和 \(r^2\) 的关系即可;

前者大于后者,则意味着点在圆外

前者小于后者,则意味着点在圆内

前者等于后者,则意味着点在圆上

同时为了消除圆本身的位置偏移,需要减去圆心的偏移量(a,b)

$$(x-a)^2+(y-b)^2=r^2$$

程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
float a=200, b=200, x, y;

void setup() {
size(400, 400);
}

void draw() {
background(100);
x = mouseX;
y = mouseY;

ellipse(a, b, 120, 120);

if (sq(x - a) + sq(y - b) <= sq(60))
fill(255, 0, 0);
else
fill(255);
}

圆和圆的接触

圆与圆的接触也很简单,只需要检测两个圆心点的距离是否小于半径之和即可

原理和上面的实质是相同的:即检测(A圆心——B圆心)两点间的距离

$$(Xa-Xb)^2+(Ya-Yb)^2=(Ra+Rb)^2$$

(其中 Xa Ya 代表圆A的坐标,Xb Yb 代表圆B的坐标,Ra Rb 代表两圆的半径)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PVector Ca = new PVector(100, 100);  //圆 A
PVector Cb = new PVector(200, 200); //圆 B

void setup() {
size(400, 400);
}

void draw() {
background(0);
Ca = new PVector(mouseX, mouseY);

ellipse(Ca.x, Ca.y, 120, 120);
ellipse(Cb.x, Cb.y, 80, 80);

if (sq(Ca.x - Cb.x) + sq(Ca.y - Cb.y) <= sq(60+40))
fill(255, 0, 0);
else
fill(255);
}

或者直接使用Processing计算距离的函数dist()

1
if (PVector.dist(Ca, Cb) <= 60+40)

圆和方块的接触

圆和方块的接触
只需将上面的方法稍作扩展即可

计算距离的两点,从(圆心——圆心),变成了(圆心——方块上离圆心最近的点)

这时,问题转化成了:
如何计算出方块上距圆心最近的点?


我们先规定:以方块左上角为坐标点,然后只考虑X方向,假设出不同条件:

—— 以下是只考虑 X方向 时的三种条件:——

条件一:

方块顶点在圆心点右侧:


可以看出,方块距圆心最近的X,是顶点:Rx


条件二:

方块顶点已经到了圆心另一侧,但其他部分还没有完全移动到另一侧:


可以看出,方块距圆心最近的X,是圆心:Cx


条件三:

整个方块已经完全移动到了圆心另一侧:


可以看出,方块距圆心最近的X,是方块顶点+方块宽度:Rx+Rw


这样一来,就可以写出X方向上,方块距离圆心最近距离的计算公式:

$$Px = max(Rx,min(Rx+Rw, Cx))$$

用程序表达起来也很简单:

1
2
3
4
5
6
7
8
9
if(Rx > Cx){            //条件一
Px = Rx;
}
else if(Rx+Rw > Cx){ //条件二
Px = Cx;
}
else if(Rx+Rw < Cx){ //条件三
Px = Rx+Rw;
}

同理,Y方向上一样能写出:

$$Py = max(Ry,min(Ry+Rh, Cy))$$

1
2
3
4
5
6
7
8
9
if(Ry > Cy){            //条件一
Py = Ry;
}
else if(Ry+Rh > Cy){ //条件二
Py = Cy;
}
else if(Ry+Rh < Cy){ //条件三
Py = Ry+Rh;
}

得到Px和Py后,我们就回到了最原始的方法:

$$(Cx-Px)^2+(Cy-Py)^2=Cr^2$$

为了避免if语句表达起来太累赘,可以直接使用min() max()函数来表示公式:

1
2
Px = max(Rx, min(Rx+Rw, Cx));
Py = max(Ry, min(Ry+Rh, Cy));

最终程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
float Cx, Cy;
float Px, Py;
float Rx=150, Ry=150, Rw=100, Rh=100;

void setup() {
size(400, 400);
}

void draw() {
background(0);

Cx = mouseX;
Cy = mouseY;
ellipse(Cx, Cy, 100, 100);
rect(Rx, Ry, Rw, Rh);

Px = max(Rx, min(Rx+Rw, Cx));
Py = max(Ry, min(Ry+Rh, Cy));

if (sq(Cx - Px) + sq(Cy - Py) <= sq(50))
fill(255, 0, 0);
else
fill(255);
}

本期引用:

https://yal.cc/rectangle-circle-intersection-test/

【第二期】角度与弧度的转换问题

发表于 2018-12-14 | 更新于 2019-02-24

本期知识点密度:★✩✩✩✩

角度与弧度的转换

radians() 和 degrees()

1
2
println(radians(180));  // 3.1415927
println(degrees(PI)); // 180.0

这应该是最正式的回答了

然而,作为Java的衍生产物,Processing支持非常多的Java特性;
其中,就包括我们接下来要说的:
Java的数学函数库:java.lang.Math;


Math.toRadians() 和 Math.toDegrees()

1
2
println(Math.toRadians(180));       // 3.141592653589793
println(Math.toDegrees(Math.PI)); // 180.0

无需在开头写 import,直接用即可
原生函数的特点是计算结果更精确(使用double类型的数据,支持15-16个有效数字的精确度)
相比于Processing原生的7-8个有效数字,要精确了不少;


DEG_TO_RAD 和 RAD_TO_DEG

1
2
println(180 * DEG_TO_RAD);  // 3.1415927
println(PI * RAD_TO_DEG); // 180.0

这是Processing提供的另一个快捷的方法:
内置数据中有两个变量,DEG_TO_RAD 和 RAD_TO_DEG
它们的定义方式分别是:

1
2
DEG_TO_RAD = 0.017453292F;  // PI÷180
RAD_TO_DEG = 57.295776F; // 180÷PI

也就意味着,我们可以直接在弧度数据上乘以RAD_TO_DEG,或在角度数据上乘以DEG_TO_RAD即可完成转换;


【第一期】Processing中的绘制模式

发表于 2018-12-07 | 更新于 2019-02-25

本期知识点密度:★★★✩✩

Processing 一共有7种绘制模式,9种写法

  • 置空
  • JAVA2D(等价于置空)
  • P2D (是OPENGL的子集)
  • P3D
  • OPENGL(等价于P3D)
  • FX2D
  • PDF
  • SVG
  • DXF

我们接下来详细阐述每一种绘制模式:


JAVA2D(或置空)

1
2
3
4
5
6
void setup(){
size(500, 500, JAVA2D);
}
void setup(){
size(500, 500);
}

此模式调用Java中最原始的awt模块,使用CPU渲染,没有GPU开销。
优点:支持平台最广,bug最少,渲染效果一般;
缺点:不支持3D渲染,帧率不稳定;


P2D

1
2
3
void setup(){
size(500, 500, P2D);
}

此模式调用Java OpenGL进行绘制,有GPU开销;
优点:帧率相对稳定,可自由调节抗锯齿程度;
缺点:不支持3D渲染,在某些非主流显卡上无法正常运行;


P3D(或OPENGL)

1
2
3
4
5
6
void setup(){
size(500, 500, P3D);
}
void setup(){
size(500, 500, OPENGL);
}

此模式和P2D类似,调用Java OpenGL进行绘制,有GPU开销;
优点:支持3D渲染,性能最好,可定制性最高;
缺点:在某些非主流显卡上无法正常运行;


FX2D

1
2
3
void setup(){
size(500, 500, FX2D);
}

此模式使用JavaFX实现,可一定程度上调用操作系统的DirectX和OpenGL进行绘制,有GPU开销;
优点:效果细腻,支持平台广;
缺点:无法自由调节抗锯齿,逻辑帧率稳定,但显示帧率不太稳定,有一定的bug;


PDF

1
2
3
void setup(){
size(500, 500, PDF, "rect.pdf");
}

or

1
2
3
4
5
6
7
void setup(){
size(500, 500, Java2D/P2D/P3D/FX2D);

beginRecord(PDF, "rect.pdf");
...
endRecord();
}

此模式直接将绘制结果保存为PDF文件,文件内包含矢量图元;
优点:精准,细腻,适合高分辨率打印;
缺点:保存3D场景时会有摄像机裁剪的问题;


SVG

1
2
3
void setup(){
size(500, 500, SVG, "rect.svg");
}

or

1
2
3
4
5
6
7
void setup(){
size(500, 500, Java2D/P2D/P3D/FX2D);

beginRecord(SVG, "rect.svg");
...
endRecord();
}

此模式直接将绘图结果保存为SVG矢量图文件;
优点:同上,可以直接在网页中使用,对设计师友好;
缺点:同上;


DXF

1
2
3
4
5
6
7
8
9
10
11
void setup(){
size(500, 500, DXF, "rect.dxf");
}
或
void setup(){
size(500, 500, P2D/P3D/FX2D);

beginRaw(DXF, "rect.dxf");
...
endRaw();
}

此模式可以导出DXF格式的3D场景文件,提供给后续工作流;
优点:emmmm 真的非常强;
缺点:不支持动画;

《白问》的起源

发表于 2018-12-01 | 更新于 2019-06-07

本栏目《白问》这个奇葩名字的由来,是因为我很喜欢看STN快报的一档节目,主持人名叫水蛭,故该节目叫做《蛭问》。
而我叫墨白,所以本栏目就因此得名:《白问》

墨白

墨白

9 日志
2 标签
© 2019 墨白
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Gemini v6.7.0