[Godot 4.2] Sprite2Dの不透明箇所のクリック判定

タイトル通り、表示されている画像の不透明箇所をクリックしたことを検知したいです。

# 結論のコード

```gdscript
extends Sprite2D


func _unhandled_input(event):
	if event is InputEventMouseButton:
		if event.pressed and _is_mouse_in_sprite_opaque(event.position):
			print("%s clicked" % get_path())


func _is_mouse_in_sprite_opaque(mouse_position):
	var transform_to_local = get_global_transform_with_canvas().affine_inverse()
	var sprite_local = transform_to_local * mouse_position
	return is_pixel_opaque(sprite_local)
```
これに至った経緯を解説していきます。

# 画像の不透明箇所を判定する
[Sprite2D.is_pixel_opaque(pos)](https://docs.godotengine.org/ja/4.x/classes/class_sprite2d.html#class-sprite2d-method-is-pixel-opaque) を使うことで、引数として渡したローカル座標にあたる画像のピクセルが不透明かどうかを判定できます。
つまり、最終的にクリック時のマウス座標を Sprite2D のローカル座標に変換すれば、目的が達成できるというわけです。

# マウス座標を取得する
[`InputEventMouse.position`](https://docs.godotengine.org/ja/4.x/classes/class_inputeventmouse.html#class-inputeventmouse-property-position) で取得できます。
説明を読むと「ノードが属している Viewport 座標系での座標を返す」とのことです。

Godot に限らず座標系ってたくさん種類があってなんだかややこしいイメージがあるので、つらくなってきました。

<details><summary>余談: 「Control だと普通に event.position はローカル座標だったような？」</summary>

その通りです。これもプロパティの説明に書いてあります。
`_gui_input` で処理する場合のみそのノードのローカル座標が入っているとのことです。
</details>


# Viewport → local 座標変換
[CanvasItem.get_global_transform_with_canvas()](https://docs.godotengine.org/ja/4.x/classes/class_canvasitem.html#class-canvasitem-method-get-global-transform-with-canvas) を使います。
説明には「ローカル座標系から Viewport 座標系への変換」とあり、今回やりたいこととは逆のようですが、いくつも変換を重ねる必要はなさそうです。
[`Transform2D.affine_inverse()`](https://docs.godotengine.org/ja/4.x/classes/class_transform2d.html#class-transform2d-method-affine-inverse) を使うと逆の変換ができるそうなのでこれを使います。
```gdscript
var transform_to_local = get_global_transform_with_canvas().affine_inverse()
var sprite_local = transform_to_local * mouse_position
```
座標変換の乗算は行列とベクトルの演算なので、順序を変えると結果が変わります。気をつけましょう。

<details><summary>余談1: Godot での座標変換行列について</summary>

[Viewportおよびキャンバスの幾何学変換 — Godot Engine (4.x)の日本語のドキュメント](https://docs.godotengine.org/ja/4.x/tutorials/2d/2d_transforms.html#transform-functions) に書かれていますが、 Godot における座標変換のための行列はローカル側からグローバル側への変換に統一されているようです。
覚えやすくていいですね。
</details>


<details><summary> 余談2: Node2D.global_position とは？ Viewport 座標とは違うの？</summary>

「global って具体的に何処？シーンルート？画面？」という疑問です。
場合によっては今回の目的に合致してそうな雰囲気がありますよね。
プロパティの説明を見ても「Global position.」という情報量ゼロの1文しか書いてないので分かりません。

[`CanvasItem.get_global_transform()`](https://docs.godotengine.org/ja/4.x/classes/class_canvasitem.html#class-canvasitem-method-get-viewport-transform) の説明を読んで察するに、「最上位の CanvasItem の座標系が global である」ということのようです。
その上には CanvasLayer が有り、さらにその上にようやく Viewport が存在します。

つまりこういうコードだと CanvasLayer に offset を設定している場合などに上手くいきません。
座標系が違うからですね。
```gdscript
func _is_mouse_in_sprite_opaque(mouse_position):
    var sprite_local = to_local(mouse_position)
    return is_pixel_opaque(sprite_local)
```
</details>

これにてマウス座標取得、座標変換、不透明判定ができるようになり、冒頭のコードが完成します。

# 参考文献
## マニュアル
[Using InputEvent — Godot Engine (4.x)の日本語のドキュメント](https://docs.godotengine.org/ja/4.x/tutorials/inputs/inputevent.html)
[Viewportおよびキャンバスの幾何学変換 — Godot Engine (4.x)の日本語のドキュメント](https://docs.godotengine.org/ja/4.x/tutorials/2d/2d_transforms.html)

## クラス リファレンス
[Sprite2D — Godot Engine (4.x)の日本語のドキュメント](https://docs.godotengine.org/ja/4.x/classes/class_sprite2d.html)
[InputEventMouse — Godot Engine (4.x)の日本語のドキュメント](https://docs.godotengine.org/ja/4.x/classes/class_inputeventmouse.html)

## 今回登場した行列について強くなる
[完全に理解するアフィン変換 #Python - Qiita](https://qiita.com/koshian2/items/c133e2e10c261b8646bf)