Skip to content

Features UdonSharp does not have

A list of the ways of writing and the tools this language has that UdonSharp does not. The full list is in Finding it by what you want to do.

I want a change to take effect on its own [Reactive]

Example
using UnityEngine;
using Tsukimi;
using TMPro;
// Ammo display: each shot lowers the count, and the display and the gun's color catch up on their own.
//
// Setup:
// - Put this script on the object you touch to shoot (it needs a Collider).
// - Pass the TextMeshProUGUI that shows the count to ammoText, and the Renderer whose color changes to body.
// - The count changes only on the screen of the player who touched it (it is not synced).
public class GoalsReactiveField : TsukimiBehaviour
{
public TextMeshProUGUI ammoText;
public Renderer body;
// A field tracked for changes. Make it private.
[Reactive] private int ammo = 6;
// Runs whenever ammo changes. It also runs once at startup, so the display is right from the start.
// It is not called from anywhere.
[Effect]
private void ShowAmmo()
{
ammoText.text = ammo + " / 6";
body.material.color = ammo > 0 ? Color.white : Color.red;
}
// Each touch fires one shot. At 0, it reloads to 6.
public override void Interact()
{
if (ammo > 0) ammo = ammo - 1;
else ammo = 6;
}
}

Note

  • On a synced field, values that arrive over the network do not run the effect (warning TUKI0115).
  • Writing back, inside an effect, to a value that the effect reads is a circular dependency and a compile error.

I want a value derived from other values [Computed]

Example
using UnityEngine;
using Tsukimi;
// Two-key door: the door disappears and lets you through only when both the left and right switches are on.
//
// Setup:
// - Put this script on an empty object that manages the door.
// - Pass the door object to door.
// - Call ToggleLeft / ToggleRight from the left and right switches
// (for example, with SendCustomEvent from each switch's Interact).
public class GoalsReactiveComputed : TsukimiBehaviour
{
public GameObject door;
[Reactive] private bool left;
[Reactive] private bool right;
// A value derived from left and right. It is recalculated when either one changes.
[Computed] private bool Open => left && right;
// It reads only Open. If only left is on, Open stays false, so this does not run.
[Effect]
private void ApplyDoor()
{
door.SetActive(!Open);
}
public void ToggleLeft() { left = !left; }
public void ToggleRight() { right = !right; }
}

I want something to happen only at the moment it changes [On(nameof(charge))]

Example
using UnityEngine;
using Tsukimi;
// Score chime: plays a sound only at the moment the score goes up. Not when it goes down, and not at startup.
//
// Setup:
// - Put this script on the object that counts the score.
// - Pass the AudioSource to play to chime.
// - Call AddPoint to add a point, and ResetScore to start over.
public class GoalsReactiveOn : TsukimiBehaviour
{
public AudioSource chime;
[Reactive] private int score;
// Runs at the moment score changes. It does not run at startup.
// The argument holds the value from just before the change.
[On(nameof(score))]
private void OnScoreChanged(int before)
{
if (score > before) chime.Play();
}
public void AddPoint() { score = score + 1; }
public void ResetScore() { score = 0; }
}

Note

  • It does not run at startup. To match the look right from startup, use [Effect].

I want to write the dependencies myself [Effect(nameof(width), nameof(height))]

Example
using UnityEngine;
using Tsukimi;
// Light dimmer: sets the light again only when the brightness or warmth changes.
// Another value on the same Behaviour (the blink count) changing does not touch the light.
//
// Setup:
// - Put this script on the panel that controls the light.
// - Pass the Light to set to lamp.
// - Call Brighter / Warmer / Blink from the panel's buttons.
public class GoalsReactiveEffectDeps : TsukimiBehaviour
{
public Light lamp;
[Reactive] private float brightness = 1f;
[Reactive] private float warmth;
[Reactive] private int blinkCount;
// It reads only the two names in the parentheses. A change to blinkCount does not run it.
// It also runs once at startup.
[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; }
}

I want to know how a change is detected Equals

Example
using UnityEngine;
using Tsukimi;
// Marker: even when the same position is written every frame, the marker moves and logs only when the position really changes.
//
// Setup:
// - Put this script on the object that manages the marker.
// - Pass the object to follow to target, and the marker to move to marker.
public class GoalsReactiveEquals : TsukimiBehaviour
{
public Transform target;
public Transform marker;
[Reactive] private Vector3 spot;
// Runs only when spot "changes". Vector3 is compared with its typed Equals, so
// writing the same value again does not run it, and even a tiny difference counts as a change (it does not swallow error the way == does).
[Effect]
private void MoveMarker()
{
marker.position = spot;
Debug.Log("Moved the marker to " + spot);
}
// Every frame, write the target's position. While the target is still, the effect above does not run.
private void Update()
{
spot = target.position;
}
}

Note

  • Quaternion and Color treat NaN as equal to NaN. Other value types keep counting as changed once NaN gets in.

I want to set the colour of a surface without writing a shader [Surface] static Color4 M(SurfaceId id, ...)

Example
using UnityEngine;
using Tsukimi;
// Health gauge: paints the board green from the left for the fraction that remains, and dark gray for the rest.
// No shader file is written. The code that decides the color is written in C#.
//
// Setup:
// - Put this script on the object that manages the gauge.
// - Pass the Renderer of the board used as the gauge (a Quad, for example) to gauge.
// - Put the remaining fraction (0 to 1) in hp. Call Hit when taking damage.
public class GoalsSurfaceColor : TsukimiBehaviour
{
public Renderer gauge;
public float hp = 1f;
// Called for each pixel of the surface, and the returned color appears on that pixel. id.UV is the position on the surface (0 to 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);
}
// Values are passed on every Gpu.Show call. It is called every frame to repaint with the current hp.
void Update()
{
Gpu.Show(nameof(Bar), gauge, hp);
}
public void Hit()
{
hp = Mathf.Max(0f, hp - 0.1f);
}
}

Note

  • This chapter is experimental. The way to write it may change.
  • Gpu.Show uses the values as they were when it was called. After changing a value, call it again.

I want shading that follows the direction the surface faces Vector3.Dot(id.Normal, toLight)

