前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >太极与流体Taichi & PixelFlow

太极与流体Taichi & PixelFlow

作者头像
ChildhoodAndy
发布2021-07-15 09:40:09
1.6K0
发布2021-07-15 09:40:09
举报
文章被收录于专栏:小菜与老鸟小菜与老鸟

Processing 的 PixelFlow 是一个高性能的流体粒子物理仿真库,为数不多的代码可以呈现出非凡的视觉效果,在互动交互中使用比较广泛。

小菜写了一个太极与流体的效果,效果如下

本篇文章会按步骤分析视频中效果小菜是如何实现的。

太极的绘制

啥也不说,我们先建立一个空白的画布

代码语言:javascript
复制
int viewport_w = 600; // 定义窗口宽
int viewport_h = 600; // 定义窗口高

void settings() {
  size(viewport_w, viewport_h);
}

void setup() {
  
}

void draw() {
  
}

太极怎么绘制呢?

下面看下小菜的动态演示

代码语言:javascript
复制
int viewport_w = 600; // 定义窗口宽
int viewport_h = 600; // 定义窗口高

float taichi_radius = 100;

void settings() {
  size(viewport_w, viewport_h);
  
}

void setup() {
  background(200);
}

void draw() {
  float d = 2 * taichi_radius;
  
  pushMatrix();
  noStroke();
  translate(width / 2, height / 2);
  fill(0);
  arc(0, 0, d, d, PI / 2, PI * 3 / 2);
  
  fill(255);
  arc(0, 0, d, d, -PI / 2, PI / 2);
  
  fill(255);
  circle(0, d / 4, taichi_radius); 
   
  fill(0);
  circle(0, -d / 4, taichi_radius);
  
  fill(0);
  circle(0, d / 4, taichi_radius / 5);
  
  fill(255);
  circle(0, -d / 4, taichi_radius / 5);
  
  popMatrix();
}

下面我们把太极封装下,放到一个 Taichi 类里,封装的好处不言而喻,我们很容易将太极的绘制类共享,且代码进行了分离,使得代码更具可读性、维护性。

代码语言:javascript
复制
// 欢迎关注 小菜与老鸟 (公众号、视频号、Bilibili号)

int viewport_w = 600; // 定义窗口宽
int viewport_h = 600; // 定义窗口高

float taichi_radius = 100;
Taichi taichi;

void settings() {
  size(viewport_w, viewport_h);
  
}

void setup() {
  background(200);
  taichi = new Taichi(new PVector(width / 2, height / 2), 100);
}

void draw() {
  background(200);
  taichi.display();
}

class Taichi {
  PVector location;
  float radius;

  Taichi(PVector location, float radius) {
    this.location = location;
    this.radius = radius;
  }

  void display() {
    float d = 2 * taichi_radius;

    pushMatrix();
    noStroke();
    translate(location.x, location.y);  // 平移坐标系,方便使用相对位置进行绘制
    fill(0);
    arc(0, 0, d, d, PI / 2, PI * 3 / 2);

    fill(255);
    arc(0, 0, d, d, -PI / 2, PI / 2);

    fill(255);
    circle(0, d / 4, taichi_radius);

    fill(0);
    circle(0, -d / 4, taichi_radius);

    fill(0);
    circle(0, d / 4, taichi_radius / 5);

    fill(255);
    circle(0, -d / 4, taichi_radius / 5);

    popMatrix();
  }
}

下面我们要让太极图旋转起来

这里我们将太极图绘制在一个 PGraphics 图层上,然后对图层的坐标系进行旋转,就得到了上面旋转的样子。

代码语言:javascript
复制
int viewport_w = 600; // 定义窗口宽
int viewport_h = 600; // 定义窗口高

float taichi_radius = 100;
Taichi taichi;
PGraphics pg_taichi;

void settings() {
  size(viewport_w, viewport_h);
  
}

void setup() {
  background(200);
  
  pg_taichi = createGraphics(width , height);
  
  taichi = new Taichi(new PVector(width / 2, height / 2), 100, pg_taichi);
}

void draw() {
  background(200);
  
  taichi.display();
  image(pg_taichi, 0, 0);
}

// 欢迎关注 小菜与老鸟 (公众号、视频号、Bilibili号)

class Taichi {
  PVector location;
  float radius;
  PGraphics pg;
  float angle = 0;

  Taichi(PVector location, float radius, PGraphics pg) {
    this.location = location;
    this.radius = radius;
    this.pg = pg;
  }

