1
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 1 year has passed since last update.

Pythonista3Advent Calendar 2022

Day 8

Pythonista3 のscene モジュールでShader コーディング。GLSL をやる。

Last updated at Posted at 2022-12-07

この記事は、Pythonista3 Advent Calendar 2022 の08日目の記事です。

一方的な偏った目線で、Pythonista3 を紹介していきます。

ほぼ毎日iPhone(Pythonista3)で、コーディングをしている者です。よろしくお願いします。

以下、私の2022年12月時点の環境です。

sysInfo.log
--- 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) のようなもの)では、鉛筆をxy 座標に持ってきて線を引く(それを繰り返す)。といった処理のイメージを持っています。

Shader (これ以降は、GLSLと記載)では、事前にGPU で演算をし、結果の全てをドカっと描画する。みたいなイメージで、理解をしています。

「線を引く」ことも、今までとは全く違う書き方をするので、最初はびっくりするかもしれませんが、GPU 演算のパワーで面白い表現ができます。

Pythonista3 Documentation でも「複雑なトピック」とあるように、よちよち歩きの私では簡単に概略を説明することは難しいです。。。

サンプルを動かしてみる

とにかく、Pythonista3 でどうやってGLSL やるのか体験してみましょう。

shader | scene — 2D Games and Animations — Python 3.6.1 documentation

shader の項目から下へスクロールすると、サンプルコードがあり[Open in Editor] をタップすると、エディタにサンプルコードがコピーされた状態で出てきます。

をタップすると、コードが実行されます。

img221125_150211.gif

画像に波紋のようなエフェクトが出ており、タップするとその場所を中心とした波紋に変わります。

このような処理は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 のようにオフサイドルールはありません。インデントは気にしなくて大丈夫です。

をタップし実行してみましょう。

img221126_134233.png

この絵が出てきたら成功です。

サンプルコードから、コメントアウトして実行させるならこのようになります:

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 側での動きを整理します。

複数の事象が絡み合っている時には、それぞれ切り分けて考えることが大事ですね!

SceneNode

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 として

もう少し詳細な継承関係としては

  • Node
    • SpriteNode
      • LabelNode
      • ShapeNode
    • EffectNode

今回「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;

コードの先頭部分で宣言をし:

テクスチャ情報を渡す.glsl
gl_FragColor = texture2D(u_texture,uv);

渡したテクスチャ情報を、他の処理と混ぜ合わせて描画してね。という流れになります。

テクスチャ情報を渡していない.glsl
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

img221126_190752.gif

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)

  1. GLSL のコードを(Pythonista3 で)開き編集
  2. Editor Action で、該当スクリプトを実行
    1. editor.get_text で、編集中のコードを取得
    2. scene.Shader で、取得したコードを適用
    3. scene で実行
  3. GLSL のコードが描画される

img221126_190717.gif

画面サイズから0.957 倍のサイズで表示させることにより、端の描画確認をしやすいようにしています。

テクスチャ情報や、touch 情報を渡すuniform は設定していないので、独自に使いやすいように追加してみてください。

ファイル拡張子を気にせず実行できる

GLSL のコードが「記載」してあるファイルの「内容」を取得しているので、.py でも、.js でも拡張子を気にせず使用できます(Pythonista3 で認識できない拡張子は、ファイル開く確認を都度されるので、逆に.glsl や、.frag 等はあまりおすすめできません)。

.py であれば、コード上に記載がある内容は一部補完が効きます、.js であれば、補完は効きませんが、コメントアウトのシンタックスハイライト等がわかりやすいです。

よっしゃ!GLSL 入門や!((ここでは)しません)

ものすごく記事が長くなり、内容の自信もないので、私が参照している先を紹介していきます。

このあたりを主軸にしつつ、独自に調べたことにチャレンジしてみるのがおすすめです。

注意点

Pythonista3 でGLSL をチャレンジしてみる際に、数点読み替えが必要な箇所があります。

uniform 変数

  • u_time
  • 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);
}

img221126_200639.gif

正方形だからこそ使える力技で、なんとかする方法もあります。

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 お待ちしておりますー

  • Twitter

なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー

  • GitHub

基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。

1
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
1
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?