見出し画像

【Three.js】Webカメラから取得したRGB情報を使って3次元空間上にパーティクルを表示してみる【Shader】

こんにちは。デザイニウムのBBOY/エンジニアの平澤@eatora22)です。今回は、Webカメラから取得したRGB情報を用いて、Webブラウザ上でパーティクル(大量の粒子)を表示してみたいと思います。

はじめに

まずはこちらの動画をご覧ください。

Webカメラから取得した映像が3次元空間にパーティクルで描画されています。また、以下URLより実際に試すこともできるのでぜひチェックしてみてください(一部スマホだと見れないかも)。

コード解説

今回はソースコードもGitHub上で公開しています。本記事では一部コードを抜粋して解説していきたいと思います。


three.jsのカメラやライト等の設定は基本的なものなので省略して、Webカメラ映像の取得部分から紹介します。<video>要素を作成しMediaDevices.getUserMedia()を使用してストリームの取得に成功したらパーティクルの作成処理に移行します。

webCam = document.createElement('video');
webCam.id = 'webcam';
webCam.autoplay = true;
webCam.width    = 640;
webCam.height   = 480;

const option = {
   video: true,
   audio: false,
}

// Get image from camera
media = navigator.mediaDevices.getUserMedia(option)
.then(function(stream) {
   webCam.srcObject = stream;
   createParticles();
}).catch(function(e) {
   alert("ERROR: " + e.message);
});


ただし、パーティクル作成関数内部では先行して<canvas>要素に画像を描画してImageDataオブジェクトを取得します。

const w = image.width;
const h = image.height;

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

canvas.width = w;
canvas.height = h;

ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, w, h);


取得したImageDataオブジェクトのピクセルサイズに合わせてパーティクルを作成していきます。細かい部分は正直そこまで把握していないのですが、three.jsのGeometryではなくBufferGeometryを使うことで最適なGPU処理(頂点データへのアクセスを効率化)ができるようです。最後にBufferGeometryに対してsetAttributeを使い頂点のposition&color属性を追加します。

const geometry = new THREE.BufferGeometry();
const vertices_base = [];
const colors_base = [];

const width = imageData.width;
const height = imageData.height;

// Set particle info
for (let y = 0; y < height; y++) {
   for (let x = 0; x < width; x++) {
       const posX = 0.03*(-x + width / 2);
       const posY = 0; //0.1*(-y + height / 2)
       const posZ = 0.03*(y - height / 2);
       vertices_base.push(posX, posY, posZ);

       const r = 1.0;
       const g = 1.0;
       const b = 1.0;
       colors_base.push(r, g, b);
   }
}
const vertices = new Float32Array(vertices_base);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const colors = new Float32Array(colors_base);
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

ちなみにGeometryとPointsMaterialを使ってもっと簡単にパーティクルを作成することもできますが、頂点色が1色に限定されてしまうようです。各頂点ごとにPontMaterialを生成する荒業もあるのですが処理が非常に重くなってしまいます(無駄にARですが下記動画も参照)。


続いてShaderMaterialの設定をします。WebGLではシェーダーを扱うためにGLSL(OpenGL Shading Language)という言語を使用します。ShaderMaterialによりattributeとuniformの組み込み変数が使用可能となりコーディングが少し楽になるようです(シェーダーの記述自体は後ほど)。最後に、用意したBufferGeometryとShaderMaterialを使ってPointsクラスを作成しsceneに追加します。

// Set shader material
const material = new THREE.ShaderMaterial({
   uniforms: {
       time: {
           type: 'f',
           value: 0.0
       },
       size: {
           type: 'f',
           value: 5.0
       }
   },
   vertexShader: vertexSource,
   fragmentShader: fragmentSource,
   transparent: true,
   depthWrite: false,
   blending: THREE.AdditiveBlending
});

particles = new THREE.Points(geometry, material);
scene.add(particles);


こちらでは先ほど省略したシェーダーの記述(テンプレートリテラルを利用)について紹介します。前半のvertexShaderでは頂点の座標やサイズを設定し、後半のfragmentShaderでは色の塗り方を決定します。少し工夫している点として、ピクセルのRGB平均からグレースケール値を算出し頂点サイズの計算に利用しています。また、varying変数として値を保持しfragmentShaderへと渡します。後半ではその受け取ったグレースケール値を使ってパーティクルを描画するかしないか(透明度)を決定します。これにより白壁の部屋等でデモページを開くと人や物体だけが浮き出たように描画することができます。

