end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

hands-on three.js

hands-on three.js + cannon.js - end0tknr's kipple - web写経開発 の更に前段です。

目次

1. cubeのグルーピング

以下の実際の動作は、github pagesをご覧ください。 https://end0tknr.github.io/sandbox/threejs_cannonjs_misc/test_threejs.html

f:id:end0tknr:20210813075300p:plain

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r109/examples/js/controls/OrbitControls.js"></script>
  
</head>
<body>
  <div id="stage"></div>

  <script>
  (function() {
    'use strict';

    var width = 500;
    var height = 250;

      var scene = new THREE.Scene();

      // mesh 物体
      var head = new THREE.Mesh(
          new THREE.BoxGeometry(20, 20, 20),
          new THREE.MeshLambertMaterial({ color: 0xff0000 })
      );
      head.position.set(0, 40, 0);
      // scene.add(head);
      
      var body = new THREE.Mesh(
          new THREE.BoxGeometry(40, 60, 40),
          new THREE.MeshLambertMaterial({ color: 0xff0000 })
      );
      body.position.set(0, 0, 0);
      // scene.add(body);

      // 頭部と体をグループ化
      var person = new THREE.Group();
      person.add(head);
      person.add(body);
      scene.add(person);

      // light
      var light = new THREE.DirectionalLight(0xffffff, 1);
      light.position.set(0, 100, 30);
      scene.add(light);
      // 環境光源
      var ambient = new THREE.AmbientLight(0x404040);
      scene.add(ambient);

      // camera
      var camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000);
      camera.position.set(200, 100, 300);
      camera.lookAt(scene.position);

      // helper
      var gridHelper = new THREE.GridHelper(200, 50);
      scene.add(gridHelper);
      var axisHelper = new THREE.AxisHelper(1000);
      scene.add(axisHelper);
      var lightHelper = new THREE.DirectionalLightHelper(light, 20);
      scene.add(lightHelper);

        // controls - マウスによる操作
        var controls = new THREE.OrbitControls(camera);
        // controls.autoRotate = true;

      // renderer
      var renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.setSize(width, height);
      renderer.setClearColor(0xefefef);
      renderer.setPixelRatio(window.devicePixelRatio);
      document.getElementById('stage').appendChild(renderer.domElement);
      
      function render() {
          requestAnimationFrame(render);

          person.rotation.y += 0.01;

          controls.update();
          
          renderer.render(scene, camera);
      }
      render();
  })();
  </script>
  
</body>
</html>

2. 様々な形状の表示

https://ozateck.sakura.ne.jp/wordpress/category/three-js/

上記urlからの写経です。

https://end0tknr.github.io/sandbox/threejs_cannonjs_shimeji/index.html

f:id:end0tknr:20210816050639p:plain

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <!-- refer to https://ozateck.sakura.ne.jp/wordpress/ -->
  <title>Hello Three.js</title>
  <style>
    body{
    margin: 0;
    overflow: hidden;
    }
  </style>
</head>
<body>
  <div id="stage"></div>
  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js"></script>
  <!-- 処理速度やフレームレートを確認する為 -->
  <script src="https://cdn.jsdelivr.net/npm/stats-js@1.0.1/build/stats.min.js"></script>
  <!-- マウスによるカメラ操作 -->
  <script
    src="https://cdn.jsdelivr.net/npm/three@0.105.2/examples/js/controls/TrackballControls.js">
  </script>
  <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
  <script src="js/app.js"></script>
</body>
</html>
// refer to http://ozateck.sakura.ne.jp/wordpress

var width  = 480;
var height = 320;
var fov    = 60;
var aspect = width / height;
var near   = 1;
var far    = 1000;

//座標系   Z↑ /Y
//          │/
//         原点─→X

// Scene - 各オブジェクト(円、四角等)を表示
var scene = new THREE.Scene();
 
