UdonSharp にない機能
UdonSharp では書けない、この言語だけの書き方とツールの一覧です。全部を並べた一覧はやりたいことから探すにあります。
リアクティブ
Section titled “リアクティブ”値が変わったら自動で反映したい
[Reactive]
例
using UnityEngine;using Tsukimi;using TMPro;
// 残弾の表示: 撃つたびに残りの数が減り、表示と銃の色が自動で追いつく。//// 前提:// - このスクリプトは、撃つ操作をするオブジェクト(Collider 付き)に付ける。// - 残りの数を出す TextMeshProUGUI を ammoText に、色を変える Renderer を body に渡す。// - 数は触った人の画面でだけ変わる(同期はしていない)。public class GoalsReactiveField : TsukimiBehaviour{ public TextMeshProUGUI ammoText; public Renderer body;
// 変化を追うフィールド。private にする。 [Reactive] private int ammo = 6;
// ammo が変わるたびに実行される。起動したときにも 1 回実行されるので、最初から表示が合う。 // 呼び出しはどこにも書いていない。 [Effect] private void ShowAmmo() { ammoText.text = ammo + " / 6"; body.material.color = ammo > 0 ? Color.white : Color.red; }
// 触るたびに 1 発撃つ。0 のときは、6 発へ詰め直す。 public override void Interact() { if (ammo > 0) ammo = ammo - 1; else ammo = 6; }}注意
- 同期するフィールドに付けると、ネットワーク越しに届いた値では効果が実行されません(警告
TUKI0115)。 - 効果の中で、自分が見ている値へ書き戻すと、循環した依存としてコンパイルのエラーになります。
ほかの値から導かれる値を置きたい
[Computed]
例
using UnityEngine;using Tsukimi;
// 2 つの鍵の扉: 左右のスイッチが両方入ったときだけ、扉が消えて通れるようになる。//// 前提:// - このスクリプトは、扉を管理する空のオブジェクトに付ける。// - 扉のオブジェクトを door に渡す。// - 左右のスイッチからは、それぞれ ToggleLeft / ToggleRight を呼ぶ// (例: スイッチの Interact から SendCustomEvent で呼ぶ)。public class GoalsReactiveComputed : TsukimiBehaviour{ public GameObject door;
[Reactive] private bool left; [Reactive] private bool right;
// left と right から導かれる値。どちらかが変わったときに計算し直される。 [Computed] private bool Open => left && right;
// 見ているのは Open だけ。left だけが入っても Open は false のままなので、この処理は実行されない。 [Effect] private void ApplyDoor() { door.SetActive(!Open); }
public void ToggleLeft() { left = !left; } public void ToggleRight() { right = !right; }}変わった瞬間だけ何かしたい
[On(nameof(charge))]
例
using UnityEngine;using Tsukimi;
// 得点の効果音: 得点が増えた瞬間にだけ音を鳴らす。減ったときや、起動したときには鳴らさない。//// 前提:// - このスクリプトは、得点を数えるオブジェクトに付ける。// - 鳴らす AudioSource を chime に渡す。// - 得点を足すときは AddPoint、やり直すときは ResetScore を呼ぶ。public class GoalsReactiveOn : TsukimiBehaviour{ public AudioSource chime;
[Reactive] private int score;
// score が変わった瞬間に実行される。起動したときには実行されない。 // 引数には、変わる直前の値が入る。 [On(nameof(score))] private void OnScoreChanged(int before) { if (score > before) chime.Play(); }
public void AddPoint() { score = score + 1; } public void ResetScore() { score = 0; }}注意
- 起動したときには実行されません。起動した直後から見た目を合わせたいときは
[Effect]を使います。
何に依存しているかを自分で書きたい
[Effect(nameof(width), nameof(height))]
例
using UnityEngine;using Tsukimi;
// 照明の調光: 明るさと色味が変わったときだけ、照明を設定し直す。// 同じ Behaviour にある別の値(点滅の回数)が変わっても、照明には触らない。//// 前提:// - このスクリプトは、照明を操作するパネルに付ける。// - 設定し直す Light を lamp に渡す。// - パネルのボタンから Brighter / Warmer / Blink を呼ぶ。public class GoalsReactiveEffectDeps : TsukimiBehaviour{ public Light lamp;
[Reactive] private float brightness = 1f; [Reactive] private float warmth; [Reactive] private int blinkCount;
// 括弧に書いた 2 つだけを見る。blinkCount が変わっても実行されない。 // 起動したときにも 1 回実行される。 [Effect(nameof(brightness), nameof(warmth))] private void ApplyLight() { lamp.intensity = brightness; lamp.color = Color.Lerp(Color.white, new Color(1f, 0.7f, 0.4f), warmth); }
public void Brighter() { brightness = brightness >= 3f ? 0.5f : brightness + 0.5f; } public void Warmer() { warmth = warmth >= 1f ? 0f : warmth + 0.25f; } public void Blink() { blinkCount = blinkCount + 1; }}変わったかどうかの判定を知りたい
Equals
例
using UnityEngine;using Tsukimi;
// 目印の移動: 毎フレーム同じ場所を書いても、場所が本当に変わったときだけ目印を動かし、ログを出す。//// 前提:// - このスクリプトは、目印を管理するオブジェクトに付ける。// - 追いかける対象を target に、動かす目印を marker に渡す。public class GoalsReactiveEquals : TsukimiBehaviour{ public Transform target; public Transform marker;
[Reactive] private Vector3 spot;
// spot が「変わった」ときだけ実行される。Vector3 は型付きの Equals で比べるので、 // 同じ値を書き直しても実行されず、ごく小さな差でも変わったと見る(== のように誤差を握り潰さない)。 [Effect] private void MoveMarker() { marker.position = spot; Debug.Log("目印を " + spot + " へ動かした"); }
// 毎フレーム、対象の場所を書く。対象が止まっている間は、上の処理は実行されない。 private void Update() { spot = target.position; }}注意
QuaternionとColorは NaN どうしを等しいと見ます。ほかの値型は、NaN が入ると変わり続けたと見ます。
シェーダーを書かずに面の色を決めたい
[Surface] static Color4 M(SurfaceId id, ...)
例
using UnityEngine;using Tsukimi;
// 体力ゲージ: 板の左から、残りの割合のぶんだけを緑に塗り、残りを暗い灰色に塗る。// シェーダーのファイルは書かない。色を決める処理を C# で書く。//// 前提:// - このスクリプトは、ゲージを管理するオブジェクトに付ける。// - ゲージにする板(Quad など)の Renderer を gauge に渡す。// - 残りの割合(0〜1)は hp に入れる。ダメージを受けたら Hit を呼ぶ。public class GoalsSurfaceColor : TsukimiBehaviour{ public Renderer gauge; public float hp = 1f;
// 面のピクセルごとに呼ばれ、返した色がそのピクセルに出る。id.UV は面の上の位置(0〜1)。 [Surface] static Color4 Bar(SurfaceId id, float rest) { if (id.UV.x < rest) return new Color4(0.2f, 0.9f, 0.3f, 1f); return new Color4(0.15f, 0.15f, 0.15f, 1f); }
// 値は Gpu.Show を呼ぶたびに渡す。毎フレーム呼んで、いまの hp で塗り直す。 void Update() { Gpu.Show(nameof(Bar), gauge, hp); }
public void Hit() { hp = Mathf.Max(0f, hp - 0.1f); }}注意
- この章は試験中です。書き方が変わることがあります。
Gpu.Showに渡す値は、呼んだときのものが使われます。値を変えたら、もう一度呼びます。
面の向きで陰影を付けたい
Vector3.Dot(id.Normal, toLight)
例
using UnityEngine;using Tsukimi;
// トゥーン調の石像: 光の当たり方を 3 段階に分けて塗る。明るい面・中間・影の 3 色だけで描く。//// 前提:// - このスクリプトは、石像のオブジェクトに付ける。石像の Renderer を statue に渡す。// - 色は baseColor(明るい面の色)で決める。影の側は同じ色を暗くして塗る。public class GoalsSurfaceShading : TsukimiBehaviour{ public Renderer statue; public Vector3 baseColor = new Vector3(0.8f, 0.75f, 0.7f);
[Surface] static Color4 Toon(SurfaceId id, Vector3 baseColor) { // 光の向きは自分で決める(場に置かれたライトは読めない)。 Vector3 toLight = new Vector3(0.4f, 1f, 0.3f).normalized; // id.Normal は面の向き(世界座標・長さ 1)。光の向きと揃うほど 1 に近い。 float lit = Vector3.Dot(id.Normal, toLight); float shade = lit > 0.5f ? 1f : (lit > 0f ? 0.7f : 0.4f); return new Color4(baseColor.x * shade, baseColor.y * shade, baseColor.z * shade, 1f); }
void Update() { Gpu.Show(nameof(Toon), statue, baseColor); }}注意
- 場に置かれたライトは読めません。光の向きは自分で決めます。影も受けません。
見ている向きで見え方を変えたい
Vector3.Dot(id.Normal, id.ViewDir)
例
using UnityEngine;using Tsukimi;
// 幽霊の縁の光: 見ている人から見て、輪郭に近い(面が横を向いている)ところほど青白く光らせる。//// 前提:// - このスクリプトは、幽霊のオブジェクトに付ける。幽霊の Renderer を ghost に渡す。// - 光の強さは glow(0 で光らない)で決める。public class GoalsSurfaceRim : TsukimiBehaviour{ public Renderer ghost; public float glow = 1f;
[Surface] static Color4 Rim(SurfaceId id, float glow) { // id.ViewDir はそのピクセルから見ている人へ向かう向き。 // 面が見ている人を向いていると内積は 1 に近く、輪郭では 0 に近い。 float edge = 1f - Mathf.Abs(Vector3.Dot(id.Normal, id.ViewDir)); float t = Mathf.Clamp01(edge * edge * glow); return Color4.Lerp(new Color4(0.1f, 0.1f, 0.15f, 1f), new Color4(0.6f, 0.8f, 1f, 1f), t); }
void Update() { Gpu.Show(nameof(Rim), ghost, glow); }}面に値を渡したい
static Color4 M(SurfaceId id, GpuBuffer2D buf, int n, bool b, Vector2 v, Vector3 w)
例
using UnityEngine;using Tsukimi;
// 流れる縞模様の床: 縞の本数・色・流れる速さを Behaviour から渡し、時間とともに縞を流す。//// 前提:// - このスクリプトは、床のオブジェクトに付ける。床の Renderer を floor に渡す。// - 縞の本数は stripes、色は color、横へ流れる速さは speed(UV の単位で 1 秒あたり)で決める。public class GoalsSurfaceArgs : TsukimiBehaviour{ public Renderer floor; public int stripes = 8; public Vector3 color = new Vector3(0.2f, 0.6f, 1f); public float speed = 0.1f;
// 引数には float のほかに int・bool・Vector2・Vector3・Vector4 と GpuBuffer2D が使える。 [Surface] static Color4 Stripes(SurfaceId id, int stripes, Vector3 color, float offset) { float u = id.UV.x + offset; float band = Mathf.Floor(u * stripes) % 2f; float t = band > 0.5f ? 1f : 0.3f; return new Color4(color.x * t, color.y * t, color.z * t, 1f); }
// 値は呼ぶたびに渡す。Gpu.Show の引数の並びは、上のメソッドの引数(id の後ろ)と同じ順にする。 void Update() { Gpu.Show(nameof(Stripes), floor, stripes, color, speed * Time.time); }}注意
Gpu.Showに渡す値は、メソッドの引数(idの後ろ)と同じ位置に並べます。
頂点そのものを動かしたい
[Surface] static Vector3 M(VertexId v, ...)
例
using UnityEngine;using Tsukimi;
// 風に揺れる旗: 旗の頂点を時間で波打たせる。竿に付いている側(UV の x が 0)は動かさない。//// 前提:// - このスクリプトは、旗のオブジェクトに付ける。旗のメッシュ(細かく分割した Plane など)の Renderer を flag に渡す。// - 揺れの大きさは amplitude(メッシュ座標の単位)で決める。public class GoalsSurfaceVertex : TsukimiBehaviour{ public Renderer flag; public float amplitude = 0.2f;
// 頂点ごとに呼ばれ、返した位置へ頂点を動かす。v.Position と戻り値はメッシュ座標。 [Surface] static Vector3 Wave(VertexId v, float time, float amplitude) { float sway = Mathf.Sin(time * 3f + v.UV.x * 6f) * amplitude * v.UV.x; return v.Position + new Vector3(0f, sway, 0f); }
// 同じ名前で色の側も書く(2 つで 1 つのシェーダーになる)。 [Surface] static Color4 Wave(SurfaceId id, float time, float amplitude) { return id.UV.y > 0.5f ? new Color4(0.9f, 0.1f, 0.1f, 1f) : new Color4(1f, 1f, 1f, 1f); }
void Update() { Gpu.Show(nameof(Wave), flag, Time.time, amplitude); }}注意
- 頂点を動かしても、色の側で読む
id.Normalは元の向きのままです。 - 位置の側だけを書くとエラーになります。同じ名前で色の側も書きます。
カーネルを書き始めたい
return new Color4(...)
例
using UnityEngine;using Tsukimi;
// 色が巡る看板: 板の全面を、時間とともに赤・緑・青へゆっくり移り変わる色で塗る(GPU で計算する)。//// 前提:// - このスクリプトは、看板にする板のオブジェクトに付ける。板の Renderer を display に渡す。public class GoalsGpuReturn : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D board;
void Start() { board = Gpu.Buffer(64, 64); // 横 64 × 縦 64 のセル }
// [Kernel] を付けたメソッドは、セルごとに 1 回ずつ GPU で実行される。 // 返した色が、そのセルに書かれる(書き込む手段はこれだけ)。 [Kernel] static Color4 Paint(KernelId id, float time) { float r = Mathf.Sin(time) * 0.5f + 0.5f; float g = Mathf.Sin(time + 2.1f) * 0.5f + 0.5f; float b = Mathf.Sin(time + 4.2f) * 0.5f + 0.5f; return new Color4(r, g, b, 1f); }
void Update() { Gpu.Run(nameof(Paint), board, Time.time); // board の全セルで Paint を実行する Gpu.Show(board, display); // 結果を板に映す }}注意
- 書き込む手段はカーネルの戻り値だけです。ほかのセルへ代入すると、エラーになります(
CS0200)。
短いカーネルを 1 行で書きたい
static Color4 Step(...) => prev[id] * 0.5f
例
using UnityEngine;using Tsukimi;
// 残像: 触るたびに板を白く光らせ、そのあと毎フレーム少しずつ暗くして消していく。//// 前提:// - このスクリプトは、板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。public class GoalsGpuExpression : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(64, 64); next = Gpu.Buffer(64, 64); }
// 本体が 1 つの式なら => で書ける。前のフレームの値を 0.95 倍にする。 [Kernel] static Color4 Fade(KernelId id, GpuBuffer2D prev) => prev[id] * 0.95f;
[Kernel] static Color4 Flash(KernelId id) => Color4.White;
public override void Interact() { Gpu.Run(nameof(Flash), current); }
void Update() { Gpu.Run(nameof(Fade), next, current); // current を読んで next へ書く Gpu.Swap(ref current, ref next); // 次のフレームは、いま書いたほうを読む Gpu.Show(current, display); }}いま計算しているセルの値を読みたい
prev[id]
例
using UnityEngine;using Tsukimi;
// 消えていく足跡: 触るたびに板を明るくし、明るさを毎フレーム一定の量ずつ減らして、0 で止める。//// 前提:// - このスクリプトは、床の板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。public class GoalsGpuReadSelf : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(64, 64); next = Gpu.Buffer(64, 64); }
[Kernel] static Color4 Decay(KernelId id, GpuBuffer2D prev) { // prev[id] で、いま計算しているセルの、前のフレームの値を読む。 float v = Mathf.Max(0f, prev[id].R - 0.01f); return new Color4(v, v, v, 1f); }
[Kernel] static Color4 Light(KernelId id) => Color4.White;
public override void Interact() { Gpu.Run(nameof(Light), current); }
void Update() { Gpu.Run(nameof(Decay), next, current); Gpu.Swap(ref current, ref next); Gpu.Show(current, display); }}注意
- 既定のバッファでは、成分は 8 ビット(256 段階)に丸まります。細かい値が必要なら
GpuFormat.Halfで作るか、Gpu.Pack16x2で詰めます。
色の成分を 1 つずつ取り出したい
c.R c.G c.B c.A
例
using UnityEngine;using Tsukimi;
// 白黒の監視カメラ: カメラの映像を、明るさだけの白黒にして映す。//// 前提:// - このスクリプトは、モニターの板のオブジェクトに付ける。板の Renderer を display に渡す。// - 監視カメラ(Camera)の出力先の RenderTexture を feed に渡す。public class GoalsGpuChannels : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D gray;
void Start() { camera = Gpu.Buffer(256, 256); gray = Gpu.Buffer(256, 256); }
[Kernel] static Color4 Gray(KernelId id, GpuBuffer2D src) { Color4 c = src[id]; // c.R・c.G・c.B・c.A で成分を 1 つずつ取り出せる。人の目に合わせた重みで明るさにする。 float y = c.R * 0.299f + c.G * 0.587f + c.B * 0.114f; return new Color4(y, y, y, 1f); }
void Update() { Gpu.Load(camera, feed); // カメラの映像をバッファへ写す Gpu.Run(nameof(Gray), gray, camera); Gpu.Show(gray, display); }}返す色を、成分から組み立てたい
new Color4(r, g, b, a)
例
using UnityEngine;using Tsukimi;
// 色を入れ替える鏡: 映った像の赤と青を入れ替えて、別の世界のような色で映す。//// 前提:// - このスクリプトは、鏡の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 鏡に映すカメラの出力先の RenderTexture を feed に渡す。public class GoalsGpuCompose : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D swapped;
void Start() { camera = Gpu.Buffer(256, 256); swapped = Gpu.Buffer(256, 256); }
[Kernel] static Color4 SwapRedBlue(KernelId id, GpuBuffer2D src) { Color4 c = src[id]; // new Color4(赤, 緑, 青, 不透明度) で、返す色を成分から組み立てる。 return new Color4(c.B, c.G, c.R, 1f); }
void Update() { Gpu.Load(camera, feed); Gpu.Run(nameof(SwapRedBlue), swapped, camera); Gpu.Show(swapped, display); }}黒や白をそのまま使いたい
Color4.White
例
using UnityEngine;using Tsukimi;
// 影絵: カメラの映像を、明るいところは白、暗いところは黒の 2 色だけにして映す。//// 前提:// - このスクリプトは、スクリーンの板のオブジェクトに付ける。板の Renderer を display に渡す。// - 映すカメラの出力先の RenderTexture を feed に、白と黒の境目の明るさ(0〜1)を threshold に渡す。public class GoalsGpuConstantColor : TsukimiBehaviour{ public Renderer display; public Texture feed; public float threshold = 0.5f; private GpuBuffer2D camera; private GpuBuffer2D shadow;
void Start() { camera = Gpu.Buffer(256, 256); shadow = Gpu.Buffer(256, 256); }
[Kernel] static Color4 Silhouette(KernelId id, GpuBuffer2D src, float threshold) { Color4 c = src[id]; float y = (c.R + c.G + c.B) / 3f; // Color4.White と Color4.Black は、白と黒をそのまま返せる。 return y > threshold ? Color4.White : Color4.Black; }
void Update() { Gpu.Load(camera, feed); Gpu.Run(nameof(Silhouette), shadow, camera, threshold); Gpu.Show(shadow, display); }}いま計算しているセルの位置で、模様を変えたい
id.X id.Y
例
using UnityEngine;using Tsukimi;
// 市松模様の床: セルの位置から、8 セルごとに白と灰色が入れ替わる模様を作る。//// 前提:// - このスクリプトは、床の板のオブジェクトに付ける。板の Renderer を display に渡す。public class GoalsGpuCellPosition : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D floor;
void Start() { floor = Gpu.Buffer(64, 64); Gpu.Run(nameof(Checker), floor); // 模様は変わらないので、最初に 1 回だけ作る }
[Kernel] static Color4 Checker(KernelId id) { // id.X と id.Y は、いま計算しているセルの横と縦の位置(左下が 0, 0)。 int tile = (id.X / 8 + id.Y / 8) % 2; return tile == 0 ? Color4.White : new Color4(0.4f, 0.4f, 0.4f, 1f); }
void Update() { Gpu.Show(floor, display); }}隣のセルの値を見たい
prev[id.Offset(1, -1)]
例
using UnityEngine;using Tsukimi;
// 輪郭の線画: カメラの映像で、上下左右のセルと明るさが大きく違うところだけを白い線にする。//// 前提:// - このスクリプトは、スクリーンの板のオブジェクトに付ける。板の Renderer を display に渡す。// - 映すカメラの出力先の RenderTexture を feed に渡す。public class GoalsGpuNeighbor : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D lines;
void Start() { camera = Gpu.Buffer(256, 256); lines = Gpu.Buffer(256, 256); }
static float Luma(Color4 c) => (c.R + c.G + c.B) / 3f;
[Kernel] static Color4 Edge(KernelId id, GpuBuffer2D src) { // id.Offset(横, 縦) で、いまのセルから見た相対位置のセルを指す。 float dx = Luma(src[id.Offset(1, 0)]) - Luma(src[id.Offset(-1, 0)]); float dy = Luma(src[id.Offset(0, 1)]) - Luma(src[id.Offset(0, -1)]); float e = Mathf.Clamp01((Mathf.Abs(dx) + Mathf.Abs(dy)) * 4f); return new Color4(e, e, e, 1f); }
void Update() { Gpu.Load(camera, feed); Gpu.Run(nameof(Edge), lines, camera); Gpu.Show(lines, display); }}端のセルで、バッファの外を読んだときの値を知りたい
prev[id.Offset(-1000, -1000)]
例
using UnityEngine;using Tsukimi;
// 右へ流れる絵の具: 毎フレーム、全体を 1 セルずつ右へずらす。左の端には端のセルの色が流れ込み続ける。//// 前提:// - このスクリプトは、板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。// - 流す絵を palette に渡す(最初に 1 回だけ写す)。触ると最初の絵に戻る。public class GoalsGpuOutside : TsukimiBehaviour{ public Renderer display; public Texture palette; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(128, 64); next = Gpu.Buffer(128, 64); Gpu.Load(current, palette); }
[Kernel] static Color4 Shift(KernelId id, GpuBuffer2D prev) { // 左のセルを読む。左端(id.X が 0)では、バッファの外(-1)を指すことになるが、 // 外を指した添字は端の値に丸められるので、左端のセル自身の色が返る。範囲の検査は書かなくてよい。 return prev[id.Offset(-1, 0)]; }
public override void Interact() { Gpu.Load(current, palette); }
void Update() { Gpu.Run(nameof(Shift), next, current); Gpu.Swap(ref current, ref next); Gpu.Show(current, display); }}注意
- 端で反対側へ回して読みたいときは
Wrapを使います。
小さな模様を敷き詰めて並べたい
prev.Wrap(id.Offset(1, 0))
例
using UnityEngine;using Tsukimi;
// タイル張りの床: 16×16 の小さなタイルの絵を、128×128 の床一面に繰り返して敷き詰める。//// 前提:// - このスクリプトは、床の板のオブジェクトに付ける。板の Renderer を display に渡す。// - タイル 1 枚の絵(16×16)を tileImage に渡す。public class GoalsGpuWrap : TsukimiBehaviour{ public Renderer display; public Texture tileImage; private GpuBuffer2D tile; private GpuBuffer2D floor;
void Start() { tile = Gpu.Buffer(16, 16); floor = Gpu.Buffer(128, 128); Gpu.Load(tile, tileImage); Gpu.Run(nameof(Tile), floor, tile); }
[Kernel] static Color4 Tile(KernelId id, GpuBuffer2D tile) { // 床のセルの位置で、小さいタイルを読む。Wrap は外を指したら反対側へ回すので、 // 位置 (20, 3) はタイルの (4, 3) になり、タイルが繰り返し並ぶ。 return tile.Wrap(id); }
void Update() { Gpu.Show(floor, display); }}注意
- 幅 64 のバッファで -1 を指すと、63 のセルが返ります。
拡大しても四角い粒が見えないようにしたい
prev.Smooth(position)
例
using UnityEngine;using Tsukimi;
// 温度の地図の拡大: 16×16 の粗い温度の地図を、256×256 の板へ、四角い粒が見えないように引き伸ばして映す。//// 前提:// - このスクリプトは、地図の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 粗い地図の絵(16×16)を coarse に渡す。public class GoalsGpuSmooth : TsukimiBehaviour{ public Renderer display; public Texture coarse; private GpuBuffer2D small; private GpuBuffer2D big;
void Start() { small = Gpu.Buffer(16, 16); big = Gpu.Buffer(256, 256); Gpu.Load(small, coarse); Gpu.Run(nameof(Enlarge), big, small); // 地図は変わらないので、最初に 1 回だけ引き伸ばす }
[Kernel] static Color4 Enlarge(KernelId id, GpuBuffer2D map) { // Smooth は、位置(0〜1)で指した値を、まわりのセルと混ぜて返す。 // 大きいほうのセルの位置を 0〜1 に直して、小さいほうの地図を指す。 Vector2 position = new Vector2(id.X / 256f, id.Y / 256f); return map.Smooth(position); }
void Update() { Gpu.Show(big, display); }}注意
- 4 つのセルを読むので、1 つを読むより費用がかかります。
セルごとにばらついた値が欲しい
Gpu.Random01(n)
例
using UnityEngine;using Tsukimi;
// テレビの砂嵐: セルごと・フレームごとにばらばらの明るさで塗る。//// 前提:// - このスクリプトは、テレビの画面の板のオブジェクトに付ける。板の Renderer を display に渡す。public class GoalsGpuRandom : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D screen; private int frame;
void Start() { screen = Gpu.Buffer(128, 96); }
[Kernel] static Color4 Static(KernelId id, int frame) { // Random01 は、同じ入力なら必ず同じ値(0〜1)を返す。 // セルの位置とフレームの番号を混ぜた数を渡して、セルごと・フレームごとに違う値にする。 float v = Gpu.Random01(Gpu.Hash(Gpu.Hash(id.X, id.Y) + frame)); return new Color4(v, v, v, 1f); }
void Update() { frame = frame + 1; Gpu.Run(nameof(Static), screen, frame); Gpu.Show(screen, display); }}注意
- 同じ入力なら必ず同じ値が返ります。フレームごとに変えたいときは、フレームの番号を入力に混ぜます。
なめらかな模様を作りたい
Gpu.Noise(v)
例
using UnityEngine;using Tsukimi;
// 流れる雲: なめらかなノイズで雲のまだら模様を作り、時間とともに横へ流す。//// 前提:// - このスクリプトは、空の板のオブジェクトに付ける。板の Renderer を display に渡す。public class GoalsGpuNoise : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D sky;
void Start() { sky = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Clouds(KernelId id, float time) { // Noise は、近い位置では近い値になる、なめらかなノイズ(0〜1)。 float n = Gpu.Noise(new Vector2(id.X * 0.05f + time * 0.3f, id.Y * 0.05f)); float cloud = Mathf.SmoothStep(0.45f, 0.75f, n); return Color4.Lerp(new Color4(0.35f, 0.6f, 0.95f, 1f), Color4.White, cloud); }
void Update() { Gpu.Run(nameof(Clouds), sky, Time.time); Gpu.Show(sky, display); }}書き込み先のバッファの大きさを、カーネルの中で知りたい
Gpu.OutWidth
例
using UnityEngine;using Tsukimi;
// 夕焼けのグラデーション: 板の下から上へ、橙から紺へ変わる空を塗る。どの大きさのバッファに書いても同じ見た目になる。//// 前提:// - このスクリプトは、背景の板のオブジェクトに付ける。板の Renderer を display に渡す。// - バッファの大きさは width と height で決める。public class GoalsGpuOutSize : TsukimiBehaviour{ public Renderer display; public int width = 64; public int height = 256; private GpuBuffer2D sky;
void Start() { sky = Gpu.Buffer(width, height); Gpu.Run(nameof(Sunset), sky); }
[Kernel] static Color4 Sunset(KernelId id) { // Gpu.OutHeight は、書き込み先のバッファの縦のセル数。 // 位置を 0〜1 に直すのに使えば、大きさを引数で渡さなくてよい。 float t = id.Y / (float)Gpu.OutHeight; return Color4.Lerp(new Color4(1f, 0.55f, 0.2f, 1f), new Color4(0.05f, 0.05f, 0.25f, 1f), t); }
void Update() { Gpu.Show(sky, display); }}1 つのセルに、細かい値を 2 つ詰めたい
Gpu.Pack16x2(v)
例
using UnityEngine;using Tsukimi;
// ゆっくり動く粒: 粒の位置(横と縦)を 1 つのセルに細かく詰めて持ち、毎フレームほんの少しずつ動かす。// 8 ビット(256 段階)のままだと、少しずつの動きは丸められて止まってしまう。//// 前提:// - このスクリプトは、粒をまとめて持つオブジェクトに付ける。// - 粒の数は 64×64(セル 1 つが粒 1 つ)。位置を見るときは positions を映すか読み戻す。public class GoalsGpuPack : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D positions; private GpuBuffer2D next;
void Start() { positions = Gpu.Buffer(64, 64); next = Gpu.Buffer(64, 64); }
[Kernel] static Color4 Drift(KernelId id, GpuBuffer2D prev) { // Unpack16x2 で、詰めた 2 つの値(各 65536 段階)を取り出す。 Vector2 p = Gpu.Unpack16x2(prev[id]); p.x = Mathf.Repeat(p.x + 0.0002f, 1f); // 1 フレームに 0.0002 だけ右へ // Pack16x2 で、2 つの値を 1 つのセルに詰めて書く(カーネルの中でしか使えない)。 return Gpu.Pack16x2(p); }
void Update() { Gpu.Run(nameof(Drift), next, positions); Gpu.Swap(ref positions, ref next); Gpu.Show(positions, display); }}注意
Gpu.Pack16x2はカーネルの中でしか使えません。GpuFormat.Halfで作ったバッファでは使いません。
2 つの色のあいだの色が欲しい
Color4.Lerp(c, c2, h)
例
using UnityEngine;using Tsukimi;
// 熱の地図の色分け: 温度(0〜1)を、冷たい青から熱い赤までの色で塗る。//// 前提:// - このスクリプトは、地図の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 温度の絵(赤の成分が温度)を heatmap に渡す。public class GoalsGpuLerp : TsukimiBehaviour{ public Renderer display; public Texture heatmap; private GpuBuffer2D heat; private GpuBuffer2D colored;
void Start() { heat = Gpu.Buffer(128, 128); colored = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Colorize(KernelId id, GpuBuffer2D src) { // Color4.Lerp(a, b, t) は、t が 0 なら a、1 なら b、そのあいだはその割合で混ぜた色。 return Color4.Lerp(new Color4(0.1f, 0.2f, 1f, 1f), new Color4(1f, 0.1f, 0.05f, 1f), src[id].R); }
void Update() { Gpu.Load(heat, heatmap); Gpu.Run(nameof(Colorize), colored, heat); Gpu.Show(colored, display); }}同じ計算を、カーネルの外の関数にまとめたい
Falloff(d)
例
using UnityEngine;using Tsukimi;
// スポットライトの輪: 板の中心から離れるほど暗くなる光の輪を、2 つのカーネルで同じ減り方を使って描く。// 減り方の式は 1 つの関数にまとめ、どちらのカーネルからも呼ぶ。//// 前提:// - このスクリプトは、床の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 光の色は warm(true で暖色)で切り替える。public class GoalsGpuHelper : TsukimiBehaviour{ public Renderer display; public bool warm = true; private GpuBuffer2D floor;
void Start() { floor = Gpu.Buffer(128, 128); }
// カーネルから呼ぶ補助の関数。[Kernel] は付けない。 static float Falloff(float d) { return Mathf.Clamp01(1f - d * d); }
static float DistanceFromCenter(KernelId id) { return Vector2.Distance(new Vector2(id.X, id.Y), new Vector2(64f, 64f)) / 64f; }
[Kernel] static Color4 WarmLight(KernelId id) { float k = Falloff(DistanceFromCenter(id)); return new Color4(k, k * 0.8f, k * 0.5f, 1f); }
[Kernel] static Color4 CoolLight(KernelId id) { float k = Falloff(DistanceFromCenter(id)); return new Color4(k * 0.6f, k * 0.8f, k, 1f); }
void Update() { if (warm) Gpu.Run(nameof(WarmLight), floor); else Gpu.Run(nameof(CoolLight), floor); Gpu.Show(floor, display); }}GPU で計算するためのバッファをまず作りたい
Gpu.Buffer(64, 64)
例
using UnityEngine;using Tsukimi;
// ライフゲーム: 128×128 のセルで、生きているセル(白)と死んでいるセル(黒)の世代を毎フレーム進める。//// 前提:// - このスクリプトは、盤の板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。// - 触ると、ランダムな配置から始め直す。public class GoalsGpuHostBuffer : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next; private int seed;
void Start() { // GPU で計算するためのバッファ。横と縦のセルの数で作る(1 セルに 4 成分)。 current = Gpu.Buffer(128, 128); next = Gpu.Buffer(128, 128); Reseed(); }
[Kernel] static Color4 Seed(KernelId id, int seed) { return Gpu.Random01(Gpu.Hash(Gpu.Hash(id.X, id.Y) + seed)) < 0.3f ? Color4.White : Color4.Black; }
[Kernel] static Color4 Life(KernelId id, GpuBuffer2D prev) { float n = 0f; for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++) if (dx != 0 || dy != 0) n += prev.Wrap(id.Offset(dx, dy)).R; bool alive = prev[id].R > 0.5f; bool live = n > 2.5f && n < 3.5f || alive && n > 1.5f && n < 2.5f; return live ? Color4.White : Color4.Black; }
private void Reseed() { seed = seed + 1; Gpu.Run(nameof(Seed), current, seed); }
public override void Interact() { Reseed(); }
void Update() { Gpu.Run(nameof(Life), next, current); Gpu.Swap(ref current, ref next); Gpu.Show(current, display); }}0〜1 の外の値や、もっと細かい刻みを持ちたい
Gpu.Buffer(64, 64, GpuFormat.Half)
例
using UnityEngine;using Tsukimi;
// 水面の波紋: 触るたびに中心へしずくを落とし、広がって跳ね返る波を計算する。// 波の高さは負にもなるので、0〜1 の外の値も持てる形式でバッファを作る。//// 前提:// - このスクリプトは、水面の板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。public class GoalsGpuHostFormat : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D before; private GpuBuffer2D now; private GpuBuffer2D next; private GpuBuffer2D shown;
void Start() { // GpuFormat.Half で作ると、1 成分が 16 ビットの小数になり、負の値や 1 を超える値も持てる。 // 形式は書くときに決まっている必要がある(変数では渡せない)。 before = Gpu.Buffer(128, 128, GpuFormat.Half); now = Gpu.Buffer(128, 128, GpuFormat.Half); next = Gpu.Buffer(128, 128, GpuFormat.Half); shown = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Wave(KernelId id, GpuBuffer2D now, GpuBuffer2D before) { float around = (now[id.Offset(1, 0)].R + now[id.Offset(-1, 0)].R + now[id.Offset(0, 1)].R + now[id.Offset(0, -1)].R) * 0.25f; float h = (now[id].R * 2f - before[id].R + (around - now[id].R) * 0.9f) * 0.995f; return new Color4(h, 0f, 0f, 1f); }
[Kernel] static Color4 Drop(KernelId id, GpuBuffer2D now) { float d = Vector2.Distance(new Vector2(id.X, id.Y), new Vector2(64f, 64f)); return new Color4(now[id].R + (d < 3f ? 1f : 0f), 0f, 0f, 1f); }
[Kernel] static Color4 Tint(KernelId id, GpuBuffer2D h) { float v = Mathf.Clamp01(h[id].R * 0.5f + 0.5f); // -1〜1 を 0〜1 へ写して見せる return new Color4(v * 0.3f, v * 0.6f, v, 1f); }
public override void Interact() { Gpu.Run(nameof(Drop), next, now); Gpu.Swap(ref now, ref next); }
void Update() { Gpu.Run(nameof(Wave), next, now, before); GpuBuffer2D t = before; before = now; now = next; next = t; Gpu.Run(nameof(Tint), shown, now); Gpu.Show(shown, display); }}注意
- 65504 を超えた値は 65504 のまま留まります。エラーも警告も出ません。
- 8 ビットの画像を
Gpu.Loadで写すと、0〜1 の外の値は届く前に潰れます。
画像や動画やカメラの映像を GPU に渡したい
Gpu.Load(current, source)
例
using UnityEngine;using Tsukimi;
// 動画のすりガラス: 動画プレイヤーの映像を GPU に渡し、ぼかしてから映す。//// 前提:// - このスクリプトは、すりガラスの板のオブジェクトに付ける。板の Renderer を display に渡す。// - 動画プレイヤーの映像が出る RenderTexture を video に渡す。public class GoalsGpuHostLoad : TsukimiBehaviour{ public Renderer display; public Texture video; private GpuBuffer2D frame; private GpuBuffer2D blurred;
void Start() { frame = Gpu.Buffer(128, 72); blurred = Gpu.Buffer(128, 72); }
[Kernel] static Color4 Blur(KernelId id, GpuBuffer2D src) { Color4 sum = Color4.Black; for (int dy = -2; dy <= 2; dy++) for (int dx = -2; dx <= 2; dx++) sum = sum + src[id.Offset(dx, dy)]; return sum * (1f / 25f); }
void Update() { // 画像・動画・カメラの映像をバッファへ写す。大きさが違うときは拡大縮小して入る。 Gpu.Load(frame, video); Gpu.Run(nameof(Blur), blurred, frame); Gpu.Show(blurred, display); }}注意
- 画像を色ではなく数として使うときは、その画像のインポート設定で sRGB を切ります。
前のフレームの結果を、次のフレームの入力にして回したい
Gpu.Swap(ref a, ref b)
例
using UnityEngine;using Tsukimi;
// たき火の炎: 下の段から熱を吹き込み、毎フレーム「前のフレームの結果」を読んで上へ昇らせながら冷ます。//// 前提:// - このスクリプトは、炎の板のオブジェクトに付ける。板の Renderer を display に渡す。public class GoalsGpuHostSwap : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next; private GpuBuffer2D shown; private int frame;
void Start() { current = Gpu.Buffer(64, 96); next = Gpu.Buffer(64, 96); shown = Gpu.Buffer(64, 96); }
[Kernel] static Color4 Burn(KernelId id, GpuBuffer2D prev, int frame) { if (id.Y == 0) return new Color4(Gpu.Random01(Gpu.Hash(id.X + frame * 131)), 0f, 0f, 1f); float below = (prev[id.Offset(-1, -1)].R + prev[id.Offset(0, -1)].R * 2f + prev[id.Offset(1, -1)].R) * 0.25f; return new Color4(Mathf.Max(0f, below - 0.012f), 0f, 0f, 1f); }
[Kernel] static Color4 Colorize(KernelId id, GpuBuffer2D heat) { float h = heat[id].R; return new Color4(Mathf.Clamp01(h * 3f), Mathf.Clamp01(h * 3f - 1f), Mathf.Clamp01(h * 3f - 2f), 1f); }
void Update() { frame = frame + 1; Gpu.Run(nameof(Burn), next, current, frame); // 読み元と書き込み先を入れ替える。次のフレームは、いま書いたほうを読む。 Gpu.Swap(ref current, ref next); Gpu.Run(nameof(Colorize), shown, current); Gpu.Show(shown, display); }}書いたカーネルを 1 回実行したい
Gpu.Run(nameof(Step), next, current)
例
using UnityEngine;using Tsukimi;
// 写真のネガ: 触るたびに 1 回だけ、写真の明暗を反転する(毎フレームは計算しない)。//// 前提:// - このスクリプトは、写真の板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。// - 写真のテクスチャを photo に渡す。public class GoalsGpuHostRun : TsukimiBehaviour{ public Renderer display; public Texture photo; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(256, 256); next = Gpu.Buffer(256, 256); Gpu.Load(current, photo); Gpu.Show(current, display); }
[Kernel] static Color4 Invert(KernelId id, GpuBuffer2D src) { Color4 c = src[id]; return new Color4(1f - c.R, 1f - c.G, 1f - c.B, 1f); }
public override void Interact() { // 書き込み先・読み元の順に渡す。呼んだ 1 回ぶんだけ、全セルで Invert が実行される。 Gpu.Run(nameof(Invert), next, current); Gpu.Swap(ref current, ref next); Gpu.Show(current, display); }}時間のように毎フレーム変わる値を、カーネルへ渡したい
Gpu.Run(nameof(Step), next, current, phase)
例
using UnityEngine;using Tsukimi;
// 脈打つ光の輪: 時間と半径をカーネルへ渡し、中心から広がっては消える輪を描く。//// 前提:// - このスクリプトは、床の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 輪の最大の半径(セルの数)は radius で決める。public class GoalsGpuHostArgs : TsukimiBehaviour{ public Renderer display; public float radius = 60f; private GpuBuffer2D floor;
void Start() { floor = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Ring(KernelId id, float time, float radius) { float r = Mathf.Repeat(time, 1f) * radius; float d = Vector2.Distance(new Vector2(id.X, id.Y), new Vector2(64f, 64f)); float k = Mathf.Clamp01(1f - Mathf.Abs(d - r) / 3f) * (1f - r / radius); return new Color4(k * 0.4f, k, k * 0.8f, 1f); }
void Update() { // 書き込み先のあとに並べた値が、カーネルの第 2 引数以降へ順番どおりに渡る。 Gpu.Run(nameof(Ring), floor, Time.time, radius); Gpu.Show(floor, display); }}実行するカーネルを名前の文字列で選びたい
Gpu.Run("Step", next, current)
例
using UnityEngine;using Tsukimi;
// フィルターの切り替え: ボタンを押すたびに、カメラの映像にかけるフィルターを順に切り替える。//// 前提:// - このスクリプトは、切り替えボタン(Collider 付き)に付ける。映す板の Renderer を display に渡す。// - カメラの出力先の RenderTexture を feed に渡す。public class GoalsGpuHostString : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D filtered; private int current; // 0: そのまま 1: セピア 2: 暗視
void Start() { camera = Gpu.Buffer(256, 256); filtered = Gpu.Buffer(256, 256); }
[Kernel] static Color4 Plain(KernelId id, GpuBuffer2D src) => src[id];
[Kernel] static Color4 Sepia(KernelId id, GpuBuffer2D src) { float y = (src[id].R + src[id].G + src[id].B) / 3f; return new Color4(y * 1.1f, y * 0.9f, y * 0.7f, 1f); }
[Kernel] static Color4 Night(KernelId id, GpuBuffer2D src) { return new Color4(0f, Mathf.Clamp01(src[id].G * 2f), 0f, 1f); }
public override void Interact() { current = (current + 1) % 3; }
void Update() { Gpu.Load(camera, feed); // カーネルは名前の文字列でも指せる。名前は文字列の定数で書く(変数や式で組み立てた名前は書けない)。 // 綴りを間違えるとコンパイルで断られる。nameof で書けば、C# の側でも気づける。 if (current == 0) Gpu.Run("Plain", filtered, camera); else if (current == 1) Gpu.Run("Sepia", filtered, camera); else Gpu.Run("Night", filtered, camera); Gpu.Show(filtered, display); }}注意
nameofを使うと、綴りを間違えたときに C# の側で気づけます。- 名前は文字列の定数か
nameofで書きます。変数や式で組み立てた名前は書けません。
1 つの Behaviour で、カーネルを使い分けたい
Gpu.Run(nameof(Fade), current, next)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// お絵描きボード: 自分がボードの前にいる間、手の位置にペンで点を描き、消しボタンで全部消す。// 描くカーネルと消すカーネルを 1 つの Behaviour に置いて使い分ける。//// 前提:// - このスクリプトは、ボードの板のオブジェクト(Collider 付き・大きさ 1×1 の Quad)に付ける。// - 板の Renderer を display に渡す。触ると全部消える。public class GoalsGpuHostTwo : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(256, 256); next = Gpu.Buffer(256, 256); Gpu.Run(nameof(Clear), current); }
[Kernel] static Color4 Clear(KernelId id) => Color4.White;
[Kernel] static Color4 Pen(KernelId id, GpuBuffer2D prev, Vector2 tip) { float d = Vector2.Distance(new Vector2(id.X, id.Y), tip); return d < 3f ? Color4.Black : prev[id]; }
public override void Interact() { Gpu.Run(nameof(Clear), current); }
void Update() { // 右手の人差し指の位置を、板の上の位置(セルの座標)へ直す。 Vector3 hand = Networking.LocalPlayer.GetBonePosition(HumanBodyBones.RightIndexDistal); Vector3 local = transform.InverseTransformPoint(hand); if (Mathf.Abs(local.z) < 0.05f) { Vector2 tip = new Vector2((local.x + 0.5f) * 256f, (local.y + 0.5f) * 256f); Gpu.Run(nameof(Pen), next, current, tip); Gpu.Swap(ref current, ref next); } Gpu.Show(current, display); }}重みのようなたくさんの数を、カーネルへ渡したい
Gpu.Run(nameof(Blend), next, current, weights)
例
using UnityEngine;using Tsukimi;
// ドット絵のパレット: 写真の明るさを 16 段に分け、それぞれを表(パレット)の色に置き換える。//// 前提:// - このスクリプトは、額縁の板のオブジェクトに付ける。板の Renderer を display に、写真を photo に渡す。// - palette には 16 色を入れる(x,y,z が赤・緑・青)。長さが 16 でないと、実行は何も起きない。public class GoalsGpuHostTable : TsukimiBehaviour{ public Renderer display; public Texture photo; public Vector4[] palette = new Vector4[16]; private GpuBuffer2D src; private GpuBuffer2D dotted;
void Start() { src = Gpu.Buffer(64, 64); // 小さく読んで、ドット絵らしくする dotted = Gpu.Buffer(64, 64); Gpu.Load(src, photo); }
// 表の引数には、大きさを [Capacity(16)] のように書く(2 の冪・上限 1024)。 [Kernel] static Color4 Posterize(KernelId id, GpuBuffer2D src, [Capacity(16)] Vector4[] palette) { float y = (src[id].R + src[id].G + src[id].B) / 3f; Vector4 c = palette[Mathf.Min(15, (int)(y * 16f))]; return new Color4(c.x, c.y, c.z, 1f); }
void Update() { Gpu.Run(nameof(Posterize), dotted, src, palette); Gpu.Show(dotted, display); }}注意
- 渡した配列の長さが
[Capacity]に書いた数と違うと、その実行は何も起きません。 - 1 つのカーネルが取る表の大きさの合計は 4000 までです。
カーネルへ渡す表の大きさを決めたい
[Capacity(4)] Vector4[] w
例
using UnityEngine;using Tsukimi;
// 空の色の移り変わり: 朝・昼・夕・夜の 4 色だけを表で渡し、時刻に合わせて空の色を作る。//// 前提:// - このスクリプトは、空の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 1 日の長さ(秒)は dayLength で決める。public class GoalsGpuHostSmallTable : TsukimiBehaviour{ public Renderer display; public float dayLength = 120f; private GpuBuffer2D sky; private Vector4[] colors = new Vector4[4];
void Start() { sky = Gpu.Buffer(16, 64); colors[0] = new Vector4(1f, 0.7f, 0.5f, 1f); // 朝 colors[1] = new Vector4(0.4f, 0.7f, 1f, 1f); // 昼 colors[2] = new Vector4(1f, 0.45f, 0.2f, 1f); // 夕 colors[3] = new Vector4(0.05f, 0.05f, 0.2f, 1f); // 夜 }
// 4 つだけの小さい表も同じ書き方で渡せる。 [Kernel] static Color4 Sky(KernelId id, float phase, [Capacity(4)] Vector4[] colors) { int a = (int)phase % 4; int b = (a + 1) % 4; Vector4 c = Vector4.Lerp(colors[a], colors[b], phase - Mathf.Floor(phase)); float shade = 0.7f + 0.3f * id.Y / 64f; return new Color4(c.x * shade, c.y * shade, c.z * shade, 1f); }
void Update() { float phase = Mathf.Repeat(Time.time / dayLength, 1f) * 4f; Gpu.Run(nameof(Sky), sky, phase, colors); Gpu.Show(sky, display); }}読み元のバッファを使わずに、最初の状態を書き込みたい
Gpu.Run(nameof(Seed), board, 0.25f)
例
using UnityEngine;using Tsukimi;
// 雪原の最初の状態: 起動したときに、でこぼこの雪の高さを 1 回だけ書き込む。読み元のバッファは要らない。//// 前提:// - このスクリプトは、雪原の板のオブジェクトに付ける。板の Renderer を display に渡す。// - でこぼこの強さは bumpiness で決める。public class GoalsGpuHostNoBuffer : TsukimiBehaviour{ public Renderer display; public float bumpiness = 0.3f; private GpuBuffer2D snow;
void Start() { snow = Gpu.Buffer(128, 128); // 読み元を渡さずに実行する。担当するセルは書き込み先(snow)で決まる。 Gpu.Run(nameof(Initial), snow, bumpiness); }
[Kernel] static Color4 Initial(KernelId id, float bumpiness) { float n = Gpu.Noise(new Vector2(id.X * 0.08f, id.Y * 0.08f)); float h = 1f - bumpiness + n * bumpiness; return new Color4(h, h, h, 1f); }
void Update() { Gpu.Show(snow, display); }}全部のセルの合計や平均を出したい
Gpu.Reduce(nameof(Brighter), middle, full)
例
using UnityEngine;using Tsukimi;
// 明るさの自動調整: カメラの映像の平均の明るさを求め、暗いときは明るく、明るいときは暗く補正して映す。// 全セルの明るさを足し合わせて 1 セルにまとめ、セルの数で割って平均にする。//// 前提:// - このスクリプトは、モニターの板のオブジェクトに付ける。板の Renderer を display に渡す。// - カメラの出力先の RenderTexture を feed に渡す。public class GoalsGpuHostReduce : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D middle; private GpuBuffer2D total; private GpuBuffer2D corrected;
void Start() { camera = Gpu.Buffer(64, 64); // 足し合わせると 1 を超えるので、0〜1 の外の値も持てる形式にする。 middle = Gpu.Buffer(8, 8, GpuFormat.Half); total = Gpu.Buffer(1, 1, GpuFormat.Half); corrected = Gpu.Buffer(64, 64); }
// 2 つの値を 1 つにまとめる方法。[Reduce] を付ける。ここでは足し合わせる。 [Reduce] static Color4 Sum(Color4 a, Color4 b) { return a + b; }
[Kernel] static Color4 Expose(KernelId id, GpuBuffer2D src, GpuBuffer2D total) { Color4 t = total[new KernelId(0, 0)]; float mean = (t.R + t.G + t.B) / 3f / (64f * 64f); float gain = Mathf.Clamp(0.5f / Mathf.Max(mean, 0.01f), 0.5f, 4f); Color4 c = src[id]; return new Color4(Mathf.Clamp01(c.R * gain), Mathf.Clamp01(c.G * gain), Mathf.Clamp01(c.B * gain), 1f); }
void Update() { Gpu.Load(camera, feed); // 書き込み先の 1 セルに、読み元のうちそのセルが受け持つ範囲をまとめた値が入る。 // 段は自分で並べる(64×64 → 8×8 → 1×1)。 Gpu.Reduce(nameof(Sum), middle, camera); Gpu.Reduce(nameof(Sum), total, middle); Gpu.Run(nameof(Expose), corrected, camera, total); Gpu.Show(corrected, display); }}注意
- 段は自分で並べます。書いた回数が、そのまま実行の回数になります。
成分ごとの最大や最小を知りたい
Gpu.Max(peak, full)
例
using UnityEngine;using Tsukimi;
// 暗い部屋の見やすさ調整: カメラの映像の、いちばん明るい値といちばん暗い値を調べ、その幅いっぱいに引き伸ばして映す。//// 前提:// - このスクリプトは、モニターの板のオブジェクトに付ける。板の Renderer を display に渡す。// - カメラの出力先の RenderTexture を feed に渡す。public class GoalsGpuHostMax : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D full; private GpuBuffer2D stretched; private GpuBuffer2D peak; private GpuBuffer2D floor;
void Start() { full = Gpu.Buffer(64, 64); stretched = Gpu.Buffer(64, 64); peak = Gpu.Buffer(1, 1); floor = Gpu.Buffer(1, 1); }
[Kernel] static Color4 Stretch(KernelId id, GpuBuffer2D src, GpuBuffer2D peak, GpuBuffer2D floor) { Color4 hi = peak[new KernelId(0, 0)]; Color4 lo = floor[new KernelId(0, 0)]; float span = Mathf.Max(0.0001f, Mathf.Max(hi.R, Mathf.Max(hi.G, hi.B)) - Mathf.Min(lo.R, Mathf.Min(lo.G, lo.B))); float low = Mathf.Min(lo.R, Mathf.Min(lo.G, lo.B)); Color4 c = src[id]; return new Color4((c.R - low) / span, (c.G - low) / span, (c.B - low) / span, 1f); }
void Update() { Gpu.Load(full, feed); // 成分ごとに、全セルの最大と最小を 1 セルへまとめる。書き込み先は呼びごとに別のバッファにする。 Gpu.Max(peak, full); Gpu.Min(floor, full); Gpu.Run(nameof(Stretch), stretched, full, peak, floor); Gpu.Show(stretched, display); }}注意
- 同じバッファへ
Gpu.MaxとGpu.Minを続けると、後の呼び出しが前の結果を上書きします。
計算した結果を、そのままオブジェクトに映したい
Gpu.Show(current, display)
例
using UnityEngine;using Tsukimi;
// 走る光のネオン管: 管の長さに沿って光の粒が流れる模様を計算し、そのまま管の見た目に映す。//// 前提:// - このスクリプトは、ネオン管のオブジェクトに付ける。管の Renderer を tube に渡す。// - 管のメッシュの UV は、横(u)が管の長さに沿っていること。public class GoalsGpuHostShow : TsukimiBehaviour{ public Renderer tube; private GpuBuffer2D lights;
void Start() { lights = Gpu.Buffer(256, 4); }
[Kernel] static Color4 Chase(KernelId id, float time) { float k = Mathf.Pow(Mathf.Sin(id.X * 0.1f - time * 6f) * 0.5f + 0.5f, 8f); return new Color4(1f * k + 0.1f, 0.2f * k, 0.8f * k + 0.1f, 1f); }
void Update() { Gpu.Run(nameof(Chase), lights, Time.time); // 計算したバッファを、その Renderer の見た目としてそのまま映す。 Gpu.Show(lights, tube); }}計算した結果を、Unity や VRChat の API へ渡したい
Gpu.Texture(current)
例
using UnityEngine;using Tsukimi;
// 光る模様の服: 計算した模様を、別のオブジェクトのマテリアルの発光(Emission)の絵として渡す。//// 前提:// - このスクリプトは、模様を管理するオブジェクトに付ける。// - 光らせるオブジェクトの Renderer を target に渡す。そのマテリアルは Standard で、Emission を有効にしておく。public class GoalsGpuHostTexture : TsukimiBehaviour{ public Renderer target; private GpuBuffer2D pattern;
void Start() { pattern = Gpu.Buffer(64, 64); }
[Kernel] static Color4 Stripes(KernelId id, float time) { float k = Mathf.Sin(id.Y * 0.4f + time * 3f) > 0.6f ? 1f : 0f; return new Color4(0f, k, k, 1f); }
void Update() { Gpu.Run(nameof(Stripes), pattern, Time.time); // Gpu.Texture で、バッファを Unity のテクスチャとして取り出す(複製ではなくバッファそのもの)。 target.material.SetTexture("_EmissionMap", Gpu.Texture(pattern)); }}注意
- 返るのはバッファそのもので、複製ではありません。
自分で用意した RenderTexture へ、計算した結果を写したい
VRCGraphics.Blit(Gpu.Texture(current), target)
例
using UnityEngine;using VRC.SDKBase;using Tsukimi;
// 計算した絵を UI に出す: 波の模様を計算し、UI の RawImage が表示している RenderTexture へ毎フレーム写す。//// 前提:// - このスクリプトは、模様を管理するオブジェクトに付ける。// - 写す先の RenderTexture を screen に渡す。UI の RawImage の Texture にも同じものを設定しておく。// - screen は Color Space を Linear(sRGB を外す)、Filter Mode を Point で作っておく。public class GoalsGpuHostBlit : TsukimiBehaviour{ public RenderTexture screen; private GpuBuffer2D wave;
void Start() { wave = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Wave(KernelId id, float time) { float v = Mathf.Sin(id.X * 0.1f + time) * Mathf.Cos(id.Y * 0.1f - time) * 0.5f + 0.5f; return new Color4(v, v * 0.5f, 1f - v, 1f); }
void Update() { Gpu.Run(nameof(Wave), wave, Time.time); // 自分で用意した RenderTexture へ写す。 VRCGraphics.Blit(Gpu.Texture(wave), screen); }}注意
- 写す先は sRGB を外して(
RenderTextureReadWrite.Linear)作ります。既定では sRGB が付き、カーネルが返した値がそのまま入りません。
自分で用意した RenderTexture を、そのまま書き込み先にしたい
public GpuBuffer2D target;
例
using UnityEngine;using Tsukimi;
// ミニマップへ直接書く: 自分で用意した RenderTexture を書き込み先にして、カーネルの結果をそこへ直接書く。//// 前提:// - このスクリプトは、ミニマップを管理するオブジェクトに付ける。// - インスペクタの map に、ミニマップの RenderTexture を挿す(Color Space は Linear、Filter Mode は Point)。// - 地形の高さの絵を terrain に渡す。public class GoalsGpuHostSlot : TsukimiBehaviour{ // public の GpuBuffer2D は、自分で用意した RenderTexture を挿す口になる。 public GpuBuffer2D map; public Texture terrain; private GpuBuffer2D height;
void Start() { height = Gpu.Buffer(128, 128); Gpu.Load(height, terrain); }
[Kernel] static Color4 Contour(KernelId id, GpuBuffer2D h) { float v = h[id].R; bool line = Mathf.Repeat(v * 10f, 1f) < 0.08f; return line ? Color4.Black : Color4.Lerp(new Color4(0.3f, 0.6f, 0.3f, 1f), Color4.White, v); }
void Update() { Gpu.Run(nameof(Contour), map, height); // 挿した RenderTexture へ直接書く }}注意
- 挿した RenderTexture の設定はそのまま使われます。sRGB を外し、Filter Mode を Point にしておきます。
計算した結果を、数として Udon 側で読みたい
VRCAsyncGPUReadback.Request(Gpu.Texture(current), ...)
例
using UnityEngine;using VRC.SDK3.Rendering;using VRC.Udon.Common.Interfaces;using Tsukimi;using TMPro;
// スポイト: 絵の真ん中のセルの色を GPU から読み戻し、その値を数字で表示する。//// 前提:// - このスクリプトは、スポイトの装置のオブジェクトに付ける。// - 調べる絵を picture に、数字を出す TextMeshProUGUI を readout に渡す。public class GoalsGpuHostReadback : TsukimiBehaviour{ public Texture picture; public TextMeshProUGUI readout; private GpuBuffer2D buf; private byte[] cell = new byte[4]; // 1 セル = R, G, B, A の 4 バイト private bool waiting;
void Start() { buf = Gpu.Buffer(64, 64); }
void Update() { // 頼んだ答えが返るまでは、次を頼まない(バッファも書き換えない)。 if (waiting) return; Gpu.Load(buf, picture); waiting = true; // 真ん中(x = 32, y = 32)の 1 セルだけを頼む。 VRCAsyncGPUReadback.Request(Gpu.Texture(buf), 0, 32, 1, 32, 1, 0, 1, TextureFormat.RGBA32, (IUdonEventReceiver)this); }
// GPU の仕事が終わってから呼ばれる(頼んだその場では返らない)。 public override void OnAsyncGpuReadbackComplete(VRCAsyncGPUReadbackRequest request) { waiting = false; if (request.hasError) return; if (!request.TryGetData(cell, 0)) return; readout.text = "R " + cell[0] + " / G " + cell[1] + " / B " + cell[2]; // 0〜255 で届く }}注意
- 頼んだバッファは、答えが返るまでそのままにします。上書きすると届く中身が変わります(エラーにはなりません)。
- 受け皿の大きさと頼む形式が揃っていないと、エラーも警告も出ないまま受け皿の後ろが 0 のままになります。
学習済みモデル
Section titled “学習済みモデル”学習済みモデルを Udon で動かしたい
[Onnx("gesture.onnx")]
学習済みモデルを GPU で動かしたい
[OnnxGpu("filter.onnx")]
重い処理を何フレームかに分けて進めたい
async FrameTask M()
例
using UnityEngine;using Tsukimi;using TMPro;
// 得点表の並べ替え: 記録を高い順に並べ替える。1 フレームでやると止まって見えるので、// 1 周ごとに次のフレームへ分けて進め、終わったら上位 3 つを表に出す。//// 前提:// - このスクリプトは、得点表のオブジェクトに付ける。// - scores に記録が入っている。表を出す TextMeshProUGUI を board に渡す。// - 並べ替えを始めるときは Rank を呼ぶ。public class GoalsAsyncHeavySort : TsukimiBehaviour{ public int[] scores; public TextMeshProUGUI board;
// await の位置で中断して Udon へ戻り、続きは次のフレームから再開する。 // 呼び出した側は終わりを待たず、すぐ次の文へ進む。 private async FrameTask SortDescending() { board.text = "集計中…"; for (int i = 0; i < scores.Length - 1; i++) { int best = i; for (int j = i + 1; j < scores.Length; j++) if (scores[j] > scores[best]) best = j; int t = scores[i]; scores[i] = scores[best]; scores[best] = t; await Async.Frame(); // 1 周ぶん進めたら、残りは次のフレームへ } board.text = "1 位 " + scores[0] + "\n2 位 " + scores[1] + "\n3 位 " + scores[2]; }
public void Rank() { SortDescending(); }}注意
- 処理は並列には実行されません。フレームをまたいで少しずつ進むだけです。
何フレーム待つかを指定したい
await Async.Frames(30)
例
using UnityEngine;using Tsukimi;using TMPro;
// 競争のスタート: 触ると 3・2・1 と数えてから「GO」を出し、ゲートを開ける。//// 前提:// - このスクリプトは、スタートのボタン(Collider 付き)に付ける。// - 数字を出す TextMeshProUGUI を sign に、開けるゲートのオブジェクトを gate に渡す。public class GoalsAsyncCountdown : TsukimiBehaviour{ public TextMeshProUGUI sign; public GameObject gate;
private async FrameTask Countdown() { gate.SetActive(true); for (int n = 3; n >= 1; n--) { sign.text = n.ToString(); await Async.Frames(60); // 60 フレーム待つ(秒ではなくフレームで数える) } sign.text = "GO"; gate.SetActive(false); }
public override void Interact() { Countdown(); }}注意
- 待つ単位はフレームだけで、秒では待てません。フレームレートが違う人の画面では、待つ長さも違います。
同時に実行される本数を抑えたい
[MaxTasks(4)]
例
using UnityEngine;using Tsukimi;
// 打ち上げ花火: 触るたびに 1 発上げる。空に同時に出せるのは 4 発まで。//// 前提:// - このスクリプトは、発射台のオブジェクト(Collider 付き)に付ける。// - 花火に使う Light を 4 つ、lights に渡す(1 発ごとに 1 つを光らせて消す)。public class GoalsAsyncMaxTasks : TsukimiBehaviour{ public Light[] lights; private int next;
// 同じメソッドを 4 本まで同時に進められる。それぞれが自分の引数と途中の値を持つ。 // 4 本とも動いている間に呼んだ分は始まらない。 [MaxTasks(4)] private async FrameTask Burst(int slot) { Light l = lights[slot]; l.enabled = true; for (int f = 0; f < 30; f++) { l.intensity = 3f * (30 - f) / 30f; // 30 フレームかけて暗くする await Async.Frame(); } l.enabled = false; }
public override void Interact() { // 4 発とも上がっている最中なら始まらない。始まったときだけ、次の明かりへ進める。 FrameTask t = Burst(next); if (t.Started) next = (next + 1) % lights.Length; }}注意
- 本体は本数のぶん複製されるので、本数を増やすほどプログラムが大きくなります。
もう始まっているかを知りたい
FrameTask t = M(); if (!t.Started)
例
using UnityEngine;using Tsukimi;using TMPro;
// 連打しても重ならない扉: 開け閉めの動きの最中に触られたら、動かさずに案内だけ出す。//// 前提:// - このスクリプトは、扉のボタン(Collider 付き)に付ける。// - 動かす扉の Transform を door に、案内を出す TextMeshProUGUI を status に渡す。public class GoalsAsyncStarted : TsukimiBehaviour{ public Transform door; public TextMeshProUGUI status; private bool open;
// 本数を書いていないので、同時に進められるのは 1 本だけ。 private async FrameTask Swing() { float from = open ? 90f : 0f; float to = open ? 0f : 90f; for (int f = 1; f <= 45; f++) { door.localRotation = Quaternion.Euler(0f, from + (to - from) * f / 45f, 0f); await Async.Frame(); } open = !open; status.text = ""; }
public override void Interact() { FrameTask t = Swing(); // 前の動きがまだ終わっていないときは始まらず、Started が false になる。 if (!t.Started) status.text = "扉が動いている最中です"; }}注意
- 前の実行が終わっていない間の呼び出しは始まらず、
Startedは false になります。
長いループを分けて回したい
if (i % 256 == 255) await Async.Frame();
例
using UnityEngine;using Tsukimi;using TMPro;
// 陣取りの集計: 64×64 マスの盤面を数えるのを、256 マスごとに次のフレームへ分けて進める。// 1 フレームで全部数えると、その間ワールドが止まって見える。//// 前提:// - このスクリプトは、盤面を持つオブジェクトに付ける。// - cells には各マスの持ち主(0 は空き・1 は赤・2 は青)が入っている。長さは 4096。// - 結果を出す TextMeshProUGUI を result に渡す。数え始めるときは Tally を呼ぶ。public class GoalsAsyncLoopSplit : TsukimiBehaviour{ public int[] cells = new int[4096]; public TextMeshProUGUI result;
private async FrameTask Count() { int red = 0; int blue = 0; for (int i = 0; i < cells.Length; i++) { if (cells[i] == 1) red++; else if (cells[i] == 2) blue++; // 256 周ごとに中断する。i と途中の数は、中断をまたいで値を保つ。 if (i % 256 == 255) await Async.Frame(); } result.text = "赤 " + red + " / 青 " + blue; }
public void Tally() { Count(); }}跨ぐメソッドに値を渡したい
async FrameTask Fade(int frames, int target)
例
using UnityEngine;using Tsukimi;
// BGM の音量フェード: 範囲に入ると音量を上げ、出ると下げる。長さと目標の音量を引数で渡す。//// 前提:// - このスクリプトは、BGM を鳴らす範囲の Collider(Is Trigger)と同じオブジェクトに付ける。// - 鳴らしている AudioSource を bgm に渡す。public class GoalsAsyncArgs : TsukimiBehaviour{ public AudioSource bgm; private int latest;
// 引数 id・frames・target は、await のあとでも呼んだときの値のまま読める。 // 入ってすぐ出たときは 2 本が同時に進むので、新しいほうだけが音量を動かす。 [MaxTasks(2)] private async FrameTask Fade(int id, int frames, float target) { float start = bgm.volume; for (int f = 1; f <= frames; f++) { if (id != latest) return; // 後から呼ばれたフェードに譲る bgm.volume = start + (target - start) * f / frames; await Async.Frame(); } }
// 自分がこの範囲に入ったときと出たときに、自分の画面で音量を変える。 public override void OnPlayerTriggerEnter(VRC.SDKBase.VRCPlayerApi player) { if (!player.isLocal) return; latest++; Fade(latest, 60, 1f); }
public override void OnPlayerTriggerExit(VRC.SDKBase.VRCPlayerApi player) { if (!player.isLocal) return; latest++; Fade(latest, 120, 0f); }}別の跨ぐメソッドの終わりを待ちたい
await Fade(30)
例
using UnityEngine;using Tsukimi;
// 暗転して部屋を入れ替える: 触ると照明を落としきってから部屋を入れ替え、そのあと明るく戻す。//// 前提:// - このスクリプトは、切り替えのボタン(Collider 付き)に付ける。// - 部屋を照らす Light を roomLight に、入れ替える 2 つの部屋のオブジェクトを dayRoom と nightRoom に渡す。// - 入れ替えは触った人の画面でだけ起きる(同期はしていない)。public class GoalsAsyncAwaitOther : TsukimiBehaviour{ public Light roomLight; public GameObject dayRoom; public GameObject nightRoom;
private async FrameTask Fade(float from, float to) { for (int f = 1; f <= 30; f++) { roomLight.intensity = from + (to - from) * f / 30f; await Async.Frame(); } }
// await を付けて呼ぶと、相手が終わるまで待ってから次の文へ進む。 private async FrameTask Switch() { await Fade(1f, 0f); // 暗くなりきるまで待つ bool night = !nightRoom.activeSelf; dayRoom.SetActive(!night); nightRoom.SetActive(night); await Fade(0f, 1f); // 明るく戻す }
public override void Interact() { Switch(); }}イベントから跨ぐ処理を始めたい
async FrameTask Start()
例
using UnityEngine;using Tsukimi;
// 開場の演出: ワールドに入ると、通路の明かりが手前から順に 1 つずつ点いていく。//// 前提:// - このスクリプトは、通路の明かりを管理する空のオブジェクトに付ける。// - 点ける順に並べた明かりのオブジェクトを lamps に渡す(最初はどれも無効にしておく)。public class GoalsAsyncStart : TsukimiBehaviour{ public GameObject[] lamps;
// Start をフレームを跨ぐ形にできる。Udon はいつもどおり Start を呼び、 // 中断した続きは後のフレームから再開する。 private async FrameTask Start() { foreach (GameObject lamp in lamps) { lamp.SetActive(true); await Async.Frames(15); } }}注意
Updateのように毎フレーム呼ばれるイベントを跨がせると、2 回目以降は前の実行が終わっていないので始まりません。Interact()のように基底から継ぐイベントは、戻り値を変えられないので跨がせられません。
小さいメソッドの呼び出しの手間を無くしたい
[Inline]
例
using UnityEngine;using Tsukimi;
// 浮かぶ足場: たくさんの足場を、毎フレームそれぞれの位相でゆっくり上下させる。// 毎フレーム何十回も呼ぶ小さい計算を、呼び出しの場所へ展開して、呼び出しの手間を無くす。//// 前提:// - このスクリプトは、足場をまとめて動かす空のオブジェクトに付ける。// - 動かす足場の Transform を platforms に並べて渡す。public class GoalsInline : TsukimiBehaviour{ public Transform[] platforms; private Vector3[] home;
void Start() { home = new Vector3[platforms.Length]; for (int i = 0; i < platforms.Length; i++) home[i] = platforms[i].position; }
// 付けたメソッドは、大きさによらず呼び出しの場所へ展開される(メソッドとしては呼ばれない)。 // 展開したぶん、プログラムは大きくなる。小さくて何度も呼ぶものに付ける。 [Inline] private float Bob(float time, int index) { return Mathf.Sin(time * 1.5f + index * 0.7f) * 0.25f; }
void Update() { float t = Time.time; for (int i = 0; i < platforms.Length; i++) platforms[i].position = home[i] + new Vector3(0f, Bob(t, i), 0f); }}注意
- 付けたメソッドは呼び出しの場所へ展開されるので、呼ぶ場所が多いほどプログラムが大きくなります。
プロファイラ
Section titled “プロファイラ”どこが重いかを調べたい
Tsukimi Profiler
実際に何回実行されたかを測りたい
Measure
テストと解析
Section titled “テストと解析”壊れていないか、自分で試したい
[TsukimiTest]
例
using Tsukimi;
// 自動販売機: 硬貨を入れて、値段に届いていれば 1 本売る。足りなければ何もしない。//// 前提:// - このスクリプトは、自動販売機のオブジェクトに付ける。// - 硬貨を入れるボタンから Insert を、買うボタンから Buy を呼ぶ。public partial class GoalsTestVending : TsukimiBehaviour{ public int price = 120; public int coins; public int sold;
public void Insert(int amount) { coins = coins + amount; }
public void Buy() { if (coins < price) return; coins = coins - price; sold = sold + 1; }}using Tsukimi;
// 上の自動販売機を、Unity を起動せずに試す。テストは 1 本ずつ新しいインスタンスで実行される。public partial class GoalsTestVending{ // [TsukimiTest] を付けた、引数の無い public void のメソッドが 1 本のテストになる。 [TsukimiTest] public void お金が足りないと売れない() { Insert(100); Buy(); Assert.AreEqual(0, sold); Assert.AreEqual(100, coins); // 入れた硬貨はそのまま残る }
[TsukimiTest] public void 足りると1本売れておつりが残る() { Insert(100); Insert(50); Buy(); Assert.AreEqual(1, sold); Assert.AreEqual(30, coins); }}注意
Startなどの起動時のイベントは、テストでは自動で実行されません。必要なときはテストの本文でStart()と書きます。
テストをどのファイルに書けばよいか知りたい
名前.Tests.cs
例
using Tsukimi;
// 暗証番号の扉: 4 桁を押し終えたところで、番号が合っていれば開ける。押し終えたら入力は空に戻る。//// 前提:// - このスクリプトは、扉のテンキーに付ける。各キーから Press(数字) を呼ぶ。// - テストは隣の test-keypad.Tests.cs に書く(このファイルと同じ名前+.Tests.cs)。// - テスト側と同じクラスを続けて書くので、どちらにも partial を付ける。public partial class GoalsTestKeypad : TsukimiBehaviour{ public int code = 4271; public bool open; private int typed; private int count;
public void Press(int digit) { typed = typed * 10 + digit; count = count + 1; if (count < 4) return; open = typed == code; typed = 0; count = 0; }}using Tsukimi;
// 名前が「元のファイル名.Tests.cs」のファイルはコンパイルの対象から外れる。// テストがワールドのプログラムに混ざることはない。public partial class GoalsTestKeypad{ [TsukimiTest] public void 正しい番号で開く() { Press(4); Press(2); Press(7); Press(1); Assert.IsTrue(open); }
[TsukimiTest] public void 押し終えたら入力が空に戻る() { Press(1); Press(1); Press(1); Press(1); Assert.IsFalse(open); // 同じクラスの続きなので、private のフィールドにもそのまま触れる。 Assert.AreEqual(0, typed); Assert.AreEqual(0, count); }}注意
- テストを Behaviour の側のファイルに書くと、ワールドにアップロードされるプログラムに含まれます(警告
TUKI0117)。
条件が成り立っていることを確認したい
Assert.IsTrue(charge <= 100)
例
using Tsukimi;
// 充電台: 置くたびに電池を充電する。100 を超えないように止める。//// 前提:// - このスクリプトは、充電台のオブジェクトに付ける。電池を置いたら Charge(量) を呼ぶ。public partial class GoalsTestCharge : TsukimiBehaviour{ public int charge;
public void Charge(int amount) { charge = charge + amount; if (charge > 100) charge = 100; }}using Tsukimi;
public partial class GoalsTestCharge{ [TsukimiTest] public void 何度充電しても上限を超えない() { Charge(70); Charge(70); // 範囲の確認は、条件をそのまま書ける IsTrue / IsFalse で書く。 Assert.IsTrue(charge <= 100); Assert.IsFalse(charge < 0); }}注意
- 結果に出るのは真偽の値だけです。何と何を比べたかを残したいときは
Assert.AreEqualを使います。
2 つの値が同じであることを確認したい
Assert.AreEqual(1, count)
例
using Tsukimi;
// 連続ヒットの得点: 当てるたびに 10 点 × 連続数(3 まで)を足す。外すと連続数が 0 に戻る。//// 前提:// - このスクリプトは、的のオブジェクトに付ける。当たったら Hit、外れたら Miss を呼ぶ。public partial class GoalsTestCombo : TsukimiBehaviour{ public int score; private int combo;
public void Hit() { if (combo < 3) combo = combo + 1; score = score + 10 * combo; }
public void Miss() { combo = 0; }}using Tsukimi;
public partial class GoalsTestCombo{ [TsukimiTest] public void 連続数は3で止まる() { Hit(); Hit(); Hit(); Hit(); // 第 1 引数が期待した値、第 2 引数が実際の値。逆にすると結果の文面が逆になる。 Assert.AreEqual(10 + 20 + 30 + 30, score); }
[TsukimiTest] public void 外すと1からやり直し() { Hit(); Hit(); Miss(); Hit(); Assert.AreEqual(10 + 20 + 10, score); }}注意
floatを直に比べると、計算のたびに出る誤差までそのまま比べます。実数は、差が小さいことをAssert.IsTrueで確認します。
参照が空かどうかを確認したい
Assert.IsNull(current)
例
using Tsukimi;
// 競争の勝者: 最初にゴールした人の名前だけを残す。あとから来た人では書き換えない。//// 前提:// - このスクリプトは、ゴールのオブジェクトに付ける。ゴールした人がいたら Finish(名前) を呼ぶ。public partial class GoalsTestWinner : TsukimiBehaviour{ public string winner;
public void Finish(string name) { if (winner == null) winner = name; }}using Tsukimi;
public partial class GoalsTestWinner{ [TsukimiTest] public void 最初の人だけが残る() { // 参照が空かどうかは IsNull / IsNotNull で確認する。 Assert.IsNull(winner); Finish("Aoi"); Finish("Ren"); Assert.IsNotNull(winner); Assert.AreEqual("Aoi", winner); }}注意
- 破棄済みの Unity のオブジェクトは C# の
nullと違う状態を取りうるので、結果が直感と食い違うことがあります。
失敗したときに理由を残したい
Assert.IsTrue(charge > 0, "使い切っている")
例
using Tsukimi;
// 残り回数: 挑戦するたびに 1 つ減らす。0 のときは減らさず、挑戦もできない。//// 前提:// - このスクリプトは、ゲームの受付のオブジェクトに付ける。挑戦するときは TryPlay を呼ぶ。public partial class GoalsTestTries : TsukimiBehaviour{ public int left = 3; public int played;
public void TryPlay() { if (left <= 0) return; left = left - 1; played = played + 1; }}using Tsukimi;
public partial class GoalsTestTries{ [TsukimiTest] public void 回数を使い切ると挑戦できない() { TryPlay(); TryPlay(); TryPlay(); TryPlay(); // 末尾の文字列は、成り立たなかったときにそのまま結果に出る。 // 同じ型の値を並べて比べるときに、どれが外れたかが分かる。 Assert.AreEqual(0, left, "残り回数は 0 で止まるはず"); Assert.AreEqual(3, played, "4 回目は挑戦できないはず"); }}カーネルの計算結果を確認したい
static float NextHeight(float now, ...)
例
using UnityEngine;using Tsukimi;
// 冷めていく熱の模様: 毎フレーム、各セルの熱をまわりと混ぜて少しずつ冷ます(GPU で計算する)。// 混ぜ方の式は static のメソッドに切り出して、カーネルとテストの両方から呼ぶ。//// 前提:// - このスクリプトは、模様を映す板のオブジェクトに付ける。板の Renderer を display に渡す。public partial class GoalsTestKernel : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D heat; private GpuBuffer2D next;
// float・int・bool・Vector2 だけを受け取る static のメソッドは、テストからも呼べる。 public static float Cool(float self, float around) { return Mathf.Max(0f, self * 0.6f + around * 0.4f - 0.01f); }
[Kernel] static Color4 Step(KernelId id, GpuBuffer2D prev) { float around = (prev[id.Offset(1, 0)].R + prev[id.Offset(-1, 0)].R + prev[id.Offset(0, 1)].R + prev[id.Offset(0, -1)].R) * 0.25f; return new Color4(Cool(prev[id].R, around), 0f, 0f, 1f); }
void Start() { heat = Gpu.Buffer(64, 64); next = Gpu.Buffer(64, 64); }
void Update() { Gpu.Run(nameof(Step), next, heat); GpuBuffer2D t = heat; heat = next; next = t; Gpu.Show(heat, display); }}using UnityEngine;using Tsukimi;
// カーネル([Kernel])そのものはテストから呼べない。切り出した式を確かめる。public partial class GoalsTestKernel{ [TsukimiTest] public void 熱はまわりと混ざって少し冷める() { // 実数は計算の誤差があるので、AreEqual で直に比べず、差が小さいことを確かめる。 Assert.IsTrue(Mathf.Abs(Cool(1f, 0f) - 0.59f) < 0.0001f); Assert.IsTrue(Mathf.Abs(Cool(0.5f, 0.5f) - 0.49f) < 0.0001f); }
[TsukimiTest] public void 冷めきったら0より下がらない() { Assert.AreEqual(0f, Cool(0f, 0f)); }}注意
[Kernel]のメソッドと、Color4やKernelIdを受け取るメソッドは、テストから呼べません。- 切り出したメソッドを Behaviour 側からも呼ぶと、そのメソッドも Udon へ変換されるので命令数が増えます。
同期が正しく届くことを、1 人で確認したい
Mimic.Join()
例
using Tsukimi;using VRC.SDKBase;
// みんなの最高記録: 自分の記録が今の最高を超えたら、所有者になって書き換え、全員へ送る。//// 前提:// - このスクリプトは、記録板のオブジェクトに付ける。遊び終えたら Submit(得点) を呼ぶ。[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]public partial class GoalsTestSync : TsukimiBehaviour{ [UdonSynced] public int best;
public void Submit(int score) { if (score <= best) return; Networking.SetOwner(Networking.LocalPlayer, gameObject); best = score; RequestSerialization(); }}using Tsukimi;using VRC.SDKBase;
// 1 本のテストの中に 2 人を置いて、送った値が相手に届くまでを確かめる。public partial class GoalsTestSync{ [TsukimiTest] public void ほかの人の最高記録が届く() { VRCPlayerApi me = Mimic.Join(); // 最初の 1 人が自分 VRCPlayerApi other = Mimic.Join(); Mimic.Become(other); // ここからは other の画面で実行する Submit(50); Mimic.Become(me); Assert.AreEqual(0, best); // 送っただけでは、まだ届いていない Mimic.Deliver(); // 送った人以外の全員に届く Assert.AreEqual(50, best); }}注意
Mimicは通信の遅れや送信の頻度を再現しません。確認できるのは、書いた手順どおりに実行した結果までです。SendCustomNetworkEventは記録されるだけで、相手には届きません。
受け取る順番が入れ替わっても壊れないことを確認したい
Mimic.Explore()
例
using Tsukimi;using VRC.SDKBase;
// ラウンドの表示: ラウンドの番号と、そのラウンドの残り時間を同期し、受け取ったら表示の文字を作る。//// 前提:// - このスクリプトは、進行役のオブジェクトに付ける。次のラウンドへ進めるときは NextRound を呼ぶ。[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]public partial class GoalsTestExplore : TsukimiBehaviour{ [UdonSynced] public int round; [UdonSynced] public int seconds; public string label = "";
public void NextRound() { Networking.SetOwner(Networking.LocalPlayer, gameObject); round = round + 1; seconds = 60; RequestSerialization(); Show(); }
// 受け取ったときに呼ばれる。2 つの値はどちらも書き終わっている。 public override void OnDeserialization() { Show(); }
private void Show() { label = "Round " + round + " / " + seconds + "s"; }}using Tsukimi;using VRC.SDKBase;
public partial class GoalsTestExplore{ [TsukimiTest] public void どの順で届いても表示が揃う() { // テストの最初の文に書く。2 つの同期する値がどの順で書き込まれるかを全部入れ替えて、 // それぞれの順でこのテストを実行する。 Mimic.Explore(); VRCPlayerApi me = Mimic.Join(); VRCPlayerApi host = Mimic.Join(); Mimic.Become(host); NextRound(); Mimic.Become(me); Mimic.Deliver(); Assert.AreEqual("Round 1 / 60s", label); }}注意
Mimic.Explore()はテストの最初の文に書きます。入れ替えるのは、このツールが知っている箇所だけです。
別の Behaviour と繋いだ状態で試したい
public Lamp lamp;
テストが重くなりすぎていないか見張りたい
[CostLimit(steps: 544)]
例
using Tsukimi;
// 盤面のリセット: 8×8 の盤面を全部空に戻す。遊びのたびに呼ぶので、重くなっていないかを見張りたい。//// 前提:// - このスクリプトは、盤面を持つオブジェクトに付ける。cells の長さは 64。// - 新しい遊びを始めるときに Clear を呼ぶ。public partial class GoalsTestCostLimit : TsukimiBehaviour{ public int[] cells = new int[64]; public int stones;
public void Clear() { for (int i = 0; i < cells.Length; i++) cells[i] = 0; stones = 0; }}using Tsukimi;
public partial class GoalsTestCostLimit{ // 上限を超えたら、Assert が全部成り立っていてもこのテストは失敗になる。 // 数は、上限を書かずに 1 度実行して、結果に付いた steps を写したもの。 [TsukimiTest] [CostLimit(steps: 1812)] public void リセットは重くなっていない() { cells[5] = 2; stones = 1; Clear(); Assert.AreEqual(0, cells[5]); Assert.AreEqual(0, stones); }}注意
- 上限を超えると、
Assertが全部成り立っていてもそのテストは失敗になります。
テストを実行したい
Tsukimi Tests
結果をファイルで受け取りたい
Library/Tsukimi/last-test-run.json
出たエラーの意味を機械へ聞きたい
diagnostics
U# への書き出し
Section titled “U# への書き出し”U# へ出す
Section titled “U# へ出す”U# のプロジェクトへ出したい
Export as UdonSharp
出したコードがどうなるか知りたい
UdonSharp