const vertexSource = `
attribute vec3 color;
uniform float time;
uniform float size;
varying vec3 vColor;
varying float vGray;
void main() {
   // To fragmentShader
   vColor = color;
   vGray = (vColor.x + vColor.y + vColor.z) / 3.0;

   // Set vertex size
   gl_PointSize = size * vGray * 3.0;

   // Set vertex position
   gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`;

const fragmentSource = `
varying vec3 vColor;
varying float vGray;
void main() {
   float gray = vGray;

   // Decide whether to draw particle
   if(gray > 0.5){
       gray = 0.0;
   }else{
       gray = 1.0;
   }

   // Set vertex color
   gl_FragColor = vec4(vColor, gray);
}
`;

シェーダーはまだ不慣れなのですが上記のコードはもっと高速化できそうな気がします。また、変数の種類やvertexShaderにおける座標変換の流れは以下の記事も参考にさせていただきました。


最後に、レンダリングのループ処理内で頂点情報を更新します。ImageDataオブジェクトよりRGB値を取得して頂点色を更新、またこちらでもグレースケール値を計算して頂点座標の奥行に反映しています。

const imageData = getImageData(webCam);
const length = particles.geometry.attributes.position.count;
for (let i = 0; i < length; i++) {
   const index = i * 4;
   const r = imageData.data[index]/255;
   const g = imageData.data[index+1]/255;
   const b = imageData.data[index+2]/255;
   const gray = (r+g+b) / 3;

   particles.geometry.attributes.position.setY( i , gray*10);
   particles.geometry.attributes.color.setX( i , r);
   particles.geometry.attributes.color.setY( i , g);
   particles.geometry.attributes.color.setZ( i , b);
}
particles.geometry.attributes.position.needsUpdate = true;
particles.geometry.attributes.color.needsUpdate = true;


以上、コードを一部抜粋して解説しました。今回のデモ作成にあたっては以下の記事も参考にさせていただきました。

応用例

ここでは上記デモの応用例として、AR.jsを使ってパーティクルをARで表示してみたいと思います。先述したようにARにする意味は特にありません(意味ばかり追い求めていては世の中つまらなくなってしまいます)。

先ほど紹介した動画と比べるとシェーダーを利用しているおかげかパーティクルがサクサク動いています(頂点数も増やしています)。仕組みとしてはスマホ画面に映しているマーカーをPC側のWebカメラに映すことでARの位置合わせを行っています。AR.js側でWebカメラ映像を取得しているせいかthree.js側でピクセル情報の取得ができなかったので、実はWebカメラを2台使用しています(AR.js内のコードを修正したら1台で済むかもしれません)。ついでにthree.jsのAudioAnalyserを利用してオーディオファイルから周波数を取得しパーティクルの座標に反映させています。

WebARの活用方法についてはぜひ以下の記事もチェックしてみてください。また、TensorFlow.jsの技術と組み合わせても面白くなりそうです。

さいごに

以上、Webカメラから取得したRGB情報によるパーティクル描画方法について解説しました。ビジュアル表現を向上させるためにはシェーダーをもっと勉強しないといけなさそうです……

編集後記

こんにちは、広報のマリコです。いつも様々な映像を作り出しているBBOYエンジニア平澤が、今回はウェブカメラをつかってブラウザ上でパーティクルを表示させるというR&Dでした✨パーティクル??と単語を早速ググった私ですが(大量の粒子という意味でした!)記事内のリンクをクリックするだけですぐ体験できました。デザイニウムでは様々な最新のセンサーをつかったコンテンツがたくさんありますが、今回のようにウェブカメラをつかって誰でも簡単に楽しめるというコンテンツもたくさんあります。SNSでは平澤をはじめとするメンバーのR&Dをリアルタイムで発信しているので、是非そちらもチェックしてみてくださいね❗

The Designium.inc
オフィシャルサイト
Interactive website
Designium Dance
Twitter
Facebook



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