Grimoire.jsで🍣を作る

  • 14
    いいね
  • 0
    コメント

まず

みんなだいすきお寿司です🍣

お寿司🍣が3Dになってブラウザぴょんぴょんしてたら、こころがぴょんぴょんしちゃいますね。

Grimoire.jsをつかってカスタムメ●ド3Dならぬカスタム寿司3Dを作りました。

お寿司が跳ねてます。寿司ネタマテリアルがカスタムできてTwitter共有もできます。うれしいですね。

できたもの↓

Custom SUSHI 3D
https://pnlybubbles.github.io/custom-sushi-3d/

スクリーンショット 2017-01-08 19.41.12.png

一応、今までのジオメトリの話とポストエフェクトの話とつながっています。

🍣を3D空間に置く

htmlを書く感じで3Dオブジェクトの配置をsceneタグ内に記述します。

<object class="sushi" position="0,0,6">
  <mesh class="neta" geometry="neta" material="#neta-material" position="0,0,0" scale="2.2,0.2,1"></mesh>
  <object class="come" position="0,-1.2,0" scale="1.3,1,0.9">
    <mesh geometry="come" material="#come-material" position="0,0.5,0" scale="1,0.5,1"></mesh>
  </object>
</object>

ここでobjectタグというのは、Transformコンポーネントのみを持つオブジェクトです。meshタグの場合はさらにマテリアル、レンダリングのためのコンポーネントを持っています。[参照]

objectタグはレンダリングされないため、親子構造を作るときに用います。子のオブジェクトの座標は親に対する相対座標になるため、複数のオブジェクトをグループ化して動かしたいときに便利です。

さらに寿司を3つに増やすのも単純です。

<object id="sushi-group">
  <object class="sushi" position="0,0,6">
    <mesh class="neta" geometry="neta" material="#neta-material" position="0,0,0" scale="2.2,0.2,1"></mesh>
    <object class="come" position="0,-1.2,0" scale="1.3,1,0.9">
      <mesh geometry="come" material="#come-material" position="0,0.5,0" scale="1,0.5,1"></mesh>
    </object>
  </object>
  <object class="sushi" position="-5.196,0,-3" rotation="0,-120,0">
    <mesh class="neta" geometry="neta" material="#neta-material" position="0,0,0" scale="2.2,0.2,1"></mesh>
    <object class="come" position="0,-1.2,0" scale="1.3,1,0.9">
      <mesh geometry="come" material="#come-material" position="0,0.5,0" scale="1,0.5,1"></mesh>
    </object>
  </object>
  <object class="sushi" position="5.196,0,-3" rotation="0,120,0">
    <mesh class="neta" geometry="neta" material="#neta-material" position="0,0,0" scale="2.2,0.2,1"></mesh>
    <object class="come" position="0,-1.2,0" scale="1.3,1,0.9">
      <mesh geometry="come" material="#come-material" position="0,0.5,0" scale="1,0.5,1"></mesh>
    </object>
  </object>
</object>

3つの寿司を一緒に回したいのでそれらobject#sushi-groupで囲います。classやidもhtmlと同様に利用することができます。

🍣を動かす

設定したidやclassをクエリで指定して、任意のオブジェクトの属性をjavascriptから操作します。

gr('#canvas')('#sushi-group').setAttribute('rotation', '0,60,0');

回転の場合、クオータニオンを利用して直接属性のオブジェクトを渡すことも可能です。座標の場合はVector3などを直接渡せます。文字列のパースが行われないのでパフォーマンスに優れます。

gr('#canvas')('#sushi-group').setAttribute('rotation', Quaternion.angleAxis(60 * Math.PI / 180, new Vector3(0, 1, 0)))

今回はアニメーションを行うためにshifty.jsを用いました。毎フレームstepが呼ばれるようなので、その中で属性を変更する処理を行います。

const _groupRotation = gr('#canvas')('#sushi-group').getAttribute('rotation');
(new Tweenable()).tween({
  from: {
    theta: 0,
  },
  to: {
    theta: 40,
  },
  duration: 800,
  easing: 'easeOutQuint',
  step(state) {
    const q = Quaternion.multiply(_groupRotation, Quaternion.angleAxis(state.theta * Math.PI / 180, new Vector3(0, 1, 0)));
    gr('#canvas')('#sushi-group').setAttribute('rotation', q);
  },
});