// Axes - 空間のx, y, zを原点から表す
var axes = new THREE.AxisHelper(20);
scene.add(axes);
 
// Camera - 空間の視点をを決める.
// コンストラクタの引数はそれぞれ、
// 視野角(fov)、縦横幅比率(aspect)、描画領域(近い方)、描画領域(遠い方)
var camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 100, 200);
camera.lookAt(scene.position);

// Controls - マウスによる操作.
// google chromeで以下のerrorとなる場合、その更に下のurlが参考になります。
// [Intervention] Unable to preventDefault inside passive event listener
// due to target being treated as passive.
// https://note.com/cfbif/n/n92195df174bf
var controls = new THREE.TrackballControls(camera);
controls.rotateSpeed = 5.0; //回転速度
controls.zoomSpeed = 0.5;//ズーム速度
controls.panSpeed = 2.0;//パン速度


// Light - 空間への光の方向を決める
var directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, 0.7, 0.7);
scene.add(directionalLight);
 
// Stats - 処理速度やフレームレートを確認する為
var stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = "absolute";
stats.domElement.style.left = "0px";
stats.domElement.style.top  = "0px";
document.getElementById("stage").appendChild(stats.domElement);

// Plane

// Three.js のobjectには、Geometry, Material, Mesh を使う。
// Geometryはオブジェクトの形や大きさ. Materialにはオブジェクトの色や質感を表す。
// Meshオブジェクトに、これらの2つのオブジェクトを指定しインスタンス化

var geometry = new THREE.PlaneGeometry(200, 250);
var material = new THREE.MeshBasicMaterial({color: 0x666666});
var plane = new THREE.Mesh(geometry, material);
plane.position.set(0, 0, 0);
plane.rotation.set(-90 * Math.PI / 180, 0, 0);
scene.add(plane);


// Particles
var geometry = new THREE.Geometry();
var material = new THREE.PointsMaterial({color: 0xffffff,
                     size: 4,
                     vertexColors: true});
for(var x=0; x<10; x++){
    for(var y=0; y<10; y++){
    var particle = new THREE.Vector3(x*10, y*10, 0);
    geometry.vertices.push(particle);
    geometry.colors.push(new THREE.Color(Math.random() * 0x00ffff));
    }
}
// 複数の点が、1コとobjectとして扱われます
var points = new THREE.Points(geometry, material);
scene.add(points);


// Cubeを作成し、その後、順に削除
for(var i=0; i<20; i++){
    var x = getRandom(-80, 80);
    var y = getRandom(  0, 80);
    var z = getRandom(-80, 80);

    var mesh = new THREE.Mesh(
    new THREE.BoxBufferGeometry(10,10,10),
    new THREE.MeshBasicMaterial({color: 0xccffcc, wireframe: true}) );
    mesh.position.set(x, y, z);
    
    mesh.name = "tmpCube";
    scene.add(mesh);
}
 
function getRandom(min, max){
    return Math.floor(Math.random()*(max-min+1))+min;
}
 
function removeCube(){
    var total = scene.children.length;
    for(var i=0; i<total; i++){
    var obj = scene.children[i];
    if(obj.name == "tmpCube"){
        scene.remove(obj);
        return;
    }
    }
}
setInterval(removeCube, 2000);


// Cube
var geometry = new THREE.BoxGeometry(30, 30, 30);
var material = new THREE.MeshNormalMaterial();
var cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 50, 0);
scene.add(cube);
 
// Earth
var txLoader = new THREE.TextureLoader();
var earth = null;
txLoader.load(
    "img/earth.jpg", function(texture){
    var geometry = new THREE.SphereGeometry(30, 30, 30);
    var material = new THREE.MeshBasicMaterial({map:texture, overdraw:0.5});
    earth = new THREE.Mesh(geometry, material);
    earth.position.set(-30, 50, 100);
    scene.add(earth);
    });

