バッカソン に出場したら3Dと物理演算が2日でできるようになった話

2017年1月14日、九工大の学生さん主体のDreamHack事務局主催のもと、「 バッカソン 」が開催されました。会場は弊社ヌーラボ福岡本社3Fです。運営に参加しつつ、出場者としても楽しませていただいたので、作った作品の技術の解説をします。当日の模様はTwitterで#Backathon2017ハッシュタグでご覧いただけます。

バッカソン の発端

これはもともとDreamHack主催の島井さんと知り合って、弊社CEO橋本が「なんでもいいから、なんかやろう!」と言い出して始まったイベントで、たまたまその場に居あわせた私も中心メンバーになりました。開催準備やTwitterでの広報など、いろいろやってるうちに、やっぱり自分でもなにかを作りたくなって参加者として登録することにしました。

テーマ選定

バッカソンの審査基準は

  • 技術力・デザイン力
  • 作り込み度
  • ユニークさ
  • いかにビジネスにならなさそうか

で、ビジネスになることをやってはいけないという縛りがあります。他の参加者の皆さんが笑いの要素を入れてくるであろうことは予想できましたが、あえて笑いの要素は一切無し、単なる技術の習得だけして発表しようと考えました。これならまずビジネスになることはありません。

作る物についてはそれほど迷いませんでした。以前から3Dと物理演算に興味があったので、使う道具は物理演算。物理といえば機械。機械といえば歯車。萌える歯車といえば機械式時計。ということで、物理演算で時計を作ろう、と決めました。(この辺なにを言ってるかわからないかもしれませんが、「わかる!」と思ってくれる仲間を募集しています。

完全に「ハンマーを持って叩く釘を探している」状態です。そういえば、こんな純粋な気持ちでプログラム作るのは久しぶりかもしれない、バッカソンっていい企画だなあとしみじみ思いました。

言語、ライブラリの選定

ネタでやることなので、ブラウザですぐ動くJavaScriptでやることにして、また、ライブラリは「楽に始められる」が売りのWhitestormJSを選びました。JSで3Dやるときのデ・ファクトであるTHREE.jsと物理演算のPhysi.js(という知識もこの2日で得たものですが)を使いやすいようにラップしたフレームワークで、whitestorm.jsのscriptタグを一つ入れておけば、あとは素のJavaScriptを書くだけで動きます。

歯車の設計

歯車の設計図まずは歯車を噛み合わせて、片方を回したらもう片方が物理演算により回る、というところを作ります。

歯車の形状は、実際には歯のすり減りを防ぐために曲線にするとかいろいろあるのですが、まずはとりあえず動けばいいということで、単純な形を構築します。

歯の高さ、基準円周上の幅、歯先の幅等を計算で求めて、薄い円盤の側面に一定角度ごとにくっつけるという方法で作図します。詳しい人から見たら絶対下手くそなコードなので恥ずかしいですが晒しちゃいます。JavaScriptでちゃんと回る歯車を作る必要がある場合なんかに参考にしていただければと思います。

// 歯車のコンストラクタ
// @param m モジュール
// @param z 歯数
// @param options 
function Gear(m, z, options) {
  var d = z * m; // 基準円直径
  var a = Math.PI * 2 / z; // 歯ごとの角度
  var cr = d - m * 2.7; // Cylinderの半径
  // 歯の根本の座標
  var t1 = {
    x: cr * Math.cos(a / 3) * 0.95,
    y: cr * Math.sin(a / 3) * 0.95
  };
  // 基準円周上の座標
  var t2 = {
    x: d * Math.cos(a / 4.5),
    y: d * Math.sin(a / 4.5)
  };
  // 歯先の座標
  var t3 = {
    x: (d + 1.5*m) * Math.cos(a / 30),
    y: (d + 1.5*m) * Math.sin(a / 30)
  };
  // 歯一つ分を作図する
  function tooth(i) {
    var v2 = THREE.Vector2;
    var shape = new THREE.Shape([
      new v2(t1.x, t1.y), new v2(t2.x, t2.y), new v2(t3.x, t3.y),
      new v2(t3.x, -t3.y), new v2(t2.x, -t2.y), new v2(t1.x, -t1.y)
    ]);
    return new WHS.Extrude({
      geometry: {
        shapes: shape,
        options: {
          bevelEnabled: false,
          bevelSize: 0,
          amount: 0.9
        }
      },
      mass:0.01,
      position: [0, -0.45, 0],
      // Cylinderの高さはy軸方向で、Extrudeの押し出し方向がz軸方向なので、x軸で回転
      rotation: [-Math.PI/2, 0, a * i - Math.PI/2],
      material: material,
    });
  }
  // 円盤
  var c = new WHS.Cylinder(_extends({}, {
    geometry: {
      radiusTop: cr,
      radiusBottom: cr,
      height: 1
    },
    material: {
      kind: 'phong',
      color: 'white',
      map: WHS.texture('./gear_texture.jpg'),
    },
    position: [0, 0, 0],
    mass: 0.01,
  }, options));
  for (var i = 0; i < z; i++) {
    c.add(tooth(i));
  }
  this.shape = c;
  this.d = d;
  this.m = m;
  this.z = z;
}

実はこのコードになるまでには紆余曲折あって、最初適当に考えた形と寸法で作ったらまるで動きませんでした。

次にネットで「歯車の設計図」で検索して得られる機械工学的に正しい寸法で作りました。しかし物理演算エンジンの精度の問題なのか、噛み合っている歯の次の歯同士が、当たってないはずなのになぜか干渉してしまいこれも上手くいきません。

夜中までかかった試行錯誤の末、正しい寸法から若干遊びをもたせた薄めの歯にすることで、無事動くようになりました!

生成した歯車を配置して動かすところのコードはこんな感じです。

// 一番小さい歯車
var g1 = new Gear(0.2, 10);
g1.shape.position.set(0, 1.5, 0);
this.add(g1.shape);
// 平行移動せず、中心軸周りに回転する制約
var c1 = new WHS.HingeConstraint(base, g1.shape,
  g1.shape.position,
  new THREE.Vector3(0, 1, 0)
);
this.addConstraint(c1);
// 二番目
var g2 = new Gear(0.2, 15);
g2.shape.position.set(g1.d + g2.d, 1.5, 0); // 一番目から基準円の半径の和だけ離れた場所
g2.shape.rotation.set(0, Math.PI / g2.m, 0); // 置いた瞬間干渉しないようにちょっと回して置く
this.add(g2.shape);
var c2 = new WHS.HingeConstraint(base, g2.shape,
  g2.shape.position,
  new THREE.Vector3(0, 1, 0)
);
this.addConstraint(c2);
// 三番目
var g3 = new Gear(0.2, 50);
g3.shape.position.set(g1.d + g2.d, 1.5, g2.d + g3.d);
g3.shape.rotation.set(0, Math.PI / g3.m, 0);
this.add(g3.shape);
var c3 = new WHS.HingeConstraint(base, g3.shape,
  g3.shape.position,
  new THREE.Vector3(0, 1, 0)
);
this.addConstraint(c3);
// 三番目だけをアニメーションで動かす
var l = new WHS.Loop(function() {
  g3.shape.rotation.y += 0.001;
});
l.start(world);

平歯車なので、噛み合う歯車の軸間距離は基準円の半径の和となります。
それにしてもこんな簡単に物理演算のシミュレーションが書けるなんて、すごい便利ですね。

時計の設計

組み合わせ歯車
軸を一致させる

やり始める前は、最初の歯車を動かすのにバネの弾性係数と振り子の質量を使って、1秒間というリズムを生み出すムーブメントを作れたらいいなと思っていました。しかし、歯車の形だけで1日終わってしまったので、残念ですがムーブメントは諦めて、最初の歯車はアニメーションで動かすことにしました。

時計を構成するのは秒針、長針、短針で、もちろん回転数の比は720:12:1です。この回転数を、右のような大小組み合わせた歯車5組で表現するのですが、ここでまた問題が。

秒針と長針の軸を一致させるのが意外と難しかったのです。60:1という回転数の差を歯車による減速2回で実現するのに、例えばぱっと思いつく6:1、10:1という組み合わせでやろうとすると、歯数をどう選んでもうまいこと秒針と長針の軸が同じ場所にできませんでした。変数6つの4連立方程式をたててみたものの、歯数は10以上の整数(少なすぎるとうまく噛まない)、モジュールは循環小数でない小数(できれば桁数が少ないほうが嬉しい)という束縛があるため、解くのが結構難しくなっていました。

大学を出てから15年、錆びついた頭で頑張って計算して、歯数75:10と80:10、モジュール0.18と0.17という数字を見つけて嬉しかったのですが、最初からググればよかった… orz

そんなわけで、無事に長針、短針も組み合わせ、実際に動くものがこちらです(CPUすごい食うので注意。あとGoogle Chromeでしか動かないかも)。Whitestorm.jsによる物理演算で時計を作った「3Dも物理演算もやったことないプログラマが、バッカソンに出場したら2日でできるようになりました」ブログ記事で紹介した、物理演算を使った時計のプログラムです。
使用ライブラリ: WhitestormJS/whitestorm.js
https://github.com/WhitestormJS/whitestorm.js

MITライセンスですので、物理演算で時計を作る必要があるときに参考にしていただければと思います。

やはり物理演算の精度がそれほど高くないので、拡大してよく見ると歯車同士が触れてないのに回っているし、プルプル震えてます。歯数が増えるとこの誤差がどんどんひどくなるので、バネを使ったムーブメントは時間があったとしても難しいかもしれません。

副産物!?

もともと一人でやるつもりでしたが、バッカソンではチームでの参加を推奨しています。開会式の後、チーム分けをしたときに「3Dとか初めてなんで誰か手伝ってください」と募ったところ、勇気ある大学生の金岡くんがチームに加わってくれました。

%e3%83%95%e3%83%ac%e3%83%bc%e3%83%a0%e7%94%bb%e5%83%8fJavaScriptは初めてで、3Dもやったことないという彼にgitの使い方やJavaScriptの基本などを教えつつ、作品の最終的な出来栄えを左右する「ケース」と「文字盤」をBlenderで作成して、jsonエクスポートをしてWhitestormのオブジェクトとして取り込む、という作業に取り組んでもらいました。

残念ながら時間が足りずケースのみの完成でした。さらに、彼のせいではないのですが、ケースを読み込むと処理性能の限界がきて歯車が動かなくなるため、ケース無しでの発表になってしまいました。文字盤かっこよかったんですが…

2日間の成果として、JavaScript書いたことない子がちょっと書けるようになって、gitで協調開発できるようになったというのも副産物として良かったなーと思います。

結果

まさかの2位で、スペースワールドのフリーパスをいただきました。ありがとうございます。

審査員の方には変態と言われてしまいましたが

以前同じ人に一番バカと言われて、その後さらにもう一度凄いバカと言われているので、大丈夫です。

バッカソンのまとめ

全く未知の分野のプログラミングをしつつ、sinとかcosとか連立方程式とか、久しぶりにやるような計算をして、動くものを作って満足、さらに発表で皆さんのポカーンとした呆れ顔を見ることができ、大変楽しい2日間でした。またやりたいです。


ヌーラボでは3Dや物理演算は使っていませんが、知らない分野やライブラリでも楽しんで挑戦していくエンジニアを募集していますちょっと話を聞きたいだけでも受け付けておりますので、よろしければお声がけください。

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる