「Godot & 共通」幕間: CG, GPUプログラミング基礎^n と 2D Shader (August’s学習記録)

記事7へようこそ! Coding課のAugustです.


August’s学習記録の記事は, 僕が自分の勉強成果や, 作ったもは「マグレではなく, 本当に理解して作った」ことを確かめるためをメインの目的で, 追加で興味ある人にも見せよう, という意図で, 書いている.
「it works」をできるだけ「it’s good」に作れるよう, 一緒に頑張ろう!

Information

記事フォーマットでもある程度の読み易さを確保するために, コードの質を下げたり, best practiceで書かなかったりする場合も多いし, そもそも僕の理解が欠けていたこともかなり発生するので, ご注意ください

本記事は,
GPUプログラミングの基礎と, 2D Shaderの数種類の原理&作り方を紹介する.



成果はこれ:

GPUとは

Graphics Processing Unit は, Central Processing Unitと違って, 並行作業に特化した計算ユニットである. 一応Integrated GPU (iGPU), CPUチップに入れたもの(軽量Laptopによくある) と discrete GPU, 独自として存在する(GPUで検索して出たやつ) が存在して, GPUは, Nvidia社が主流, AMD社が次になるが, 稀にIntel社のものを使う人もいる.

ゲームユーザ視点から, DLSS (Nvidia Deep Learning Super Sampling) やFSR(AMD FidelityFX Super Resolution)という, 機械学習を使ってビジュアルをある程度維持したまま計算負荷を下げる機能の仕組みと, 計算性能以外, 違いはそこまで多くない (ゲームユーザに限定, AI訓練はNvidiaのCUDAの存在でほぼNvidia社一択, これは雑談だが).

よくある誤解は, GPUのGがグラフィックスなので, 画像のレンダリングしか使われていない, ということだが, それは間違えで, ニューラルネットワークの訓練やインフレンス, ファイルのバッチ処理, 計算など, 「均一処理・並行作業」で分岐の少ないものであれば, GPUの方が遥かに速いく, とはいえ, こういった処理は, 画像の表示が最も代表的で, GPUの最初の用途も, 画像のレンダリングである.

GPUプログラミングとCPUの違い

GPUは並行作業に特化した計算ユニットで, 例えると

CPUは一人の三つ星マスターシェフ:
色々な料理を心から作り方が分かって迅速に作れ, 万能で臨機応変の柔軟性を持つ. 例えば鍋で湯が沸騰するまで待つ途中で材料の下処理などができ, 材料の仕入れ(=メモリーアクセス)により適応可能だが
両手しかないので, 同時に管理できるものに限界がある.


GPUは百人のラインクック:
決められたレシピだけを, 全員で一斉に処理するのが得意. 一人一人はシンプルな作業しかできないが, 全員が同時に作業することで膨大な量を短時間で処理できる.
しかし, “経験”が少ないため, 例えば”if赤のりんごthen…をする, else if 緑のりんごthen…をする… else …”という指示が沢山出て, 分岐が多いと, 皆が混乱して効率が激減する(=Warp Divergence)

つまり, CPUは少数精鋭の万能選手, GPUは同じ処理を一斉にこなす大量処理職人.


一般的に, この段階で注意すべきもの, 通常プログラミング時の違いは基本的に:

  • if-elseやswitchは極力に避ける (look-upテーブル, select = condition ? A : B mix(), lerp(), smoothstep()など数学的式を使って代用)
  • データは全スレッドが同時に入手するため, 同1サイクルで他所で処理済みの情報を入手することはできない.

だけ, 本当はもっとあるけど, 現段階はそこまで触れることはないと思う.


余談:
一応僕は以前, よくある思考パターンをしていた:
「このコード重すぎない?」など, 要は,「まだ機能実現してないのに, 最適化を考えている

