見出し画像

背景が動いているように見せる「流し物体」ー 配列の中で要素をグルグル回す方法を解説 -JavaScriptでやる1分間プログラミング

今回から「バーチャルドライビング」のプログラミングを始めますが、まずはどんなものを作るのか、その概要は前回記事を見てください。

ボディートラッキングを利用したベースアプリ

前回記事でも説明しましたが、今回はml5.jsのPose NetというボディートラッキングのAIライブラリを利用します。それを使って、ウェブカメラからの映像の中で鼻と両肩の位置をトラックするベースアプリを用意しました。まずはこれを開いてプログラムを実行してみてください

【バーチャルドライブ ベース版1】

まずはブラウザ内にカメラ映像がきちんと出てくるか確認してください。これはカメラ内に映った鼻(赤い丸)と左右の肩(青い丸)をトラックしますが、カメラ映像が入っているとじゃまなので、ビデオ出力の部分をキャンバスの下にずらした「ベース版2」を改良していきます。

【バーチャルドライブ ベース版2】

画像1

これで鼻と肩のトラック結果はキャンバス内に表示されますが、ビデオそのものはキャンバスの下に現れるようになりました。実際のゲームはキャンバス内に作りこんでいきます。

このベースアプリそのものの解説はしていきませんので、p5エディタで開いたコードをそのまま皆さんのp5エディタのアカウント内に保存するか、あるいはindex.htmlとsketch.jsのコードをコピペすればVisual Studio Codeなど他の開発ツールでもコーディングをしていくことができます。

まずは背景を描く

キャンバスの中に、真ん中な灰いろの道路を描き、両脇を茶色の地面のようなものを描きます。イメージとしてはこんな感じです。

画像2

これは単に両サイドに茶色の四角を描いているだけです。最終的にはこの灰色が舗装された道路で、ここを車が走り、両脇の茶色の部分に街路樹が現れるということになります。

そこでコードは単純でdraw関数の中に、背景を灰色にセットした後に、茶色の塗りつぶしで四角形を二つ描くだけです。1⃣のところを見てください。あとはベースコードのままです。


function draw() {
 //キャンバスの下にビデオを表示する
 image(video, 0, 0, 640, 480);  
 background(150);
 
 //1⃣ 左右の木の地面を描く
 fill(168, 67, 0);
 rect(0, 0, 70, height);
 rect(width - 70, 0, 70, height);

 
 //鼻と肩のトラッキングポイントを表示
 if (pose) {
   fill(255, 0, 0);
   ellipse(pose.nose.x, pose.nose.y, 40);
   fill(0, 0, 255);
   ellipse(pose.rightShoulder.x, pose.rightShoulder.y, 32);
   ellipse(pose.leftShoulder.x, pose.leftShoulder.y, 32);    
 }
}

ステップ❶ 街路樹とセンターラインのクラスを作る

ここからが動きのハイライトです。

まずはこの道路の上を車が走っているように見えるには、車を進めるのではなく、ほかのものが動くようにします。具体的には、両側の地面上では「街路樹」が、そして道路上では「センターライン」が上からどんどんと流れてきて、下のほうに消えていくようにします。これで道路の上を走っているように見せるわけです。

「恐竜ゲーム」は列車が右から左にどんどんと流れてきましたが、それと同じように街路樹とセンターラインが上からどんどんと流れてくるようにします。このために必要なコーディングは、

❶ 街路樹とセンターラインのクラスを作る
❷ センターラインは画面に出てくる数だけ作り、配列に入れて表示
❸ 街路樹はランダムなタイミングでインスタンスを作って表示

今回は街路樹、センターライン、車などいくつもクラスを作るので、クラスのコードは別ファイルにまとめます。まずはclasses.jsというJavaScriptのファイルを追加してみてください。

画像3

できたらそのファイルに次のようにクラスの定義を書きます。

//================
// 街路樹のクラス
//================
class Tree {
 constructor (x, y) {
   this.x = x;
   this.y = y;
 }
 show() {
   circle (this.x, this.y, 30); //とりあえず円を描く    
 }
}

//================
// センターラインのクラス
//================
class CLine {
 constructor(y, h) {
   this.x = width/2; //キャンバスの真ん中
   this.y = y;
   this.height = h; //ライン一本の長さ
 }
 show() {
   rect(this.x, this.y, 10, this.height); //太さは10で固定
 }
}

まず街路樹はTreeというクラスにします。これはとりあえず今は円にしておきます。あとで木の画像に変更するのは簡単だというのは恐竜ゲームでもやった通りです。円はとりえあえず直径30の大きさにします。

次にセンターラインはCLineという名前にします。これは最後まで長方形となりますのでrectを使います。そこで、画面上に何本のセンターラインを描くかわからないので(画面サイズによって数が異なるため)、ラインの長さはhという変数であとで変更できるようにします。描く場所はキャンバスの真ん中です。そして太さ(横の長さ)は10に固定しておきます。