// Moon
var moon = null;
txLoader.load(
    "img/moon.jpg", function(texture){
    var geometry = new THREE.SphereGeometry(10, 10, 10);
    var material = new THREE.MeshBasicMaterial({map:texture, overdraw:0.5});
    moon = new THREE.Mesh(geometry, material);
    moon.position.set(50, 50, 50);
    // moon.position.set(100, 50, 0);
    scene.add(moon);
    });

// Renderer - 毎フレーム描画
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(width, height);
renderer.setClearColor(0xcccccc);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById("stage").appendChild(renderer.domElement);
 
// Radian -  360 = 2 * PI  , 180 = PI
var radius = 50;
var degree = 0;

// Loop
loop();
function loop(){
    stats.update();
 
    // Earth, Moon を回転
    degree += 0.5;
    if(360 <= degree) degree = 0;
    
    var radian = degree * Math.PI / 180;
    var x = radius * Math.cos(radian);
    var y = radius * Math.sin(radian);
    if(earth != null){
    earth.rotation.set(0, radian, 0);
    }
    if(moon != null){
    moon.rotation.set(0, radian, 0);
    // 以下の x,y を入れ替えると、逆向きに回転します
    moon.position.set(y-30, 50, x+50);
    }
    // Cube を回転
    cube.rotation.x += 0.05;
    cube.rotation.y += 0.05;

    controls.update();
    
    renderer.render(scene, camera);
    window.requestAnimationFrame(loop ); //再帰呼び出し
};

3. 音声の可視化

https://ozateck.sakura.ne.jp/wordpress/category/three-js/

先程と同様、上記urlからの写経です。

ブラウザ経由で、PCのマイクを使用しますので、 PCに向かって、手を叩くと、緑色の部分に波形として現れます。

ただし、navigator.getUserMedia() は、depricated のようですので、 そのうち、navigator.mediaDevices.getUserMedia() へ書き換える必要があります。

https://end0tknr.github.io/sandbox/threejs_cannonjs_shimeji/index_audio.html

f:id:end0tknr:20210816050700p:plain

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <!-- refer to https://ozateck.sakura.ne.jp/wordpress/ -->
  <title>Hello Three.js</title>
  <style>
    body{
    margin: 0;
    overflow: hidden;
    }
  </style>
</head>
<body>
  <h1>マイクからの入力音声を波形として表示</h1>
  <div id="stage"></div>
  <audio muted></audio>
  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js"></script>
  <!-- 処理速度やフレームレートを確認する為 -->
  <script src="https://cdn.jsdelivr.net/npm/stats-js@1.0.1/build/stats.min.js"></script>
  <!-- マウスによるカメラ操作 -->
  <script
    src="https://cdn.jsdelivr.net/npm/three@0.105.2/examples/js/controls/TrackballControls.js">
  </script>
  <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
  
  <script src="js/app_audio.js"></script>
</body>
</html>
console.log("Hello Three.js!!");

var width  = 480;
var height = 320;
var fov    = 60;
var aspect = width / height;
var near   = 1;
var far    = 1000;

// Scene
var scene = new THREE.Scene();

// Axes
var axes = new THREE.AxisHelper(20);
scene.add(axes);

// Stats
var stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = "absolute";
stats.domElement.style.left = "0px";
stats.domElement.style.top  = "0px";
document.getElementById("stage").appendChild(stats.domElement);

// Plane
var geometry = new THREE.PlaneGeometry(100, 200);
var material = new THREE.MeshBasicMaterial({color: 0x666666});
var plane = new THREE.Mesh(geometry, material);
plane.position.set(0, 0, 0);
plane.rotation.set(-90 * Math.PI / 180, 0, 0);
scene.add(plane);

// Camera
var camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 100, 200);
camera.lookAt(scene.position);

// Light
var directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, 0.7, 0.7);
scene.add(directionalLight);

// Renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(width, height);
renderer.setClearColor(0x333333);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById("stage").appendChild(renderer.domElement);