現代のハードウェアは, 例えば1080p (HD)解像度, 60Hzリフレッシュレートの, 恐らく今で一番流通しているモニタースペックにたいして, これは, 1920*1080個の画素に対し, 毎秒60回レンダリング計算をすることを意味する. 1920*1080*60は, 124,416,000, 1億2千4百万の計算単位を毎秒やっている, って意味をする. 要するに, 現代のハードウェアは非常に強力である.
一般用途のプログラミングは, 一旦計算負荷を忘れて機能を実現してから, 需要に応じて, 戻ってからオプティマイゼーションを考えた方がいい.

OS・ゲームエンジン(UE, Unity, Godot)におけるGraphics API (DirectX, Vulkan, など)

GPUも, CPUみたく, プログラミング言語が存在し, コンパイラー(みたいなもの)があり, Graphics APIと呼ばれる.
Graphics APIは, 主に:

API名OS/プラットフォームゲームエンジンでの使用使用言語解説
DirectX (Direct3D)WindowsUnity, Unreal, 他多数HLSLWindows向け. 最も主流のAPI.
VulkanWindows, Linux, Android, (macOS※)Godot(デフォルト),Unity&Unreal(オプション),GLSL, SPIR-V, HLSL(DXC経由)高性能・低オーバーヘッド. クロスプラットフォーム.
MetalmacOS, iOSGodot, Unity, Unreal (Apple向けビルド時)MSL (Metal Shading Language)Apple専用の高速API. OpenGLは非推奨に.
OpenGLWindows, Linux, (macOS※)一部旧Unityプロジェクト、Godot等GLSLレガシーAPI. 互換性高いが非推奨化進行中.
OpenGL ESAndroid, 旧iOSUnity (モバイル向け), Godot等GLSL ESモバイル用軽量OpenGL. 今はVulkan推奨.
WebGLブラウザ(Chrome, Firefox等)Unity(Webビルド), Three.js 等GLSL ES (via JavaScript)OpenGL ESベース. ウェブ上でGPU描画可能.
WebGPUブラウザ(新標準)Unity(将来対応予定), その他WGSL (WebGPU Shading Language)WebGLの後継. Vulkan/Metalに近い設計.

※macOSではVulkanおよびOpenGLはネイティブ対応せず、MoltenVKなどMetalラッパー経由で利用.

というものが使われていて, GodotはUE ・ Unityより新しいため, Vulkanへの方向チェンジは比較的に慣性力低く, Vulkan一択にしているが, 古いハードウェアで支障が出るかもしれない.

Shader (非Compute) の使い方

まだShaderは紹介していないが, 後で説明する概念ごと, その概念を主として使うShader例を紹介するために, 一応ゲームへの入れ方だけ先に説明する:

基本的に, 素材の入れるノードはMaterialという枠があり, そこから「ShaderMaterial」を新規作成か, 保存しているやつを入れて, Shaderを新規作成か, 入れることで導入できる.

これはゲームコードで自動化できる. (実際は多分これでやることが多い)

var shader_material:ShaderMaterial = ShaderMaterial.new()
self.material = shader_material
shader_material.shader = load("Shaderのパス")
shader_material.set_shader_parameter("Shaderパラメータの名前",0.)

Godot 4+ で使うShader文法

Godot4+は, GLSLの文法に基づいたShader言語を使っている. 一応Visual Scripting (UEのBlueprint や, Blenderなどで使ってるもの)もできるが, ここではそれは使わない.

2Dで使うShaderは, shader_type canvas_item;を最初におく.
3Dは, shader_type spatial;を使い, それ以外sky, particleなども存在するが, それも今回の記事では触らない.

具体的な文法は, cに似ていて, 行の最後に「;」, 変数前に型を強制的に定義する必要があり, float型変数は必ず「1.0」「1.」など, 整数じゃないようにする.
変数定義前, uniform付けることで, パラメーターとして外部から弄れる. C#などで言うところの public に近い概念.

ベクトル型とスウィズル(Swizzle)

