end0tknr's kipple - web写経開発

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

hands-on three.js + cannon.js

本来、 hands-on three.js + cannon.js - 3D迷路 (like fps ?) - end0tknr's kipple - web写経開発 より前に、記載すべき入門的なものですが、今更、post。

目次

three.js + cannon.js 1 - 物体の落下

https://end0tknr.github.io/sandbox/threejs_cannonjs_misc/test_cannonjs_1.html

f:id:end0tknr:20210813075321p:plain

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <!-- refer to https://liginc.co.jp/378458 -->
  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
  
  <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>

  <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
  
</head>
<body>
  <div id="stage"></div>

  <div class="kakudo">
    手玉を突く角度
    <input type="range" class="range js-range"
           value="0" step="0.1" min="-4" max="4">
    <button class="js-fire">発射</button>
    </div> <!-- #kakudo -->

  <script>
    (function() {
        var controls;
        var scene;
        var camera;
        var renderer;
        
        var phySphere;
        var phySphere2;
        var phySphere3;
        
        var viewSphere;
        var viewSphere2;
        var viewSphere3;

        var rand = Math.random()*20 - 10;       // 的玉の位置

        var world = setPhy();
        setView();
        animate();


        function setPhy() {
    
            var world = new CANNON.World();     // 物理世界
            world.gravity.set(0, -9.82, 0);     // 重力

            //「衝突可能性」の剛体同士を探索
            world.broadphase = new CANNON.NaiveBroadphase();
    
            world.solver.iterations = 5;        // 反復計算回数
            world.solver.tolerance = 0.1;       // 許容値
  
            //地面
            var groundMat = new CANNON.Material('groundMat');
            //質量定義
            var phyPlane = new CANNON.Body({
                mass: 0,
                material: groundMat
            });

            phyPlane.addShape(new CANNON.Plane());
            // X軸に90度回転
            phyPlane.quaternion.setFromAxisAngle(
                new CANNON.Vec3(1, 0, 0), -Math.PI / 2 );
            world.add(phyPlane);
  
            var sphereMat = new CANNON.Material('sphereMat');
            //質量定義
            phySphere = new CANNON.Body({
                mass: 1,
                material: sphereMat
            });

            
            phySphere.addShape(new CANNON.Sphere(1));
            phySphere.position.set(20, 1, 0);   //位置
            phySphere.velocity.set(0, 0, 0);    //角速度
            phySphere.angularDamping = 0.1;     //減衰率
            world.add(phySphere);
  
            var sphereMat2 = new CANNON.Material('sphereMat2');
            //質量定義
            phySphere2 = new CANNON.Body({
                mass: 1, material: sphereMat2   });
            
            phySphere2.addShape(new CANNON.Sphere(1));
            phySphere2.position.set(10, 1, 0);  //位置
            phySphere2.velocity.set(0, 0, 0);   //角速度
            phySphere2.angularDamping = 0.1;    //減衰率
            world.add(phySphere2);
  
            var sphereMat3 = new CANNON.Material('sphereMat3');
            //質量定義
            phySphere3 = new CANNON.Body({
                mass: 2,  material: sphereMat3  });
            
            phySphere3.addShape(new CANNON.Sphere(2));
            phySphere3.position.set(-10, 2, rand);      //位置
            phySphere3.velocity.set(0, 0, 0);           //角速度
            phySphere3.angularDamping = 0.1;            //減衰率
            world.add(phySphere3);
  
            //SphereとSphere2が接触した際のContactMaterial
            var sphereSphereCM = new CANNON.ContactMaterial(
                sphereMat,
                sphereMat2,
                {contactEquationRelaxation: 3,          //接触式の緩和性
                 contactEquationStiffness: 10000000,    //接触式の剛性
                 friction: 0.3,                         //摩擦係数
                 frictionEquationRelaxation: 3,         //摩擦式の剛性
                 frictionEquationStiffness: 10000000,   //摩擦式の緩和性
                 restitution: 0.3                       //反発係数
                }
            );
            world.addContactMaterial(sphereSphereCM);
  
            //地面とSphereが接触した際のContactMaterial
            spherePlaneCM = new CANNON.ContactMaterial(
                groundMat,
                sphereMat,
                {friction: 0,   //摩擦係数
                 restitution: 0 //反発係数
                }
            );
            world.addContactMaterial(spherePlaneCM);
  
            //地面とSphereが接触した際のContactMaterial
            spherePlaneCM2 = new CANNON.ContactMaterial(
                groundMat,
                sphereMat2,
                {friction: 0,   //摩擦係数
                 restitution: 0 //反発係数
                }
            );
            world.addContactMaterial(spherePlaneCM2);
  
            world.bsc_dist = new CANNON.Vec3();

            return world;
        }


        function setView() {
            scene = new THREE.Scene();
            scene.fog = new THREE.Fog(0x000000, 1, 100);
            camera = new THREE.PerspectiveCamera(40, 650 / 400, 1, 10000);
            camera.position.set(50, 15, 0);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);
            var light = new THREE.DirectionalLight(0xffffff, 2);
            
            light.position.set(5, 10, -10);
            light.castShadow = true;
            light.shadowMapWidth = 1024;
            light.shadowMapHeight = 1024;
            light.shadowCameraLeft = -10;
            light.shadowCameraRight = 10;
            light.shadowCameraTop = 10;
            light.shadowCameraBottom = -10;
            light.shadowCameraFar = 100;
            light.shadowCameraNear = 0;
            light.shadowDarkness = 0.5;
            scene.add(light);
            var amb = new THREE.AmbientLight(0x999999);
            scene.add(amb);
            
  
            var viewPlane = new THREE.Mesh(
                new THREE.PlaneGeometry(300, 300),
                new THREE.MeshPhongMaterial( {color: 0x333333} )
            );
            viewPlane.rotation.x = -Math.PI / 2;
            viewPlane.position.y = 1 / 30;
            viewPlane.receiveShadow = true;
            scene.add(viewPlane);
            
            viewSphere = new THREE.Mesh(
                new THREE.SphereGeometry(1, 50, 50),
                new THREE.MeshLambertMaterial( {color: 0xffffff} )
            );
            viewSphere.castShadow = true;
            viewSphere.receiveShadow = true;
            viewSphere.position = phySphere.position;
            scene.add(viewSphere);
            
            viewSphere2 = new THREE.Mesh(
                new THREE.SphereGeometry(1, 50, 50),
                new THREE.MeshLambertMaterial( {color: 0xffffff} )
            );
            viewSphere2.castShadow = true;
            viewSphere2.receiveShadow = true;
            viewSphere2.position = phySphere2.position;
            scene.add(viewSphere2);
  
            viewSphere3 = new THREE.Mesh(
                new THREE.SphereGeometry(2, 50, 50),
                new THREE.MeshLambertMaterial(
                    {side: THREE.DoubleSide // 裏からも見える為
                     //map: textureHayachi,
                    }
                )
            );
            viewSphere3.castShadow = true;
            viewSphere3.receiveShadow = true;
            viewSphere3.position = phySphere3.position;
            scene.add(viewSphere3);
 
            renderer = new THREE.WebGLRenderer({antialias: true});
            renderer.setSize(650, 400);
            renderer.setClearColor(0x000000, 1);
            renderer.shadowMapEnabled = true;
            document.body.appendChild(renderer.domElement);
            renderer.render(scene, camera);
            
            // controls
            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.minDistance = 0;           //近づける距離の最小値
            controls.maxDistance = 9800;        //遠ざかる距離の最大値
        }


        function animate() {
            requestAnimationFrame(animate);
            
            world.step(1 / 60); // 物理エンジンの時間を進める
            viewSphere.position.copy(phySphere.position);
            viewSphere.quaternion.copy(phySphere.quaternion);
            viewSphere2.position.copy(phySphere2.position);
            viewSphere2.quaternion.copy(phySphere2.quaternion);
            viewSphere3.position.copy(phySphere3.position);
            viewSphere3.quaternion.copy(phySphere3.quaternion);
            controls.update();

            renderer.render(scene, camera);
        }

        var angle = 0;

        $('.js-range').on('change', function(e) {
            e.preventDefault();
            angle = parseInt($(this).val(), 10);
        });

        $('.js-fire').on('click', function(e) {
            e.preventDefault();
            phySphere.velocity.set(-20, 0, -angle);
        });
        
  })();
  </script>
  
