本来、 hands-on three.js + cannon.js - 3D迷路 (like fps ?) - end0tknr's kipple - web写経開発 より前に、記載すべき入門的なものですが、今更、post。
目次
- three.js + cannon.js 1 - 物体の落下
- three.js + cannon.js 2 - ビリヤード ゲーム
- three.js + cannon.js 3 - ヒンジによるタイヤ接合
- three.js + cannon.js 4 - 2コのピンによる板の接合 (FPS)
three.js + cannon.js 1 - 物体の落下
https://end0tknr.github.io/sandbox/threejs_cannonjs_misc/test_cannonjs_1.html
<!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
<!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
<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
<!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>