GLSL同様, ベクトル型として vec2, vec3, vec4 が存在し, それぞれ2〜4次元の数値をまとめて扱える.

特に vec4 は以下のように複数の意味合いでアクセスできる(強制ではない, 全部xyzwやrgbaでもいいが, 非推奨):

xyzw:幾何情報(位置, 方向など)
rgba:色情報
stpq:テクスチャ座標など

これらはスウィズルと呼ばれ, 例えば以下のように使える:

vec4 v = vec4(1.0, 2.0, 3.0, 4.0);

v.xyz; // ここで=(1.0, 2.0, 3.0)
v.rgba; // (1.0, 2.0, 3.0, 4.0)
v.stp; // (1.0, 2.0, 3.0)

Cのmainや, gdscriptの_ready()などのように, Shaderでは,

void vertex() {}という, 物体で今見える全ての頂点に対して操作するやつと

void fragment() {}物体で今見える全ての画素に対する操作
をする関数がある

雑にCGを語る

Shaderを紹介する前に, まず現代コンピューターグラフィックス(CG)の原理から説明する必要がある. とはいえ, 能力・効率上, 詳細は僕より遥かに説明に上手い人から聞くといい(興味ある人は, Rendering Pipelineについてから調べられる). ここで説明するのは, あくまで簡単なShaderに使う概念のみ.

  • Vertex, Edge, Face
  • Colour (RGBA)
  • Pixel Rasterisation & UV

頂点(Vertex)と, Vertex Shader

2D Spriteから, 3Dモデルまで, ゲームの物体は, すべて頂点(Vertex)から線(Edge)から面(Face), 三角形で構築される.

2D の場合, どれだけ大きく見える絵でも, 頂点4つ, 三角2個で構成される.

3D モデルのUVは, 僕の知る限り, Shaderで直接触ることは相当少ない. せいぜい2DテキスチャーのUVを3D (spatial) Shaderで使うぐらい.

vertex()を使ってみる

Vertex Shaderは名前通り, (この物体の現在見える)全ての頂点に対して操作を行うものである.

ここで, 2D物体の頂点のローカル座標のx,y要素を全部変数倍にしている.

Pixel Rasterisation & UV と Fragment Shader

一般的に, 3Dモデルは頂点の位置情報と, それらを使って三角形を構成するインデックス情報のみを保持している.
このデータを元に, 3Dモデルは2D画面へ投影される.

しかし, モニター上の画素は四角形であり, これは見た目に大きな影響を与える. Minecraftを遊んだことがある人なら想像がつくだろうが, 四角形だけで滑らかな曲線を表現するには, 相当工夫された配置が必要になる.

このとき重要なのがラスタライゼーション(rasterisation)という処理である. これは, 頂点情報から線や面を構成し, 解像度に応じてその物体の形に沿ったピクセル(画素)を塗る過程である.

つまり, ポリゴンの形状を, 最終的にモニター上の四角いピクセルに落とし込む工程と言える.
2Dになっら, あとは2D Shaderで自由に操れる.

その画素を操るものは, fragment(), Fragment Shaderである.

2DにおけるUVとは, 要は物体を構成する画素を正規化したものである.
とはいえ, Rasterisation後のUVは, 定数になるため, UVへの直接変更は, vertex()で行う.

これだけで, 結構面白いものができる
例えば, 時間の経過を係数として入れると?

Fragment Shaderを使って色を弄る

2D物体では,COLORというキーワードを使って画素の色を弄れる. (3DはALBEDO)
とはいえ, 複数操作するまえ, まず素材の本来を色をサンプリングして, どこかに保存したほうがいい.
texture(TEXTURE, UV)でできる. TEXTUREとUV大文字は, 現物体のもので, 一応サンプリングする画像を別のものに指定することもできる.

モノクロフィルター

さて, ここからは画素操作を行う.
まず, 画素の色は, (Red,Green,Blue,Alpha)4つのチャンネルから構成する (.jpg はalphaチャンネル持たない・常に1)