</body>
</html>

three.js + cannon.js 2 - ビリヤード ゲーム

Cannon.jsで簡単なゲームを作ってみよう! | 株式会社LIG の写経 + 少々、リファクタリングです。

https://end0tknr.github.io/sandbox/threejs_cannonjs_misc/test_cannonjs_2.html

f:id:end0tknr:20210813075341p:plain

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <!-- refer to https://liginc.co.jp/378458 -->
  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
  
  <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>

  <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
  
</head>
<body>
  <div id="stage"></div>

  <div class="kakudo">
    手玉を突く角度
    <input type="range" class="range js-range"
           value="0" step="0.1" min="-4" max="4">
    <button class="js-fire">発射</button>
    </div> <!-- #kakudo -->

  <script>
    (function() {
        var controls;
        var scene;
        var camera;
        var renderer;
        
        var phySphere;
        var phySphere2;
        var phySphere3;
        
        var viewSphere;
        var viewSphere2;
        var viewSphere3;

        var rand = Math.random()*20 - 10;       // 的玉の位置

        var world = setPhy();
        setView();
        animate();


        function setPhy() {
    
            var world = new CANNON.World();     // 物理世界
            world.gravity.set(0, -9.82, 0);     // 重力

            //「衝突可能性」の剛体同士を探索
            world.broadphase = new CANNON.NaiveBroadphase();
    
            world.solver.iterations = 5;        // 反復計算回数
            world.solver.tolerance = 0.1;       // 許容値
  
            //地面
            var groundMat = new CANNON.Material('groundMat');
            //質量定義
            var phyPlane = new CANNON.Body({
                mass: 0,
                material: groundMat
            });

            phyPlane.addShape(new CANNON.Plane());
            // X軸に90度回転
            phyPlane.quaternion.setFromAxisAngle(
                new CANNON.Vec3(1, 0, 0), -Math.PI / 2 );
            world.add(phyPlane);
  
            var sphereMat = new CANNON.Material('sphereMat');
            //質量定義
            phySphere = new CANNON.Body({
                mass: 1,
                material: sphereMat
            });

            
            phySphere.addShape(new CANNON.Sphere(1));
            phySphere.position.set(20, 1, 0);   //位置
            phySphere.velocity.set(0, 0, 0);    //角速度
            phySphere.angularDamping = 0.1;     //減衰率
            world.add(phySphere);
  
            var sphereMat2 = new CANNON.Material('sphereMat2');
            //質量定義
            phySphere2 = new CANNON.Body({
                mass: 1, material: sphereMat2   });
            
            phySphere2.addShape(new CANNON.Sphere(1));
            phySphere2.position.set(10, 1, 0);  //位置
            phySphere2.velocity.set(0, 0, 0);   //角速度
            phySphere2.angularDamping = 0.1;    //減衰率
            world.add(phySphere2);
  
            var sphereMat3 = new CANNON.Material('sphereMat3');
            //質量定義
            phySphere3 = new CANNON.Body({
                mass: 2,  material: sphereMat3  });
            
            phySphere3.addShape(new CANNON.Sphere(2));
            phySphere3.position.set(-10, 2, rand);      //位置
            phySphere3.velocity.set(0, 0, 0);           //角速度
            phySphere3.angularDamping = 0.1;            //減衰率
            world.add(phySphere3);
  
            //SphereとSphere2が接触した際のContactMaterial
            var sphereSphereCM = new CANNON.ContactMaterial(
                sphereMat,
                sphereMat2,
                {contactEquationRelaxation: 3,          //接触式の緩和性
                 contactEquationStiffness: 10000000,    //接触式の剛性
                 friction: 0.3,                         //摩擦係数
                 frictionEquationRelaxation: 3,         //摩擦式の剛性
                 frictionEquationStiffness: 10000000,   //摩擦式の緩和性
                 restitution: 0.3                       //反発係数
                }
            );
            world.addContactMaterial(sphereSphereCM);
  
            //地面とSphereが接触した際のContactMaterial
            spherePlaneCM = new CANNON.ContactMaterial(
                groundMat,
                sphereMat,
                {friction: 0,   //摩擦係数
                 restitution: 0 //反発係数
                }
            );
            world.addContactMaterial(spherePlaneCM);
  
            //地面とSphereが接触した際のContactMaterial
            spherePlaneCM2 = new CANNON.ContactMaterial(
                groundMat,
                sphereMat2,
                {friction: 0,   //摩擦係数
                 restitution: 0 //反発係数
                }
            );
            world.addContactMaterial(spherePlaneCM2);
  
            world.bsc_dist = new CANNON.Vec3();

            return world;
        }


        function setView() {
            scene = new THREE.Scene();
            scene.fog = new THREE.Fog(0x000000, 1, 100);
            camera = new THREE.PerspectiveCamera(40, 650 / 400, 1, 10000);
            camera.position.set(50, 15, 0);
            camera.lookAt(new THREE.Vector3(0, 0, 0));
            scene.add(camera);
            var light = new THREE.DirectionalLight(0xffffff, 2);
            
            light.position.set(5, 10, -10);
            light.castShadow = true;
            light.shadowMapWidth = 1024;
            light.shadowMapHeight = 1024;
            light.shadowCameraLeft = -10;
            light.shadowCameraRight = 10;
            light.shadowCameraTop = 10;
            light.shadowCameraBottom = -10;
            light.shadowCameraFar = 100;
            light.shadowCameraNear = 0;
            light.shadowDarkness = 0.5;
            scene.add(light);
            var amb = new THREE.AmbientLight(0x999999);
            scene.add(amb);
            
  
            var viewPlane = new THREE.Mesh(
                new THREE.PlaneGeometry(300, 300),
                new THREE.MeshPhongMaterial( {color: 0x333333} )
            );
            viewPlane.rotation.x = -Math.PI / 2;
            viewPlane.position.y = 1 / 30;
            viewPlane.receiveShadow = true;
            scene.add(viewPlane);
            
            viewSphere = new THREE.Mesh(
                new THREE.SphereGeometry(1, 50, 50),
                new THREE.MeshLambertMaterial( {color: 0xffffff} )
            );
            viewSphere.castShadow = true;
            viewSphere.receiveShadow = true;
            viewSphere.position = phySphere.position;
            scene.add(viewSphere);
            
            viewSphere2 = new THREE.Mesh(
                new THREE.SphereGeometry(1, 50, 50),
                new THREE.MeshLambertMaterial( {color: 0xffffff} )
            );
            viewSphere2.castShadow = true;
            viewSphere2.receiveShadow = true;
            viewSphere2.position = phySphere2.position;
            scene.add(viewSphere2);
  
            viewSphere3 = new THREE.Mesh(
                new THREE.SphereGeometry(2, 50, 50),
                new THREE.MeshLambertMaterial(
                    {side: THREE.DoubleSide // 裏からも見える為
                     //map: textureHayachi,
                    }
                )
            );
            viewSphere3.castShadow = true;
            viewSphere3.receiveShadow = true;
            viewSphere3.position = phySphere3.position;
            scene.add(viewSphere3);
 
            renderer = new THREE.WebGLRenderer({antialias: true});
            renderer.setSize(650, 400);
            renderer.setClearColor(0x000000, 1);
            renderer.shadowMapEnabled = true;
            document.body.appendChild(renderer.domElement);
            renderer.render(scene, camera);
            
            // controls
            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.minDistance = 0;           //近づける距離の最小値
            controls.maxDistance = 9800;        //遠ざかる距離の最大値
        }


        function animate() {
            requestAnimationFrame(animate);
            
            world.step(1 / 60); // 物理エンジンの時間を進める
            viewSphere.position.copy(phySphere.position);
            viewSphere.quaternion.copy(phySphere.quaternion);
            viewSphere2.position.copy(phySphere2.position);
            viewSphere2.quaternion.copy(phySphere2.quaternion);
            viewSphere3.position.copy(phySphere3.position);
            viewSphere3.quaternion.copy(phySphere3.quaternion);
            controls.update();

            renderer.render(scene, camera);
        }

        var angle = 0;

        $('.js-range').on('change', function(e) {
            e.preventDefault();
            angle = parseInt($(this).val(), 10);
        });

        $('.js-fire').on('click', function(e) {
            e.preventDefault();
            phySphere.velocity.set(-20, 0, -angle);
        });
        
  })();
  </script>
  
