Ren’Py演出強化の観点からGLSLの最低限の機能を使いながら把握する

本ページの趣旨

 Ren’Pyでは凝った演出をしたい場合、GLSLを活用させる方針になっているようです。GLSLを使用して、トランジション、アニメーション、ATLプロパティーを追加できます。

 しかし、公式ドキュメントでの説明はRen’PyでのGLSLの使い方を軽く説明しているのみであり、GLSLそのものは分かっている方を対象にしています。

余談 翻訳してる本人も理解していないのでほぼ直訳しています。分かる方いたら校正お願いしたい。

 GLSLはグラフィックを処理するための言語であり、しっかり学ぼうとすると時間がかかりそうですが、Ren’Pyでの演出強化という観点からすると経過時間に対応してピクセルの描画位置や色を変更するだけで必要十分でしょう。

 本ページでは上記を達成するために、最低限の機能を把握するボトムアップよりのアプローチでRen’PyでGLSLをどう使えるかを検討していきその結果を記録します。きちんと調べている訳ではないので、正確な情報というわけではありません。動けばそれでよしの精神です。

サンプルコードを動かす

公式のサンプルコードを元に以下をスクリプトに貼り付けてRen’Pyで動かしてみます。

https://ja.renpy.org/doc/html/model.html#renpy.register_shader

init python:

renpy.register_shader("example.gradient", variables="""
uniform vec4 u_gradient_left;
uniform vec4 u_gradient_right;
uniform vec2 u_model_size;
varying float v_gradient_done;
attribute vec4 a_position;
""", vertex_300="""
v_gradient_done = a_position.x / u_model_size.x;
""", fragment_300="""
float gradient_done = v_gradient_done;
gl_FragColor *= mix(u_gradient_left, u_gradient_right, gradient_done);
""")

label start:
scene bg whitehouse
show eileen happy at center:
shader "example.gradient"
u_gradient_left (1.0, 0.0, 0.0, 1.0)
u_gradient_right (0.0, 0.0, 1.0, 1.0)

e "Ren'Py の新しいゲームを作成しました。"

 結果は以下のようにエイリーンさんにグラデーションがかかりました。

 このコードを読み解くと、renpy.register_shader関数によってshaderにexample.gradientとその内容を設定し、ATLプロパティーから example.gradient で 使用している変数である u_gradient_left と u_gradient_right を設定できるようにしています。

init python:

renpy.register_shader("example.gradient", variables="""
uniform vec4 u_gradient_left;
uniform vec4 u_gradient_right;
uniform vec2 u_model_size;
varying float v_gradient_done;
attribute vec4 a_position;
""", vertex_300="""
v_gradient_done = a_position.x / u_model_size.x;
""", fragment_300="""
float gradient_done = v_gradient_done;
gl_FragColor *= mix(u_gradient_left, u_gradient_right, gradient_done);
""")

label start:
scene bg whitehouse
show eileen happy at center:
shader "example.gradient"
u_gradient_left (1.0, 0.0, 0.0, 1.0)
u_gradient_right (0.0, 0.0, 1.0, 1.0)

e "Ren'Py の新しいゲームを作成しました。"

 少し調べたところでは、uniform, attribute, varyingの修飾子はそれぞれ次のような意味を持ちます。

uniform

 アプリケーション独自で使用する値。Ren’Pyの場合はATLから指定できる値やRen’Py側で用意している値になります。u_renpy_で始まる名前はRen’Pyで予約済みです。ユーザーがよく使いそうなuniform変数を以下にまとめました。

vec2 u_model_size

モデルの幅と高さです。u_model_size.xで横幅、u_model_size.yで縦幅を所得できます。

float u_time

挙動をみるにゲーム開始からの秒数

vec4 u_random

フレームごとに (非常に高い確率で) 異なる 0.0 から 1.0 の間の 4 つの乱数です。

sampler2D tex0, sampler2D tex1, sampler2D tex2

画像の情報がこの変数に格納されます。

attribute

 attributeは頂点の情報が格納されるようです。よく使いそうなものを以下にまとめました。

vec4 a_position

処理する頂点の位置です。a_position.x, yがそれぞれ処理対象のx, y座標になります。これはモデル上での絶対値のようです。u_model_sizeで割れば相対値になります。

vec2 a_tex_coord

この頂点のテクスチャ内部での座標情報です。a_position.xyが処理対象の(x, y)座標になるようです。a_position.stはxyのエイリアスのようです。これはテクスチャのサイズに対する相対値のようです。

Varying

 上記 attribute 変数は Vertex shader で参照でき、 Fragment shader では参照できないようです。そのため、 attribute 変数の情報を Fragment shader で参照したい場合は Varying 変数を間に挟みます。また、Fragment shader 中で定義した変数が、他のシェーダーから参照可能なのに対して、 Varying は定義中のシェーダーからのみ参照可能なので、名前の衝突が起こりにくいようです。

vec型について

vecX型の変数はvecX.x, y, zという形式で指定の要素にアクセスでき、vec4.xyのような形式で指定の要素の第一、第二までを含むvec型にできるようです。