とはいえ, 我々人間の目は, 色への敏感度は均一ではない.
r * 0.2126 + g * 0.7152 + b * 0.0722; はRec. 709標準による色係数とはいえ, それは線形明度スケール用で,
luminance ≈ 0.299 * R + 0.587 * G + 0.114 * B; のはsRGBなど非線形標準に使われる.

とはいえ, ゲームのビジュアルは色ー>明度の科学的標準より, 美学で適当に弄れる.
(r + g + b)/3の均一スケールの方が, 僕は好む.

これを画面の全要素に適応する方法は応用編で教える

混色

mix()という関数を使って, 混色ができる.

時間と三角関数を使ってShaderアニメーションを作る

sine, cosine などの三角関数は, その-1~1まで無限にループする性質は, 望ましい.
さらに, 絶対値を取れば, 0~1で保たれる.

例えば, 呼吸している用に, フラッシュしているようなHit Effectは,

こうやって簡単に作れる.

Shader応用編

ここからちょっとだけ難易度を上げる

ノイズ(Noise) 素材

ノイズとは, 連続的または疑似ランダムな値の分布を指し, コンピュータグラフィックスでは自然現象やランダム性の表現に用いられる.
代表的な関数に Perlin noise, Simplex noise, Worley noise, value noise などがある.

ノイズは完全な乱数ではなく, 空間的に滑らかな変化(コヒーレンス)を持つことが多い.
これにより, 違和感のない模様や変形を生成できる. たとえば, 隣接するピクセル間での色の急激な変化を防ぎつつ, 不規則性を導入できる.

NoiseはShader内で, 以下のような用途に使われる:

  • 表面の質感表現: 汚れ, サビ, 岩肌, 木目など, テクスチャに頼らずに手続き的な模様を生成.
  • アニメーション表現: 時間軸を加えて揺らぎを生成し, 風になびく葉や水面の波紋などを再現.
  • UV変形: ノイズでUVをずらすことで, 流動的なエフェクトや熱気の歪みを演出.
  • ライティング補助: ハイライトの揺れや影の揺らぎによって, 自然で粗い反射や遮蔽効果を付与.

シグモイド(Sigmoid)曲線 & SmoothStep関数

機械学習分野の ニューラルネットワーク訓練のニューロンの発火閾値判定として, 昔よく使われるものは, Sigmoid関数である.

smoothstep関数は広義的にシグモイド曲線の拡張で


min, max 閾値を定義し, min以下は0, max以上は1になる.

ここで,
仮にmin, max閾値を変数にしたら?

Masking/溶け込み効果

なんか面白そうに見えるかもしれないけど, これは失敗作 見せしめに特化したもの.
このコードの問題は, sineを使っているから0~1にした後, 1~0に戻ることである.

このShaderは, 僕が思いつける限り, スクリプトからパラメータを提供するか, TweenでShaderを動かすか, どっちもShaderだけでは足りないと思う (そもそもこういうShaderは常にOnにしないと思う)

ではTweenで動かしてみよう (実は以前の記事で既にやったことである)

@export var dissolve_duration:float = 1.0
@export var dissolve_texture:Texture2D

func _ready() -> void:
	var shader_mat:= ShaderMaterial.new()
	shader_mat.shader = load("res://test_2d.gdshader")
	shader_mat.set_shader_parameter("noise_texture",dissolve_texture)
	self.material = shader_mat
	
	
	var dissolve_tween = create_tween()
	dissolve_tween.tween_method(
		func(value:float): shader_mat.set_shader_parameter("dissolve_percent",value),
		0.0,
		1.0,
		dissolve_duration
	)

全画面(Screen Space)フィルター

fragment()関数で, UVではなく,
uniform sampler2D screen_texture:hint_screen_texture;
hint_screen_textureでViewport画面を2D画像としてサンプリングすることができ, これはフィルターになる.