Example
using UnityEngine;
using Tsukimi;
// Toon-style statue: splits how the light falls into 3 steps, and draws with only 3 colors: lit, middle, and shadow.
//
// Setup:
// - Put this script on the statue object. Pass the statue's Renderer to statue.
// - baseColor (the color of the lit side) sets the color. The shadow side is painted with the same color darkened.
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)
{
// Choose the light direction yourself (lights placed in the scene cannot be read).
Vector3 toLight = new Vector3(0.4f, 1f, 0.3f).normalized;
// id.Normal is the direction the surface faces (world space, length 1). The closer it is to the light direction, the closer this is to 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);
}
}

Note

  • Lights placed in the scene cannot be read. Choose the light direction yourself. Shadows are not received either.

I want the look to change with the viewing direction Vector3.Dot(id.Normal, id.ViewDir)

Example
using UnityEngine;
using Tsukimi;
// Ghost rim light: the closer a spot is to the outline as seen by the viewer (where the surface faces sideways), the more it glows pale blue.
//
// Setup:
// - Put this script on the ghost object. Pass the ghost's Renderer to ghost.
// - glow sets the strength of the glow (0 means no glow).
public class GoalsSurfaceRim : TsukimiBehaviour
{
public Renderer ghost;
public float glow = 1f;
[Surface]
static Color4 Rim(SurfaceId id, float glow)
{
// id.ViewDir is the direction from that pixel toward the viewer.
// Where the surface faces the viewer the dot product is close to 1, and at the outline it is close to 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);
}
}

I want to pass a value into a surface static Color4 M(SurfaceId id, GpuBuffer2D buf, int n, bool b, Vector2 v, Vector3 w)

Example
using UnityEngine;
using Tsukimi;
// Flowing striped floor: the number of stripes, the color, and the speed are passed from the Behaviour, and the stripes flow over time.
//
// Setup:
// - Put this script on the floor object. Pass the floor's Renderer to floor.
// - stripes sets the number of stripes, color the color, and speed how fast they flow sideways (in UV units per second).
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;
// Besides float, the arguments can be int, bool, Vector2, Vector3, Vector4, and 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);
}
// Values are passed on every call. Order the Gpu.Show arguments the same as this method's arguments (after id).
void Update()
{
Gpu.Show(nameof(Stripes), floor, stripes, color, speed * Time.time);
}
}

Note

  • Pass the values to Gpu.Show in the same positions as the method’s arguments (after id).

I want to move the vertices themselves [Surface] static Vector3 M(VertexId v, ...)

Example
using UnityEngine;
using Tsukimi;
// Flag in the wind: makes the flag's vertices wave over time. The side attached to the pole (UV x of 0) does not move.
//
// Setup:
// - Put this script on the flag object. Pass the Renderer of the flag mesh (a finely divided Plane, for example) to flag.
// - amplitude sets how far it sways (in mesh units).
public class GoalsSurfaceVertex : TsukimiBehaviour
{
public Renderer flag;
public float amplitude = 0.2f;
// Called for each vertex, and moves the vertex to the returned position. v.Position and the return value are in mesh space.
[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);
}
// Also write the color side with the same name (the two make one shader).
[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);
}
}

Note

  • Even when vertices move, the id.Normal read on the color side keeps the original direction.
  • Writing only the position side is an error. Also write the color side with the same name.

I want to start writing a kernel return new Color4(...)

Example
using UnityEngine;
using Tsukimi;
// Color-cycling sign: paints the whole board with a color that slowly shifts through red, green, and blue over time (computed on the GPU).
//
// Setup:
// - Put this script on the board used as the sign. Pass the board's Renderer to display.
public class GoalsGpuReturn : TsukimiBehaviour
{
public Renderer display;
private GpuBuffer2D board;
void Start()
{
board = Gpu.Buffer(64, 64); // 64 cells wide × 64 cells tall
}
// A method with [Kernel] runs on the GPU once for each cell.
// The returned color is written to that cell (this is the only way to write).
[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); // run Paint on every cell of board
Gpu.Show(board, display); // show the result on the board
}
}

Note

  • The only way to write is the kernel’s return value. Assigning to another cell is an error (CS0200).

I want a short kernel on one line static Color4 Step(...) => prev[id] * 0.5f

Example
using UnityEngine;
using Tsukimi;
// Afterimage: each touch makes the board flash white, then it darkens a little every frame until it fades out.
//
// Setup:
// - Put this script on the board object (it needs a Collider). Pass the board's Renderer to 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);
}
// If the body is a single expression, it can be written with =>. Multiplies the previous frame's value by 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); // read current and write next
Gpu.Swap(ref current, ref next); // next frame, read the one just written
Gpu.Show(current, display);
}
}

I want to read the cell this kernel is computing prev[id]

Example
using UnityEngine;
using Tsukimi;
// Fading footprints: each touch brightens the board, then the brightness drops by a fixed amount every frame and stops at 0.
//
// Setup:
// - Put this script on the floor board object (it needs a Collider). Pass the board's Renderer to 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] reads the previous frame's value of the cell being computed.
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);
}
}

Note

  • In the default buffer, components are rounded to 8 bits (256 steps). For finer values, create it with GpuFormat.Half or pack with Gpu.Pack16x2.

I want the components of a colour one at a time c.R c.G c.B c.A

Example
using UnityEngine;
using Tsukimi;
// Black-and-white security camera: shows the camera image as black and white, brightness only.
//
// Setup:
// - Put this script on the monitor board object. Pass the board's Renderer to display.
// - Pass the RenderTexture that the security camera (Camera) renders to, to 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, and c.A take out one component at a time. Weighted to match the human eye to get the brightness.
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); // copy the camera image into the buffer
Gpu.Run(nameof(Gray), gray, camera);
Gpu.Show(gray, display);
}
}

I want to build the colour I return from its components new Color4(r, g, b, a)

Example
using UnityEngine;
using Tsukimi;
// Color-swapping mirror: swaps red and blue in the reflected image, showing it in otherworldly colors.
//
// Setup:
// - Put this script on the mirror board object. Pass the board's Renderer to display.
// - Pass the RenderTexture of the camera that the mirror shows, to 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(red, green, blue, opacity) builds the returned color from its components.
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);
}
}

I want to use black or white as they are Color4.White

Example
using UnityEngine;
using Tsukimi;
// Shadow play: shows the camera image in just 2 colors, white where it is bright and black where it is dark.
//
// Setup:
// - Put this script on the screen board object. Pass the board's Renderer to display.
// - Pass the RenderTexture of the camera to show to feed, and the brightness (0 to 1) where white turns to black to 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 and Color4.Black return white and black as they are.
return y > threshold ? Color4.White : Color4.Black;
}
void Update()
{
Gpu.Load(camera, feed);
Gpu.Run(nameof(Silhouette), shadow, camera, threshold);
Gpu.Show(shadow, display);
}
}

