3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Grimoire.jsAdvent Calendar 2016

Day 8

Grimoire.jsのジオメトリについて

Last updated at Posted at 2016-12-08

WebGLを使って何かを描画するなら、何かしらの頂点データの入力が必要です。Grimoire.jsもその例外ではありません。
多くの場合、モデルデータの読み込みによるものか、プリミティブによって目的とするものを描画できるかもしれないですが、いくつかの場合、ジオメトリを自身で作る必要がある場合があるかもしれません。

しかし、Grimoire.jsではその必要性に応じて、既存の資産をある程度再利用することができる。ここでは、その再利用性に富んだGrimoire.jsのジオメトリ周辺のシステムについて説明します。

タグでジオメトリを作る

デフォルトジオメトリ

   <mesh geometry="cube" color="red"/>

などとシーン内に書けば赤い立方体が描画される。しかし、cubeというジオメトリをユーザーが追加する必要は存在しませんでした。

このように、いくつかのジオメトリは何もしなくても名前だけ指定すれば描画することができるジオメトリをここでは、デフォルトジオメトリと呼ぶことにしましょう。

以下は、Grimoire.jsが初期の状態で持っているデフォルトジオメトリは以下の通りです。

  • quad (ポリゴン2枚からなる最もシンプルな四角形)
  • cube(立方体)
  • sphere(球体)
   <mesh geometry="cube" color="red"/>
   <mesh geometry="quad" color="green" position="2,0,0"/>
   <mesh geometry="sphere" color="yellow" position="-2,0,0"/>

などとしてみれば、これらの形状は容易に用いることができるとわかる。

サンプル

image

## geometryタグとジオメトリジェネレーター

デフォルトジオメトリに含まれていない、Grimoire.jsに含まれている形状を追加するには、geometryタグを用いることで達成できます。
例えば、以下のようにgeometryタグを用いれば、分割数を縦7、横7に減らした球体を意味するsphere2を用いれる。

<geometry type="sphere" name="sphere2" divHorizontal="7" divVertical="7"/>
<scene>
   <camera/>
   <mesh geometry="sphere" color="green" position="2,0,0"/>
   <mesh geometry="sphere2" color="red" position="-2,0,0"/>
</scene>

サンプル

image

このgeometryタグは必ず、type属性及びname属性を持つ。このtype属性からジオメトリジェネレーターと呼ばれる、ジオメトリの生成関数を取り、指定したnameに結びついたジオメトリのインスタンスを生成します。

name,type以外の属性(上の例ではdivHorizontaldivVertical)はこのジオメトリジェネレーターにパラメーターとして渡されることでジオメトリが生成されます。(どんなパラメーターがあるかどうかは指定するtypeつまり、ジオメトリジェネレーターによって異なります)

ジオメトリジェネレーターとデフォルトジオメトリ

実は、デフォルトジオメトリも、このジオメトリジェネレーターによってあらかじめ生成されたものに過ぎないのです。

あくまで、最初に

<geometry type="cube" name="cube"/>
<geometry type="quad" name="quad"/>
<geometry type="sphere" name="sphere"/>

が挿入されているものと等価なことをやっているだけで、このようなプリミティブはジオメトリジェネレーターによって生成されます。

あらかじめ定義されたジオメトリジェネレーター

Grimoire.jsでは以下のようなジオメトリジェネレーターがあらかじめ定義されています。

type名 説明 パラメーター
quad 2ポリゴンからなる正方形 なし
cube 立方体 なし
sphere 球体 divHorizontal(整数、水平方向への分割数),divVertical(整数,垂直方向への分割数)
circle divide(整数,円状の分割数)
cylinder 円柱 divide(整数、筒の分割数)
cone 円錐 divide(整数、円状の分割数)
plane 複数ポリゴンからなる正方形 divide(整数,分割数)

サンプル

image

MeshRendererの定義するtargetBuffer属性

例えば、meshタグが用いているMeshRendererコンポーネントにはtargetBuffer属性というものがあり、これを用いて同じ形状であっても、どのような面のつなぎ方をするかどの頂点を用いるかなどを選択することができます。

