前言
Processing 一直是我 (阿吉) 的心頭好,只要少量又簡易的程式碼就能產生各式各樣的花式介面,極簡風或是華麗風都能輕鬆掌握,還能結合 Arduino 做到軟硬整合。對於初學者還有非常多的小範例,幾乎都能直接執行看到效果,是非常方便好用的教學工具。
除了 Processing 之外,還有 p5.js、Processing Android 與 Processing Python 等,但後兩者看起來已較少維護。
作業系統方面,Windows, MAC 與 Linux 甚至 Raspberry Pi 都能免安裝直接執行。
當然啦,透過 chatGPT 這類工具去發想新點子或修改舊的程式碼都非常方便,多多嘗試就能迸發更多有趣的點子,非常有趣喔!有興趣的讀者們請看看我錄製的 Processing 結合 Arduino 的系列教學。
程式重點說明
以下說明重點程式碼,完整程式碼請參考本文末。
1. 聲音輸入與分析
import processing.sound.*;
Amplitude amp;
AudioIn mic;
-
使用 Processing 的
sound
函式庫來取得麥克風聲音輸入,用於分析音量變化,並以此作為互動依據。 -
如果尚未安裝請由 Processing 介面中選擇 Sketch > Import Library > Manage Libraries… ,就會出現 Contribution Manager 視窗。在這個視窗中輸入 sound,找到 Sound 函式庫,按下 Install 安裝即可,如下圖:
2. 風車動畫變數
float windmillAngle, windmillSpeed;
int bladeCount = 4;
color[] bladeColors;
-
這些變數用於控制風車的轉速與角度。
-
每片扇葉有不同顏色,呈現動態色彩變化。
3. 環境設定、背景與太陽
float grassHeight;
ArrayList<Cloud> clouds;
ArrayList<Particle> particles;
float sunSize, sunGlow, skyHue;
-
模擬草地、雲朵、粒子(如蒲公英、灰塵等),營造動態自然環境。Processing 針對粒子的運動效果有很好的函式庫與範例。
- 根據聲音大小,改變太陽大小、光暈與天空顏色 (
sunSize
,sunGlow
,skyHue
),呈現各種氣氛。
4. 夜間模式與星星
boolean nightMode;
PVector[] stars;
float[] starBrightness;
-
根據音量或特定互動觸發「夜晚場景」,顯示星星與暗色背景。
5. setup()
初始設定
size(800, 600);
colorMode(HSB);
mic = new AudioIn(this, 0);
mic.start();
-
設定畫布尺寸與色彩模式。
-
啟動音訊輸入,執行程式後會在 console 看到所執行機器上可用的音訊裝置,如下圖
6. draw()
主迴圈
void draw() {
volume = amp.analyze() * 5;
smoothedVolume = lerp(smoothedVolume, volume, 0.1);
windmillSpeed = smoothedVolume * 10;
windmillAngle += windmillSpeed;
}
核心功能說明:
-
音量控制風車轉速:聲音越大,風車轉越快。
-
偵測「拍點」:若音量突升(如掌聲、拍手),觸發粒子特效與太陽光暈增強。
-
夜間模式切換:按下
'n'
鍵切換日夜背景。 -
畫面組成 (細節程式碼請自行參考以下程式碼):
-
drawBackground()
:天空與太陽(根據音量變色)。 -
updateAndDrawClouds()
:雲朵移動。 -
drawGrass()
:前景草地。 -
drawWindmill()
:風車旋轉。 -
updateAndDrawParticles()
:粒子隨聲音爆發。 -
drawWaveform()
:底部音量視覺化。 -
displayVolumeInfo()
:顯示目前音量與風車轉速,位於畫面左下角。 displayInstructions()
:顯示目前音量數值,位於畫面右下角。
-
7. 自定義類別(共 2 個)
為了更有效呈現雲、粒子等不規則的物體動態,定義了兩個自訂物件類別來管理:
class Cloud
-
雲朵的 位置、速度、大小 設定。
-
持續向右飄動,出畫面後重生。
class Particle
-
用於表現「音量爆發」的粒子煙火效果。
-
每個粒子有位置、速度、透明度,並會隨時間淡出。
完整程式碼如下
// 聲控風車互動視覺系統
// 使用麥克風音量控制風車旋轉速度和視覺效果
import processing.sound.*;
// 聲音分析變數
Amplitude amp;
AudioIn mic;
float volume = 0;
float smoothedVolume = 0;
float[] volumeHistory = new float[100];
// 風車變數
float windmillAngle = 0;
float windmillSpeed = 0;
float bladeLength = 120;
int bladeCount = 4;
color[] bladeColors = {
color(255, 100, 100), // 紅色
color(255, 200, 100), // 橙色
color(100, 255, 100), // 綠色
color(100, 200, 255), // 藍色
};
// 環境變數
float grassHeight = 100;
ArrayList<Cloud> clouds = new ArrayList<Cloud>();
ArrayList<Particle> particles = new ArrayList<Particle>();
float windStrength = 0;
float sunSize = 80;
float sunGlow = 0;
float skyHue = 210;
int lastBeat = 0;
boolean nightMode = false;
int starCount = 100;
PVector[] stars;
float[] starBrightness;
// 設定初始參數
void setup() {
size(800, 600);
colorMode(HSB, 360, 100, 100, 100);
// 初始化麥克風輸入和音量檢測
Sound.list();
Sound s = new Sound(this);
amp = new Amplitude(this);
mic = new AudioIn(this, 0);
mic.start();
amp.input(mic);
// 初始化音量歷史紀錄
for (int i = 0; i < volumeHistory.length; i++) {
volumeHistory[i] = 0;
}
// 初始化雲朵
for (int i = 0; i < 5; i++) {
clouds.add(new Cloud(random(width), random(height/3)));
}
// 初始化星星
stars = new PVector[starCount];
starBrightness = new float[starCount];
for (int i = 0; i < starCount; i++) {
stars[i] = new PVector(random(width), random(height/2));
starBrightness[i] = random(30, 100);
}
}
void draw() {
// 獲取麥克風音量並平滑處理
volume = amp.analyze() * 5; // 放大音量值
smoothedVolume = lerp(smoothedVolume, volume, 0.1);
// 更新音量歷史紀錄
for (int i = 0; i < volumeHistory.length-1; i++) {
volumeHistory[i] = volumeHistory[i+1];
}
volumeHistory[volumeHistory.length-1] = smoothedVolume;
// 根據音量更新風車速度
windmillSpeed = smoothedVolume * 10;
windmillAngle += windmillSpeed;
// 根據音量更新風力
windStrength = smoothedVolume * 2;
// 根據音量檢測重拍並創建特效
if (volume > 0.4 && millis() - lastBeat > 300) {
createParticleExplosion();
sunGlow = min(sunGlow + 20, 50);
lastBeat = millis();
}
// 降低太陽光暈
sunGlow = max(sunGlow - 0.5, 0);
// 切換日夜模式
if (keyPressed && key == 'n') {
nightMode = !nightMode;
}
// 繪製背景
drawBackground();
// 繪製雲朵
updateAndDrawClouds();
// 繪製草地
drawGrass();
// 繪製風車
drawWindmill();
// 更新並繪製粒子
updateAndDrawParticles();
// 繪製視覺化的音量波形
drawWaveform();
// 顯示音量數值
displayVolumeInfo();
// 顯示操作說明
displayInstructions();
}
// 繪製背景(天空、太陽/月亮)
void drawBackground() {
// 天空顏色根據音量微調
float skyBrightness = nightMode ? 20 : 90 - smoothedVolume * 10;
float skySaturation = nightMode ? 60 : 20 + smoothedVolume * 20;
// 繪製漸變天空
for (int y = 0; y < height * 0.7; y++) {
float gradientPos = map(y, 0, height * 0.7, 0, 1);
float hue = nightMode ? 240 : skyHue;
float brightness = nightMode ?
map(gradientPos, 0, 1, 20, 5) :
map(gradientPos, 0, 1, skyBrightness, skyBrightness - 10);
float saturation = nightMode ?
map(gradientPos, 0, 1, 30, 60) :
map(gradientPos, 0, 1, skySaturation, skySaturation + 10);
stroke(hue, saturation, brightness);
line(0, y, width, y);
}
// 繪製星星(夜間模式)
if (nightMode) {
for (int i = 0; i < starCount; i++) {
float brightness = starBrightness[i] + sin(frameCount * 0.05 + i) * 20;
fill(60, 10, brightness);
noStroke();
float starSize = 1 + smoothedVolume;
ellipse(stars[i].x, stars[i].y, starSize, starSize);
}
}
// 繪製太陽或月亮
if (nightMode) {
// 月亮
pushMatrix();
translate(width * 0.8, height * 0.2);
fill(60, 10, 90);
noStroke();
ellipse(0, 0, sunSize, sunSize);
// 月球表面紋理
fill(60, 10, 80, 30);
ellipse(-sunSize/4, -sunSize/4, sunSize/2, sunSize/2);
ellipse(sunSize/5, sunSize/5, sunSize/3, sunSize/3);
popMatrix();
} else {
// 太陽及其光暈
pushMatrix();
translate(width * 0.8, height * 0.2);
// 太陽光暈
for (int i = 0; i < 3; i++) {
float alpha = map(i, 0, 3, sunGlow, 0);
float size = sunSize * (1 + i * 0.5 * (1 + smoothedVolume));
fill(40, 80, 100, alpha);
ellipse(0, 0, size, size);
}
// 太陽本體
fill(40, 80, 100);
ellipse(0, 0, sunSize, sunSize);
// 太陽光芒
stroke(40, 80, 100, 70);
for (int i = 0; i < 12; i++) {
float angle = i * TWO_PI / 12;
float rayLength = sunSize * (0.7 + smoothedVolume);
pushMatrix();
rotate(angle + frameCount * 0.01);
strokeWeight(3);
line(0, 0, rayLength, 0);
popMatrix();
}
popMatrix();
}
}
// 繪製風車
void drawWindmill() {
float centerX = width / 2;
float centerY = height * 0.7 - grassHeight * 0.7;
// 風車支柱
fill(30, 30, 80);
noStroke();
rect(centerX - 15, centerY, 30, height - centerY);
// 風車機艙
fill(0, 0, 60);
ellipse(centerX, centerY, 40, 40);
// 繪製風車葉片
pushMatrix();
translate(centerX, centerY);
rotate(radians(windmillAngle));
// 聲音對葉片的形變效果
float bladeLengthFactor = 1 + smoothedVolume * 0.2;
float bladeWidthFactor = 1 + smoothedVolume * 0.5;
// 風車葉片,數量由 bladeCount 決定
for (int i = 0; i < bladeCount; i++) {
pushMatrix();
rotate(i * TWO_PI / bladeCount);
// 葉片顏色根據位置和音量變化
color bladeColor = bladeColors[i % bladeColors.length];
fill(hue(bladeColor), saturation(bladeColor), brightness(bladeColor));
// 繪製葉片 - 根據音量變形
beginShape();
vertex(0, 0);
vertex(-15 * bladeWidthFactor, bladeLength * 0.3 * bladeLengthFactor);
vertex(0, bladeLength * bladeLengthFactor);
vertex(15 * bladeWidthFactor, bladeLength * 0.3 * bladeLengthFactor);
endShape(CLOSE);
// 當音量足夠大時,加入葉片光澤
if (smoothedVolume > 0.2) {
fill(hue(bladeColor), saturation(bladeColor) * 0.5, brightness(bladeColor) * 1.5, 70);
ellipse(0, bladeLength * 0.5 * bladeLengthFactor,
15 * bladeWidthFactor, bladeLength * 0.5 * bladeLengthFactor);
}
popMatrix();
}
// 風車中心
fill(0, 0, 90);
ellipse(0, 0, 20, 20);
popMatrix();
// 當轉速足夠快時,產生旋轉粒子
if (windmillSpeed > 1) {
createWindmillParticles(centerX, centerY);
}
}
// 繪製草地
void drawGrass() {
// 大面積草地
fill(120, 70, 60);
noStroke();
rect(0, height * 0.7, width, height * 0.3);
// 個別草葉 - 根據音量波動
int grassCount = 50;
stroke(120, 70, 60);
for (int i = 0; i < grassCount; i++) {
float x = map(i, 0, grassCount, 0, width);
float grassWave = sin(frameCount * 0.05 + i * 0.3) * windStrength;
float grassHeight = this.grassHeight * (1 + volumeHistory[i % volumeHistory.length] * 0.5);
pushMatrix();
translate(x, height * 0.7);
// 草葉顏色根據音量變化
float greenHue = 120 + volumeHistory[i % volumeHistory.length] * 20;
stroke(greenHue, 70, 60);
strokeWeight(2 + volumeHistory[i % volumeHistory.length] * 3);
// 繪製彎曲的草葉
beginShape();
for (int j = 0; j < 10; j++) {
float t = j / 9.0;
float grassX = grassWave * t * t;
float grassY = -grassHeight * t;
vertex(grassX, grassY);
}
endShape();
popMatrix();
}
}
// 更新並繪製雲朵
void updateAndDrawClouds() {
for (Cloud cloud : clouds) {
cloud.update();
cloud.display();
}
}
// 創建風車旋轉產生的粒子
void createWindmillParticles(float x, float y) {
float angle = random(TWO_PI);
float distance = random(20, 30);
float particleX = x + cos(angle) * distance;
float particleY = y + sin(angle) * distance;
// 粒子顏色根據風車速度變化
float hue = map(windmillSpeed, 0, 20, 200, 300);
color particleColor = color(hue, 80, 100, 70);
particles.add(new Particle(particleX, particleY, particleColor));
}
// 創建音量激發的粒子爆發
void createParticleExplosion() {
float centerX = width / 2;
float centerY = height * 0.7 - grassHeight * 0.7;
// 創建從風車中心發散的粒子
for (int i = 0; i < 20; i++) {
float angle = random(TWO_PI);
float distance = random(30, 50);
float particleX = centerX + cos(angle) * distance;
float particleY = centerY + sin(angle) * distance;
// 彩虹色粒子
float hue = random(360);
color particleColor = color(hue, 80, 100, 80);
Particle p = new Particle(particleX, particleY, particleColor);
p.velocity = new PVector(cos(angle) * random(1, 3), sin(angle) * random(1, 3));
particles.add(p);
}
}
// 更新並繪製所有粒子
void updateAndDrawParticles() {
for (int i = particles.size() - 1; i >= 0; i--) {
Particle p = particles.get(i);
p.update();
p.display();
if (p.isDead()) {
particles.remove(i);
}
}
}
// 繪製音量波形
void drawWaveform() {
// 繪製音量歷史波形
stroke(nightMode ? color(210, 70, 90) : color(30, 70, 90));
strokeWeight(2);
noFill();
beginShape();
for (int i = 0; i < volumeHistory.length; i++) {
float x = map(i, 0, volumeHistory.length, width * 0.1, width * 0.9);
float y = height * 0.85 - volumeHistory[i] * 100;
vertex(x, y);
}
endShape();
}
// 顯示音量數值信息
void displayVolumeInfo() {
fill(nightMode ? color(0, 0, 90) : color(0, 0, 30));
textSize(14);
text("環境音量: " + nf(volume, 0, 2), 20, height - 40);
text("風車速度: " + nf(windmillSpeed, 0, 2), 20, height - 20);
}
// 顯示操作說明
void displayInstructions() {
fill(nightMode ? color(0, 0, 90) : color(0, 0, 30));
textSize(14);
text("按 'n' 切換日/夜模式", width - 150, height - 20);
}
// 雲朵類別
class Cloud {
float x, y;
float speed;
float size;
float opacity;
float[] bumpSizes;
float[] bumpOffsets;
Cloud(float x, float y) {
this.x = x;
this.y = y;
this.speed = random(0.2, 0.5);
this.size = random(50, 120);
this.opacity = random(70, 90);
// 雲朵的不規則形狀
int bumpCount = int(random(4, 7));
bumpSizes = new float[bumpCount];
bumpOffsets = new float[bumpCount];
for (int i = 0; i < bumpCount; i++) {
bumpSizes[i] = random(0.6, 1.0);
bumpOffsets[i] = random(TWO_PI);
}
}
void update() {
// 根據風力移動雲朵
x += speed + windStrength * 0.2;
// 當雲朵移出畫面時,從左側重新進入
if (x > width + size) {
x = -size;
y = random(height/4);
}
// 雲朵微妙上下浮動
y += sin(frameCount * 0.01 + x * 0.01) * 0.2;
}
void display() {
noStroke();
// 雲朵的顏色 - 日/夜模式有不同效果
if (nightMode) {
fill(230, 20, 40, opacity);
} else {
fill(0, 0, 100, opacity);
}
// 繪製雲朵的基本形狀
pushMatrix();
translate(x, y);
// 根據音量調整雲朵透明度和大小
float cloudSizeFactor = 1 + smoothedVolume * 0.1;
float cloudOpacity = opacity * (1 - smoothedVolume * 0.3);
// 繪製雲朵的多個圓形組成不規則形狀
ellipse(0, 0, size * cloudSizeFactor, size * 0.6 * cloudSizeFactor);
for (int i = 0; i < bumpSizes.length; i++) {
float angle = map(i, 0, bumpSizes.length, 0, TWO_PI) + bumpOffsets[i];
float bx = cos(angle) * size * 0.4;
float by = sin(angle) * size * 0.3;
ellipse(bx, by, size * bumpSizes[i] * cloudSizeFactor, size * bumpSizes[i] * cloudSizeFactor);
}
popMatrix();
}
}
// 粒子類別
class Particle {
PVector position;
PVector velocity;
PVector acceleration;
color particleColor;
float size;
float lifespan;
Particle(float x, float y, color c) {
position = new PVector(x, y);
velocity = new PVector(random(-1, 1), random(-2, -0.5));
acceleration = new PVector(0, 0.05);
particleColor = c;
size = random(3, 8);
lifespan = 255;
}
void update() {
// 添加風力影響
PVector wind = new PVector(windStrength * 0.1, 0);
velocity.add(wind);
// 更新位置
velocity.add(acceleration);
position.add(velocity);
// 減少壽命
lifespan -= 5;
// 應用阻力
velocity.mult(0.98);
}
void display() {
noStroke();
// 設置粒子顏色和透明度
color displayColor = color(
hue(particleColor),
saturation(particleColor),
brightness(particleColor),
map(lifespan, 0, 255, 0, 100)
);
fill(displayColor);
// 根據音量調整粒子大小和形狀
if (smoothedVolume > 0.3) {
// 音量大時粒子變成星形
pushMatrix();
translate(position.x, position.y);
rotate(frameCount * 0.1);
beginShape();
for (int i = 0; i < 5; i++) {
float angle = i * TWO_PI / 5;
float x1 = cos(angle) * size;
float y1 = sin(angle) * size;
vertex(x1, y1);
angle += TWO_PI / 10;
float x2 = cos(angle) * (size * 0.4);
float y2 = sin(angle) * (size * 0.4);
vertex(x2, y2);
}
endShape(CLOSE);
popMatrix();
} else {
// 音量小時粒子為圓形
ellipse(position.x, position.y, size, size);
}
}
boolean isDead() {
return lifespan <= 0;
}
}
// 當按鍵時切換日/夜模式(在 draw 中已處理)
執行
請按下 Processing IDE 中的執行按鈕,即可看到以下畫面。雲朵飄動的效果我很喜歡。
拍拍手或是對著麥克風吹氣就會看到風車轉動速度以及粒子噴發速度會隨著環境音量變化,太陽與背景的草也會跟著變動。按下鍵盤的 n
鍵就會切換白天晚上喔!
Processing 提供的範例真的很豐富,歡迎大家多多開起來玩玩看!沒有安裝也可以從網頁來看各個範例的執行效果。
Related Posts
-
130507 國北教大數資系 Processing/Arduino 互動專題課程
很快地,國北教大數資系 Pro...
-
【教學】 Arduino 101結合 Processing 互動滾球遊戲
本文章要告訴您如何結合 Pro...
-
140222 Processing Android 數位互動研習
這次的研習主題是使用 Proc...
-
140222 Processing 互動媒體結合 Android智慧型手機研習營
報名頁面請按我 Process...