この記事は、Pythonista3 Advent Calendar 2022 の08日目の記事です。
一方的な偏った目線で、Pythonista3 を紹介していきます。
ほぼ毎日iPhone(Pythonista3)で、コーディングをしている者です。よろしくお願いします。
以下、私の2022年12月時点の環境です。
--- SYSTEM INFORMATION ---
* Pythonista 3.3 (330025), Default interpreter 3.6.1
* iOS 16.0.2, model iPhone12,1, resolution (portrait) 828.0 x 1792.0 @ 2.0
他の環境(iPad や端末の種類、iOS のバージョン違い)では、意図としない挙動(エラーになる)なる場合もあります。ご了承ください。
ちなみに、model iPhone12,1
は、iPhone11 です。
Shader(しぇーだー)? GLSL(じーえるえすえる)?🤔
shader | scene — 2D Games and Animations — Python 3.6.1 documentation
シェーダーオブジェクトは、カスタムOpenGLフラグメントシェーダーを表し、スプライトノードおよびエフェクトノードオブジェクトのレンダリング動作を(それぞれのシェーダー属性を通じて)変更するために使用することができます。
シェーダプログラミングは複雑なトピックであり、完全な紹介はこのドキュメントの範囲外ですが、基本的なコンセプトは非常にシンプルです。フラグメントシェーダは本質的に GPU 上で直接実行される関数/プログラムであり、各ピクセルの色値を生成する責任を負っています。シェーダは GLSL(GL Shading Language)で書かれ、これは C 言語に非常によく似ています。
私は、ui
モジュールのdraw
メソッドや、scene
での描画(Processing や、JavaScript のcanvas(CanvasRenderingContext2D) のようなもの)では、鉛筆をx
とy
座標に持ってきて線を引く(それを繰り返す)。といった処理のイメージを持っています。
Shader (これ以降は、GLSLと記載)では、事前にGPU で演算をし、結果の全てをドカっと描画する。みたいなイメージで、理解をしています。
「線を引く」ことも、今までとは全く違う書き方をするので、最初はびっくりするかもしれませんが、GPU 演算のパワーで面白い表現ができます。
Pythonista3 Documentation でも「複雑なトピック」とあるように、よちよち歩きの私では簡単に概略を説明することは難しいです。。。
サンプルを動かしてみる
とにかく、Pythonista3 でどうやってGLSL やるのか体験してみましょう。
shader | scene — 2D Games and Animations — Python 3.6.1 documentation
shader
の項目から下へスクロールすると、サンプルコードがあり[Open in Editor]
をタップすると、エディタにサンプルコードがコピーされた状態で出てきます。
▷
をタップすると、コードが実行されます。
画像に波紋のようなエフェクトが出ており、タップするとその場所を中心とした波紋に変わります。
このような処理はGLSL 以外で実装すると、結構処理が重くなってしまう実装の部類になると思われますが、GLSL のおかげでぬるぬる動いてくれています。
サンプルのGLSL コード
GLSL のコード部分です:
precision highp float;
varying vec2 v_tex_coord;
// These uniforms are set automatically:
uniform sampler2D u_texture;
uniform float u_time;
uniform vec2 u_sprite_size;
// This uniform is set in response to touch events:
uniform vec2 u_offset;
void main(void) {
vec2 p = -1.0 + 2.0 * v_tex_coord + (u_offset / u_sprite_size * 2.0);
float len = length(p);
vec2 uv = v_tex_coord + (p/len) * 1.5 * cos(len*50.0 - u_time*10.0) * 0.03;
gl_FragColor = texture2D(u_texture,uv);
}
GLSL のコードをPython の文字列として、変数ripple_shader
に代入しています:
ripple_shader = '''
precision highp float;
varying vec2 v_tex_coord;
// These uniforms are set automatically:
uniform sampler2D u_texture;
uniform float u_time;
uniform vec2 u_sprite_size;
// This uniform is set in response to touch events:
uniform vec2 u_offset;
void main(void) {
vec2 p = -1.0 + 2.0 * v_tex_coord + (u_offset / u_sprite_size * 2.0);
float len = length(p);
vec2 uv = v_tex_coord + (p/len) * 1.5 * cos(len*50.0 - u_time*10.0) * 0.03;
gl_FragColor = texture2D(u_texture,uv);
}
'''
画像が入っているSpriteNode
へ、.shader
でGLSL コードを反映させることになります:
self.sprite = SpriteNode('test:Pythonista', parent=self)
self.sprite.shader = Shader(ripple_shader)
サンプルコードというのは、往々にして複数の要素が混じり合い複雑になっているものなので、シンプルに解体し整理していきます。
GLSL のHello World!
サンプルコードを改造する
少々力技ではありますが、先ほど使った波紋のコードをお借りし、ゴリッと書き換えます。
precision highp float;
varying vec2 v_tex_coord;
// These uniforms are set automatically:
uniform sampler2D u_texture;
uniform float u_time;
uniform vec2 u_sprite_size;
// This uniform is set in response to touch events:
uniform vec2 u_offset;
void main(void) {
vec2 p = -1.0 + 2.0 * v_tex_coord + (u_offset / u_sprite_size * 2.0);
float len = length(p);
vec2 uv = v_tex_coord + (p/len) * 1.5 * cos(len*50.0 - u_time*10.0) * 0.03;
gl_FragColor = texture2D(u_texture,uv);
}
↑ のコードを↓ に書き換える。
precision highp float;
varying vec2 v_tex_coord;
void main(void) {
gl_FragColor = vec4(v_tex_coord, 0.0, 1.0);
}
余裕がある方は、最初のサンプルコードから何が残って何が削られたか見比べてみましょう。
GLSL の1行コメントアウトは//
、/* 複数行 */
で1行以上の範囲でコメントアウトできます。Python のようにオフサイドルールはありません。インデントは気にしなくて大丈夫です。
▷
をタップし実行してみましょう。
この絵が出てきたら成功です。
サンプルコードから、コメントアウトして実行させるならこのようになります:
precision highp float;
varying vec2 v_tex_coord;
// These uniforms are set automatically:
//uniform sampler2D u_texture;
//uniform float u_time;
//uniform vec2 u_sprite_size;
// This uniform is set in response to touch events:
//uniform vec2 u_offset;
void main(void) {
/*
vec2 p = -1.0 + 2.0 * v_tex_coord + (u_offset / u_sprite_size * 2.0);
float len = length(p);
vec2 uv = v_tex_coord + (p/len) * 1.5 * cos(len*50.0 - u_time*10.0) * 0.03;
gl_FragColor = texture2D(u_texture,uv);
*/
gl_FragColor = vec4(v_tex_coord, 0.0, 1.0);
}
おめでとうございます!!GLSL の第一歩を踏み出せましたね☺️
SpriteNode
とGLSL 間で、何が起きているのか?
GLSL の基本に入る前に、Pythonista3 側での動きを整理します。
複数の事象が絡み合っている時には、それぞれ切り分けて考えることが大事ですね!
Scene
とNode
scene
モジュールを実行するための構成要素は、ひとつのScene
と複数のNode
の組み合わせで実装されます。
scene.Scene | scene — 2D Games and Animations — Python 3.6.1 documentation
scene.Node | scene — 2D Games and Animations — Python 3.6.1 documentation
外側のScene
の中に、さまざまな役割を持ったNode
を.add_child
し、update
時やtouch
イベントにて状態変化をさせていくのです。
Scene
| (.add_child してNode を取り込む)
├ Node(並列で存在も可能)
└ Node
| (Node がNode を.add_child も可能)
└ Node
ui
モジュールの、View にadd_subview
して、画面を構成していく流れと同様。と、今のところは考えていても問題ありません。
さまざまなNode
Node
自体は、基本的なNode
の機能しか持ち合わせていませんが、他機能を持ち合わせたNode
として
- SpriteNode
- LabelNode
- ShapeNode
- EffectNode
もう少し詳細な継承関係としては
- Node
- SpriteNode
- LabelNode
- ShapeNode
- EffectNode
- SpriteNode
今回「GLSL のshader
について」に注目すると、.shader('glslコード')
が呼び出せるのは
- SpriteNode
- EffectNode
の、2つのNode
です。
SpriteNode
で、テクスチャを呼び出しているのに表示されていない?
書き換えたGLSL のコードでは、Pythonista のアイコンをSpriteNode
で呼び出しているのに、最終的な描画では表示がされていませんでした。
通常の、scene.Shader
を呼び出さずに実行した場合にはアイコンが表示されます:
self.sprite = SpriteNode('test:Pythonista', parent=self)
# ↓.shader で代入しない
#self.sprite.shader = Shader(ripple_shader)
scene.Shader
を使用することにより、GLSL コード内で最終描画の決定が一任されます。SpriteNode
は、描画されるはずであった「エリアの範囲」や「サイズ」の情報以外は、GLSL 処理のされるがままとなるのです。
GLSL に処理を託す関係上、SpriteNode
が持っている情報を渡して、GLSL にいい感じにやってもらうための処理が必要です。
そのための橋渡し処理が:
// These uniforms are set automatically:
uniform sampler2D u_texture;
uniform vec2 u_sprite_size;
// This uniform is set in response to touch events:
uniform vec2 u_offset;
コードの先頭部分で宣言をし:
gl_FragColor = texture2D(u_texture,uv);
渡したテクスチャ情報を、他の処理と混ぜ合わせて描画してね。という流れになります。
precision highp float;
varying vec2 v_tex_coord;
void main(void) {
// texture2D(u_texture,uv) を渡していない
gl_FragColor = vec4(v_tex_coord, 0.0, 1.0);
}
Hello World!
的なGLSL コードでは、橋渡しの情報を何も記載せずに、SpriteNode
が保持している描画範囲にGLSL 側で好きに描画指示を出しているだけなので、テクスチャは出ずに、グラデーションがかかった絵が出力されたのです。
「描画される」とかは、まだ考えなくていいです
Pythonista3 側の処理の整理なので、GLSL で何が起こっているか?については、いまは意味不明で問題ありません。
ここでは、scene
モジュールの処理として
- 実行するための外側の呼び出し
-
SpriteNode
にテクスチャを呼んでいる -
.shader
で、よしなにGLSL が処理をしている - 最終的な出力が決まる
の流れと、
- 描画される範囲は
SpriteNode
が持っている
と、いう部分を知っていただければとOKです。
Pythonista3 でGLSL やろうぜ
GLSL のコードを書いて、Pythonista3(scene
モジュール)で実行できることがわかりました。
今後もサンプルのコードを使い、書き換える方法でも問題は無いですが、Python の文字列で宣言されており少々編集が面倒です。
# 文字列として、毎回書き換えるのは気が引ける
ripple_shader = '''
precision highp float;
varying vec2 v_tex_coord;
// These uniforms are set automatically:
uniform sampler2D u_texture;
uniform float u_time;
uniform vec2 u_sprite_size;
// This uniform is set in response to touch events:
uniform vec2 u_offset;
void main(void) {
vec2 p = -1.0 + 2.0 * v_tex_coord + (u_offset / u_sprite_size * 2.0);
float len = length(p);
vec2 uv = v_tex_coord + (p/len) * 1.5 * cos(len*50.0 - u_time*10.0) * 0.03;
gl_FragColor = texture2D(u_texture,uv);
}
'''
GLSL のコードは別ファイルで書き、個別管理する方が何かと建設的ですよね。
そこで、Pythonista3 Advent Calendar 2022 で大人気のEditor Action を使って気軽にGLSL コードを実行できるようにしてみましょう。
Editor Action にするコード
(Editor Action の設定方法は、過去記事を参照してください)
Editor Action | Pythonista3 のeditor module を使い、Pythonista3 のコーディングを楽にする - Qiita
import scene
import editor
shader_code = editor.get_text()
class MyScene(scene.Scene):
def setup(self):
self.div = scene.Node()
self.add_child(self.div)
self.shdr = scene.SpriteNode(parent=self.div)
self.shdr.shader = scene.Shader(f'{shader_code}')
self.set_shader_area()
def did_change_size(self):
self.set_shader_area()
def set_shader_area(self):
self.div.position = self.size / 2
_size = min(self.size.x, self.size.y) * .957
self.shdr.size = (_size, _size)
if __name__ == '__main__':
main = MyScene()
scene.run(main, show_fps=True, frame_interval=2)
- GLSL のコードを(Pythonista3 で)開き編集
- Editor Action で、該当スクリプトを実行
-
editor.get_text
で、編集中のコードを取得 -
scene.Shader
で、取得したコードを適用 -
scene
で実行
-
- GLSL のコードが描画される
画面サイズから0.957
倍のサイズで表示させることにより、端の描画確認をしやすいようにしています。
テクスチャ情報や、touch 情報を渡すuniform
は設定していないので、独自に使いやすいように追加してみてください。
ファイル拡張子を気にせず実行できる
GLSL のコードが「記載」してあるファイルの「内容」を取得しているので、.py
でも、.js
でも拡張子を気にせず使用できます(Pythonista3 で認識できない拡張子は、ファイル開く確認を都度されるので、逆に.glsl
や、.frag
等はあまりおすすめできません)。
.py
であれば、コード上に記載がある内容は一部補完が効きます、.js
であれば、補完は効きませんが、コメントアウトのシンタックスハイライト等がわかりやすいです。
よっしゃ!GLSL 入門や!((ここでは)しません)
ものすごく記事が長くなり、内容の自信もないので、私が参照している先を紹介していきます。
-
The Book of Shaders
- GLSL をしている方なら誰でも通る道(サイト)かもしれません
- 原文が英語ですが、有志の方々により日本語翻訳があります
-
The Book of Shaders: What is a shader?
- GPU のイラスト図解がわかりやすくて好き
-
[連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(1) - Qiita
- 日本WebGL の父doxas さんの連載記事
- WebGL ?あ、アタラシイ、コトバ、、タンゴ、、、と混乱させてすみません。いまは、気にせずで大丈夫(な、はず)
このあたりを主軸にしつつ、独自に調べたことにチャレンジしてみるのがおすすめです。
注意点
Pythonista3 でGLSL をチャレンジしてみる際に、数点読み替えが必要な箇所があります。
uniform
変数
-
u_time
-
iTime
等で表記されている可能性あり - The Book of Shaders: uniforms
-
-
u_resolution
- (ざっくり言うと)
scene.Shader
では、存在していません -
v_tex_coord
でいい感じになんとかします
- (ざっくり言うと)
正規化
GLSL コードで(当たり前のように)使われる処理として正規化があります。
vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);
スクリーンの中心位置を原点にする処理です。
[連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(4) - Qiita
[連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(5) - Qiita
動くオーブのサンプルコードをお借りし、Pythonista3 で適用するとなると:
precision highp float;
uniform float u_time;
varying vec2 v_tex_coord;
void main(){
vec2 p = (v_tex_coord - vec2(0.5)) * 2.0;
p += vec2(cos(u_time * 5.0), sin(u_time)) * 0.5;
float l = 0.1 / length(p);
gl_FragColor = vec4(vec3(l), 1.0);
}
正方形だからこそ使える力技で、なんとかする方法もあります。
SpriteKit
でググる
Pythonista3 のscene
モジュールでは、解決策を検索してもなかなか見つかりません。方法として一番可能性が高いのがSpriteKit
をキーワードに追加して検索をしてみることです。
scene
モジュールは、iOS, iPadOS のSpriteKit framework がベース(ラップ)となっています。案外SpriteKit
で調べた方が解決が早い場合があります(情報が突然増え混乱する可能性もありますが)。
次回は
Pythonista3 でGLSL が実行できるんです。という、紹介のみでGLSL 自体が疎かになってしまいました。
GLSL 奥が深いので、興味が湧いた方はぜひ調べてみてください!「は?意味わからん」の感動や疑問や恐怖が入り混じる素敵な世界です。
ここ最近は、GLSL が実行できる環境も簡単になってきました。が、まだまだ事前準備が必要な手順も多いです。
The Book of Shaders: Running your shader
準備で迷子になってしまい、Hello World!
まで辿り着けないこともあります。
また、他の簡単な実行環境はブラウザがメインであり。PC での体験は最高なのですが、モバイルとなるとコード編集の煩わしさを強く感じます。
そういった観点でみると、Pythonista3 のscene
を使ったGLSL 実行環境は比較的カジュアルに実装できるのではないかと感じています。
決してモバイルオンリーでGLSL を完結させたい訳ではないです。ふとした時に「この処理だとどうなる?」の思いつきに、素早く編集ができ実行ができる環境が素敵だなと思っているだけです。
次回は、今回使用したscene
モジュールと、ui
モジュールの合わせ技で、いい感じにアプリっぽくする方法を紹介します。
ここまで、読んでいただきありがとうございました。
せんでん
Discord
Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。
書籍
iPhone/iPad でプログラミングする最強の本。
その他
- サンプルコード
Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。
コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー
なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー
やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか?って話よ!(クズ)
— pome-ta (@pome_ta93) November 4, 2022
Pythonista3 Advent Calendar 2022 https://t.co/JKUxA525Pt #Qiita
- GitHub
基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。