//==========
// Web Audio
navigator.getUserMedia = 
    navigator.getUserMedia || navigator.webkitGetUserMedia ||
    navigator.mozGetUserMedia || navigator.msGetUserMedia;

navigator.getUserMedia({audio : true}, onSuccess, onError);

function onSuccess(stream){
    console.log("onSuccess");

    // document.querySelector("audio").src = URL.createObjectURL(stream);
    // ↑old. ↓new
    document.querySelector("audio").src =
    window.URL.createObjectURL(new Blob([stream], {type: "application/zip"}))
    
    var audioContext = new AudioContext();
    var analyser     = audioContext.createAnalyser();
    var timeDomain   = new Float32Array(analyser.frequencyBinCount);
    var frequency    = new Uint8Array(analyser.frequencyBinCount);
    audioContext.createMediaStreamSource(stream).connect(analyser);

    console.log("frequency:" + frequency.length);

    loop();
    function loop(){
    analyser.getFloatTimeDomainData(timeDomain);
    analyser.getByteFrequencyData(frequency);
    // update
    updateThree(timeDomain, frequency);
    window.requestAnimationFrame(loop);
    }
}

function onError(e){
    console.log("onError:" + e);
}

// Points
var points;

// Draw
function updateThree(timeDomain, frequency){

    // Stats
    stats.update();

    // Remove Points
    if(points != null) scene.remove(points);
    
    // Particles
    var geom = new THREE.Geometry();
    var material = new THREE.PointsMaterial({
    color: 0xffffff, size: 2, vertexColors: true, transparent: true, opacity: 1.0
    });

    var paddingX = 2;  // パーティクルの間隔
    var offset   = 4;  // 1024を4分割
    var total    = Math.floor(frequency.length / offset);

    var minX     = total * paddingX / -2;  // 開始位置x
    var maxY     = 100;// 最大の高さy

    console.log(total);

    for(var i=0; i<total; i++){

    var particle = new THREE.Vector3(
        minX + i * paddingX,
        Math.max(0, frequency[i*offset] * maxY / 255), 0);
    geom.vertices.push(particle);
    
    var color = new THREE.Color(0x00ff00);
    color.setHSL(color.getHSL().h, color.getHSL().s, color.getHSL().l);
    geom.colors.push(color);
    }
    
    // Add Points
    points = new THREE.Points(geom, material);
    scene.add(points);
    
    // Render
    renderer.render(scene, camera);
}

4. 音声の可視化 その2

「音声の可視化 」の別バージョンです。

PCマイクに向かって、手をたたくと、cubeが追加されます。

反応が悪いようですが、src修正は実施していません。

https://end0tknr.github.io/sandbox/threejs_cannonjs_shimeji/index_audio_2.html

f:id:end0tknr:20210816050714p:plain

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <!-- refer to https://ozateck.sakura.ne.jp/wordpress/ -->
  <title>Hello Three.js</title>
  <style>
    body{
    margin: 0;
    overflow: hidden;
    }
  </style>
</head>
<body>
  <h1>マイクからの(拍手)入力で、Cubeを追加</h1>

  反応が悪いようですが、js srcは修正していません。
  
  <div id="stage"></div>
  <audio muted></audio>
  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js"></script>
  <!-- 処理速度やフレームレートを確認する為 -->
  <script src="https://cdn.jsdelivr.net/npm/stats-js@1.0.1/build/stats.min.js"></script>
  <!-- マウスによるカメラ操作 -->
  <script
    src="https://cdn.jsdelivr.net/npm/three@0.105.2/examples/js/controls/TrackballControls.js">
  </script>
  <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
  
  <script src="js/app_audio_2.js"></script>
</body>
</html>
console.log("Hello Three.js!!");

var width  = 480;
var height = 320;
var fov    = 60;
var aspect = width / height;
var near   = 1;
var far    = 1000;

// Scene
var scene = new THREE.Scene();