vec4 screen_colour = texture(screen_texture, SCREEN_UV);
こうやって, 今画面上のすべての要素をピクセル情報として処理できる

上の式にSCREEN_UVを使ったらフィルター, しかし, もしUV, つまり, 画面のUVではなく, 自分のUVを使ったら?

UVを使ったら, 全画面をその物体に投影するように見える.

実際にこうやってSCREEN_UVを使って色んな処理フィルターをかけることが, 反射, コントラスト調整などのPost Processingのやり方であり, ゲームの画質設定で見る SSAO (Screen Space Ambient Occlusion)など, SSなんちゃらは, 大抵Screen Space処理で, こういう原理で行われる.


このShaderをUIノードに適用し, Canvas Layerに上げると, 主要カメラにフィルターをかけられる.

空間・空気の歪み(Distortion)効果を作る

今までの手法を応用して, 色んな効果を作り始めよう.

Noise素材2枚作って, 無限背景Shaderで使ったと同様に, UVに異なるオフセット*時間を足せる. (実際に1枚でも作れる)
そのNoiseの値をかけ合わせる.
Noiseの画素値は0~1
UVも0~1の値
Noise1 * Noise2でかなり小さい値が得る, それを更に”強度”というパラメーターを掛けて, 素材のUV, 或いはSCREEN_UVに足せる.

例えば, UV(0.2,0.2)位置の画素をサンプリングする時, 0.1*0.1*0.02という数値のオフセットを足せると, 実際にサンプリングされたのは, (0.2002,0.2002)位置の画素値になる.
要は, ちょっとだけ違う位置の画素値になていて, Noiseを使っているので, 「規則あるランダム性」をもつ. (使わなかったらただの固定移動)

その結果は, こうなる:

shader_type canvas_item;

uniform sampler2D screen_texture:hint_screen_texture;
uniform vec2 offset1 = vec2(.1);
uniform vec2 offset2 = vec2(.3);
uniform float distortion_strength:hint_range(-1.,1.) = 0.02;
uniform sampler2D noise_texture1:repeat_enable;
uniform sampler2D noise_texture2:repeat_enable;


void fragment() {
	vec4 noise_colour1 = texture(noise_texture1,UV+offset1*TIME);
	vec4 noise_colour2 = texture(noise_texture2,UV+offset2*TIME);
	float final_noise = noise_colour1.r * noise_colour2.r;
	vec2 distorted_uv = SCREEN_UV+final_noise*distortion_strength;
	vec4 final_colour = texture(screen_texture,distorted_uv);
	COLOR = final_colour;
}

注意: NoiseテキスチャーにSeamless (連続性)の選択を選択する必要ある. (まぁしなかったらどうなるか, 試してみて. 相当集中して見ないと気付かないかも)

Chromatic Aberration (色収差) 効果を作る

色収差効果は基本的に歪みと同じ原理でUVにオフセットを加える. 違いは, 足すのみではなく, 足す, 引く, 変更なしを, R,G,Bチャンネルに個別に適応することである.

vec2 red_uv = distorted_uv + chroma_offset * chroma_amount;
vec2 blue_uv = distorted_uv - chroma_offset * chroma_amount;
vec2 green_uv = distorted_uv;


ここで, Noise使うと使わない違いを動画で見せる:

何か格好良くない?

shader_type canvas_item;

uniform sampler2D screen_texture:hint_screen_texture;
uniform vec2 chroma_offset = vec2(.005);

uniform float chroma_amount: hint_range(0.0, 1.0) = 1.0;
uniform float distortion_strength:hint_range(-1.,1.) = 0.02;
uniform sampler2D noise_texture1:repeat_enable;