I want the pattern to change with the position of the cell id.X id.Y

Example
using UnityEngine;
using Tsukimi;
// Checkerboard floor: from each cell's position, builds a pattern where white and gray swap every 8 cells.
//
// Setup:
// - Put this script on the floor board object. Pass the board's Renderer to display.
public class GoalsGpuCellPosition : TsukimiBehaviour
{
public Renderer display;
private GpuBuffer2D floor;
void Start()
{
floor = Gpu.Buffer(64, 64);
Gpu.Run(nameof(Checker), floor); // the pattern never changes, so build it once at the start
}
[Kernel]
static Color4 Checker(KernelId id)
{
// id.X and id.Y are the horizontal and vertical position of the cell being computed (0, 0 is the bottom left).
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);
}
}

I want to look at a neighbouring cell prev[id.Offset(1, -1)]

Example
using UnityEngine;
using Tsukimi;
// Outline drawing: in the camera image, only where brightness differs a lot from the cells above, below, left, and right becomes a white line.
//
// Setup:
// - Put this script on the screen board object. Pass the board's Renderer to display.
// - Pass the RenderTexture of the camera to show to 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(x, y) points at a cell relative to the current cell.
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);
}
}

I want to know what a read outside the buffer gives at the edge prev[id.Offset(-1000, -1000)]

Example
using UnityEngine;
using Tsukimi;
// Paint flowing right: every frame, shifts the whole picture right by 1 cell. The color of the edge cell keeps flowing in at the left edge.
//
// Setup:
// - Put this script on the board object (it needs a Collider). Pass the board's Renderer to display.
// - Pass the picture to flow to palette (it is copied once at the start). Touching it restores the first picture.
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)
{
// Reads the cell to the left. At the left edge (id.X is 0) this points outside the buffer (-1),
// but an index outside is clamped to the edge, so the left edge cell's own color comes back. No range check is needed.
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);
}
}

Note

  • To wrap around to the opposite side at the edge, use Wrap.

I want to tile a small pattern across the buffer prev.Wrap(id.Offset(1, 0))

Example
using UnityEngine;
using Tsukimi;
// Tiled floor: repeats a small 16×16 tile picture across the whole 128×128 floor.
//
// Setup:
// - Put this script on the floor board object. Pass the board's Renderer to display.
// - Pass the picture of one tile (16×16) to 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)
{
// Reads the small tile at the floor cell's position. Wrap wraps around to the opposite side when pointing outside,
// so position (20, 3) becomes (4, 3) on the tile, and the tiles line up repeatedly.
return tile.Wrap(id);
}
void Update()
{
Gpu.Show(floor, display);
}
}

Note

  • In a buffer 64 wide, pointing at -1 returns cell 63.

I want no square blocks to show when it is scaled up prev.Smooth(position)

Example
using UnityEngine;
using Tsukimi;
// Enlarging a temperature map: stretches a coarse 16×16 temperature map onto a 256×256 board without visible square blocks.
//
// Setup:
// - Put this script on the map board object. Pass the board's Renderer to display.
// - Pass the coarse map picture (16×16) to 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); // the map never changes, so stretch it once at the start
}
[Kernel]
static Color4 Enlarge(KernelId id, GpuBuffer2D map)
{
// Smooth returns the value at a position (0 to 1), blended with the surrounding cells.
// Converts the larger buffer's cell position to 0 to 1 and points into the smaller map.
Vector2 position = new Vector2(id.X / 256f, id.Y / 256f);
return map.Smooth(position);
}
void Update()
{
Gpu.Show(big, display);
}
}

Note

  • It reads 4 cells, so it costs more than reading 1.

I want a different value in each cell Gpu.Random01(n)

Example
using UnityEngine;
using Tsukimi;
// TV static: paints each cell with a scattered brightness that changes every frame.
//
// Setup:
// - Put this script on the TV screen board object. Pass the board's Renderer to 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 always returns the same value (0 to 1) for the same input.
// Passing a number mixed from the cell position and the frame number gives a different value per cell and per frame.
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);
}
}

Note

  • The same input always returns the same value. To change it every frame, mix the frame number into the input.

I want a smooth pattern Gpu.Noise(v)

Example
using UnityEngine;
using Tsukimi;
// Drifting clouds: builds a mottled cloud pattern from smooth noise and moves it sideways over time.
//
// Setup:
// - Put this script on the sky board object. Pass the board's Renderer to 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 is smooth noise (0 to 1) where nearby positions give nearby values.
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);
}
}

I want the size of the destination buffer inside a kernel Gpu.OutWidth

Example
using UnityEngine;
using Tsukimi;
// Sunset gradient: paints a sky that turns from orange to navy from the bottom of the board to the top. It looks the same whatever size the buffer is.
//
// Setup:
// - Put this script on the background board object. Pass the board's Renderer to display.
// - width and height set the buffer size.
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 is the number of cells vertically in the buffer being written.
// Using it to convert the position to 0 to 1 means the size does not have to be passed as an argument.
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);
}
}

I want two fine-grained values in one cell Gpu.Pack16x2(v)

Example
using UnityEngine;
using Tsukimi;
// Slowly moving particles: keeps each particle's position (x and y) finely packed in one cell and moves it very slightly every frame.
// At 8 bits (256 steps), such small moves would be rounded away and the particles would stop.
//
// Setup:
// - Put this script on the object that holds the particles.
// - There are 64×64 particles (one cell per particle). To see the positions, show positions or read them back.
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 takes out the 2 packed values (65536 steps each).
Vector2 p = Gpu.Unpack16x2(prev[id]);
p.x = Mathf.Repeat(p.x + 0.0002f, 1f); // 0.0002 to the right per frame
// Pack16x2 packs 2 values into one cell for writing (it can only be used inside a kernel).
return Gpu.Pack16x2(p);
}
void Update()
{
Gpu.Run(nameof(Drift), next, positions);
Gpu.Swap(ref positions, ref next);
Gpu.Show(positions, display);
}
}

Note

  • Gpu.Pack16x2 can only be used inside a kernel. It is not used with buffers created with GpuFormat.Half.

I want a colour between two colours Color4.Lerp(c, c2, h)

