Movatterモバイル変換


[0]ホーム

URL:


科学技術計算ツール

CAE科学技術計算のためのオンライン計算ツール

フォローする

【科学技術計算講座ミニ】四分木と空間分割②

2021/7/2

前回の四分木と空間分割の続きです。

四分木と空間分割について解説します。空間に粒子を配置した場合の空間分割を四分木を使って行います。JavaScriptとp5.jsでブラウザ上で実際に動くプログラムを作ります。
cattech-lab.com
2021-07-01 10:20

四分木による空間分割のプログラム(続き)

四分木クラスのメソッド

四分木クラスにメソッドを追加していきます。次の5つのメソッドを作ります。

①四分木に粒子を挿入するメソッド
②ノードに粒子が含まれるかを判定するメソッド
③ノードを分割するメソッド
④粒子が所属するノードを見つけるメソッド
⑤四分木を走査するメソッド

四分木に粒子を挿入するメソッド

まず、四分木に粒子を挿入するメソッドinsertを作ります。

  insert(p) {    if (!this.include(p)) {      return;    }    if (this.sw === null)  {      if (this.bucket.length < this.limit) {        this.bucket.push(p);        return;      } else {        this.divide();      }    }    this.sw.insert(p);    this.se.insert(p);    this.nw.insert(p);    this.ne.insert(p);  }

引数pは粒子オブジェクトです。

まず、最初のif文、if (!this.include(p))でノードに粒子が含まれるかどうかを判定し、含まれない場合は何もせずにreturnしています。includeはノードに粒子が含まれるかの判定メソッドで、別途定義します。

次のif文ではthis.swnull、つまり分割されているかどうかを判定しています。子ノードswnullであれば分割されていない末端ノードを意味します。このとき、bucket配列の大きさがlimitより小さければ、bucketに粒子pを追加します。もし、limitを超えていれば、後で定義するdivideメソッドでノードを分割します。

最後に4つの子ノードにpを渡して再帰的に挿入操作を繰り返します。これで、末端のノードに粒子が格納されます。

※このプログラムでは隣り合う分割ノードのちょうど境界線上に粒子があると、どちらのノードにも粒子が格納されます。もし、どちらか一方のノードにのみ粒子を入れたい場合は、以下で説明するincludeメソッドの判定を変更するか、子ノードへの挿入部分に分岐を設けるなどの変更が必要です。

ノードに粒子が含まれるかを判定するメソッド

先程出てきたノードに粒子が含まれるかの判定メソッドincludeを定義します。

  include(p) {    return (      (this.region.left <= p.x && p.x <= this.region.right) &&      (this.region.bottom <= p.y && p.y <= this.region.top)    );  }

粒子px座標とy座標がノードの領域に入っていたらtrueが返ります。

ノードを分割するメソッド

ノードを分割するメソッドdivideです。

  divide() {    const x = this.region.left;    const y = this.region.bottom;    const width = 0.5 * this.region.width;    const height = 0.5 * this.region.height;    this.sw = new QuadTree(x, y, width, height, this.limit);    this.se = new QuadTree(x + width, y, width, height, this.limit);    this.nw = new QuadTree(x, y + height, width, height, this.limit);    this.ne = new QuadTree(x + width, y + height, width, height, this.limit);    for (let i = 0, n = this.bucket.length; i < n; i++) {      const p = this.bucket[i];      this.sw.insert(p);      this.se.insert(p);      this.nw.insert(p);      this.ne.insert(p);    }    this.bucket = [];  }

xyはノードの左下角の座標を表し、widthheightはそれぞれ半分にして、新しくQuadTreeの四分木オブジェクトを子ノードに作成しています。

後半のfor文はノードに含まれる粒子に対するループで、各粒子を分割後の子ノードに振り分けています。振り分けたら、最後にbucketは空にしておきます。

粒子が所属するノードを見つけるメソッド

今回の問題ではマウスの位置のノードを見つける必要があります。そこで、ある位置のノードを返すメソッドfindを作ります。

  find(p) {    if (!this.include(p)) {      return null;    }    if (this.sw === null) {      return this;    }    return (this.sw.find(p) ||            this.se.find(p) ||            this.nw.find(p) ||            this.ne.find(p));  }

引数pは粒子オブジェクトですが、ここではマウスカーソルの位置を表していると思ってください。

まず最初のif文でpが含まれなければnullを返すようにします。次のif文で末端ノードであれば、そのノードを返します。

最後に、子ノードがあれば再帰的にfindを繰り返します。ここで、各ノードのfind||でつながれています。普通は、||ORを意味しますが、JavaScriptでは

a = 5 || null

の場合、a5となります。つまり、pが含まれない子ノードからはnullが返ってきて、pが含まれる子ノードからはそのノードが返されるため、最終的にpが位置するノードが返されることになります。

四分木を走査するメソッド