// Axes
var axes = new THREE.AxisHelper(20);
scene.add(axes);

// Stats
var stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = "absolute";
stats.domElement.style.left = "0px";
stats.domElement.style.top  = "0px";
document.getElementById("stage").appendChild(stats.domElement);

// Plane
var geometry = new THREE.PlaneGeometry(100, 200);
var material = new THREE.MeshBasicMaterial({color: 0x666666});
var plane = new THREE.Mesh(geometry, material);
plane.position.set(0, 0, 0);
plane.rotation.set(-90 * Math.PI / 180, 0, 0);
scene.add(plane);

// Camera
var camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 100, 200);
camera.lookAt(scene.position);

// Light
var directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, 0.7, 0.7);
scene.add(directionalLight);

// Renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(width, height);
renderer.setClearColor(0x333333);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById("stage").appendChild(renderer.domElement);

//==========
// Web Audio
navigator.getUserMedia = 
    navigator.getUserMedia || navigator.webkitGetUserMedia || 
    navigator.mozGetUserMedia || navigator.msGetUserMedia;

navigator.getUserMedia({audio : true}, onSuccess, onError);

function onSuccess(stream){
    console.log("onSuccess");

    //document.querySelector("audio").src = URL.createObjectURL(stream);
    // ↑old. ↓new
    document.querySelector("audio").src =
    window.URL.createObjectURL(new Blob([stream], {type: "application/zip"}))

    
    var audioContext = new AudioContext();
    var analyser     = audioContext.createAnalyser();
    var timeDomain   = new Float32Array(analyser.frequencyBinCount);
    var frequency    = new Uint8Array(analyser.frequencyBinCount);
    audioContext.createMediaStreamSource(stream).connect(analyser);

    //==========
    // 拍手を検出するクラス
    // 第一引数:次の拍手までの待機時間(ミリ秒)
    // 第二引数:反応する拍手音量の閾値
    var cManager = new CrapManager(500, 2);

    loop();
    function loop(){
    // Analiser
    analyser.getFloatTimeDomainData(timeDomain);
    analyser.getByteFrequencyData(frequency);
    
    // Stats
    stats.update();

    // 拍手の判定

    if(cManager.trigger(timeDomain, frequency) == true){
        console.log("start addCube()");
        // Cubeを追加する
        addCube();
    }
    
    // Render
    renderer.render(scene, camera);
    // Request
    window.requestAnimationFrame(loop);
    }
}

function onError(e){
    console.log("onError:" + e);
}

//==========
// Cubeをランダムで配置する関数
function addCube(){
    var x = getRandom(-80, 80);
    var y = getRandom(0, 80);
    var z = getRandom(-80, 80);
    var cube = createCube(5, 0xffffff, x, y, z);
    scene.add(cube);
}

//==========
// Cubeを指定のサイズ、色、座標で作り出す関数
function createCube(size, color, x, y, z){
    var geometry = new THREE.BoxBufferGeometry(size, size, size);
    var material = new THREE.MeshBasicMaterial({color: 0xccffcc, wireframe: true});
    var mesh = new THREE.Mesh(geometry, material);
    mesh.position.set(x, y, z);
    return mesh;
}

function getRandom(min, max){
    return Math.floor(Math.random()*(max-min+1))+min;
}

//==========
// CrapManager
class CrapManager{
    constructor(interTime, interThreshold){
    this.interTime      = interTime;
    this.interThreshold = interThreshold;
    this.interFlg       = false;
    }
    
    trigger(timeDomain, frequency){
    if(this.interFlg == true) return false;
    
    var offset = 30;
    var total  = Math.floor(frequency.length / offset);
    var volume = 0;
    for(var i=0; i<total; i+=offset){
        volume += frequency[i * offset];
    }
    
    if(volume < this.interThreshold) return false;

    this.interFlg = true;
    setInterval(()=>{this.interFlg = false;}, this.interTime);
    return true;
    }
}