Example
using UnityEngine;
using Tsukimi;
// Heat map coloring: paints temperature (0 to 1) with colors from cold blue to hot red.
//
// Setup:
// - Put this script on the map board object. Pass the board's Renderer to display.
// - Pass the temperature picture (the red component is the temperature) to 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) is a when t is 0, b when t is 1, and in between it mixes them in that proportion.
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);
}
}

I want to put a repeated calculation in a function outside the kernel Falloff(d)

Example
using UnityEngine;
using Tsukimi;
// Spotlight ring: draws a ring of light that darkens with distance from the board's center, using the same falloff in 2 kernels.
// The falloff formula is gathered into one function, called from both kernels.
//
// Setup:
// - Put this script on the floor board object. Pass the board's Renderer to display.
// - warm switches the light color (true for warm).
public class GoalsGpuHelper : TsukimiBehaviour
{
public Renderer display;
public bool warm = true;
private GpuBuffer2D floor;
void Start()
{
floor = Gpu.Buffer(128, 128);
}
// A helper function called from kernels. It does not get [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);
}
}

I want to make a buffer to compute into Gpu.Buffer(64, 64)

Example
using UnityEngine;
using Tsukimi;
// Game of Life: advances generations of live cells (white) and dead cells (black) on 128×128 cells every frame.
//
// Setup:
// - Put this script on the board object (it needs a Collider). Pass the board's Renderer to display.
// - Touching it starts over from a random layout.
public class GoalsGpuHostBuffer : TsukimiBehaviour
{
public Renderer display;
private GpuBuffer2D current;
private GpuBuffer2D next;
private int seed;
void Start()
{
// A buffer for computing on the GPU. Created with the number of cells across and down (4 components per cell).
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);
}
}

I want values outside 0..1, or finer steps Gpu.Buffer(64, 64, GpuFormat.Half)

Example
using UnityEngine;
using Tsukimi;
// Water ripples: each touch drops a droplet in the center and computes the waves that spread and bounce back.
// Wave height can go negative, so the buffers are created in a format that can hold values outside 0 to 1.
//
// Setup:
// - Put this script on the water surface board object (it needs a Collider). Pass the board's Renderer to display.
public class GoalsGpuHostFormat : TsukimiBehaviour
{
public Renderer display;
private GpuBuffer2D before;
private GpuBuffer2D now;
private GpuBuffer2D next;
private GpuBuffer2D shown;
void Start()
{
// Created with GpuFormat.Half, each component is a 16-bit float and can hold negative values and values above 1.
// The format must be fixed when written (it cannot be passed in a variable).
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); // map -1 to 1 onto 0 to 1 for display
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);
}
}

Note

  • Values above 65504 stay at 65504. No error or warning appears.
  • Copying an 8-bit image with Gpu.Load crushes values outside 0 to 1 before they arrive.

I want to send an image, a video or a camera feed to the GPU Gpu.Load(current, source)

Example
using UnityEngine;
using Tsukimi;
// Frosted glass video: passes the video player's image to the GPU, blurs it, then shows it.
//
// Setup:
// - Put this script on the frosted glass board object. Pass the board's Renderer to display.
// - Pass the RenderTexture that the video player outputs to, to 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()
{
// Copies an image, video, or camera image into the buffer. If the size differs, it is scaled to fit.
Gpu.Load(frame, video);
Gpu.Run(nameof(Blur), blurred, frame);
Gpu.Show(blurred, display);
}
}

Note

  • To use an image as numbers rather than colors, turn off sRGB in that image’s import settings.

I want the last frame’s result to be the next frame’s input Gpu.Swap(ref a, ref b)

Example
using UnityEngine;
using Tsukimi;
// Campfire flames: blows heat in at the bottom row, and every frame reads "the previous frame's result" to raise it upward while it cools.
//
// Setup:
// - Put this script on the flame board object. Pass the board's Renderer to 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);
// Swaps the source and destination. Next frame, read the one just written.
Gpu.Swap(ref current, ref next);
Gpu.Run(nameof(Colorize), shown, current);
Gpu.Show(shown, display);
}
}

I want to run the kernel I wrote, once Gpu.Run(nameof(Step), next, current)

Example
using UnityEngine;
using Tsukimi;
// Photo negative: each touch inverts the photo's light and dark just once (nothing is computed every frame).
//
// Setup:
// - Put this script on the photo board object (it needs a Collider). Pass the board's Renderer to display.
// - Pass the photo texture to 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()
{
// Pass the destination first and then the source. For that one call, Invert runs on every cell.
Gpu.Run(nameof(Invert), next, current);
Gpu.Swap(ref current, ref next);
Gpu.Show(current, display);
}
}

I want to pass a value that changes every frame, such as time, into a kernel Gpu.Run(nameof(Step), next, current, phase)

Example
using UnityEngine;
using Tsukimi;
// Pulsing ring of light: passes time and a radius to the kernel and draws rings that spread from the center and fade.
//
// Setup:
// - Put this script on the floor board object. Pass the board's Renderer to display.
// - radius sets the largest radius of the ring (in cells).
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()
{
// The values listed after the destination go, in order, to the kernel's second and later arguments.
Gpu.Run(nameof(Ring), floor, Time.time, radius);
Gpu.Show(floor, display);
}
}

I want to pick the kernel to run by its name as a string Gpu.Run("Step", next, current)

Example
using UnityEngine;
using Tsukimi;
// Switching filters: each button press switches, in order, the filter applied to the camera image.
//
// Setup:
// - Put this script on the switch button (it needs a Collider). Pass the Renderer of the board to show on to display.
// - Pass the RenderTexture the camera renders to, to feed.
public class GoalsGpuHostString : TsukimiBehaviour
{
public Renderer display;
public Texture feed;
private GpuBuffer2D camera;
private GpuBuffer2D filtered;
private int current; // 0: as is 1: sepia 2: night vision
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);
// A kernel can also be pointed at by a name string. Write the name as a constant string (a name built from a variable or an expression cannot be written).
// A misspelled name is rejected at compile time. Written with nameof, it is also caught on the C# side.
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);
}
}

Note

  • With nameof, a misspelling is caught on the C# side.
  • Write the name as a constant string or with nameof. A name built from a variable or an expression cannot be written.

I want more than one kernel in the same behaviour Gpu.Run(nameof(Fade), current, next)

