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.
Motion and display
Section titled “Motion and display”Reactive
Section titled “Reactive”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
QuaternionandColortreat NaN as equal to NaN. Other value types keep counting as changed once NaN gets in.
Surfaces
Section titled “Surfaces”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.Showuses 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.Showin the same positions as the method’s arguments (afterid).
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.Normalread 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.
Performance
Section titled “Performance”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.Halfor pack withGpu.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.Pack16x2can only be used inside a kernel. It is not used with buffers created withGpuFormat.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.Loadcrushes 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.Maxand thenGpu.Minon 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.
Trained models
Section titled “Trained models”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
Startedis 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.
Inlining
Section titled “Inlining”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.
Profiler
Section titled “Profiler”I want to find out what is slow
Tsukimi Profiler
I want to measure how many times it actually ran
Measure
Testing and analysis
Section titled “Testing and analysis”Testing
Section titled “Testing”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
Startdo not run automatically in a test. When you need one, writeStart()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
floatvalues directly also compares the error from each calculation. For real numbers, check that the difference is small withAssert.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 takeColor4orKernelId, 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
Mimicdoes not reproduce network delay or how often values are sent. What it can confirm is the result of running the steps as written.SendCustomNetworkEventis 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
Assertholds.
I want to run the tests
Tsukimi Tests
I want the results written to a file
Library/Tsukimi/last-test-run.json
Static analysis
Section titled “Static analysis”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
Exporting to U#
Section titled “Exporting to U#”Exporting to U#
Section titled “Exporting to U#”I want to export into a U# project
Export as UdonSharp