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"/>
などとしてみれば、これらの形状は容易に用いることができるとわかる。
サンプル
## 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>
サンプル
このgeometryタグ
は必ず、type
属性及びname
属性を持つ。このtype
属性からジオメトリジェネレーターと呼ばれる、ジオメトリの生成関数を取り、指定したname
に結びついたジオメトリのインスタンスを生成します。
name
,type
以外の属性(上の例ではdivHorizontal
とdivVertical
)はこのジオメトリジェネレーターにパラメーターとして渡されることでジオメトリが生成されます。(どんなパラメーターがあるかどうかは指定する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(整数,分割数) |
サンプル
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"/>
サンプル
ジオメトリを自分で定義する
ジオメトリの中身
すこし、概念的な、理論的な話から入ります。
Grimoire.jsのジオメトリは、形状の各頂点の持っている情報と、つなぎ方のデータを持っています。逆に言えば、これらを準備することができれば自分でジオメトリを準備することができます。
各頂点の持っている情報
ジオメトリによって定義される形状は、ほとんどが三角形の集合体です。(場合によっては点や、線の場合は存在します)
各頂点が持っている情報というと、座標は直感的かもしれませんが、実はそれ以外にも法線(光の反射の計算のため)や、テクスチャ座標(テクスチャのどの位置が、その頂点に貼られるかという情報)が入っています。
これが、形状の各頂点の持っている情報です。必ず全ての頂点に共通してデータがなければいけません。つまり、頂点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_は不要です)
球のジオメトリを改造する
球のジオメトリに新たにcolorなる頂点情報を追加し、これが乱数により生成されるようにしてみよう。
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();
}
}
};
}
}
}
});
});
@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
<!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>