void fragment() {
	vec4 noise_colour1 = texture(noise_texture1,UV);
	vec2 distorted_uv = SCREEN_UV+noise_colour1.r*distortion_strength;
	//vec2 distorted_uv = SCREEN_UV+.1*distortion_strength;

	vec2 red_uv = distorted_uv + chroma_offset * chroma_amount;
	vec2 blue_uv = distorted_uv - chroma_offset * chroma_amount;
	vec2 green_uv = distorted_uv;

	float r = texture(screen_texture, red_uv).r;
    float g = texture(screen_texture, green_uv).g;
    float b = texture(screen_texture, blue_uv).b;

	vec4 scene_color = vec4(r, g, b, texture(screen_texture, SCREEN_UV).a);
	COLOR = scene_color;
}

発展: Outline Highlight効果 (ボーダー)を作る

ただ画像の周りに色を着けるだけだよ. How hard can that be?

うん…意外と今回の記事で最も難しい.

問い: エッジの定義は?
画像処理では, Sobelフィルターなど, 微分, 要は, 画素値の変化量を使ってエッジを定義できる.

しかし, それは今回の目的に適していない. 我々は単純にエッジの場合を知りたいわけではなく, 分厚い線を着けたい.
微分処理は単純, 早い, 効果的だが, エッジのところにせいぜい1ピクセルの線を着けるだけ. 分厚い線を着けるには, 追加処理が必要.


まぁ, 将来それを使って特殊スタイル着けShader作るかもしれないが.

Signed Distance Field (SDF) (利用しない)

もう一つ今回使わないやり方は, Signed Distance Fieldによる処理である.

SDFは, 非常に雑に説明すると: 画面上の任意の点が, 最も近い物体への距離を記載する表のようなもの.
これはエンジンですでにSDFGIなど, あらゆる画面処理で使われていて, 本気で画面効果を弄りたいなら, これの生成パイプラインは用意したほうが良いらしい.


この方法は, 計算力方面も, (総合的)便利さも, 効果も, あらゆる方面で最適解に近い.
とはいえ, 今回の記事はすでに情報量過剰になっていると思い, また今度にしよう.

Circular SamplingでOutlineを書く

今回使うのは, まあ…周りの点を全部見て, スタイルによって, 空白点が見つかったら直にbreakしてOutlineをalpha = 1 で付けるか, 柔軟性を対応して点のalpha値の和で付ける.

何個までサンプリングするかは, パラメーターで調整できる.

Outlineのスペースを確保する

Shaderの作業空間は, あくまでその物体が定義された空間まで. この理由で, 多くのゲーム素材は, 敢えてalpha=0で余分の空間を画像に付けている.
とはいえ, つけてない画像に対応する必要があるため, vertex shaderで線の厚さ分空間を確保する:

void vertex() {
	VERTEX += (UV * 2.0 - 1.0) * line_thickness;
}


2D物体は, (-1, 1)の整数配列で4つの頂点をもつ. UVは0~1なので, 2倍にし-1したら, -1~1に調整できる. こうやって, vertexのローカル位置を弄って線の空間を確保していく.

shader_type canvas_item;

uniform sampler2D screen_texture : hint_screen_texture;
uniform vec4 line_colour : source_color = vec4(1.0);
uniform float line_thickness : hint_range(0.0, 10.0) = 4.0;

const int SAMPLE_COUNT = 12;

void vertex() {
	VERTEX += (UV * 2.0 - 1.0) * line_thickness;
}

void fragment() {
	vec2 size = SCREEN_PIXEL_SIZE * line_thickness;
	vec2 screen_uv = SCREEN_UV;

	float outline_alpha = 0.0;
	// Sample in a circle to find adjacent pixels
	for (int i = 0; i < SAMPLE_COUNT; i++) {
		float angle = float(i) * 2.0 * PI / float(SAMPLE_COUNT);
		vec2 offset = vec2(cos(angle), sin(angle)) * size;
		outline_alpha += texture(screen_texture, screen_uv + offset).a;
	}

	outline_alpha = min(outline_alpha, 1.0);

	float existing_alpha = texture(screen_texture, screen_uv).a;

	// The mix factor is the detected outline alpha, minus the alpha of the
	// original pixel. This prevents drawing the outline over the object itself.
	float mix_factor = clamp(outline_alpha - existing_alpha, 0.0, 1.0);

	vec4 original_colour = texture(screen_texture, screen_uv);
	COLOR = mix(original_colour, line_colour, mix_factor);
}