とは言っても、文字だけで言ってもよくわからないので、例えば、以下のようにsphereにtargetBuffer="wireframe"と何も指定しないものを同時に描画しようとすると、片方だけワイヤフレーム表示になります。

<mesh geometry="sphere" color="red" position="-2,0,0"/>
<mesh geometry="sphere" color="blue" position="2,0,0" targetBuffer="wireframe"/>

サンプル

image

ジオメトリを自分で定義する

ジオメトリの中身

すこし、概念的な、理論的な話から入ります。
Grimoire.jsのジオメトリは、形状の各頂点の持っている情報と、つなぎ方のデータを持っています。逆に言えば、これらを準備することができれば自分でジオメトリを準備することができます。

各頂点の持っている情報

image

ジオメトリによって定義される形状は、ほとんどが三角形の集合体です。(場合によっては点や、線の場合は存在します)

各頂点が持っている情報というと、座標は直感的かもしれませんが、実はそれ以外にも法線(光の反射の計算のため)や、テクスチャ座標(テクスチャのどの位置が、その頂点に貼られるかという情報)が入っています。

これが、形状の各頂点の持っている情報です。必ず全ての頂点に共通してデータがなければいけません。つまり、頂点Aには座標と法線はあるけど、頂点Bには法線がないなどは一つのジオメトリの中では許されません

あるジオメトリの中で頂点Aが座標と法線とテクスチャ座標持っていると言えば、頂点BだろうがCだろうがすべてこれらの情報を持っていなければなりません

各頂点の持っている情報とattribute変数

ちょっとだけシェーダーファイルをのぞいてみれば以前の説明がより実感湧くかもしれません。(とはいえ、今回の説明では最小限度にとどめます)
今回は、Grimoire.jsのシェーダーファイルの中の頂点シェーダーを切り出したものです。

#ifdef VS
attribute vec3 position;
attribute vec2 texCoord;
uniform mat4 _matPVM;
void main()
{
  gl_Position = _matPVM * vec4(position,1.0);
  vTexCoord = texCoord;
}
#endif

attributeから始まる部分が各頂点のデータが入ります。ここでは、position(座標)とtexCoord(テクスチャ座標)のみ受け取ります。
この頂点シェーダーが各頂点ごとにこれらのデータを用いて処理をして、描画される形状が決定するわけです。

つなぎ方のデータ

頂点データを与えただけでは描画はできず、かならず、インデックスバッファと呼ばれる、三角形や線などのつなぎ方や描画する頂点の番号の連番を定義した配列も必要になります。

例えば、三角形で描画すると言ったら、[0,1,2,2,3,4]と書くと、0番目、1番目,2番目の頂点で三角形を一つ、2番目、3番目、4番目の頂点で三角形一つを描画することになります。

実際にジオメトリをいじってみる

GeometryBuilderクラスを用いる

実際にジオメトリを作る上では、ES6のジェネレーターを用いた、ジオメトリ作成のためのヘルパークラスであるGeometryBuilderを用いると簡易的に達成できます。(まだES6のジェネレーターをサポートしていない古いブラウザなどでは、babelを通す必要があります)

sphereのジェネレーターを見てみる

GeometryFactory.addType("sphere", {
      divVertical: { 
        converter: "Number",
        defaultValue: 100
      },
      divHorizontal: {
        converter: "Number",
        defaultValue: 100
      }
    }, (gl, attrs) => {
      const dH = attrs["divHorizontal"];
      const dV = attrs["divVertical"];
      return GeometryBuilder.build(gl, {
        vertices: {
          main: {
            size: {
              position: 3,
              normal: 3,
              texCoord: 2
            },
            count: GeometryUtility.sphereSize(dH, dV),
            getGenerators: () => {
              return {
                position: function* () {
                  yield* GeometryUtility.spherePosition(Vector3.Zero, Vector3.YUnit, Vector3.XUnit, Vector3.ZUnit.negateThis(), dH, dV);
                },
                normal: function* () {
                  yield* GeometryUtility.sphereNormal(Vector3.YUnit, Vector3.XUnit, Vector3.ZUnit.negateThis(), dH, dV);
                },
                texCoord: function* () {
                  yield* GeometryUtility.sphereTexCoord(dH, dV);
                }
              };
            }
          }
        },
        indices: {
          default: {
            generator: function* () {
              yield* GeometryUtility.sphereIndex(0, dH, dV);
            },
            topology: WebGLRenderingContext.TRIANGLES
          },
          wireframe: {
            generator: function* () {
              yield* GeometryUtility.linesFromTriangles(GeometryUtility.sphereIndex(0, dH, dV));
            },
            topology: WebGLRenderingContext.LINES
          }
        }
      });
    });

