Three JS:Geometryでバネの形状を作ってみる
前回は基礎固めと言う事で、three.jsで正八方体のGeometryを自作する方法を覚えました。なので今回はもう少し複雑な形状をプログラムで作ってみたいと思います。
何にしようかなーと考えた結果、三角関数で割と簡単に描けそうなバネの形状を作ってみようと思います。びよーんってやつ。
GFDL, https://ja.wikipedia.org/w/index.php?curid=256994
線で描いてみる
いきなりポリゴンをプログラミングで描くのはまだ辛いので、まずは3D空間に線でバネ(らせん)を描いてみます。
バネ上の頂点を繋げて一本の線にしていきます。バネをY軸にそって立たせるとして、x座標(横)とz座標(奥行き)は円軌道、y座標(高さ)は一定値を増加させていけばいいだけなので
1 2 3 4 5 6 7 8 9 10 11 12 | var geometry = new THREE.Geometry(); var zoukaY = 0.01;// 1°ごとのy軸の増加 var laps = 8;// 何周するか var split = 10;// 何度ずつ頂点を置くか var radius = 2;// 円の半径 var totalDeg = laps * 360;//全周分の角度 for(var i = 0; i <= totalDeg; i = i + split){ var x = radius * Math.cos( Math.PI / 180 * i); var z = radius * Math.sin( Math.PI / 180 * i); var y = i * zoukaY; geometry.vertices.push(new THREE.Vector3(x, y, z)); } |
角度の分だけforループを回し、geomety.vertices配列に計算した座標を追加していきます。
線の描画にはLineBasicMaterialを使いましょう。
1 2 3 | var line = new THREE.LineBasicMaterial( { color: 0xFCB64F} ); var mesh = new THREE.Line(geometry, line); scene.add(mesh); |
ここまでは三角関数に慣れていれば割と簡単でした。
平面で描いてみる
それでは次に平面をばね(らせん)状にグルグルさせます。考え方としては、前章の頂点を半径違いで2セット作り、繋げて三角形の平面にしていきます。
まずは頂点を配列に入れ込みます。前章と同じく角度の分だけforループで回し、内側と外側の頂点をそれぞれvertices配列に追加します。vertices配列の0,2,4,6,….番目が内側の頂点、1,3,5,7,…番目が外側の頂点です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var geometry = new THREE.Geometry(); var zoukaY = 0.0025;// 1°ごとのy軸の増加 var laps = 8;// 何周するか var split = 10;// 何度ずつ頂点を置くか var radius = 2;// 内側の円半径 var outRadius = 3; //外側の円半径 var totalDeg = laps * 360;//全周分の角度 for(var i = 0; i <=totalDeg; i = i + split){ var x = radius * Math.cos( Math.PI / 180 * i);//内側の頂点のx座標 var z = radius * Math.sin( Math.PI / 180 * i);//内側の頂点のz座標 var _x = outRadius * Math.cos( Math.PI / 180 * i);//外側の頂点のx座標 var _z = outRadius * Math.sin( Math.PI / 180 * i);//外側の頂点のz座標 var y = i * zoukaY; geometry.vertices.push(new THREE.Vector3(x, y, z)); geometry.vertices.push(new THREE.Vector3(_x, y, _z)); } |
次は三角形の平面をfaces配列に追加します。2つの三角形の面を一つと考え、四角形を作りながらループ処理をしていきます。少し複雑ですが、上の図と照らし合わせながら見て下さい。
1 2 3 4 5 6 7 8 9 | var length = geometry.vertices.length;//頂点の数 for(var i = 0 ; i < length - 3; i = i + 2){ geometry.faces.push(new THREE.Face3( i+2, i+1, i)); geometry.faces.push(new THREE.Face3( i+3, i+1, i+2)); } geometry.computeFaceNormals();//法線ベクトル(面の向き)自動計算 var material = new THREE.MeshPhongMaterial({color: 0xFCB64F}); var mesh = new THREE.Mesh(geometry, material); scene.add(mesh); |
少し陰影が分かりづらいですが、下のように表示されました。
立体で描いてみる
いよいよ立体でバネを作ってみましょう。平面でも立体と言えば立体なのですが、切断面を多角形にしてみます。
いろいろと複雑になって来ますが、基本は平面と同じように2つの三角形の面を一つと考え、四角形を作りながらループを回す感じです。コード中のコメントを参考にして下さい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | var zoukaY = 0.0025;// 1°でy軸の増加 var laps = 8;// 何周するか var split = 10;// 何度ずつ頂点を置くか var radius = 2;// 円の半径 var tickness = 0.5; // 切断面の太さ var totalDeg = laps * 360; // 全周分の角度 var v = 20; //切断面(多角形)の頂点数 var surfaceSplit = 360 / v; //切断面(多角形)の中心角 //全周分の角度の分ループ for(var i = 0; i <= totalDeg; i = i + split){ //切断面の頂点数分ループ for(var j = 0; j < v; j++){ // 頂点jの切断面の太さ分のバネ半径x,z値の増加 var increase = tickness / 2 * Math.sin( Math.PI / 180 * (surfaceSplit*j)); // 頂点jの切断面の太さ分のバネ半径y値の増加 var increaseY = tickness / 2 * Math.cos( Math.PI / 180 * (surfaceSplit*j)); var x = (radius + increase) * Math.cos( Math.PI / 180 * i); var z = (radius+ increase) * Math.sin( Math.PI / 180 * i); var y = (i * zoukaY) + increaseY; geometry.vertices.push(new THREE.Vector3(x, y, z)); } } var verLen = geometry.vertices.length;//頂点の数 //切断面の数だけループ for(var i = 0 ; i < (verLen / v) -1 ; i++){ //切断面の頂点数分ループ for(var j = 0; j < v ; j++){ //現在:i切断面目のj番目頂点 //(次の切断面のj番目頂点,i番目切断面の次の頂点,i番目切断面のj番目の頂点) geometry.faces.push(new THREE.Face3( (i+1)*v+j, i*v+j+1, i*v+j)) //最後のループ時は面を作らない //(次の切断面の次の頂点,i番目切断面の次の頂点,次の切断面のj番目の頂点) if(i != (verLen / v) -2 || j != v-1)geometry.faces.push(new THREE.Face3( (i+1)*v+j+1, i*v+j+1, (i+1)*v+j)); } } geometry.computeFaceNormals();//法線ベクトル(面の向き)自動計算 geometry.computeVertexNormals();//法線ベクトル(面の向き)自動計算 var material = new THREE.MeshPhongMaterial({ color: 0xFCB64F, specular: 0xFCCC8C, shininess: 15, side: THREE.DoubleSide }); var mesh = new THREE.Mesh(geometry, material); scene.add(mesh); |
見事バネができました!!
DEMO
それでは下記のボタンを押してデモを見てみてください。
念の為、今回使ったコード全文を載せておきます。下記コードをこのまま使う場合はthree.jsの他にDetector.jsとOrbitControls.jsを読み込んでおく必要があります。createBane関数の中でバネの線・平面・立体を作ってます。冗長ですみません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | (function(){ var canW = window.innerWidth; //canvas横:任意値 var canH = window.innerHeight; //canvas縦:任意値 var scene,camera,renderer,controls,datObj,cubeTexture,mesh; function init(){ if(!Detector.webgl)Detector.addGetWebGLMessage();//WebGL環境確認 scene = new THREE.Scene(); // カメラ:透視投影 camera = new THREE.PerspectiveCamera( 60, canW / canH, 1, 100); scene.add(camera); camera.position.set( 4, 9, 16); // レンダラー renderer = new THREE.WebGLRenderer({antialias: true}); renderer.setSize( canW, canH); renderer.setPixelRatio( window.devicePixelRatio); document.body.appendChild( renderer.domElement); var mesh = createBane("solid");//"line"or"plane"or"solid" scene.add(mesh); // 自然光 var ambientLight = new THREE.AmbientLight( 0x999999); // スポットライト var directionalLight = new THREE.DirectionalLight(0xaaaaaa); directionalLight.position.set(1,1,1); scene.add( ambientLight, directionalLight); //コントローラー controls = new THREE.OrbitControls(camera, renderer.domElement); controls.maxDistance = 100; //軸 var axis = new THREE.AxisHelper( 50 ); scene.add( axis); resizeSet(); setTimeout(resize, 1); rendering(); } function createBane(type){ var geometry = new THREE.Geometry(); var zoukaY = 0.0025;// 1°ごとのy軸の増加 var laps = 8;// 何周するか var split = 10;// 何度ずつ頂点を置くか var radius = 2;// 円の半径 var totalDeg = laps * 360;//全周分の角度 if(type == "line"){ for(var i = 0; i <= totalDeg; i = i + split){ var x = radius * Math.cos( Math.PI / 180 * i); var z = radius * Math.sin( Math.PI / 180 * i); var y = i * zoukaY; geometry.vertices.push(new THREE.Vector3(x, y, z)); } var line = new THREE.LineBasicMaterial( { color: 0xFCB64F} ); var mesh = new THREE.Line(geometry, line); }else if(type == "plane"){ var outRadius = 3; //外側の円半径 for(var i = 0; i <=totalDeg; i = i + split){ var x = radius * Math.cos( Math.PI / 180 * i); var z = radius * Math.sin( Math.PI / 180 * i); var _x = outRadius * Math.cos( Math.PI / 180 * i); var _z = outRadius * Math.sin( Math.PI / 180 * i); var y = i * zoukaY; geometry.vertices.push(new THREE.Vector3(x, y, z)); geometry.vertices.push(new THREE.Vector3(_x, y, _z)); } var length = geometry.vertices.length; for(var i = 0 ; i < length-3; i = i + 2){ geometry.faces.push(new THREE.Face3( i+2, i+1, i)); geometry.faces.push(new THREE.Face3( i+3, i+1, i+2)); } geometry.computeFaceNormals(); var material = new THREE.MeshPhongMaterial({color: 0xFCB64F}); var mesh = new THREE.Mesh(geometry, material); }else if(type == "solid"){ var tickness = 0.5; // 切断面の太さ var v = 20; //切断面(多角形)の頂点数 var surfaceSplit = 360 / v; //切断面(多角形)の中心角 //全周分の角度の分ループ for(var i = 0; i <= totalDeg; i = i + split){ //切断面の頂点数分ループ for(var j = 0; j < v; j++){ // 頂点jの切断面の太さ分のバネ半径x,z値の増加 var increase = tickness / 2 * Math.sin( Math.PI / 180 * (surfaceSplit*j)); // 頂点jの切断面の太さ分のバネ半径y値の増加 var increaseY = tickness / 2 * Math.cos( Math.PI / 180 * (surfaceSplit*j)); var x = (radius + increase) * Math.cos( Math.PI / 180 * i); var z = (radius+ increase) * Math.sin( Math.PI / 180 * i); var y = (i * zoukaY) + increaseY; geometry.vertices.push(new THREE.Vector3(x, y, z)); } } var verLen = geometry.vertices.length;//頂点の数 //切断面の数だけループ for(var i = 0 ; i < (verLen / v) -1 ; i++){ //切断面の頂点数分ループ for(var j = 0; j < v ; j++){ //現在:i切断面目のj番目頂点 //(次の切断面のj番目頂点,i番目切断面の次の頂点,i番目切断面のj番目の頂点) geometry.faces.push(new THREE.Face3( (i+1)*v+j, i*v+j+1, i*v+j)) //最後のループ時は面を作らない //(次の切断面の次の頂点,i番目切断面の次の頂点,次の切断面のj番目の頂点) if(i != (verLen / v) -2 || j != v-1)geometry.faces.push(new THREE.Face3( (i+1)*v+j+1, i*v+j+1, (i+1)*v+j)); } } geometry.computeFaceNormals();//法線ベクトル(面の向き)自動計算 geometry.computeVertexNormals();//法線ベクトル(面の向き)自動計算 var material = new THREE.MeshPhongMaterial({ color: 0xFCB64F, specular: 0xFCCC8C, shininess: 15, side: THREE.DoubleSide }); var mesh = new THREE.Mesh(geometry, material); } return mesh; } function rendering(){ requestAnimationFrame(rendering); controls.update(); renderer.render( scene, camera); } //画面サイズ変更時 function resizeSet(){ var queue = null; // キューをストック var wait = 300; // 0.3秒後に実行の場合 window.addEventListener( 'resize', function() { clearTimeout( queue ); queue = setTimeout(function() { resize(); }, wait ); }, false ); }; function resize(){ var width = window.innerWidth; var height = window.innerHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); } init(); }()); |
まとめ
いかがでしたでしょうか。立体まで行くと少し複雑でしたが、基本的な三角関数が分かれば割と簡単にこのくらいの形状を作れます。次のステップとしてはこのバネをびよーんとアニメーションを付けることが出来ればと思っております。また別の機会に。
また、よくよく考えたら今回のような管状の形状を作るならTubeGeometryなどを使った方が早そうですね。。まぁ、作れたからいいかな。