最後に、四分木を走査して何かの処理をするメソッドtraverseを定義しておきます。

  traverse(func) {    if (this.sw === null) {      func(this);    } else {      this.sw.traverse(func);      this.se.traverse(func);      this.nw.traverse(func);      this.ne.traverse(func);    }  }

ここでは処理は末端のノードでのみ行います。行う処理は引数funcとして関数で受け取ります。

if文は末端ノードかどうかの判定で、末端ノードならfuncにそのノードthisを渡して実行します。子ノードがあれば再帰的に走査していきます。

p5.jsのセットアップ

次にp5.js用にsetup関数を定義します。setup関数はp5.js内で最初に一度だけ呼ばれます。

function setup() {  createCanvas(canvasWidth, canvasHeight);  qtree = new QuadTree(0, 0, 1, 1, 5);  randomSeed(1);  p = new Array(2000);  for (let i = 0, n = 2000; i < n; i++) {    const r = 0.5 * random();    const t = 2 * Math.PI * random();    const x = r * Math.cos(t) + 0.5;    const y = r * Math.sin(t) + 0.5;    p[i] = new Particle(x, y);    qtree.insert(p[i]);  }}

createCanvasで描画領域を定義します。

次に、qtreeに四分木オブジェクトを作成します。ここで、ルートノードは座標(0,0)に縦横1×1の大きさの空間を作っています。またノードの粒子数制限は5個までとします。

randomSeed(1)はp5.jsの関数ですが乱数発生のシードを設定しています(これを指定しない場合、実行のたびに乱数の結果が変わります)。

粒子pは配列として2000個粒子オブジェクトを定義します。粒子の座標$(x,y)$は

$$ x = r \cos(\theta)+0.5$$

$$y = r \sin(\theta)+0.5$$

とし、$r$と$\theta$をrandom()を使って乱数で計算しています。こうすると空間の中央に集中した分布を持つ粒子配置になります。

そしてqtree.insertで先程作成した四分木オブジェクトに挿入しています。

p5.jsの描画

次にp5.jsで画面に描画を行う関数drawを作成します。

