この記事では書籍「世界一美しい錯視アート」に掲載されている「光るニジュウヤホシテントウ」という作品をProcessingを使って再現してみます。
光るニジュウヤホシテントウ
今回作成した錯視アート「光るニジュウヤホシテントウ」です。
書籍では「ピカピカして見える。」と解説されています。別の作品「きらめき格子錯視の地球」と比べると、ぼかしが入った分、目のちかちかが少し和らいでいる気がします。
描き方のポイント
まず球体を描き、その半径を算出する
まず、球体を描きます。描き方は別記事「きらめき格子錯視の地球」で解説していますので、そちらをご覧ください。
この画像から球面の領域を抽出するために、球面の半径を算出します。単純に考えると球面を描くときに半径を指定しているのでそれを使えばいいと思ってしまいますが、3Dで表された球面の場合、奥行方向や遠近法の影響で、描画の際に指定した半径と画像上で見える半径とは値が変わってきます。そのため、画像から直接、半径を算出します。
ぼかしを入れる
次に、球体の画像にぼかしを入れます。今回、ぼかしは
filter(BLUR,10);
のように少し強めに入れています。
この画像を画像クラスPImageに一旦格納しておきます。
背景の格子柄を作成する
あとは、背景の格子柄を作ります。今回は100×100の格子を作り、その各格子の色を白色または黒色にランダムに与えていきます。
この画像に対して、前に算出した球体の半径を利用して、ぼかした球体部分だけを描画することでこの作品は完成します。
プログラムコード
今回作成した錯視アート「光るニジュウヤホシテントウ」のプログラムコードを載せておきます。
void setup() {
size(1000, 1000, P3D);
PImage img = createImage(width, height, RGB);
PImage img2 = createImage(width, height, RGB);
PImage img3 = createImage(width, height, RGB);
translate(width/2.0, height/2.0, -width*0.15);
background(100,100,100); // 背景をグレーで塗りつぶす
// まず、黒色の球体を描く
float radius = width/2.0;
noStroke();
fill(0,0,0);
sphere(radius);
// 経度方向にグレーの線を引いていく
// 線は小さな矩形を並べていくことで表現している
float divLat = 0.1;
float divLon = 360/30;
for (float lon = -divLon*6; lon <= divLon*5; lon += divLon) {
float radLon = radians(lon)+radians(divLon/2.0);
for (float lat = 0.0; lat <= 180.0; lat += divLat) {
float radLat = radians(lat);
float cX = radius * sin(radLon) * sin(radLat);
float cY = radius * cos(radLat);
float cZ = radius * cos(radLon) * sin(radLat);
pushMatrix();
// 座標は translate で設定
translate(cX, cY, cZ);
rotateX(radLat-PI/2.0);
rotateY(radLon);
fill(100,100,100);
rectMode(CENTER);
rect(0,0,16,10);
popMatrix();
}
}
// 緯度方向にグレーの線を引いていく
// 線は小さな矩形を並べていくことで表現している
divLat = 360/30;
divLon = 0.1;
for (float lat = 0.0; lat <= 180.0; lat += divLat) {
float radLat = radians(lat);
// 経度
for (float lon = -90.0; lon <=90.0 ; lon += divLon) {
float radLon = radians(lon);
float cX = radius * sin(radLon) * sin(radLat);
float cY = radius * cos(radLat);
float cZ = radius * cos(radLon) * sin(radLat);
pushMatrix();
// 座標は translate で設定
translate(cX, cY, cZ);
rotateX(radLat-PI/2.0);
rotateY(radLon);
fill(100,100,100);
rectMode(CENTER);
rect(0,0,10,16);
popMatrix();
}
}
// グレーの経線、緯線の交点に白色の円を置いていく
divLat = 360/30;
divLon = 360/30;
float delta_r = 2.0; // 球面の表面から少しだけ浮かす
// 経度
for (float lon = -divLon*6; lon <= divLon*5; lon += divLon) {
float radLon = radians(lon)+radians(divLon/2.0);
// 緯度
for (float lat = 0.0; lat <= 180.0; lat += divLat) {
float radLat = radians(lat);
float cX = (radius+delta_r) * sin(radLon) * sin(radLat);
float cY = (radius+delta_r) * cos(radLat);
float cZ = (radius+delta_r) * cos(radLon) * sin(radLat);
pushMatrix();
// 座標は translate で設定
translate(cX, cY, cZ);
rotateX(radLat-PI/2.0);
rotateY(radLon);
fill(255,255,255);
rectMode(CENTER);
ellipse(0,0,16*sqrt(2.0),16*sqrt(2.0));
popMatrix();
}
}
// 画像を一旦PImageに格納する
loadPixels();
for(int j=0; j<img.height; j++){
for(int i=0; i<img.width; i++){
img.set(i, j, pixels[j*width + i]);
}
}
// 画像から球体部分の領域の半径を算出する
int radius_temp = 0;
while(pixels[radius_temp*width + width/2] == color(100,100,100)){
radius_temp++;
}
radius_temp = width/2 - radius_temp;
// 再度画像を描画してぼかしを入れる
translate(0,0,width*0.15-1);
image(img, -width/2.0, -height/2.0, width, height);
filter(BLUR,10);
// ぼかしを入れた画像を再度PImageに保存する
loadPixels();
for(int j=0; j<img2.height; j++){
for(int i=0; i<img2.width; i++){
img2.set(i, j, pixels[j*width + i]);
}
}
// 背景に白と黒のランダムなチェック柄を描く
background(255,255,255);
rectMode(CORNER);
int cell_num = 125;
int cell_size = width / cell_num;
int cell_x = 0;
int cell_y = 0;
for(int i=0; i<cell_num; i++){
for(int j=0; j<cell_num; j++){
if( random(1.0) < 0.5 ){
fill(0,0,0);
} else {
fill(255,255,255);
}
rect(cell_x-width/2, cell_y-width/2, cell_size, cell_size);
cell_x += cell_size;
}
cell_x = 0;
cell_y += cell_size;
}
// 球体の領域の外側のみ白と黒の格子柄を残す形で画像を描画する
loadPixels();
for(int j=0; j<img3.height; j++){
for(int i=0; i<img3.width; i++){
if( sqrt(pow(i-width/2, 2) + pow(j-width/2, 2) ) > radius_temp ){
img3.set(i, j, pixels[j*width + i]);
} else {
img3.set(i, j, img2.get(i, j));
}
}
}
image(img3, -width/2.0, -height/2.0, width, height);
}