  void display() {
    float d = 2 * radius;
    angle += 0.01;
    
    pg.beginDraw();
    pg.clear();

    pg.pushMatrix();
    pg.noStroke();
    pg.translate(location.x, location.y);  // 平移坐标系,方便使用相对位置进行绘制
    pg.rotate(angle);
    pg.fill(0);
    pg.arc(0, 0, d, d, PI / 2, PI * 3 / 2);

    pg.fill(255);
    pg.arc(0, 0, d, d, -PI / 2, PI / 2);

    pg.fill(255);
    pg.circle(0, d / 4, radius);

    pg.fill(0);
    pg.circle(0, -d / 4, radius);

    pg.fill(0);
    pg.circle(0, d / 4, radius / 5);

    pg.fill(255);
    pg.circle(0, -d / 4, radius / 5);

    pg.popMatrix();
    
    pg.endDraw();
  }
}

单个流体绕圈

太极元素绘制完毕后,我们现在开始处理流体的绕圈。先来实现单个流体绕圈效果。

1)引入流体库 PixelFlow,这里首先导入流体库,菜单栏的速写板 -> 添加库文件 -> PixelFlow,就可以导入PixelFlow库,但import的类有些多。其实我们这个例子只需要部分类,如下

代码语言:javascript
复制
import com.thomasdiewald.pixelflow.java.DwPixelFlow;
import com.thomasdiewald.pixelflow.java.fluid.DwFluid2D;

import processing.core.*;
import processing.opengl.PGraphics2D;

2)我们要将画布的渲染模式修改成 P2D,使得 Processing 可以使用 GPU 高效渲染。

代码语言:javascript
复制
void settings() {
  size(viewport_w, viewport_h, P2D);
}

3)我们声明流体对象以及流体层

代码语言:javascript
复制
DwFluid2D fluid; // 流体
PGraphics2D pg_fluid; // 流体层

4)设置流体

代码语言:javascript
复制
  // 初始化pixelflow
  DwPixelFlow context = new DwPixelFlow(this);
  context.print();
  context.printGL();

  // 流体模拟
  fluid = new DwFluid2D(context, width, height, 1);
  fluid.param.dissipation_velocity = 0.70f;
  fluid.param.dissipation_density  = 0.60f;

  fluid.addCallback_FluiData(new MyFluidData());

  // 流体层
  pg_fluid = (PGraphics2D)createGraphics(width, height, P2D);
}

5)更新绘制流体

代码语言:javascript
复制
void drawFluid() {
  fluid.update();
  fluid.renderFluidTextures(pg_fluid, 0);

  // 显示流体层
  image(pg_fluid, 0, 0);
}

6)编写 MyFluidData 类,这个类继承 FluidData, 我们需要重写它的public void update(DwFluid2D fluid) 方法。具体思路见代码注释。

代码语言:javascript
复制

public class MyFluidData implements DwFluid2D.FluidData {
  float x;  // 圆心位置x
  float y; // 圆心位置y
  float radius = 100; // 半径
  boolean isClockwise; // 是否是顺时针
  float angleSpeed = 0.04;
  float rx, ry, prx, pry; // 圆周运动,弧上的点位置,以及上一帧的点位置
  float angle = 0; // 角度
  color c; // 颜色

  MyFluidData() {
    x = width / 2;
    y = height / 2;
    radius = 130;
    isClockwise = true; // 是否是顺时针旋转
    angleSpeed = 0.04; // 角速度
    c = color(0.0, 0.0, 0.0);
    angle = PI / 2;
  }

  void my_update(DwFluid2D fluid) {
    float vscale = 14;

    float delta = random(-3, 3);
    // 极坐标下计算弧上点的位置,用一个随机量进行抖动
    rx = x + (radius + delta) * cos(angle); 
    ry = y + (radius + delta) * sin(angle);

    // 计算速度
    float vx = (rx - prx) * vscale;
    float vy = (ry - pry) * vscale;
    // 顺时针的话,需要乘以-1,因为y轴相反
    if (isClockwise) {
      vy = (ry - pry) * (-vscale);
    }

    float px = rx;
    float py = ry;
    // 顺时针的话,需要乘以-1,因为y轴相反
    if (isClockwise) {
      py = height - ry;
    }

    // 给流体上的点添加速度
    fluid.addVelocity(px, py, 16, vx, vy);
    float radius1 = 15;
    float radius2 = 8;
    // 给流体上的点添加密度,颜色为c,半径为radius1,稍微大点
    fluid.addDensity(px, py, radius1, red(c) / 255.0, green(c) / 255.0, blue(c) / 255.0, 1.0f);
    // 给流体上的点添加密度,颜色为白色,半径为radius2,稍微小点
    fluid.addDensity(px, py, radius2, 1.0f, 1.0f, 1.0f, 1.0f);
    //fluid.addTemperature(px, py, 30, 10);

    // 现在终将成为过去
    prx = rx;
    pry = ry;

    // 增加弧度角,用于下一帧计算,才能旋转
    float angleSpeed = 0.04;
    angle += angleSpeed;
  }

