概要
Akashic Engine Advent Calendar 2019 22日目の記事です。
Akashic Engine のシェーダ機能を作って、2D ゲームに今風でワンランク上の演出を導入しました。
- 記事の中のゲームは_なにかしらツク〜ル_さんのマップチップ・背景素材を使用させていただいております。いつもありがとうございます。
はじめに
みなさん、ゲームを作られていますか。作られていますよね。
私は昨年に意気揚々と作り始めたゲーム制作の進捗が、
Date: Fri Jan 4 17:08:36 2019 +0900
feat(shader): add snow shader
で止まっていることからお察しです。
コミットメッセージはどうやら「雪のシェーダを追加した」と思わしきものになっています。そう、私は Akashic Engine のシェーダ機能を使いたいのです。
以下は今年最後に作っていたシーンの一部です。どうやらゲームのオープニングとして降雪演出を調整していたようです。
-
Shader: Just snow
- Created by baldand
- Copyright (c) 2013 Andrew Baldwin (twitter: baldand, www: http://thndl.com), License = Attribution-NonCommercial-ShareAlik
シェーダは、スプライトアニメーションのように画像を用意する手間が省け、演算内容の細かな調整が行え、何より既成のシェーダ資産をそのまま活用できるという素晴らしいメリットがあります。ハードウェアアクセラレーションが有効な環境では CPU にも優しく、リッチな演出が期待できます。これは使わざるを得ません。
本文書では、Akashic Engine のサポートするシェーダ機能を確認しつつ、実際に自作ゲームへシェーダを導入するまでを本文書で紹介します。皆さんのゲーム制作の一助となれば幸いです。
シェーダ機能
Akashic Engine がサポートするシェーダ機能に関しては公式サイトの文書に記述があります。WebGL 1.0 を使用可能な環境が必要です。
2019 年 12 月現在、WebGL 1.0 は主要な環境でサポートされています。ポータビリティを是とする Akashic Engine においてカジュアルに使っても許される大変ありがたい機能となっております。胸を張って使っていきましょう。
シェーダを使うには
ここでは Shadertoy で公開されている、いわゆる DEMO をゲームに導入してみます。
せっかくなので上記で紹介した Just snow を使用させていただきましょう。
シェーダアセットのロードとシーンへの反映
Akashic Engine はシェーダアセットというものがないため、DEMO のフラグメントシェーダをテキストファイルのアセットとして取り込みます。
$ akashic scan asset text --text-asset-extension=txt
成功すると game.json に登録され、コード上から識別子でアクセスできるようになります。(以下は Snow
)
"Snow": {
"type": "text",
"path": "text/glsl/Snow.txt"
}
以下はフラグメントシェーダのテキストを読み込み g.ShaderProgram
を作る実装です:
// シーンとシェーダアセットID、シェーダに紐づく uniforms の取得方法を与えて `g.ShaderProgram` を返す:
export function makeShaderProgram(
scene: g.Scene,
assetId: string,
uniformsGetter?: () => { [key: string]: g.ShaderUniform }
) {
const fragmentShaderText = (scene.assets[assetId] as g.TextAsset).data;
return new g.ShaderProgram({
fragmentShader: fragmentShaderText,
uniforms: uniformsGetter ? uniformsGetter() : undefined
});
}
これをシーンの中で使ってみましょう。ひとまず背景となるスプライトを以下のように表示させてみました。
「急に何?」「どういうゲーム?」 と疑問に思われたかもしれませんが気にしないでください。今回の目的はシェーダの演出です。
このスプライトの上にシェーダを重ねることにします:
export class DemoScene extends g.Scene {
static ASSET_IDS = [
Shader.SNOW,
Image.BG_DEMO
];
// シーンに追加するための背景オブジェクトを作成する
makeBackground(): g.E {
const snow = makeShaderProgram(this, Shader.SNOW, () => ({ // 後述
}));
// 背景のスプライト:
const background = new g.Sprite({
scene: this,
src: this.assets[Image.BG_DEMO],
x: -160,
y: -32,
width: this.game.width,
height: this.game.height
});
// スプライトの上に重ねるシェーダ領域:
const shader = new g.Pane({
scene: this,
width: this.game.width,
height: this.game.height,
shaderProgram: snow,
opacity: 0.5
});
background.append(shader);
return background;
}
// 以下略
}
フラグメントシェーダを Akashic Engine 用に編集する
先の実装ではシェーダに与える uniform 変数が与えられていません。シェーダのパラメータとして g.ShaderUniform
を定義し、実装します。
Shadertoy の Input Uniforms は、そのままでは Akashic Engine に対応していません。元のシェーダに含まれる定義を編集しましょう。改変にあたってはシェーダのライセンスにご注意ください。
// "Just snow"
// Simple (but not cheap) snow made from multiple parallax layers with randomly positioned
// flakes and directions. Also includes a DoF effect. Pan around with mouse.
precision mediump float;
uniform vec3 iResolution;
uniform float iTime;
// (*1) uniform float iTimeDelta;
// (*1) uniform float iFrame;
// (*1) uniform float iChannelTime[4];
uniform vec4 iMouse;
// (*1) uniform vec4 iDate;
// (*1) uniform float iSampleRate;
// (*1) uniform vec3 iChannelResolution[4];
// (*1) uniform samplerXX iChanneli;
// (*2) Akashic Engine に必要なパラメータ:
varying vec2 vTexCoord;
uniform sampler2D uSampler;
uniform float uAlpha;
- (*1) 不要のため消します。利用している場合は参照箇所を定数に置き換えてしまいましょう。
- (*2) Akashic Engine に必要なパラメータを追加します。
そして、フラグメントシェーダの実装に、以下の Akashic Engine から読みだすためのエントリポイントを追加します。基本は Shadertoy のエントリである mainImage()
に描画元オブジェクトのテクスチャを与えています。
void main(void) {
vec4 color = texture2D(uSampler, vTexCoord);
mainImage(color, gl_FragCoord.xy);
color.a *= uAlpha;
gl_FragColor = color;
}
シェーダにパラメータを設定する
先ほどの makeShaderProgram()
の紹介で省略していた、シェーダに紐づく uniforms を与えるように設定します。ゲームの画面に合わせて解像度などを指定していきます。
// シーンに追加するための背景オブジェクトを作成する
makeBackground(): g.E {
const snow = makeShaderProgram(this, Shader.SNOW, () => ({
iResolution: {
type: "vec3",
value: new Float32Array([width, height, 1.0])
},
iTime: {
type: "float",
value: 0.0
},
iMouse: {
type: "vec4",
value: new Float32Array([width / 2, height / 2, 0.0, 0.0])
}
}));
// 省略
さらに、これをゲームのフレームが進行するごとに変化させていきましょう。ここでは背景の更新時にシェーダの iTime
変数を更新するようにしています。
background.update.add(() => {
const timeScaleFactor = Math.sin(this.frame * 1e-3) + 1;
snow.uniforms.iTime.value = (this.frame + timeScaleFactor) * 1e-3;
background.modified();
});
描画
スプライトの上に DEMO が表示されました。やりましたね!
おわりに
既成のシェーダを使い 2D ゲームにワンランク上の演出を導入することに成功しました。今回は静的なシーンでの導入になりましたが、次回はより動きのあるシーンでの活用とチューニングを行っていきたいものですね。皆様も是非お試しください。
ところで、シェーダを使う目的は果たしたのですが、それを使ったオレゲー制作の進捗に関しては今年一年間を通してゼロでした。
私はもう疲れました。また来年お会いしましょう。良いお年を。