</body>
</html>

three.js + cannon.js 3 - ヒンジによるタイヤ接合

Javascript で動く軽量物理エンジン Cannon.js と3Dレンダラ Three.js で書いた短いサンプルコード - Qiita の写経 + 少々、リファクタリングです。

https://end0tknr.github.io/sandbox/threejs_cannonjs_misc/test_cannonjs_3.html

f:id:end0tknr:20210813112529p:plain

<html>
<head>
<!-- refer to https://qiita.com/yamazaki3104/items/fafb7879591caf137b52 -->

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.js"></script>
<script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r109/examples/js/controls/TrackballControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.js"></script>
</head>

<body>
<script>
    
  class THREEJS {
      
      constructor(){
          const w = document.body.clientWidth
          const h = document.body.clientHeight
          
          this.renderer = new THREE.WebGLRenderer()
          this.renderer.setClearColor( 0x8888dd )
          this.renderer.setSize( w, h )
          
          this.camera = new THREE.PerspectiveCamera( 40, w / h, 0.1, 1000 )
          this.camera.position.x =  0
          this.camera.position.y = 20
          this.camera.position.z = 30
          this.trackball = new THREE.TrackballControls( this.camera )
          
          this.scene = new THREE.Scene()
          
          let directionalLight = new THREE.DirectionalLight( 0xffffff, 1 )
          directionalLight.position.set( 0.2, 0.5, 0.3 )
          this.scene.add( directionalLight )
          
          // this.scene.add( new THREE.AmbientLight( 0x101020 ) )
          
          document.body.appendChild( this.renderer.domElement )
      }
      
      render(){
          for ( let mesh of this.scene.children ) {
              if ( ! mesh.cannon_rigid_body ) continue
              
              mesh.position.copy(   mesh.cannon_rigid_body.position   )
              mesh.quaternion.copy( mesh.cannon_rigid_body.quaternion )
          }
          
          this.trackball.update()
          this.renderer.render( this.scene, this.camera )
      }
  }
  
  class CANNON_PHYSICS {
      constructor( _threejs ) {
          this.cannon_world = new CANNON.World()
          this.cannon_world.gravity.set( 0, -9.80665, 0 )
          this.cannon_world.broadphase = new CANNON.NaiveBroadphase()
        this.cannon_world.solver.iterations = 10

        this.threejs = _threejs
    }

    add_box( _arg ) {
        const body  = new CANNON.Body( {
            mass:       _arg.mass,
            shape:      new CANNON.Box( new CANNON.Vec3(_arg.w/2, _arg.h/2, _arg.d/2 )),
            position:   new CANNON.Vec3( _arg.x, _arg.y, _arg.z ),
            // 摩擦係数 0.1 マテリアルを作成
            material:   new CANNON.Material( { friction: 0.1, } ),
        } )
        this.add_body( body, _arg.color )
    }

    add_body( _body, _color ){
        this.cannon_world.addBody( _body )

        var obj = new THREE.Object3D();
        const color_mat = new THREE.MeshLambertMaterial( { color: _color } )

        // ここのコードは connon.demo.js の 977 shape2mesh() から、まるっと借りてきた。
        for (var l = 0; l < _body.shapes.length; l++) {
            var shape = _body.shapes[l];
            
            var mesh;

            switch(shape.type){
                
            case CANNON.Shape.types.SPHERE:
                var sphere_geometry = new THREE.SphereGeometry( shape.radius, 8, 8);
                mesh = new THREE.Mesh( sphere_geometry, color_mat );
                break;

            case CANNON.Shape.types.PARTICLE:
                mesh = new THREE.Mesh( this.particleGeo, this.particleMaterial );
                var s = this.settings;
                mesh.scale.set(s.particleSize,s.particleSize,s.particleSize);
                break;

            case CANNON.Shape.types.PLANE:
                var geometry = new THREE.PlaneGeometry(10, 10, 4, 4);
                mesh = new THREE.Object3D();
                var submesh = new THREE.Object3D();
                var ground = new THREE.Mesh( geometry, color_mat );
                ground.scale.set(100, 100, 100);
                submesh.add(ground);

                ground.castShadow = true;
                ground.receiveShadow = true;

                mesh.add(submesh);
                break;

            case CANNON.Shape.types.BOX:
                var box_geometry = new THREE.BoxGeometry(
                    shape.halfExtents.x*2,
                    shape.halfExtents.y*2,
                    shape.halfExtents.z*2 );
                mesh = new THREE.Mesh( box_geometry, color_mat );
                break;

            case CANNON.Shape.types.CONVEXPOLYHEDRON:
                var geo = new THREE.Geometry();

                // Add vertices
                for (var i = 0; i < shape.vertices.length; i++) {
                    var v = shape.vertices[i];
                    geo.vertices.push(new THREE.Vector3(v.x, v.y, v.z));
                }

                for(var i=0; i < shape.faces.length; i++){
                    var face = shape.faces[i];

                    // add triangles
                    var a = face[0];
                    for (var j = 1; j < face.length - 1; j++) {
                        var b = face[j];
                        var c = face[j + 1];
                        geo.faces.push(new THREE.Face3(a, b, c));
                    }
                }
                geo.computeBoundingSphere();
                geo.computeFaceNormals();
                mesh = new THREE.Mesh( geo, color_mat );
                break;

            case CANNON.Shape.types.HEIGHTFIELD:
                var geometry = new THREE.Geometry();

                var v0 = new CANNON.Vec3();
                var v1 = new CANNON.Vec3();
                var v2 = new CANNON.Vec3();
                for (var xi = 0; xi < shape.data.length - 1; xi++) {
                    for (var yi = 0; yi < shape.data[xi].length - 1; yi++) {
                        for (var k = 0; k < 2; k++) {
                            shape.getConvexTrianglePillar(xi, yi, k===0);
                            v0.copy(shape.pillarConvex.vertices[0]);
                            v1.copy(shape.pillarConvex.vertices[1]);
                            v2.copy(shape.pillarConvex.vertices[2]);
                            v0.vadd(shape.pillarOffset, v0);
                            v1.vadd(shape.pillarOffset, v1);
                            v2.vadd(shape.pillarOffset, v2);
                            geometry.vertices.push(
                                new THREE.Vector3(v0.x, v0.y, v0.z),
                                new THREE.Vector3(v1.x, v1.y, v1.z),
                                new THREE.Vector3(v2.x, v2.y, v2.z)
                            );
                            var i = geometry.vertices.length - 3;
                            geometry.faces.push(new THREE.Face3(i, i+1, i+2));
                        }
                    }
                }
                geometry.computeBoundingSphere();
                geometry.computeFaceNormals();
                mesh = new THREE.Mesh(geometry, color_mat);
                break;

            case CANNON.Shape.types.TRIMESH:
                var geometry = new THREE.Geometry();

                var v0 = new CANNON.Vec3();
                var v1 = new CANNON.Vec3();
                var v2 = new CANNON.Vec3();
                for (var i = 0; i < shape.indices.length / 3; i++) {
                    shape.getTriangleVertices(i, v0, v1, v2);
                    geometry.vertices.push(
                        new THREE.Vector3(v0.x, v0.y, v0.z),
                        new THREE.Vector3(v1.x, v1.y, v1.z),
                        new THREE.Vector3(v2.x, v2.y, v2.z)
                    );
                    var j = geometry.vertices.length - 3;
                    geometry.faces.push(new THREE.Face3(j, j+1, j+2));
                }
                geometry.computeBoundingSphere();
                geometry.computeFaceNormals();
                mesh = new THREE.Mesh(geometry, color_mat);
                break;

            default:
                throw "Visual type not recognized: "+shape.type;
            }

            var o = _body.shapeOffsets[l];
            var q = _body.shapeOrientations[l];
            mesh.position.set(o.x, o.y, o.z);
            mesh.quaternion.set(q.x, q.y, q.z, q.w);

            obj.add(mesh);
        }

        obj.cannon_rigid_body = _body
        this.threejs.scene.add( obj )
    }


    render( _sec ) {
        this.cannon_world.step( _sec )
        this.threejs.render()
    }
}


  let cannon_phy = new CANNON_PHYSICS( new THREEJS() )
  //床面
  cannon_phy.add_box( {
      mass: 0, x: 0, y: -0.2, z: 0, w: 150, h: 0.4, d: 150, color: 0x333333,
  } )
  
  //ドミノ
  const box_size = 1.5
  for ( let y=0 ; y<16; y++ ) {
      for ( let x=0 ; x<16; x++ ) {
          cannon_phy.add_box( {
              mass: 1,
              x: (x-7) * box_size * 0.95,
              y: box_size * 0.5,
              z: (y-7) * box_size * 1.2,
              w: box_size*0.1, h: box_size*1, d: box_size*1,
              color:0xDCAA6B
          })
      }
  }
  
  
  var w_mat           = new CANNON.Material()
  // CANNON.Cylinder()の引数は
  // radiusTop, radiusBottom, height, numSegments.
  var ws              = new CANNON.Cylinder( 1.9, 1.2, 1, 8 )

  var leftFrontWheel  = new CANNON.Body({
      mass: 1,
      material: w_mat,
      shape: ws,
      position: { x: 5, y:  5+20, z: 0 },
      //以下で向きを決めていますが、結局、HingeConstraintの axisB で補正.
      quaternion: new CANNON.Quaternion( 1, 0, 0, -Math.PI / 4 ) } );
  
  var rightFrontWheel = new CANNON.Body({
      mass: 1,
      material: w_mat,
      shape: ws,
      position: { x: 5, y: -5+20, z: 0 },
      quaternion: new CANNON.Quaternion( 1, 0, 0,  Math.PI / 4 ) } );
  
  var leftRearWheel   = new CANNON.Body({
      mass: 1,
      material: w_mat,
      shape: ws,
      position: { x:-5, y:  5+20, z: 0 },
      quaternion: new CANNON.Quaternion( 1, 0, 0, -Math.PI / 4 ) } );
  
  var rightRearWheel  = new CANNON.Body({
      mass: 1,
      material: w_mat,
      shape: ws,
      position: { x:-5, y: -5+20, z: 0 },
      quaternion: new CANNON.Quaternion( 1, 0, 0,  Math.PI / 4 ) } );

  // シャシー
  var chassis = new CANNON.Body({
      mass: 5,
      shape: new CANNON.Box( new CANNON.Vec3( 5, 2, 0.5 ) ),
      position: { x: 0, y: 20, z: 0 }
  })

  //制約

  //前輪は、やや斜めにして接続(ハンドルを切った状態)
  var constraint_leftFront = new CANNON.HingeConstraint(
      chassis,
      leftFrontWheel,
      { pivotA: new CANNON.Vec3(  5,  5, 0 ),   axisA: new CANNON.Vec3( 1, 1, 0 ),
        pivotB: new CANNON.Vec3(),              axisB: new CANNON.Vec3( 0, 0, -1 ) } );
  cannon_phy.cannon_world.addConstraint( constraint_leftFront );
  
  var constraint_rightFront = new CANNON.HingeConstraint(
      chassis,
      rightFrontWheel,
      { pivotA: new CANNON.Vec3(  5, -5, 0 ),   axisA: new CANNON.Vec3( 1, 1, 0  ),
        pivotB: new CANNON.Vec3(),              axisB: new CANNON.Vec3( 0, 0, -1 ) } );
  cannon_phy.cannon_world.addConstraint( constraint_rightFront );
  
  var constraint_leftRear = new CANNON.HingeConstraint(
      chassis,
      leftRearWheel,
      { pivotA: new CANNON.Vec3( -5,  5, 0 ),   axisA: new CANNON.Vec3( 0, 1, 0 ),
        pivotB: new CANNON.Vec3(),              axisB: new CANNON.Vec3( 0, 0, -1 ) } );
  cannon_phy.cannon_world.addConstraint( constraint_leftRear );

  var constraint_rightRear = new CANNON.HingeConstraint(
      chassis,
      rightRearWheel,
      { pivotA: new CANNON.Vec3( -5, -5, 0 ),   axisA: new CANNON.Vec3( 0, 1, 0 ),
        pivotB: new CANNON.Vec3(),              axisB: new CANNON.Vec3( 0, 0, -1, ) } );
  cannon_phy.cannon_world.addConstraint( constraint_rightRear );

  //前輪駆動として回転させる
  constraint_leftFront.enableMotor();
  constraint_rightFront.enableMotor();
  constraint_leftFront.setMotorSpeed( 7 );
  constraint_rightFront.setMotorSpeed( -7 );
  
  for ( const body of [ chassis,
                        leftFrontWheel,
                        rightFrontWheel,
                        leftRearWheel,
                        rightRearWheel ] ){
      cannon_phy.add_body( body, 0x556677 )
  }
  
  
  function animate(){
      cannon_phy.render( 1 / 60 )
      window.requestAnimationFrame( animate )
  }
  
  window.requestAnimationFrame( animate )