function draw() {  const scale = 600;  background(220);  // particle  noStroke();  fill("blue");  drawParticle(p, scale);  // tree node  stroke("black");  noFill();  qtree.traverse(function(node) {    drawNode(node, scale);  });  // highlight particle on mouse   const x = mouseX / scale;  const y = (canvasHeight - mouseY) / scale;  const pos = new Particle(x, y);  const node = qtree.find(pos);  if (node !== null) {    stroke("red");    drawNode(node, scale);      noStroke();    fill("red");    drawParticle(node.bucket, scale)  }}

draw関数の詳細は以前の粒子法の講座を参照してください。

drawParticleは粒子を描画する関数で後で説明します。

qtree.traverseの箇所は四分木の末端ノードの四角形を描画しています。drawNodeも後述します。

マウスカーソル位置のハイライト部分が最後にあります。マウスカーソルの位置は、p5.jsではmouseXmouseYで画面ピクセルの座標として取得できます。これを元に空間座標(x,y)に変換しています。

空間座標でのマウス位置はposとして粒子オブジェクトを使って作成します。そしてqtree.find(pos)でその位置にある末端ノードを返しています。nodenullでなければ、赤色でそのノードとノードに含まれる粒子を描画してハイライト処理しています。

描画用の関数

最後に描画用の関数drawParticledrawNodeを説明します。

function drawParticle(p, scale) {  for (let i = 0, n = p.length; i < n; i++) {    const x = p[i].x * scale;    const y = canvasHeight - p[i].y * scale;    ellipse(x, y, 5);  }}function drawNode(node, scale) {  const x = node.region.left * scale;  const y = canvasHeight - node.region.top * scale;  const width = node.region.width * scale;  const height = node.region.height * scale;  rect(x, y, width, height);}

drawParticleは粒子を描画する関数で、粒子配列pと空間座標をキャンバスのピクセル座標に変換する係数scaleを引数に受け取ります。座標変換を行って、ellipse関数で円を描きます。

drawNodeはノードの四角形を描く関数で、同じく座標変換を行ってrect関数で四角形を表示します。

ellipserectはp5.jsの関数です。

全体のプログラム

ここまで説明したプログラム全体を載せておきます。

プログラムの表示
"use strict";// global variables ***************************************************const canvasWidth = 600, canvasHeight = 600;let qtree;let p;// class *********************************************************class Rectangle {  constructor(x, y, width, height) {    this.width = width;    this.height = height;    this.left = x;    this.right = x + width;    this.bottom = y;    this.top = y + height;  }}class Particle {  constructor(x, y) {    this.x = x;    this.y = y;  }}class QuadTree {  constructor(x, y, width, height, limit) {    this.region = new Rectangle(x, y, width, height);    this.sw = null;    this.se = null;    this.nw = null;    this.ne = null;    this.bucket = [];    this.limit = limit;  }  insert(p) {    if (!this.include(p)) {      return;    }    if (this.sw === null)  {      if (this.bucket.length < this.limit) {        this.bucket.push(p);        return;      } else {        this.divide();      }    }    this.sw.insert(p);    this.se.insert(p);    this.nw.insert(p);    this.ne.insert(p);  }  include(p) {    return (      (this.region.left <= p.x && p.x <= this.region.right) &&      (this.region.bottom <= p.y && p.y <= this.region.top)    );  }  divide() {    const x = this.region.left;    const y = this.region.bottom;    const width = 0.5 * this.region.width;    const height = 0.5 * this.region.height;    this.sw = new QuadTree(x, y, width, height, this.limit);    this.se = new QuadTree(x + width, y, width, height, this.limit);    this.nw = new QuadTree(x, y + height, width, height, this.limit);    this.ne = new QuadTree(x + width, y + height, width, height, this.limit);    for (let i = 0, n = this.bucket.length; i < n; i++) {      const p = this.bucket[i];      this.sw.insert(p);      this.se.insert(p);      this.nw.insert(p);      this.ne.insert(p);    }    this.bucket = [];  }  find(p) {    if (!this.include(p)) {      return null;    }    if (this.sw === null) {      return this;    }    return (this.sw.find(p) ||            this.se.find(p) ||            this.nw.find(p) ||            this.ne.find(p));  }  traverse(func) {    if (this.sw === null) {      func(this);    } else {      this.sw.traverse(func);      this.se.traverse(func);      this.nw.traverse(func);      this.ne.traverse(func);    }  }}// functions ************************************************************function setup() {  createCanvas(canvasWidth, canvasHeight);  qtree = new QuadTree(0, 0, 1, 1, 5);  randomSeed(1);  p = new Array(2000);  for (let i = 0, n = 2000; i < n; i++) {    const r = 0.5 * random();    const t = 2 * Math.PI * random();    const x = r * Math.cos(t) + 0.5;    const y = r * Math.sin(t) + 0.5;    p[i] = new Particle(x, y);    qtree.insert(p[i]);  }}function draw() {  const scale = 600;  background(220);  // particle  noStroke();  fill("blue");  drawParticle(p, scale);  // tree node  stroke("black");  noFill();  qtree.traverse(function(node) {    drawNode(node, scale);  });  // highlight particle on mouse   const x = mouseX / scale;  const y = (canvasHeight - mouseY) / scale;  const pos = new Particle(x, y);  const node = qtree.find(pos);  if (node !== null) {    stroke("red");    drawNode(node, scale);      noStroke();    fill("red");    drawParticle(node.bucket, scale)  }}function drawParticle(p, scale) {  for (let i = 0, n = p.length; i < n; i++) {    const x = p[i].x * scale;    const y = canvasHeight - p[i].y * scale;    ellipse(x, y, 5);  }}function drawNode(node, scale) {  const x = node.region.left * scale;  const y = canvasHeight - node.region.top * scale;  const width = node.region.width * scale;  const height = node.region.height * scale;  rect(x, y, width, height);}

プログラムの実行

プログラムの実行も以前の通りでindex.htmlを作ってブラウザで開いて実行します。

<!DOCTYPE html><html>  <head>    <meta charset="utf-8">    <title>Quad Tree</title>  </head>  <body>    <script type="text/javascript" src="p5.min.js"></script>    <script type="text/javascript" src="quad_tree.js"></script>  </body></html>

実行結果はこちら(別ウインドウが開きます)

四分木による粒子空間の分割

最後に

今回は四分木を使った空間分割について2回に分けて説明しました。このような空間分割法は実際様々な分野で使われています。

ここで説明した方法はオーソドックスな方法ですが、実際のアプリケーションではさらなる高速化が求められる場合があります。

例えばマウスが位置するノードを見つける箇所ですが、今回のプログラムでは四分木をルートノードからたどっています。これだとある程度の計算量が必要となります。そこで、指定した位置のノードをより少ない計算量で求めるアルゴリズム(例えば、Z階数曲線(Z-order curve))なども考えられています。ここでは詳しくは述べませんが、様々な改良を加えることにより、より高速に計算できるようになります。


全体の目次

スポンサーリンク
科学技術計算のご相談は「キャットテックラボ」へ

科学技術計算やCAEに関するご相談、計算用プログラムの開発などお困りのことは「株式会社キャットテックラボ」へお問い合わせください。

お問い合わせはこちら

シェアする

フォローする

TwitterYouTube
スポンサーリンク
CATCFDzero
CATTHM
CATGRP
科学技術計算サービス
計算サイト作成サービス
スポンサーリンク
科学技術計算プログラム開発サービス
科学技術計算FAQサービス

[8]ページ先頭

©2009-2025 Movatter.jp