少し長いですが、いくつかに分けて解説します。


GeometryFactory.addType(typeName,attributes,generatorFunc)

最初に呼び出されている、addTypeメソッドによって、それぞれ、ジオメトリジェネレーター名受け取るパラメーターと型のリストジオメトリのインスタンスを生成する関数を受け取ります。

ジオメトリのインスタンスを生成する関数内で好きにジオメトリを生成すれば型として登録できるので、もしこの後に解説するGeometryBuilderを使用しなくても自分の好きな実装でジオメトリジェネレーターを定義することができます。

2つめのパラメーターリストは、コンポーネントの定義でattributesに記述する内容と全く同一で、コンバーターやデフォルト値が指定できます。

GeometryBuilder.build(gl,recipe)

ジェネレーターを用いて簡易的にプリミティブを生成するために利用しているクラスです。
返り値がGeometryになっています。ここからはこのパラメータについて解説していきます。

vertices

この中には複数個のオブジェクトを持ちますが、基本的にはなんでも名前をつければ問題ないです。ここではmainになっています。
(このオブジェクトごとにバッファをインターリーブになるように生成するので、もし、一つのバッファだけ更新したいときは別のオブジェクトにその中身を書くと、更新がしやすくなります)

この中には以下の3つの値を持ちます。

  • size
  • count
  • getGenerators

sizeは実際に作成する頂点あたりのデータの名前と、その要素数です。
つまり、座標(position)なら3次元ベクトルなはずなので3,テクスチャ座標(texCoord)なら2次元ベクトルなので2になります。

countは全体の頂点数です。(indexの長さではないことに注意してください。)四角形ならば4になるはずの数値になります。

getGeneratorsはsizeに数を指定した各頂点情報に対して、その内容を返すためのジェネレーターです。
それぞれ単に数値をyieldするだけのジェネレーターで、例えば3次元ベクトルならば1頂点を生成する際、3回ずつ呼び込まれます。

実際に例えばこれらの中で呼ばれているspherePositionのは以下のコードです。

  }
  public static *spherePosition(center: Vector3, up: Vector3, right: Vector3, forward: Vector3, rowDiv: number, circleDiv: number): IterableIterator<number> {
    yield* center.addWith(up).rawElements as number[];
    yield* center.subtractWith(up).rawElements as number[];
    const ia = 2 * Math.PI / circleDiv;
    const ja = Math.PI / (rowDiv + 1);
    for (let j = 1; j <= rowDiv; j++) {
      const phi = ja * j;
      const sinPhi = Math.sin(phi);
      const upVector = up.multiplyWith(Math.cos(phi));
      for (let i = 0; i <= circleDiv; i++) {
        const theta = ia * i;
        yield* (right.multiplyWith(Math.cos(theta)).addWith(forward.multiplyWith(Math.sin(theta)))).multiplyWith(sinPhi).addWith(upVector).rawElements as number[];
      }
    }
  }

これは一見わかりにくいですが、ジェネレーターにすることで、プリミティブのなかのいくつかの頂点や、法線の生成部分などを混ぜることができるようになるということです。

indices

defaultの中のgeneratorが三角形のリストをひたすら返すジェネレーターになります。このジェネレーターの終端までがインデックスバッファの終点になります。