最終Code

Outline Shader


sine, cosineを同じ角度毎回毎回計算させるのは非常に効率悪い.
よるあるやり方は, 事前に計算して, 値を定数として入れる.

Vector (x, y)AngleTrigonometry
(0, 1)90°(cos(90°), sin(90°))
(1, 0)(cos(0°), sin(0°))
(0, -1)270° (−90°)(cos(270°), sin(270°))
(-1, 0)180°(cos(180°), sin(180°))
(0.5, 0.866)60°(cos(60°), sin(60°))
(0.866, 0.5)30°(cos(30°), sin(30°))
(0.866, -0.5)330° (−30°)(cos(330°), sin(330°))
(0.5, -0.866)300° (−60°)(cos(300°), sin(300°))
(-0.5, -0.866)240° (−120°)(cos(240°), sin(240°))
(-0.866, -0.5)210° (−150°)(cos(210°), sin(210°))
(-0.866, 0.5)150°(cos(150°), sin(150°))
(-0.5, 0.866)120°(cos(120°), sin(120°))


shader_type canvas_item;

uniform sampler2D screen_texture : hint_screen_texture;
uniform vec4 line_colour : source_color = vec4(1.);
uniform float line_thickness = 4.0;

void vertex() {
    VERTEX += (UV * 2.0 - 1.0) * line_thickness;
}

void fragment() {
    vec2 uv = SCREEN_UV;
    vec2 size = (SCREEN_PIXEL_SIZE * line_thickness);

    float outline = texture(screen_texture, uv + vec2(-size.x, 0)).a;
	outline += texture(screen_texture, uv + vec2(0, size.y)).a;
	outline += texture(screen_texture, uv + vec2(size.x, 0)).a;
	outline += texture(screen_texture, uv + vec2(0, size.y * -1.0)).a;
	outline += texture(screen_texture, uv + vec2(size.x * -0.866, size.y * 0.5)).a;
	outline += texture(screen_texture, uv + vec2(size.x * -0.5, size.y * 0.866)).a;
	outline += texture(screen_texture, uv + vec2(size.x * 0.866, size.y * 0.5)).a;
	outline += texture(screen_texture, uv + vec2(size.x * 0.5, size.y * 0.866)).a;
	outline += texture(screen_texture, uv + vec2(size.x * -0.866, size.y * -0.5)).a;
	outline += texture(screen_texture, uv + vec2(size.x * -0.5, size.y * -0.866)).a;
	outline += texture(screen_texture, uv + vec2(size.x * 0.866, size.y * -0.5)).a;
	outline += texture(screen_texture, uv + vec2(size.x * 0.5, size.y * -0.866)).a;
	outline = min(outline, 1.0);


    vec4 screen_colour = texture(screen_texture, SCREEN_UV);
    COLOR = mix(screen_colour, line_colour, outline - screen_colour.a);
}
Demoで使った コードを使ってShaderの随時適用用コード(GDscript)
extends Area2D


@export var highlight_shader:Shader = preload("res://outline.gdshader")
@onready var canvas_group:CanvasGroup = $CanvasGroup
var highlight_mat:ShaderMaterial
func _ready() -> void:
	self.mouse_entered.connect(_on_mouse_entered)
	self.mouse_exited.connect(_on_mouse_exited)
	highlight_mat = ShaderMaterial.new()
	highlight_mat.shader = highlight_shader
	canvas_group.material = null
	
func _on_mouse_entered():
	canvas_group.material = highlight_mat
	
func _on_mouse_exited():
	canvas_group.material = null

コメントを残す

CAPTCHA