Example
using UnityEngine;
using Tsukimi;
using VRC.SDKBase;
// Drawing board: while you are in front of the board, draws dots with a pen at your hand's position, and the erase button clears everything.
// A kernel that draws and a kernel that erases sit in one Behaviour and are used as needed.
//
// Setup:
// - Put this script on the board object (it needs a Collider; a 1×1 Quad).
// - Pass the board's Renderer to display. Touching it clears everything.
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()
{
// Converts the right index finger's position into a position on the board (cell coordinates).
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);
}
}

I want to pass many numbers, such as weights, into a kernel Gpu.Run(nameof(Blend), next, current, weights)

Example
using UnityEngine;
using Tsukimi;
// Pixel-art palette: splits the photo's brightness into 16 levels and replaces each with a color from a table (palette).
//
// Setup:
// - Put this script on the frame board object. Pass the board's Renderer to display and the photo to photo.
// - palette holds 16 colors (x, y, z are red, green, blue). If its length is not 16, the run does nothing.
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); // read it small to look like pixel art
dotted = Gpu.Buffer(64, 64);
Gpu.Load(src, photo);
}
// For a table argument, write its size like [Capacity(16)] (a power of 2, up to 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);
}
}

Note

  • If the length of the array passed differs from the number written in [Capacity], the run does nothing.
  • The total size of the tables one kernel takes is at most 4000.

I want to set the size of the table a kernel takes [Capacity(4)] Vector4[] w

Example
using UnityEngine;
using Tsukimi;
// Changing sky colors: passes just 4 colors — morning, noon, evening, night — in a table and builds the sky color for the time of day.
//
// Setup:
// - Put this script on the sky board object. Pass the board's Renderer to display.
// - dayLength sets the length of one day (in seconds).
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); // morning
colors[1] = new Vector4(0.4f, 0.7f, 1f, 1f); // noon
colors[2] = new Vector4(1f, 0.45f, 0.2f, 1f); // evening
colors[3] = new Vector4(0.05f, 0.05f, 0.2f, 1f); // night
}
// A small table of just 4 is passed the same way.
[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);
}
}

I want to write a starting state without reading from a buffer Gpu.Run(nameof(Seed), board, 0.25f)

Example
using UnityEngine;
using Tsukimi;
// Initial state of a snowfield: at startup, writes the bumpy snow height just once. No source buffer is needed.
//
// Setup:
// - Put this script on the snowfield board object. Pass the board's Renderer to display.
// - bumpiness sets how bumpy it is.
public class GoalsGpuHostNoBuffer : TsukimiBehaviour
{
public Renderer display;
public float bumpiness = 0.3f;
private GpuBuffer2D snow;
void Start()
{
snow = Gpu.Buffer(128, 128);
// Runs without a source. The cells handled are decided by the destination (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);
}
}

I want the sum or the average of every cell Gpu.Reduce(nameof(Brighter), middle, full)

Example
using UnityEngine;
using Tsukimi;
// Automatic brightness: finds the average brightness of the camera image, and brightens it when dark and darkens it when bright.
// Adds up the brightness of every cell into one cell, then divides by the number of cells for the average.
//
// Setup:
// - Put this script on the monitor board object. Pass the board's Renderer to display.
// - Pass the RenderTexture the camera renders to, to 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);
// The sum goes above 1, so use a format that can hold values outside 0 to 1.
middle = Gpu.Buffer(8, 8, GpuFormat.Half);
total = Gpu.Buffer(1, 1, GpuFormat.Half);
corrected = Gpu.Buffer(64, 64);
}
// How to combine 2 values into 1. It gets [Reduce]. Here they are added.
[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);
// Each destination cell gets the combined value of the range of the source that cell covers.
// You lay out the stages yourself (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);
}
}

Note

  • You lay out the stages yourself. The number of calls you write is the number of runs.

I want the maximum or minimum of each component Gpu.Max(peak, full)

Example
using UnityEngine;
using Tsukimi;
// Visibility in a dark room: finds the brightest and darkest values in the camera image and stretches the image to fill that range.
//
// Setup:
// - Put this script on the monitor board object. Pass the board's Renderer to display.
// - Pass the RenderTexture the camera renders to, to 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);
// For each component, gathers the maximum and minimum of all cells into one cell. Use a separate destination buffer for each call.
Gpu.Max(peak, full);
Gpu.Min(floor, full);
Gpu.Run(nameof(Stretch), stretched, full, peak, floor);
Gpu.Show(stretched, display);
}
}

Note

  • Calling Gpu.Max and then Gpu.Min on the same buffer makes the later call overwrite the earlier result.

I want to show the result on an object Gpu.Show(current, display)

Example
using UnityEngine;
using Tsukimi;
// Neon tube with running light: computes a pattern of light that flows along the tube and shows it directly as the tube's look.
//
// Setup:
// - Put this script on the neon tube object. Pass the tube's Renderer to tube.
// - The tube mesh's UV must run along the tube's length horizontally (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);
// Shows the computed buffer directly as that Renderer's look.
Gpu.Show(lights, tube);
}
}

I want to pass the result to a Unity or VRChat API Gpu.Texture(current)

Example
using UnityEngine;
using Tsukimi;
// Glowing patterned clothes: passes the computed pattern as the emission picture of another object's material.
//
// Setup:
// - Put this script on the object that manages the pattern.
// - Pass the Renderer of the object to light up to target. Its material is Standard with Emission turned on.
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 takes the buffer out as a Unity texture (the buffer itself, not a copy).
target.material.SetTexture("_EmissionMap", Gpu.Texture(pattern));
}
}

Note

  • What comes back is the buffer itself, not a copy.

I want to copy the result into my own RenderTexture VRCGraphics.Blit(Gpu.Texture(current), target)

Example
using UnityEngine;
using VRC.SDKBase;
using Tsukimi;
// Showing the computed picture in UI: computes a wave pattern and copies it every frame into the RenderTexture shown by a UI RawImage.
//
// Setup:
// - Put this script on the object that manages the pattern.
// - Pass the destination RenderTexture to screen. Set the same one as the UI RawImage's Texture.
// - Create screen with Color Space set to Linear (sRGB off) and Filter Mode set to 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);
// Copy into a RenderTexture you prepared yourself.
VRCGraphics.Blit(Gpu.Texture(wave), screen);
}
}

Note

  • Create the destination without sRGB (RenderTextureReadWrite.Linear). By default sRGB is on, and the values the kernel returned do not go in as they are.

