こんにちは。
今回の題材は「3D」です。
基本的にprocessingは2Dで描画されるものですが、3Dでも描画可能です。
3Dでプログラムをつくる際はprocessingに3Dの描画をすることを伝える必要があり、そのために画面のサイズを指定するときの「size」で、引数の3つ目に「P3D」を足します。
size (500, 500, P3D);
ここで、箱を描いてみます。
size (500, 500, P3D); noFill(); translate(width/2, height/2); box(100, 200, 300);
こうすると、画面の真ん中に辺だけの直方体が描かれます。
「box」の引数は幅、高さ、奥行きのみで位置が含まれないので箱が原点に描かれますが、「translate」で原点を任意の位置(今回は画面の真ん中)に移してから描くと、その位置に描かれるようになります。
では次に、見る方向を変えられるようにしてみます。
void setup(){ size (500, 500, P3D); noFill(); } void draw(){ background(255); camera(width/2, height/2, 500, width/2, height/2, 0, 1, 0, 0); translate(width/2, height/2); rotateX(map(mouseX, 0, width, 0, 2*PI)); rotateY(map(mouseY, 0, height, 0, 2*PI)); box(100, 200, 300); }
「camera」は引数にカメラの位置、中心の位置、座標軸の向きを指定することで使えます。向きについては、引数で1とした軸を下に向けるようになっています。
「map」は範囲を変える関数で、map(a, b, c, d, e)の形で用いますが、aという値でb~cの範囲をd~eの範囲と対応させるという意味です。今回の場合、mouseX(マウスのx座標)で0~width(画面の左端から右端まで)と0~2$\pi$(0ラジアンから2$\pi$ラジアン(一周分))を対応させています。こうすることで、画面の左端から右端へ行くにつれて箱が回転し、右端に着いたところで一周するということになります。(mouseYについても同様)
さて、ここまでで今回やってみたかったことを実行できます。
何かというと、「VESTA」をまねてみよう、ということです。
「VESTA」とは、分子の形を表示するソフトの一つで、テキストファイルで各原子の種類と座標を指定し、それを読み込ませればその位置に原子が描かれ、分子模型ができあがるというものです。
ファイルを読み込む機能を追加すると、テキストファイルから座標を指定して描画を行えるのでより近づきますが、ファイルの読み込みはまた別の回で取り上げたいと思います。
ということで、座標の指定はプログラム内で行うとして、簡単のためにメタンでやってみようと思います。
メタンは$\mathrm{CH}_4$で表される分子で、炭素を中心に正四面体の頂点方向に水素が存在するという形です。
今プログラムでは、炭素を原点とし、4つの水素の座標を指定します。
本来のVESTAなら、原子の種類を指定すればソフトが勝手に大きさや色を決めてくれますし結合の手も描いてくれますが、このプログラムにはそんな機能はついていませんので、それぞれ指定する必要があります。
座標については、炭素原子は原点なので$(x,y,z)=(0,0,0)$、炭素-水素結合の距離を109 pm、水素-炭素-水素の角度を109度とすれば、導出は数学というか算数の問題なので省略しますが、各水素原子の座標はpm単位で$(x,y,z)=(0,0,109),(0,103,-35.5),(88.7,37.2,-35.5),(-88.7,37.2,-35.5)$と求められます。
原子の半径は、炭素原子が77 pm、水素原子が31 pmとします。
色は、炭素原子を黒、水素原子を青とします。
さて、それでは実際に描画してみます。
void setup(){ size (500, 500, P3D); noFill(); noStroke(); } void draw(){ background(255); camera(width/2, height/2, 500, width/2, height/2, 0, 1, 0, 0); translate(width/2, height/2); rotateX(map(mouseX, 0, width, 0, 2*PI)); rotateY(map(mouseY, 0, height, 0, 2*PI)); //炭素原子 fill(0); sphere(77); //水素原子 fill(0, 0, 255); pushMatrix(); translate(0, 0, 109); sphere(31); popMatrix(); pushMatrix(); translate(0, 103, -35.5); sphere(31); popMatrix(); pushMatrix(); translate(88.7, -37.2, -35.5); sphere(31); popMatrix(); pushMatrix(); translate(-88.7, -37.2, -35.5); sphere(31); popMatrix(); }
これで、メタンを表せました。
描いてみると球が接したような形になりました。
よく見る棒と球のモデルにしてみましょう。
棒と球のモデルでは、原子半径は共有結合半径の半分としているらしいです。棒の部分はline関数で描けます。
drawブロックの中に以下のコードを追加して、sphere関数の引数をそれぞれ2分の1にすると、棒と球のモデルっぽくなります。
stroke(100); strokeWeight(10); line(0, 0, 0, 0, 0, 109); line(0, 0, 0, 0, 103, -35.5); line(0, 0, 0, 88.7, -37.2, -35.5); line(0, 0, 0, -88.7, -37.2, -35.5); noStroke();
アイキャッチにあるようなモデルは、ファンデルワールス半径を用いたものです。
ファンデルワールス半径は、分子間相互作用の境界線のようなもので、これより遠いと引力が、これより近いと斥力がはたらくという感じです。
その値は、炭素で170 pm、水素で120 pmのようなので、sphere関数の引数にそれぞれ入れれば描けます。
これで基本の形はできました。
あとは、前に述べたようにファイルを読み込むようにしたり、原子の情報を入れてみたりと様々遊べると思います。もちろん、メタン以外の分子を描くのも面白いと思います。
が、分子モデルをつくりたいという理由だけで今回のようなプログラムをつくるくらいなら、VESTAや他のソフトをダウンロードした方がいいでしょう。あくまでprocessingの練習です。
というわけで、メタン分子の描画を例にprocessingで3Dを扱ってみました。
今回はここまでにしたいと思います。また次回。
おまけ(2021/10/30)
processingで3Dの描画を行う際の「いろんな角度から見たい」という願いを叶えたコードをつくってみました。
マウスの左ドラッグでモデルの回転、ホイールコロコロでズームイン・アウト、ホイールドラッグでモデルの平行移動ができます。
float rotX, rotY; float protX, protY; float xc, yc, zc; float pxc, pyc, pzc; float s; void setup(){ size(500, 500, P3D); xc = 0; yc = 0; zc = 500; s = 1; } void draw(){ background(255); camera(0, 0, 1000, xc, yc, zc, 1, 0, 0); rotateX(rotX); rotateY(rotY); scale(s); //ここに処理を書く stroke(0); sphere(100); // } void mouseDragged(){ if(mouseButton == LEFT){ if(mouseX-pmouseX >= 0){ rotX = map(mouseX-pmouseX, 0, width, 0, 2*PI); } if(mouseX-pmouseX <= 0){ rotX = map(mouseX-pmouseX, -width, 0, -2*PI, 0); } rotX += protX; protX = rotX; if(mouseY-pmouseY >= 0){ rotY = map(mouseY-pmouseY, 0, height, 0, 2*PI); } if(mouseY-pmouseY <= 0){ rotY = map(mouseY-pmouseY, -height, 0, -2*PI, 0); } rotY += protY; protY = rotY; } if(mouseButton == CENTER){ xc += pmouseY-mouseY; pxc = xc; yc += mouseX-pmouseX; pyc = yc; } } void mouseWheel(MouseEvent e){ float mw = e.getCount(); if(mw == 1){ s *= 0.9; } if(mw == -1){ s *= 1.1; } }
追記(2021/12/18)
撮りたいコードに上のコードを貼り付けることで撮れるようにはなりますが、何行もあるものを移すとなると移す途中でコードが書き換わってしまったり、そもそも面倒だったりと問題がありました。
なので、それぞれ関数としてまとめてしまって、移すとしても少量になるようにしてみました。
(今の知識では撮りたいコードに何か書き加えないとできないので最小限に…)
float rotX, rotY; float protX, protY; float xc, yc, zc; float pxc, pyc, pzc; float s; void cameraSetting(){ xc = 0; yc = 0; zc = 500; s = 1; } void cameraMove(){ camera(0, 0, 1000, xc, yc, zc, 1, 0, 0); rotateX(rotX); rotateY(rotY); scale(s); } void mouseDragged(){ if(mouseButton == LEFT){ if(mouseX-pmouseX >= 0){ rotX = map(mouseX-pmouseX, 0, width, 0, 2*PI); } if(mouseX-pmouseX <= 0){ rotX = map(mouseX-pmouseX, -width, 0, -2*PI, 0); } rotX += protX; protX = rotX; if(mouseY-pmouseY >= 0){ rotY = map(mouseY-pmouseY, 0, height, 0, 2*PI); } if(mouseY-pmouseY <= 0){ rotY = map(mouseY-pmouseY, -height, 0, -2*PI, 0); } rotY += protY; protY = rotY; } if(mouseButton == CENTER){ xc += pmouseY-mouseY; pxc = xc; yc += mouseX-pmouseX; pyc = yc; } } void mouseWheel(MouseEvent e){ float mw = e.getCount(); if(mw == 1){ s *= 0.9; } if(mw == -1){ s *= 1.1; } }
使い方としては、
適当なpdeファイルに書いて、撮りたいプログラムがあるフォルダに入れる
setup()に「cameraSetting();」と書き加える
draw()に「cameraMove();」と書き加える
実行
という感じです。
ただし、本文のコードにmouseDragged()やmouseWheel()があると競合するので、そのときは直接書くとかしないといけないかもしれません。
以上