  @Override
    public void update(DwFluid2D fluid) {
    my_update(fluid);
  }
}

7)完善 setup 和 draw 方法

代码语言:javascript
复制
void setup() {
  background(bg_color);
  frameRate(60);

  // 设置流体参数
  setupFluid();
  // 设置太极图
  setupTaichi();
}

void draw() {
  // 流体更新
  drawFluid();
  // 太极图
  drawTaichi();
}

双个流体绕圈

刚才在MyFluidData中,我们只能处理一个流体,为了处理多个流体,我们需要抽象出一个流体的数据类MyFluidDataConfig。使用两个流体数据,对画面中的流体粒子进行处理。

代码语言:javascript
复制
public class MyFluidDataConfig {
  float x;  // 圆心位置x
  float y; // 圆心位置y
  float radius = 100; // 半径
  boolean isClockwise; // 是否是顺时针
  float angleSpeed = 0.04;
  float rx, ry, prx, pry; // 圆周运动,弧上的点位置,以及上一帧的点位置
  float angle = 0; // 角度
  color c; // 颜色
}


public class MyFluidData implements DwFluid2D.FluidData {
  MyFluidDataConfig config1;
  MyFluidDataConfig config2;

  MyFluidData() {
    float x1 = width * 0.5;
    float y1 = height * 0.5;
    
    config1 = new MyFluidDataConfig();
    config1.x = x1;
    config1.y = y1;
    config1.radius = 130;
    config1.isClockwise = true;
    config1.angleSpeed = 0.05;
    config1.c = color(0.0, 0.0, 0.0);
    config1.angle = PI / 2;

    config2 = new MyFluidDataConfig();
    config2.x = x1;
    config2.y = y1;
    config2.radius = 130;
    config2.isClockwise = true;
    config2.angleSpeed = 0.04;
    config2.c = color(0.0, 0.0, 0.0);
    config2.angle = - PI / 2;
  }
  void my_update(DwFluid2D fluid, MyFluidDataConfig config) {
    float vscale = 14;

    float delta = random(-3, 3);
    // 极坐标下计算弧上点的位置,用一个随机量进行抖动
    config.rx = config.x + (config.radius + delta) * cos(config.angle); 
    config.ry = config.y + (config.radius + delta) * sin(config.angle);

    // 计算速度
    float vx = (config.rx - config.prx) * vscale;
    float vy = (config.ry - config.pry) * vscale;
    // 顺时针的话,需要乘以-1,因为y轴相反
    if (config.isClockwise) {
      vy = (config.ry - config.pry) * (-vscale);
    }

    float px = config.rx;
    float py = config.ry;
    // 顺时针的话,需要乘以-1,因为y轴相反
    if (config.isClockwise) {
      py = height - config.ry;
    }

    // 给流体上的点添加速度
    fluid.addVelocity(px, py, 16, vx, vy);
    float radius1 = 15;
    float radius2 = 8;
    // 给流体上的点添加密度,颜色为c,半径为radius1,稍微大点
    fluid.addDensity(px, py, radius1, red(config.c) / 255.0, green(config.c) / 255.0, blue(config.c) / 255.0, 1.0f);
    // 给流体上的点添加密度,颜色为白色,半径为radius2,稍微小点
    fluid.addDensity(px, py, radius2, 1.0f, 1.0f, 1.0f, 1.0f);
    //fluid.addTemperature(px, py, 30, 10);

    // 现在终将成为过去
    config.prx = config.rx;
    config.pry = config.ry;

    // 增加弧度角,用于下一帧计算,才能旋转
    float angleSpeed = 0.04;
    config.angle += angleSpeed;
  }

  @Override
    public void update(DwFluid2D fluid) {
    my_update(fluid, config1);
    my_update(fluid, config2);
  }
}

边界-障碍物添加

我们可以给流体增加一个边界,使得流体的扩散不能流到边界之外。障碍物很容易添加。