I want my own RenderTexture to be the destination public GpuBuffer2D target;

Example
using UnityEngine;
using Tsukimi;
// Writing straight into a minimap: uses a RenderTexture you prepared as the destination and writes the kernel's result straight into it.
//
// Setup:
// - Put this script on the object that manages the minimap.
// - Plug the minimap's RenderTexture into map in the Inspector (Color Space Linear, Filter Mode Point).
// - Pass the terrain height picture to terrain.
public class GoalsGpuHostSlot : TsukimiBehaviour
{
// A public GpuBuffer2D is a slot for plugging in a RenderTexture you prepared yourself.
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); // write straight into the plugged-in RenderTexture
}
}

Note

  • The plugged-in RenderTexture’s settings are used as they are. Turn off sRGB and set Filter Mode to Point.

I want to read the result back as numbers in Udon VRCAsyncGPUReadback.Request(Gpu.Texture(current), ...)

Example
using UnityEngine;
using VRC.SDK3.Rendering;
using VRC.Udon.Common.Interfaces;
using Tsukimi;
using TMPro;
// Color picker: reads the color of the picture's center cell back from the GPU and shows the values as numbers.
//
// Setup:
// - Put this script on the color picker device object.
// - Pass the picture to examine to picture, and the TextMeshProUGUI that shows the numbers to readout.
public class GoalsGpuHostReadback : TsukimiBehaviour
{
public Texture picture;
public TextMeshProUGUI readout;
private GpuBuffer2D buf;
private byte[] cell = new byte[4]; // 1 cell = R, G, B, A, 4 bytes
private bool waiting;
void Start()
{
buf = Gpu.Buffer(64, 64);
}
void Update()
{
// Until the requested answer comes back, do not request the next one (and do not rewrite the buffer).
if (waiting) return;
Gpu.Load(buf, picture);
waiting = true;
// Request only the 1 center cell (x = 32, y = 32).
VRCAsyncGPUReadback.Request(Gpu.Texture(buf), 0, 32, 1, 32, 1, 0, 1,
TextureFormat.RGBA32, (IUdonEventReceiver)this);
}
// Called after the GPU finishes its work (it does not return right when requested).
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]; // arrives as 0 to 255
}
}

Note

  • Leave the requested buffer as it is until the answer comes back. Overwriting it changes what arrives (it is not an error).
  • If the receiving array’s size and the requested format do not match, the end of the array stays 0 with no error or warning.

I want to run a trained model on Udon [Onnx("gesture.onnx")]

I want to run a trained model on the GPU [OnnxGpu("filter.onnx")]

I want heavy work spread over several frames async FrameTask M()

Example
using UnityEngine;
using Tsukimi;
using TMPro;
// Score table sort: sorts the records from highest to lowest. Doing it in one frame makes the world look frozen, so
// it moves on to the next frame after each pass, and when it is done it shows the top 3 on the table.
//
// Setup:
// - Put this script on the score table object.
// - scores holds the records. Pass the TextMeshProUGUI that shows the table to board.
// - Call Rank to start sorting.
public class GoalsAsyncHeavySort : TsukimiBehaviour
{
public int[] scores;
public TextMeshProUGUI board;
// It pauses at await and returns to Udon, and resumes from the next frame.
// The caller does not wait for it to finish and moves straight on to its next statement.
private async FrameTask SortDescending()
{
board.text = "Counting…";
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(); // after one pass, leave the rest for the next frame
}
board.text = "1st " + scores[0] + "\n2nd " + scores[1] + "\n3rd " + scores[2];
}
public void Rank()
{
SortDescending();
}
}

Note

  • It does not run in parallel. It only moves forward a little at a time across frames.

I want to say how many frames to wait await Async.Frames(30)

Example
using UnityEngine;
using Tsukimi;
using TMPro;
// Race start: when touched, counts 3, 2, 1, shows "GO", and opens the gate.
//
// Setup:
// - Put this script on the start button (it needs a Collider).
// - Pass the TextMeshProUGUI that shows the numbers to sign, and the gate object to open to 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); // wait 60 frames (counted in frames, not seconds)
}
sign.text = "GO";
gate.SetActive(false);
}
public override void Interact()
{
Countdown();
}
}

Note

  • It can wait only in frames, not in seconds. On the screen of a player with a different frame rate, the wait is a different length.

I want to limit how many run at once [MaxTasks(4)]

Example
using UnityEngine;
using Tsukimi;
// Fireworks: each touch launches one. Up to 4 can be in the sky at the same time.
//
// Setup:
// - Put this script on the launcher object (it needs a Collider).
// - Pass 4 Lights for the fireworks to lights (each shot lights one up and fades it out).
public class GoalsAsyncMaxTasks : TsukimiBehaviour
{
public Light[] lights;
private int next;
// Up to 4 runs of the same method can be in progress at once. Each has its own arguments and in-progress values.
// Calls made while all 4 are running do not start.
[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; // fade out over 30 frames
await Async.Frame();
}
l.enabled = false;
}
public override void Interact()
{
// If all 4 are still in the air, it does not start. Move on to the next light only when it started.
FrameTask t = Burst(next);
if (t.Started) next = (next + 1) % lights.Length;
}
}

Note

  • The body is copied once per slot, so more slots make the program larger.

I want to know whether it has already started FrameTask t = M(); if (!t.Started)

Example
using UnityEngine;
using Tsukimi;
using TMPro;
// A door that does not stack up when mashed: if it is touched while it is still opening or closing, it only shows a message.
//
// Setup:
// - Put this script on the door button (it needs a Collider).
// - Pass the door's Transform to door, and the TextMeshProUGUI that shows the message to status.
public class GoalsAsyncStarted : TsukimiBehaviour
{
public Transform door;
public TextMeshProUGUI status;
private bool open;
// No count is written, so only 1 run can be in progress at a time.
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();
// While the previous movement has not finished, it does not start and Started is false.
if (!t.Started) status.text = "The door is still moving";
}
}

Note

  • A call made while the previous run has not finished does not start, and Started is false.

I want to split a long loop across frames if (i % 256 == 255) await Async.Frame();