変数の定義の仕方

 変数の宣言はregister関数中のvariables引数で文字列の形で指定しています。多分内部で合体させて定義関数作っているのでしょう。ここではATLから設定できるuniform変数u_gradient_left, u_gradient_right, Ren’Pyから提供されるu_model_size, a_position, a_positionの情報をフラグメントシェーダーに渡すための v_gradient_doneの使用を宣言しています。

variables="""
uniform vec4 u_gradient_left;
uniform vec4 u_gradient_right;
uniform vec2 u_model_size;
varying float v_gradient_done;
attribute vec4 a_position;
"""

頂点シェーダーの定義

vertex_XXX形式の引数に文字列として頂点シェーダーの処理を定義しています。XXXの部分は処理を行う順番設定に使用しているようです。頂点シェーダーはテクスチャの頂点ごとの処理をするところで、その後にフラグメントシェーダーでピクセルごとの処理をしているようです。ここでは頂点のx座標a_position.xをモデルの横幅u_model_size.xで割って現在の頂点位置の横幅に対する割合をv_gradient_doneに入れています。

vertex_300="""
v_gradient_done = a_position.x / u_model_size.x;
"""

フラグメントシェーダーの定義

Fragment_XXX形式の引数に文字列としてフラグメントシェーダーの処理を定義しています。ここではgradient_doneにv_gradient_doneの結果を代入しています。直接使用しても特にエラーはでず、なぜgradient_doneを挟んでいるのかは不明です。mix関数はmix(x, y, a)としたときx(1-a)+y*aの形式で値を返します。つまり、ここではgradient_doneの数値によってu_gradient_left, rightに指定した値間の値が算出され、それがgl_FragColorに乗算されています。これを見るとgl_FragColorには元の色情報が入っており、これを変更すると処理後の色を変更できると分かります。