topologyにはつなぎ方を指定します。WebGLRenderingContext.TRIANGLESでgeneratorの中身を三角形のリストとしてつなぎ、WebGLRenderingContext.LINESで線のリストとして描画することになります。これはWebGLのパラメーターそのままで、その他にも以下のようなものがあります。(GL_は不要です)

image

球のジオメトリを改造する

球のジオメトリに新たにcolorなる頂点情報を追加し、これが乱数により生成されるようにしてみよう。

index.js
const GeometryFactory = gr.lib.fundamental.Geometry.GeometryFactory;
const GeometryBuilder = gr.lib.fundamental.Geometry.GeometryBuilder;
const GeometryUtility = gr.lib.fundamental.Geometry.GeometryUtility;
const Vector3 = gr.lib.math.Vector3;
GeometryFactory.addType("colored-sphere", {
    divVertical: {
        converter: "Number",
        defaultValue: 100
    },
    divHorizontal: {
        converter: "Number",
        defaultValue: 100
    }
}, (gl, attrs) => {
    const dH = attrs["divHorizontal"];
    const dV = attrs["divVertical"];
    return GeometryBuilder.build(gl, {
        indices: {
            default: {
                generator: function*() {
                    yield* GeometryUtility.sphereIndex(0, dH, dV);
                },
                topology: WebGLRenderingContext.TRIANGLES
            },
            wireframe: {
                generator: function*() {
                    yield* GeometryUtility.linesFromTriangles(GeometryUtility.sphereIndex(0, dH, dV));
                },
                topology: WebGLRenderingContext.LINES
            }
        },
        vertices: {
            main: {
                size: {
                    position: 3,
                    normal: 3,
                    texCoord: 2,
                    color: 3
                },
                count: GeometryUtility.sphereSize(dH, dV),
                getGenerators: () => {
                    return {
                        position: function*() {
                            yield* GeometryUtility.spherePosition(Vector3.Zero, Vector3.YUnit, Vector3.XUnit, Vector3.ZUnit.negateThis(), dH, dV);
                        },
                        normal: function*() {
                            yield* GeometryUtility.sphereNormal(Vector3.YUnit, Vector3.XUnit, Vector3.ZUnit.negateThis(), dH, dV);
                        },
                        texCoord: function*() {
                            yield* GeometryUtility.sphereTexCoord(dH, dV);
                        },
                        color: function*() {
                            while (true) {
                                yield Math.random();
                            }
                        }
                    };
                }
            }
        }
    });
});
custom.sort
@Pass
@BlendFunc(SRC_ALPHA,ONE_MINUS_SRC_ALPHA)
FS_PREC(mediump,float)
varying vec3 vColor;
#ifdef VS
attribute vec3 position;
attribute vec3 color;
uniform mat4 _matPVM;
void main()
{
  gl_Position = _matPVM * vec4(position,1.0);
  vColor = color;
}
#endif
#ifdef FS

@{default:1}
uniform float alpha;
void main(void)
{
  gl_FragColor = vec4(vColor,alpha);
}
#endif
index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link href="../static/index.css" rel="stylesheet" />
  <script src="../static/grimoire-preset-basic.js" charset="utf-8"></script>
  <script src="./index.js"></script>
</head>

<body>
  <script type="text/goml">
    <goml height="fit">
      <import-material typeName="custom" src="./custom.sort" />
      <geometry name="s1" type="colored-sphere" divHorizontal="4" divVertical="4" />
      <geometry name="s2" type="colored-sphere" divHorizontal="10" divVertical="10" />
      <geometry name="s3" type="colored-sphere" divHorizontal="30" divVertical="30" />
      <geometry name="s4" type="colored-sphere" divHorizontal="60" divVertical="60" />
      <scene>
        <camera>
          <camera.components>
            <MouseCameraControl center="10" />
          </camera.components>
        </camera>
        <mesh geometry="s1" material="new(custom)" position="-4,0,0" />
        <mesh geometry="s2" material="new(custom)" position="-2,0,0" />
        <mesh geometry="s3" material="new(custom)" position="2,0,0" />
        <mesh geometry="s4" material="new(custom)" position="4,0,0" />
      </scene>
    </goml>
  </script>
</body>

</html>

image

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?