Example
using UnityEngine;
using Tsukimi;
using TMPro;
// Territory count: counts a 64×64 board, moving on to the next frame every 256 cells.
// Counting it all in one frame makes the world look frozen for that time.
//
// Setup:
// - Put this script on the object that holds the board.
// - cells holds each cell's owner (0 is empty, 1 is red, 2 is blue). Its length is 4096.
// - Pass the TextMeshProUGUI that shows the result to result. Call Tally to start counting.
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++;
// Pause every 256 iterations. i and the running counts keep their values across the pause.
if (i % 256 == 255) await Async.Frame();
}
result.text = "Red " + red + " / Blue " + blue;
}
public void Tally()
{
Count();
}
}

I want to pass a value into a method that spans frames async FrameTask Fade(int frames, int target)

Example
using UnityEngine;
using Tsukimi;
// BGM volume fade: turns the volume up when you enter the area and down when you leave. The length and target volume are passed as arguments.
//
// Setup:
// - Put this script on the same object as the Collider (Is Trigger) of the area where the BGM plays.
// - Pass the AudioSource that is playing to bgm.
public class GoalsAsyncArgs : TsukimiBehaviour
{
public AudioSource bgm;
private int latest;
// The arguments id, frames, and target still read as the values they were called with after await.
// If you enter and leave right away, 2 runs are in progress at once, so only the newer one moves the volume.
[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; // give way to a fade called later
bgm.volume = start + (target - start) * f / frames;
await Async.Frame();
}
}
// When you enter or leave this area, change the volume on your own screen.
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);
}
}

I want to wait for another method that spans frames await Fade(30)

Example
using UnityEngine;
using Tsukimi;
// Fade to switch rooms: when touched, turns the light all the way down, switches rooms, then brings the light back.
//
// Setup:
// - Put this script on the switch button (it needs a Collider).
// - Pass the Light for the room to roomLight, and the two room objects to switch to dayRoom and nightRoom.
// - The switch happens only on the screen of the player who touched it (it is not synced).
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();
}
}
// Calling with await waits until the other method finishes before moving on to the next statement.
private async FrameTask Switch()
{
await Fade(1f, 0f); // wait until it is fully dark
bool night = !nightRoom.activeSelf;
dayRoom.SetActive(!night);
nightRoom.SetActive(night);
await Fade(0f, 1f); // bring the light back
}
public override void Interact()
{
Switch();
}
}

I want an event to start work that spans frames async FrameTask Start()

Example
using UnityEngine;
using Tsukimi;
// Opening show: when you enter the world, the corridor lights turn on one at a time from the front.
//
// Setup:
// - Put this script on an empty object that manages the corridor lights.
// - Pass the light objects, in the order to turn on, to lamps (leave them all inactive at first).
public class GoalsAsyncStart : TsukimiBehaviour
{
public GameObject[] lamps;
// Start can span frames. Udon calls Start as usual,
// and the paused rest resumes from a later frame.
private async FrameTask Start()
{
foreach (GameObject lamp in lamps)
{
lamp.SetActive(true);
await Async.Frames(15);
}
}
}

Note

  • If an event called every frame, such as Update, spans frames, later calls do not start because the previous run has not finished.
  • Events inherited from the base class, such as Interact(), cannot span frames because their return type cannot change.

I want the cost of calling a small method gone [Inline]

Example
using UnityEngine;
using Tsukimi;
// Floating platforms: moves many platforms up and down slowly every frame, each at its own phase.
// A small calculation called dozens of times every frame is expanded at the call sites to remove the cost of the call.
//
// Setup:
// - Put this script on an empty object that moves the platforms together.
// - Pass the Transforms of the platforms to move to 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;
}
// A method with this attribute is expanded at its call sites whatever its size (it is not called as a method).
// The program grows by what is expanded. Put it on small methods that are called often.
[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);
}
}

Note

  • A method with this attribute is expanded at its call sites, so the more places call it, the larger the program becomes.

I want to find out what is slow Tsukimi Profiler

I want to measure how many times it actually ran Measure

I want to check for myself that nothing is broken [TsukimiTest]

Example
using Tsukimi;
// Vending machine: insert coins, and if they reach the price, sell one. Otherwise do nothing.
//
// Setup:
// - Put this script on the vending machine object.
// - Call Insert from the coin button and Buy from the buy button.
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;
// Tries the vending machine above without starting Unity. Each test runs on a new instance.
public partial class GoalsTestVending
{
// A public void method with no arguments and [TsukimiTest] is one test.
[TsukimiTest]
public void NotEnoughMoneySellsNothing()
{
Insert(100);
Buy();
Assert.AreEqual(0, sold);
Assert.AreEqual(100, coins); // the inserted coins stay
}
[TsukimiTest]
public void EnoughMoneySellsOneAndKeepsTheChange()
{
Insert(100);
Insert(50);
Buy();
Assert.AreEqual(1, sold);
Assert.AreEqual(30, coins);
}
}

Note

  • Start-up events such as Start do not run automatically in a test. When you need one, write Start() in the test body.

I want to know which file to put tests in Name.Tests.cs

Example
using Tsukimi;
// Code lock door: once 4 digits have been pressed, opens if the number matches. After the fourth digit the input is cleared.
//
// Setup:
// - Put this script on the door's keypad. Call Press(digit) from each key.
// - Write the tests in test-keypad.Tests.cs next to it (the same name as this file plus .Tests.cs).
// - Both files continue the same class, so both have 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;
// A file named "original file name.Tests.cs" is left out of compilation.
// The tests never end up in the world's program.
public partial class GoalsTestKeypad
{
[TsukimiTest]
public void OpensWithTheRightCode()
{
Press(4); Press(2); Press(7); Press(1);
Assert.IsTrue(open);
}
[TsukimiTest]
public void InputIsClearedAfterFourDigits()
{
Press(1); Press(1); Press(1); Press(1);
Assert.IsFalse(open);
// It is the rest of the same class, so private fields can be used directly.
Assert.AreEqual(0, typed);
Assert.AreEqual(0, count);
}
}

Note

  • Tests written in the Behaviour’s own file are included in the program uploaded to the world (warning TUKI0117).

I want to check that a condition holds Assert.IsTrue(charge <= 100)

Example
using Tsukimi;
// Charging stand: charges the battery each time it is placed. Stops so it does not go over 100.
//
// Setup:
// - Put this script on the charging stand object. Call Charge(amount) when a battery is placed.
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 NeverGoesOverTheLimit()
{
Charge(70);
Charge(70);
// Check ranges with IsTrue / IsFalse, which take the condition as it is.
Assert.IsTrue(charge <= 100);
Assert.IsFalse(charge < 0);
}
}