</script>
</body>
</html>

three.js + cannon.js 4 - 2コのピンによる板の接合 (FPS)

先程の「three.js + cannon.js 3 - ヒンジによるタイヤ接合」では、 HingeConstraint により、車体とタイヤを接続しましたが、 以下では、CANNON.PointToPointConstraint により、板を接合しています。

元々、cannon.js の examples にあったものの、写経 + 少々、リファクタリングです。

https://github.com/schteppe/cannon.js/tree/master/examples

https://schteppe.github.io/cannon.js/examples/threejs_fps.html

https://end0tknr.github.io/sandbox/cannonjs_examples/threejs_fps.html

f:id:end0tknr:20210813163119p:plain

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>cannon.js + three.js physics shooter</title>
    <style>
      #blocker {
          position: absolute;
          width: 100%;
          height: 100%;
          background-color: rgba(0,0,0,0.5);
      }
      
      #instructions {
          width: 100%;
          height: 100%;
          color: #ffffff;
          text-align: center;
          font-size:20px;
          padding-top:20px;
          cursor: pointer;
      }
    </style>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/68/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
    <!-- マウスによるカメラ制御 -->
    <!-- https://developer.mozilla.org/ja/docs/Web/API/Pointer_Lock_API -->
    <script src="js/PointerLockControls.js"></script>
  </head>
  <body>

    <div id="blocker">
      <div id="instructions">
        Click to play<br/>
        (W,A,S,D = Move, SPACE = Jump, MOUSE = Look, CLICK = Shoot)
      </div>
    </div>

    <script>
      var world;
      var camera;
      var scene;
      var renderer;

      var sphereShape;
      var sphereBody;
      var physicsMaterial;
      var walls=[];
      var balls=[];     var ballMeshes=[];
      var boxes=[];     var boxMeshes=[];

      var geometry;
      var material;
      var mesh;
      var controls,time = Date.now();
      
      var blocker      = document.getElementById( 'blocker' );
      // 操作方法の表示
      var instructions = document.getElementById( 'instructions' );
      

      if ( 'pointerLockElement' in document ) {
          main();
      } else {
          instructions.innerHTML = 'Your browser doesn\'t seem to support Pointer Lock API';
      }

      function main(){
          var element = document.body;

          document.addEventListener(
              'pointerlockchange',
              function ( event ) {
                  if ( document.pointerLockElement === element ) {
                      controls.enabled = true;
                      blocker.style.display = 'none';
                  } else {
                      controls.enabled = false;
                      blocker.style.display = 'box';
                      instructions.style.display = '';
                  }
              },
              false );
          
          document.addEventListener(
              'pointerlockerror',
              function(event){instructions.style.display = ''; },
              false );
          
          instructions.addEventListener(
              'click',
              function ( event ) {
                  instructions.style.display = 'none';
                  
                  // Ask the browser to lock the pointer
                  element.requestPointerLock = element.requestPointerLock;
                  
                  element.requestPointerLock(); },
              false );
          
          initCannon();
          init();
          animate();
      }
      
      function initCannon(){
          // Setup our world
          world = new CANNON.World();
          world.quatNormalizeSkip = 0;
          world.quatNormalizeFast = false;
          
          var solver = new CANNON.GSSolver();
          //接触式の剛性
          world.defaultContactMaterial.contactEquationStiffness = 1e9
          //接触式の緩和性
          world.defaultContactMaterial.contactEquationRelaxation = 4;
          
          solver.iterations = 7;
          solver.tolerance = 0.1;
          
          world.solver = new CANNON.SplitSolver(solver);
          
          world.gravity.set(0,-20,0);
          world.broadphase = new CANNON.NaiveBroadphase();
          
          // Create a slippery material (friction coefficient = 0.0)
          physicsMaterial = new CANNON.Material("slipperyMaterial");
          var physicsContactMaterial =
              new CANNON.ContactMaterial(physicsMaterial,
                                         physicsMaterial,
                                         0.0, // friction coefficient
                                         0.3  // restitution
                                        );
          // We must add the contact materials to the world
          world.addContactMaterial(physicsContactMaterial);
          
          // 自分自身を表す球体
          var mass = 5;
          var radius = 2;
          sphereShape = new CANNON.Sphere(radius);
          sphereBody  = new CANNON.Body({ mass: mass });
          sphereBody.addShape(sphereShape);
          sphereBody.position.set(0, 5, 0);
          sphereBody.linearDamping = 0.9;
          world.addBody(sphereBody);
          
          // 物理的な床面
          var groundShape = new CANNON.Plane();
          var groundBody = new CANNON.Body({ mass: 0 });
          groundBody.addShape(groundShape);
          groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1,0,0),-Math.PI/2);
          world.addBody(groundBody);
      }
      
      function init() {
          
          camera = new THREE.PerspectiveCamera(
              75,
              window.innerWidth / window.innerHeight,
              0.1,
              1000 );
          
          scene = new THREE.Scene();
          scene.fog = new THREE.Fog( 0x000000, 0, 500 );
          
          var ambient = new THREE.AmbientLight( 0x111111 );
          scene.add( ambient );
          
          light = new THREE.SpotLight( 0xffffff );
          light.position.set( 10, 30, 20 );
          light.target.position.set( 0, 0, 0 );
          if(true){
              light.castShadow = true;
              
              light.shadowCameraNear = 20;
              light.shadowCameraFar = 50;//camera.far;
              light.shadowCameraFov = 40;
              
              light.shadowMapBias = 0.1;
              light.shadowMapDarkness = 0.7;
              light.shadowMapWidth = 2*512;
              light.shadowMapHeight = 2*512;
              
              //light.shadowCameraVisible = true;
          }
          scene.add( light );
          
          
          controls = new PointerLockControls( camera , sphereBody );
          scene.add( controls.getObject() );
          
          // 画面上の床面
          geometry = new THREE.PlaneGeometry( 300, 300, 50, 50 );
          geometry.applyMatrix( new THREE.Matrix4().makeRotationX( - Math.PI / 2 ) );
          
          material = new THREE.MeshLambertMaterial( { color: 0xdddddd } );
          
          mesh = new THREE.Mesh( geometry, material );
          mesh.castShadow = true;
          mesh.receiveShadow = true;
          scene.add( mesh );

          
          renderer = new THREE.WebGLRenderer();
          renderer.shadowMapEnabled = true;
          renderer.shadowMapSoft = true;
          renderer.setSize( window.innerWidth, window.innerHeight );
          renderer.setClearColor( scene.fog.color, 1 );
          
          document.body.appendChild( renderer.domElement );
          
          window.addEventListener( 'resize', onWindowResize, false );
          
          // 的となる箱の追加
          var halfExtents = new CANNON.Vec3(1,1,1);
          var boxShape = new CANNON.Box(halfExtents);
          var boxGeometry =
              new THREE.BoxGeometry(halfExtents.x*2,halfExtents.y*2,halfExtents.z*2);
          for(var i=0; i<15; i++){
              var x = (Math.random()-0.5)*20;
              var y = 1 + (Math.random()-0.5)*1;
              var z = (Math.random()-0.5)*20;
              var boxBody = new CANNON.Body({ mass: 5 });
              boxBody.addShape(boxShape);
              var boxMesh = new THREE.Mesh( boxGeometry, material );
              world.addBody(boxBody);
              scene.add(boxMesh);
              boxBody.position.set(x,y,z);
              boxMesh.position.set(x,y,z);
              boxMesh.castShadow = true;
              boxMesh.receiveShadow = true;
              boxes.push(boxBody);
              boxMeshes.push(boxMesh);
          }
          
          // 的となる「連結された箱」の追加
          var size = 0.5;
          var he = new CANNON.Vec3(size,size,size*0.1);
          var boxShape = new CANNON.Box(he);
          var mass = 0;
          var space = 0.1 * size;
          var N = 5;
          var last;
          var boxGeometry = new THREE.BoxGeometry(he.x*2,he.y*2,he.z*2);
          
          for(var i=0; i<N; i++){
              var boxbody = new CANNON.Body({ mass: mass });
              boxbody.addShape(boxShape);
              var boxMesh = new THREE.Mesh(boxGeometry, material);
              boxbody.position.set(5,(N-i)*(size*2+2*space) + size*2+space,0);
              boxbody.linearDamping = 0.01;
              boxbody.angularDamping = 0.01;
              // boxMesh.castShadow = true;
              boxMesh.receiveShadow = true;
              world.addBody(boxbody);
              scene.add(boxMesh);
              boxes.push(boxbody);
              boxMeshes.push(boxMesh);
              
              if(i!=0){
                  // 板の両端で連結
                  var c1 = new CANNON.PointToPointConstraint(
                      boxbody,  new CANNON.Vec3(-size, size+space,0),
                      last,     new CANNON.Vec3(-size,-size-space,0));
                  
                  var c2 = new CANNON.PointToPointConstraint(
                      boxbody,  new CANNON.Vec3(size, size+space,0),
                      last,     new CANNON.Vec3(size,-size-space,0));
                  
                  world.addConstraint(c1);
                  world.addConstraint(c2);
              } else {
                  mass=0.3;
              }
              last = boxbody;
          }
      }
      
      function onWindowResize() {
          camera.aspect = window.innerWidth / window.innerHeight;
          camera.updateProjectionMatrix();
          renderer.setSize( window.innerWidth, window.innerHeight );
      }
      
      var dt = 1/60;
      function animate() {
          requestAnimationFrame( animate );
          if(controls.enabled){
              world.step(dt);
              
              // Update ball positions
              for(var i=0; i<balls.length; i++){
                  ballMeshes[i].position.copy(balls[i].position);
                  ballMeshes[i].quaternion.copy(balls[i].quaternion);
              }
              
              // Update box positions
              for(var i=0; i<boxes.length; i++){
                  boxMeshes[i].position.copy(boxes[i].position);
                  boxMeshes[i].quaternion.copy(boxes[i].quaternion);
              }
          }
          
          controls.update( Date.now() - time );
          renderer.render( scene, camera );
          time = Date.now();
          
      }
      
      var ballShape      = new CANNON.Sphere(0.2);
      var ballGeometry   = new THREE.SphereGeometry(ballShape.radius, 32, 32);
      var shootDirection = new THREE.Vector3();
      var shootVelo = 15;
      var projector = new THREE.Projector();
      
      function getShootDir(targetVec){
          var vector = targetVec;
          targetVec.set(0,0,1);
          projector.unprojectVector(vector, camera);
          var ray = new THREE.Ray(
              sphereBody.position,
              vector.sub(sphereBody.position).normalize() );
          
          targetVec.copy(ray.direction);
      }
      
      window.addEventListener("click",function(e){
          if(controls.enabled==true){
              var x = sphereBody.position.x;
              var y = sphereBody.position.y;
              var z = sphereBody.position.z;
              var ballBody = new CANNON.Body({ mass: 1 });
              ballBody.addShape(ballShape);
              var ballMesh = new THREE.Mesh( ballGeometry, material );
              world.addBody(ballBody);
              scene.add(ballMesh);
              ballMesh.castShadow = true;
              ballMesh.receiveShadow = true;
              balls.push(ballBody);
              ballMeshes.push(ballMesh);
              getShootDir(shootDirection);
              ballBody.velocity.set(  shootDirection.x * shootVelo,
                                      shootDirection.y * shootVelo,
                                      shootDirection.z * shootVelo);
              
              // Move the ball outside the player sphere
              x += shootDirection.x * (sphereShape.radius*1.02 + ballShape.radius);
              y += shootDirection.y * (sphereShape.radius*1.02 + ballShape.radius);
              z += shootDirection.z * (sphereShape.radius*1.02 + ballShape.radius);
              ballBody.position.set(x,y,z);
              ballMesh.position.set(x,y,z);
          }
      });
      
    </script>
  </body>
</html>