🍣マテリアルを適用する

上のコードの#come-materialや#neta-materialはsceneタグの外側に定義されています。いままでのシーンの配置以外のリソースはこのように定義されています。

<goml width="512" height="512">
  <geometry type="sushi" name="neta" div="10,1,1"/>
  <geometry type="sushi" name="come" div="1,1,1"/>
  <import-material typeName="come" src="./come.sort"/>
  <import-material typeName="neta" src="./neta.sort"/>
  <material class="neta-material" id="neta-material" type="neta" sun="n(1,-1.1,-1.2)" ambient="#332" color="#d22" colorStripe="#d55" width="0.01" margin="0.05" offset="0,0" rotation="10" />
  <material id="come-material" type="come" color="#fff" sun="n(1,-1.1,-1.2)" ambient="#332" shade="0.3" blur="0.5"/>
  <scene>
    <!-- sceneの定義 -->
  </scene>
</goml>

geometryタグでジオメトリの話で作ったものを読み込み、今回新たにネタマテリアルと米マテリアルを作りました。

米マテリアルはただ白いだけで、ネタマテリアルは縞模様をカスタマイズできるように幾つかのパラメータを用意しました。

以下がGrimoire.jsがマテリアルを定義するために使うsortファイルで、neta.sortの一部省略したものです。ポイントが@から始まる記法で、WebGLに関する設定を記述できます。@import("path/to/sort")といったシェーダーのモジュールシステムも提供されています。[参照]

@Pass {
  @DepthFunc(LEQUAL)
  @BlendFunc(SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
  @CullFace(BACK)

  FS_PREC(mediump,float)

  varying vec2 vTexCoord;
  varying vec3 vNormal;
  varying vec3 vPosition;

  #ifdef VS
    // 頂点シェーダー
  #endif

  #ifdef FS
    // フラグメントシェーダー
    @HAS_TEXTURE{sampler:"texture"}
    uniform bool _textureUsed;
    uniform sampler2D texture;
    @{type:"color", default:"#000000"}
    uniform vec4 ambient;
    @{default:"10"}
    uniform vec3 sun;
    @{type:"color", default:"#fff"}
    uniform vec4 color;
    @{type:"color", default:"#ccc"}
    uniform vec4 colorStripe;
    @{default:"0,0"}
    uniform vec2 offset;
    @{default:"0.05"}
    uniform float width;
    @{default:"0.07"}
    uniform float margin;
    @{default:"45"}
    uniform float rotation;
    @{default:"0.01"}
    uniform float blur;
    void main(){
      // メインの処理
    }
  #endif
}

ここでフラグメントシェーダーの中にある下のような@記法で、uniform変数をタグの属性にバンディングすることができます。floatといった変数型を識別して属性の文字列から対象の型に変換してくれます。

@{default:"0.05"}
uniform float width;

これで下のようにxmlで指定した属性がシェーダーに渡されるので、縞模様のカスタムするためのパラメータをタグに露出させることができました。

<material type="neta" width="0.1" />

これらをjavascriptからsetAttributeを使って変更することで、htmlのform要素とマテリアルの動的な連携が行えます。

今回はUIのjsフレームワークとしてvue@2を用いました。

<div id="customize">
  <input type="text" v-model="width">
</div>
new Vue({
  el: '#customize',
  data: {
    width: 0.1,
  },
  watch: {
    width(v) {
      gr('#canvas')('#neta-material').setAttribute('width', v);
    },
  },
});

inputの変更を監視して、値を#neta-materialに適用しています。

で

具体的なシェーダーの実装などは省略しましたが、Grimoire.jsを使って宣言的にシーンを記述し、htmlと同じ要領で3D空間やシェーダーを弄れる感覚が伝わればいいなと思います。

下のリンクであそべます。

Custom SUSHI 3D
https://pnlybubbles.github.io/custom-sushi-3d/

ソースコードはこちらです。

https://github.com/pnlybubbles/custom-sushi-3d