Note

  • Only a true or false value appears in the result. To keep what was compared with what, use Assert.AreEqual.

I want to check that two values are the same Assert.AreEqual(1, count)

Example
using Tsukimi;
// Combo score: each hit adds 10 points × the combo (up to 3). A miss resets the combo to 0.
//
// Setup:
// - Put this script on the target object. Call Hit on a hit and Miss on a 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 ComboStopsAtThree()
{
Hit(); Hit(); Hit(); Hit();
// The first argument is the expected value and the second is the actual value. Swapping them swaps the wording of the result.
Assert.AreEqual(10 + 20 + 30 + 30, score);
}
[TsukimiTest]
public void AMissStartsOverFromOne()
{
Hit(); Hit(); Miss(); Hit();
Assert.AreEqual(10 + 20 + 10, score);
}
}

Note

  • Comparing float values directly also compares the error from each calculation. For real numbers, check that the difference is small with Assert.IsTrue.

I want to check whether a reference is null Assert.IsNull(current)

Example
using Tsukimi;
// Race winner: keeps only the name of the first player to reach the goal. Later players do not overwrite it.
//
// Setup:
// - Put this script on the goal object. Call Finish(name) when someone reaches the goal.
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 OnlyTheFirstOneStays()
{
// Check whether a reference is empty with IsNull / IsNotNull.
Assert.IsNull(winner);
Finish("Aoi");
Finish("Ren");
Assert.IsNotNull(winner);
Assert.AreEqual("Aoi", winner);
}
}

Note

  • A destroyed Unity object can be in a state different from C# null, so the result may not match your intuition.

I want a reason recorded when a check fails Assert.IsTrue(charge > 0, "使い切っている")

Example
using Tsukimi;
// Tries left: each attempt uses one. At 0 nothing is used and no attempt is possible.
//
// Setup:
// - Put this script on the game's reception object. Call TryPlay to make an attempt.
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 NoAttemptsAfterUsingThemAllUp()
{
TryPlay(); TryPlay(); TryPlay(); TryPlay();
// The string at the end appears in the result as it is when the check does not hold.
// When comparing several values of the same type, it shows which one was off.
Assert.AreEqual(0, left, "tries left should stop at 0");
Assert.AreEqual(3, played, "the fourth attempt should not be allowed");
}
}

I want to check what a kernel computed static float NextHeight(float now, ...)

Example
using UnityEngine;
using Tsukimi;
// Cooling heat pattern: every frame, each cell's heat is mixed with its neighbors and cools a little (computed on the GPU).
// The mixing formula is split out into a static method, called from both the kernel and the test.
//
// Setup:
// - Put this script on the board that shows the pattern. Pass the board's Renderer to display.
public partial class GoalsTestKernel : TsukimiBehaviour
{
public Renderer display;
private GpuBuffer2D heat;
private GpuBuffer2D next;
// A static method that takes only float, int, bool, and Vector2 can also be called from a test.
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;
// The kernel itself ([Kernel]) cannot be called from a test. Check the formula that was split out.
public partial class GoalsTestKernel
{
[TsukimiTest]
public void HeatMixesWithNeighborsAndCoolsALittle()
{
// Real numbers carry calculation error, so instead of comparing directly with AreEqual, check that the difference is small.
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 DoesNotGoBelowZeroOnceCold()
{
Assert.AreEqual(0f, Cool(0f, 0f));
}
}

Note

  • Methods with [Kernel], and methods that take Color4 or KernelId, cannot be called from a test.
  • If the split-out method is also called from the Behaviour, it is converted to Udon as well, so the instruction count goes up.

I want to check that synced values arrive, on my own Mimic.Join()

Example
using Tsukimi;
using VRC.SDKBase;
// Everyone's best score: when your score beats the current best, become the owner, rewrite it, and send it to everyone.
//
// Setup:
// - Put this script on the score board object. Call Submit(score) after playing.
[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;
// Puts 2 players in one test and checks that a sent value reaches the other one.
public partial class GoalsTestSync
{
[TsukimiTest]
public void AnotherPlayersBestArrives()
{
VRCPlayerApi me = Mimic.Join(); // the first one is yourself
VRCPlayerApi other = Mimic.Join();
Mimic.Become(other); // from here on, run on other's screen
Submit(50);
Mimic.Become(me);
Assert.AreEqual(0, best); // sending alone has not delivered it yet
Mimic.Deliver(); // delivered to everyone except the sender
Assert.AreEqual(50, best);
}
}

Note

  • Mimic does not reproduce network delay or how often values are sent. What it can confirm is the result of running the steps as written.
  • SendCustomNetworkEvent is only recorded and does not reach the other player.

I want to check that nothing breaks when the arrival order changes Mimic.Explore()

Example
using Tsukimi;
using VRC.SDKBase;
// Round display: syncs the round number and the time left in that round, and builds the display text when received.
//
// Setup:
// - Put this script on the host object. Call NextRound to move to the next round.
[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();
}
// Called when received. Both values have already been written.
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 DisplayMatchesInWhateverOrderValuesArrive()
{
// Write it as the first statement of the test. It swaps every order in which the 2 synced values can be written,
// and runs this test once for each order.
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);
}
}

Note

  • Write Mimic.Explore() as the first statement of the test. It swaps only the places this tool knows about.

I want to test with another behaviour wired up public Lamp lamp;

I want to catch a test that has grown too expensive [CostLimit(steps: 544)]

Example
using Tsukimi;
// Board reset: clears the whole 8×8 board. It runs every game, so we want to watch that it has not become heavy.
//
// Setup:
// - Put this script on the object that holds the board. cells has a length of 64.
// - Call Clear when starting a new game.
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
{
// If it goes over the limit, this test fails even when every Assert holds.
// The number was copied from the steps in the result of one run without a limit.
[TsukimiTest]
[CostLimit(steps: 1812)]
public void ResetHasNotBecomeHeavy()
{
cells[5] = 2;
stones = 1;
Clear();
Assert.AreEqual(0, cells[5]);
Assert.AreEqual(0, stones);
}
}

Note

  • Going over the limit fails the test even when every Assert holds.

I want to run the tests Tsukimi Tests

I want the results written to a file Library/Tsukimi/last-test-run.json

I want to ask the machine whether a form compiles, before writing it spec

I want to ask the machine what an error means diagnostics

I want to export into a U# project Export as UdonSharp

I want to know what the exported code looks like UdonSharp