1)声明一个障碍物层

代码语言:javascript
复制
PGraphics2D pg_obstacles; // 障碍物

2)定义障碍物层

代码语言:javascript
复制
void setupFluid() {
   ···
  // 障碍物层
  pg_obstacles = (PGraphics2D)createGraphics(viewport_w, viewport_h, P2D);
  pg_obstacles.smooth(4);
}

3)绘制障碍物

代码语言:javascript
复制
void drawFluid() {
  // 绘制障碍物
  drawObstacles();

  // 给流体增加障碍物
  fluid.addObstacles(pg_obstacles);
  // 流体更新
  fluid.update();
  fluid.renderFluidTextures(pg_fluid, 0);

  // 显示流体层
  image(pg_fluid, 0, 0);
  // 显示障碍物层
  image(pg_obstacles, 0, 0);
}

// 绘制圆心在画布中间,直径为500的障碍物圆
void drawObstacles() {
  pg_obstacles.beginDraw();
  pg_obstacles.blendMode(REPLACE);
  pg_obstacles.clear();

  float x = width * 0.5;
  float y = height * 0.5;
  pg_obstacles.pushMatrix();
  pg_obstacles.translate(x, y);
  pg_obstacles.stroke(line_color);
  pg_obstacles.strokeWeight(2);
  pg_obstacles.noFill();
  pg_obstacles.circle(0, 0, 500);
  pg_obstacles.popMatrix();

  pg_obstacles.endDraw();
}

声音可视化

看着边界好像没作用,是因为我们的流体还没扩散到边界,我们来使用 Processing 的 Sound 库进行声音可视化处理。这里简单使用声音的音量进行处理。

我们简单的让音量直接影响流体粒子旋转的速度,粒子的半径,以及太极的旋转速度。

1)添加声音库

代码语言:javascript
复制
import processing.sound.*;

2)声明 AudioIn 以及 Amplitude

代码语言:javascript
复制
AudioIn audioIn;
// 振幅-音量
Amplitude rms;

3)设置声音

代码语言:javascript
复制
  ...
  // 设定声音
  setupSound();
  ...
}

void setupSound() {
  audioIn = new AudioIn(this, 0);
  audioIn.play();

  rms = new Amplitude(this);
  rms.input(audioIn);
}

4)振幅传递到 MyFluidData 和 Taichi 初始化函数中,方便两者使用

代码语言:javascript
复制
fluid.addCallback_FluiData(new MyFluidData(rms));

taichi = new Taichi(new PVector(width / 2, height / 2), 100, pg_taichi, rms);

5)流体的音量控制

代码语言:javascript
复制
float soundLevel = rms.analyze() * 1000;

// 流体密度粒子半径受音量控制
float radius1 = map(soundLevel, 10, 600, 15, 20);
float radius2 = map(soundLevel, 10, 500, 8, 12);
println(soundLevel, radius1, radius2);

// 给流体上的点添加密度,颜色为c,半径为radius1,稍微大点
fluid.addDensity(px, py, radius1, red(config.c) / 255.0, green(config.c) / 255.0, blue(config.c) / 255.0, 1.0f);
// 给流体上的点添加密度,颜色为白色,半径为radius2,稍微小点
fluid.addDensity(px, py, radius2, 1.0f, 1.0f, 1.0f, 1.0f);

...

// 增加弧度角,用于下一帧计算,才能旋转
// 角速度受音量控制
float angleSpeed = constrain(radians(soundLevel * 0.03), 0.01, 0.08) * 3;
println(soundLevel, angleSpeed);
config.angle += angleSpeed;

6)太极的旋转

代码语言:javascript
复制
void display() {
  ...
  float soundLevel = rms.analyze();
  angle += TWO_PI/360 * soundLevel * 20;
  ...
}

Mac电脑麦克风权限问题解决

mac电脑上打开 Processing,使用 Sound库(或者 minim 库)时,有可能会出现授权问题,麦克风输入没有任何的效果。

小菜的解决方案如下:

打开终端,输入

/Applications/Processing.app/Contents/MacOS/Processing

启动 Processing,打开程序代码,此时注意,会弹出麦克风许可的弹窗,选择允许,就可以使用麦克风的声音输入了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小菜与老鸟 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 太极的绘制
  • 单个流体绕圈
  • 双个流体绕圈
  • 边界-障碍物添加
  • 声音可视化
  • Mac电脑麦克风权限问题解决
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档