新しいJavaScriptファイルをHTMLで参照する

一つ気を付けてほしいのは、一部のJavaScriptをxxx.jsと別ファイルにした場合は、必ずindex.htmlでそのファイルがあることを指定してあげないといけません。以下はindex.htmlを全部掲載していますが、下のほうの2⃣のところを見て、<script>タグを加えてください。

<!DOCTYPE html>
<html lang="en">
 <head>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/addons/p5.sound.min.js"></script>
   <script src="https://unpkg.com/ml5@0.5.0/dist/ml5.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
   <meta charset="utf-8" />

 </head>
 <body>
   <script src="sketch.js"></script>
   <!-- 2⃣ 追加したclasses.jsを加える-->
   <script src="classes.js"></script>
 </body>
</html>

ステップ❷:”動く”センターラインを表示させる

車が動くかわりに、車は画面上の一定位置に置いたまま、センターラインを動かすと車が動いているように見えます。センターラインがキャンバス上部から流れ出てくるようにすれば「動くセンターライン」ができあがります。そのためにはセンターラインの「配列」を作り、その中にセンターラインの一本一本を入れておきます。そこでまずはセンターライン配列の入れ物を宣言します(コメント3⃣のところを見てください)(。

let clines = [];//3⃣ センターライン

次にsetupのところでこの配列にインスタンスを入れます(4⃣)。

 //4⃣ センターラインを作る
 for (let i=0; i < height/40; i++) {
   clines.push(new CLine(i*40, 20));
 }

ここで考えないといけない点は、キャンバス内にセンターラインを何本置くか。ここではセンターラインの長さを間のスペースを合わせて40とし、それをheightにあわせて何本置くべきかを考えています。ここでのキャンバスの高さは640なので16本のセンターラインオブジェクトを作ることになります。

そしてそれぞれのラインをYの位置を40ピクセル間隔で置きます。ただし、センターラインの長さを20にするので、ライン自体が20、残りのスペースが20で表示されます。これで間隔があいたセンターラインが表示されるはずです。

配列の中身を取り出すfor文

ではセンターラインの配列に入っている線を一本一本表示していきますが、この配列という入れ物に入った中身を一つひとつアクセスするにはどうしたらよいのでしょうか。これにはとても便利なコードがあり、それは次のようにforループ文を使う方法です。

  //5⃣ センターラインを描く
 fill(255); //背景を白にセット
 for (let c of clines) {
   c.show();
 }

let c of clines とするだけで、clines配列の中身がcに入り、それを最初から純にアクセスできるわけです。ここではc.show()としているので中に入っているラインをすべて表示してくれます。

画像4

いい感じにセンターラインが描かれたのですが、まったく動きがありません。そこでこの一つひとつを描画するごとにずらしていく必要があります。そうしないと動いているように見えないためです。

ここで「道路を動かす速さ」を決めます。それを velocity(加速の力)という変数に納めます(後で全掲載したコードでは6⃣を見てください)。

  //5⃣ センターラインを描く
 fill(255); //背景を白にセット
 for (let c of clines) {
   c.y += velocity;
   c.show();
 }

velocityにはとりあえず4という値をセットしておきます。これで4ずつセンターラインが動いていくのですが、次第に上部が消えていきます。

画像5

これは配列に入っているすべてのラインをずらしていくためで、実際には下のほうではキャンバスにはみ出てセンターラインが描画されているはずです。最終的には全部が下に消えて行ってしまいます。

そこで、キャンバス外に出たセンターラインを配列の先頭に持ってきて”再利用”すればよいのです。

  //5⃣ センターラインを描く
 fill(255); //背景を白にセット
 for (let c of clines) {
   c.y += velocity;
   //下から消えたら上に戻す
   if (c.y > height) {
     c.y = c.y - height;
   }
   c.show();
 }

これは配列の先頭に持って行っているわけではなく、外にはみ出たらYの値をheight分だけ差し引いてまた上部に表示されるようにしているだけです。単純な値の操作ですがここではこれで十分でしょう。

ここでこれまでのコードを表示します。

//=============
// PoseNetの変数
//=============
let video;
let poseNet;
let pose;
let skeleton;

//=============
// 画面スクロールの変数
//=============
let velocity = 4; //6⃣ 画面のスピード
let clines = [];//3⃣ センターライン

function setup() {
 createCanvas(640, 480);
 video = createCapture(VIDEO);
 poseNet = ml5.poseNet(video, modelLoaded);
 poseNet.on('pose', gotPoses);
 
 //4⃣ センターラインを作る
 for (let i=0; i < height/40; i++) {
   clines.push(new CLine(i*40, 20));
 }
}

function modelLoaded() {
 console.log('poseNet ready');
}

function gotPoses(poses) {
 //console.log(poses); 
 if (poses.length > 0) {
   pose = poses[0].pose;
   skeleton = poses[0].skeleton;
 }
}

function draw() {
 //キャンバスの下にビデオを表示する
 image(video, 0, 0, 640, 480);  
 background(150);
 
 //1⃣ 左右の木の地面を描く
 fill(168, 67, 0);
 rect(0, 0, 70, height);
 rect(width - 70, 0, 70, height);

 //5⃣ センターラインを描く
 fill(255); //背景を白にセット
 for (let c of clines) {
   c.y += velocity;
   //下から消えたら上に戻す
   if (c.y > height) {
     c.y = c.y - height;
   }
   c.show();
 }
 
 //鼻と肩のトラッキングポイントを表示
 if (pose) {
   fill(255, 0, 0);
   ellipse(pose.nose.x, pose.nose.y, 40);
   fill(0, 0, 255);
   ellipse(pose.rightShoulder.x, pose.rightShoulder.y, 32);
   ellipse(pose.leftShoulder.x, pose.leftShoulder.y, 32);    
 }
}

ステップ❸:街路樹はランダムに生成して表示する

基本的に街路樹もセンターラインと同じように表示しますが、一点異なるのはセンターラインのように一定間隔では不自然だということです。そこで、両側の期は一度に3本まで表示し、その位置をランダムに指定します。そうすると機械的に木が出てくることが避けられます。

  //7⃣ 左右の木を作る
 for (let i=0; i < 3; i++) {
   leftTrees.push(new Tree(35, random(10, height-10)));
   rightTrees.push(new Tree(width-35, random(10, height-10)));
 }

同時に木は3つ表示され、それをheight内のランダムな場所にセットします。これを左右の木の配列にそれぞれ入れます。

表示はセンターラインと同じで、配列それぞれをvelocity分だけずらして表示し、キャンバスの下を出たら先頭に持っていくということです。

街路樹の追加コードは7⃣で示しています。

//=============
// PoseNetの変数
//=============
let video;
let poseNet;
let pose;
let skeleton;

//=============
// 画面スクロールの変数
//=============
let velocity = 4; //6⃣ 画面のスピード
let clines = [];//3⃣ センターライン
let leftTrees = [];//7⃣ 左側の木
let rightTrees = []; //7⃣ 右側の木
let counter = 0;//パラパラDrawの回数カウンター

function setup() {
 createCanvas(640, 480);
 video = createCapture(VIDEO);
 poseNet = ml5.poseNet(video, modelLoaded);
 poseNet.on('pose', gotPoses);
 
 //4⃣ センターラインを作る
 for (let i=0; i < height/40; i++) {
   clines.push(new CLine(i*40, 20));
 }

 //7⃣ 左右の木を作る
 for (let i=0; i < 3; i++) {
   leftTrees.push(new Tree(35, random(10, height-10)));
   rightTrees.push(new Tree(width-35, random(10, height-10)));
 }
}

function modelLoaded() {
 console.log('poseNet ready');
}

function gotPoses(poses) {
 //console.log(poses); 
 if (poses.length > 0) {
   pose = poses[0].pose;
   skeleton = poses[0].skeleton;
 }
}

function draw() {
 //キャンバスの下にビデオを表示する
 image(video, 0, 0, 640, 480);  
 background(150);
 
 //1⃣ 左右の木の地面を描く
 fill(168, 67, 0);
 rect(0, 0, 70, height);
 rect(width - 70, 0, 70, height);

 //5⃣ センターラインを描く
 fill(255); //背景を白にセット
 for (let c of clines) {
   c.y += velocity;
   //下から消えたら上に戻す
   if (c.y > height) {
     c.y = c.y - height;
   }
   c.show();
 }
 
 //7⃣ 左の木を描く
 for (let t of leftTrees) {
   t.y += velocity;
   //下から消えたら上に戻す
   if (t.y > height)
     t.y = 0;
   t.show();
 }
 //7⃣ 右の木を描く
 for (let t of rightTrees) {
   t.y += velocity;
   //下から消えたら上に戻す
   if (t.y > height)
     t.y = 0;
   t.show();
 }
 
 //鼻と肩のトラッキングポイントを表示
 if (pose) {
   fill(255, 0, 0);
   ellipse(pose.nose.x, pose.nose.y, 40);
   fill(0, 0, 255);
   ellipse(pose.rightShoulder.x, pose.rightShoulder.y, 32);
   ellipse(pose.leftShoulder.x, pose.leftShoulder.y, 32);    
 }
}

これでセンターラインは等間隔で動き、街路樹はランダムに両側に表示されるようになりました。

画像6

次回は自動車をこの画面に表示させ、ボディートラッキングの肩のラインによって左右に動かすロジックを加えます。


この記事が気に入ったらサポートをしてみませんか?