Fragment_300="""
float gradient_done = v_gradient_done;
gl_FragColor *= mix(u_gradient_left, u_gradient_right, gradient_done);

最終的に変更するべきなのはgl_Positionまたは、gl_FragColorのみ

gl_FragColorには元々の色情報が含まれており、これを変更して色を変更できることが分かりました。また、頂点シェーダーにもgl_Positionという変数があり、こちらも変更すると頂点の位置(画像のピクセルを描画する位置)を変更できました。元からある画像になにかエフェクトをかけたい場合は、場所と色だけ変更できればよいので最終的にはこの2つを変更すればよいでしょう。また、この2つは宣言しなくとも使用できるようです。

gl_FragColorを使用して色を変更してみる

 gl_FragColorを使って色を返るエフェクトを作成してみました。

init python:

renpy.register_shader("example.test1", variables="""
uniform vec4 u_color;
""", fragment_300="""
gl_FragColor *= u_color;
""")

label start:
scene bg whitehouse
show eileen happy at center:
show layer master:
shader "example.test1"
u_color (0.0, 1.0, 0.0, 1.0)

今度は立ち絵だけではなくレイヤーにエフェクトをかけてみます。結果が以下になり、gl_FragColorだけ変更すれば色を変えらると分かります。

gl_Positionで位置を変えてみる

今度はgl_Positionを変更してみましょう。

init python:

renpy.register_shader("example.test1", variables="""
attribute vec4 a_position;
""", vertex_300="""
gl_Position.x += 1.0;
""")


label start:
scene bg whitehouse
show eileen happy at center:
show layer master:
shader "example.test1"

結果は以下になりました。1ピクセル横にずれることを期待していましたが、半画面移動しています。これはgl_Position.xにいれる値を整数にしても浮動小数にしても代わりませんでした。

さらに次のように頂点のy座標が1より大きいときのみx座標を変更するようにすると

init python:

renpy.register_shader("example.test1", variables="""
attribute vec4 a_position;
""", vertex_300="""
if (a_position.y > 1){
gl_Position.x += 1;
""")

結果は以下のように滑らかに斜めになりました。これを見るに頂点というのは画像でいう四角形の四隅のことのようです。

4隅のみ指定となると、波打つようにうねうねする変形はどうするのでしょう?gl_Positionではできなさそうなのでgl_FragColorの活用を考えてみます。

gl_FragColorでピクセル描画位置を操作する

画面の下半分を半画面ずらす意図で以下のコードを作成しました。a_tex_coordにはテクスチャ内での頂点の座標、tex0には元画像の情報が入っておりtexture2D(tex0, v_tex_coord.xy)とするとtex0の画像から,v_tex_coord.xy座標の色情報を取り出せます。これを利用して、半画面分ずれた位置の色情報をgl_FragColorに入れれば実質画像を移動するのと同じことになると考えました。画像の外の情報を取り出そうとすると、一番近い座標に丸め込まれるようなので、if文を使用して、はみ出た分のgl_FragColorは0埋めしています。

    renpy.register_shader("example.test1", variables="""
uniform sampler2D tex0;
attribute vec2 a_tex_coord;
varying vec2 v_tex_coord;
""", vertex_300="""
v_tex_coord = a_tex_coord;
""", fragment_300="""
if (v_tex_coord.y > 0.5){
if (v_tex_coord.x < 0.5){
gl_FragColor = texture2D(tex0, v_tex_coord.xy+vec2(0.5, 0.0));
} else {
gl_FragColor = vec4(0.0);
}
}
""")

結果は以下になりました。部分的に画像を移動するのには成功しましたが、画面全体で半画面移動するのではなく、各画像ごとに移動してしまっています。レイヤーを対象にエフェクトを書けた場合でも、まとめて処理するのではなく、そのレイヤーに含まれる画像ごとにエフェクトをかけているようです。画面全体へのエフェクトはなかなか面倒そうです。

適当に設定を弄ってみてmesh Trueを追加するとレイヤー全体にエフェクトが適用されました。どうやらmeshをTrueにすると、transformの結果を1つのモデルとして処理するようです。

label start:
show layer master:
shader "example.test1"
mesh True

GLSLを使ったトランジション

今度はGLSLを使ったトランジションを検討してみます。公式ドキュメントからサンプルを抜粋しました。

init python:
renpy.register_shader("example.test1", variables="""
uniform float u_lod_bias;
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform float u_renpy_dissolve;
attribute vec2 a_tex_coord;
varying vec2 v_tex_coord;
""", vertex_300="""
v_tex_coord = a_tex_coord;
""", fragment_300="""
vec4 color0 = texture2D(tex0, v_tex_coord.st, u_lod_bias);
vec4 color1 = texture2D(tex1, v_tex_coord.st, u_lod_bias);

gl_FragColor = mix(color0, color1, u_renpy_dissolve);
""")

transform dt(delay=1.0, new_widget=None, old_widget=None):
delay delay
Model().texture(old_widget).child(new_widget)
shader [ 'example.test1' ]

u_renpy_dissolve 0.0
linear delay u_renpy_dissolve 1.0

label start:
scene bg whitehouse
show eileen happy at center
"1"
scene bg cave with dt
"2"

上記のようにすると通常の dissolve と同じように画面がトランジションしました。

transformの定義を見ると、 Model()を displayable として transform の子に指定し、この子をディゾルブする画像にしてトランジションを実現しています。 tex0, 1 へ Displayable を渡すときも Model() を使用していて、 Model().texture の引数に指定した old_widget が tex0 に、 Model().texture(old_widget).child の引数に指定した new_widget が tex1 に渡されているようです。経過時間の処理には u_time は使用せず、ワーパー関数の linear による u_renpy_dissolve の変更を使用しています。 shader の指定がリストになっていますが、リストでなくとも動作しました。

registerの中身を見ると、texture2Dでtex0, 1それぞれから特定の座標の色を取り出して、mix 関数で混ぜているようです。v_tex_coordでxyではなくstにしていますが、これはxyのエイリアスのようです。

これを変更して画面の上下半分の位置でぱっくり割れて、上半分が右に、下半分が左に開いて新しい画面が現れるトランジションを定義してみます。

init python:
renpy.register_shader("example.test1", variables="""
uniform float u_lod_bias;
uniform sampler2D tex0;
uniform sampler2D tex1;
uniform float u_open;
attribute vec2 a_tex_coord;
varying vec2 v_tex_coord;
""", vertex_300="""
v_tex_coord = a_tex_coord;
""", fragment_300="""

if (v_tex_coord.y > 0.5){
if (v_tex_coord.x < 1.0 - u_open){
gl_FragColor = texture2D(tex0, v_tex_coord.xy+vec2(u_open, 0.0));
} else {
gl_FragColor = texture2D(tex1, v_tex_coord.xy);
}
} else {
if (v_tex_coord.x > u_open){
gl_FragColor = texture2D(tex0, v_tex_coord.xy-vec2(u_open, 0.0));
} else {
gl_FragColor = texture2D(tex1, v_tex_coord.xy);
}
}
""")


transform dt(delay=1.0, new_widget=None, old_widget=None):
delay delay
Model().texture(old_widget).child(new_widget)
shader 'example.test1'

u_open 0.0
linear delay u_open 1.0


label start:
scene bg whitehouse
show eileen happy at center
"1"
scene bg cave with dt
"2"

結果が以下のようになりました。サイズが大きいので動画にはしませんが、時間経過とともにぱっくり割れていき新しい画面が表れます(テキストウィンドウまで一緒に動いてしまっていますが)。

ちなみにこちらのサイトにGLSLを使用したトランジションが大量に公開されています。著作権には気を付けつつ参考にしましょう。

https://gl-transitions.com/gallery

これで元画像に対して部分ごとに時間経過に合わせた移動と色の変更ができるようになりました。応用すれば大抵のエフェクトは作れるでしょう。部分ごとの変更を伴わなければglslなしのATLで十分かと思います。

なお、GLSLは元画像関係ない幾何学的な模様も造れるようですが、そういうのは動画素材使った方が早いと思います。

Pocket

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です