Skip to content

Commit 5733dbc

Browse files
Scriptwonderactions-usergithub-actions[bot]
authored
Feature/multi-view screenshot (#840)
* chore: update Unity package to beta version 9.4.8-beta.8 * feat: contact sheet compositor for batch screenshots - Batch surround/orbit modes now return a single composite contact sheet image - Added orbit mode with configurable azimuth, elevations, distance, FOV - Contact sheet tiles are labeled with compass directions (front, back_left, etc.) - CLI saves contact sheet PNGs to Assets/Screenshots/ by default - ScreenshotUtility: RenderCameraToTexture, ComposeContactSheet with 5x7 pixel font - Server manage_scene.py: orbit params forwarding - CLI raw command: nargs fix for Windows argument splitting - Updated docs: CLI_USAGE, CLI_EXAMPLE, SKILL.md, tools-reference * Doc Update --------- Co-authored-by: GitHub Actions <actions@github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 08b8eac commit 5733dbc

11 files changed

Lines changed: 773 additions & 55 deletions

File tree

‎.claude/skills/unity-mcp-skill/references/tools-reference.md‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ manage_scene(
109109
include_transform=False # bool - include local transforms
110110
)
111111

112-
# Screenshot (file only — saves to Assets/)
112+
# Screenshot (file only — saves to Assets/Screenshots/)
113113
manage_scene(action="screenshot")
114114

115115
# Screenshot with inline image (base64 PNG returned to AI)
@@ -120,13 +120,13 @@ manage_scene(
120120
max_resolution=512 # int, optional - downscale cap (default 640)
121121
)
122122

123-
# Batch surround (6 angles around scene bounds, no file saved)
123+
# Batch surround — contact sheet of 6 fixed angles (front/back/left/right/top/bird_eye)
124124
manage_scene(
125125
action="screenshot",
126-
batch="surround", # str - "surround" for 6-angle capture
127-
max_resolution=256 # int - keep low for batch (6 images)
126+
batch="surround", # str - "surround" for 6-angle contact sheet
127+
max_resolution=256 # int - per-tile resolution cap
128128
)
129-
# Returns: front, back, left, right, top, bird_eye views
129+
# Returns: single composite contact sheet image with labeled tiles
130130

131131
# Batch surround centered on a specific target
132132
manage_scene(
@@ -136,6 +136,19 @@ manage_scene(
136136
max_resolution=256
137137
)
138138

139+
# Batch orbit — configurable multi-angle grid around a target
140+
manage_scene(
141+
action="screenshot",
142+
batch="orbit", # str - "orbit" for configurable angle grid
143+
look_at="Player", # str|int|list[float] - target to orbit around
144+
orbit_angles=8, # int, default 8 - number of azimuth steps
145+
orbit_elevations=[0, 30], # list[float], default [0, 30, -15] - vertical angles in degrees
146+
orbit_distance=10, # float, optional - camera distance (auto-fit if omitted)
147+
orbit_fov=60, # float, default 60 - camera FOV in degrees
148+
max_resolution=256 # int - per-tile resolution cap
149+
)
150+
# Returns: single composite contact sheet (angles × elevations tiles in a grid)
151+
139152
# Positioned screenshot (temp camera at viewpoint, no file saved)
140153
manage_scene(
141154
action="screenshot",

‎MCPForUnity/Editor/Tools/ManageScene.cs‎

Lines changed: 225 additions & 16 deletions
Large diffs are not rendered by default.

‎MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs‎

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using System.Linq;
45
using UnityEngine;
@@ -270,6 +271,232 @@ public static (string base64, int width, int height) RenderCameraToBase64(Camera
270271
}
271272
}
272273

274+
/// <summary>
275+
/// Renders a camera to a Texture2D without saving to disk.
276+
/// Caller owns the returned texture and must destroy it.
277+
/// </summary>
278+
public static Texture2D RenderCameraToTexture(Camera camera, int maxResolution = 640)
279+
{
280+
if (camera == null) throw new ArgumentNullException(nameof(camera));
281+
282+
int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
283+
int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);
284+
285+
RenderTexture prevRT = camera.targetTexture;
286+
RenderTexture prevActive = RenderTexture.active;
287+
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
288+
Texture2D tex = null;
289+
try
290+
{
291+
camera.targetTexture = rt;
292+
camera.Render();
293+
294+
RenderTexture.active = rt;
295+
tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
296+
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
297+
tex.Apply();
298+
299+
int targetMax = maxResolution > 0 ? maxResolution : 640;
300+
if (width > targetMax || height > targetMax)
301+
{
302+
var downscaled = DownscaleTexture(tex, targetMax);
303+
DestroyTexture(tex);
304+
tex = null;
305+
return downscaled;
306+
}
307+
var result = tex;
308+
tex = null; // transfer ownership to caller
309+
return result;
310+
}
311+
finally
312+
{
313+
camera.targetTexture = prevRT;
314+
RenderTexture.active = prevActive;
315+
RenderTexture.ReleaseTemporary(rt);
316+
DestroyTexture(tex);
317+
}
318+
}
319+
320+
/// <summary>
321+
/// Composites a list of tile textures into a single contact-sheet grid image.
322+
/// Labels are drawn as white text on a dark banner at the bottom of each tile.
323+
/// Returns base64 PNG plus dimensions. Destroys all input tile textures.
324+
/// </summary>
325+
public static (string base64, int width, int height) ComposeContactSheet(
326+
List<Texture2D> tiles, List<string> labels, int padding = 4)
327+
{
328+
if (tiles == null || tiles.Count == 0)
329+
throw new ArgumentException("No tiles to compose.", nameof(tiles));
330+
331+
int tileW = tiles[0].width;
332+
int tileH = tiles[0].height;
333+
int count = tiles.Count;
334+
335+
// Calculate grid: prefer wider than tall (cols >= rows)
336+
int cols = Mathf.CeilToInt(Mathf.Sqrt(count));
337+
int rows = Mathf.CeilToInt((float)count / cols);
338+
339+
int labelHeight = Mathf.Max(14, tileH / 12);
340+
int cellW = tileW + padding;
341+
int cellH = tileH + labelHeight + padding;
342+
343+
int sheetW = cols * cellW + padding;
344+
int sheetH = rows * cellH + padding;
345+
346+
Texture2D sheet = null;
347+
try
348+
{
349+
sheet = new Texture2D(sheetW, sheetH, TextureFormat.RGBA32, false);
350+
351+
// Build the full sheet in a Color32[] buffer, then upload once
352+
var bgColor = new Color32(30, 30, 30, 255);
353+
Color32[] sheetPixels = new Color32[sheetW * sheetH];
354+
for (int i = 0; i < sheetPixels.Length; i++) sheetPixels[i] = bgColor;
355+
356+
// Track label draw requests so we can apply them after the bulk upload
357+
var labelDraws = new List<(string text, int x, int y, int h)>();
358+
359+
for (int idx = 0; idx < count; idx++)
360+
{
361+
int col = idx % cols;
362+
int row = idx / cols;
363+
// Place tiles top-left to bottom-right (Unity Texture2D y=0 is bottom)
364+
int x = padding + col * cellW;
365+
int y = sheetH - padding - (row + 1) * cellH + padding;
366+
367+
// Copy tile pixels row-by-row using bulk operations
368+
Color32[] tilePixels = tiles[idx].GetPixels32();
369+
for (int ty = 0; ty < tileH; ty++)
370+
{
371+
int srcOffset = ty * tileW;
372+
int dstOffset = (y + labelHeight + ty) * sheetW + x;
373+
System.Array.Copy(tilePixels, srcOffset, sheetPixels, dstOffset, tileW);
374+
}
375+
376+
// Draw label banner (dark background strip below tile)
377+
var bannerColor = new Color32(20, 20, 20, 220);
378+
for (int ly = 0; ly < labelHeight; ly++)
379+
{
380+
int dstOffset = (y + ly) * sheetW + x;
381+
for (int lx = 0; lx < tileW; lx++)
382+
{
383+
sheetPixels[dstOffset + lx] = bannerColor;
384+
}
385+
}
386+
387+
// Queue label text drawing (applied after bulk pixel upload)
388+
if (labels != null && idx < labels.Count && !string.IsNullOrEmpty(labels[idx]))
389+
{
390+
labelDraws.Add((labels[idx], x + 3, y + 2, labelHeight - 4));
391+
}
392+
}
393+
394+
// Upload all tile + banner pixels in one SetPixels32 call
395+
sheet.SetPixels32(sheetPixels);
396+
397+
// Draw label text on top (small glyph-based writes, negligible cost)
398+
foreach (var (text, lx, ly, lh) in labelDraws)
399+
{
400+
DrawText(sheet, text, lx, ly, lh, Color.white);
401+
}
402+
403+
sheet.Apply();
404+
405+
byte[] png = sheet.EncodeToPNG();
406+
string b64 = System.Convert.ToBase64String(png);
407+
return (b64, sheetW, sheetH);
408+
}
409+
finally
410+
{
411+
foreach (var tile in tiles) DestroyTexture(tile);
412+
DestroyTexture(sheet);
413+
}
414+
}
415+
416+
private static void DrawText(Texture2D tex, string text, int startX, int startY, int charHeight, Color color)
417+
{
418+
// Simple 5x7 bitmap font for basic ASCII characters
419+
int charWidth = Mathf.Max(4, charHeight * 5 / 7);
420+
int spacing = Mathf.Max(1, charWidth / 5);
421+
int x = startX;
422+
423+
foreach (char c in text)
424+
{
425+
if (x + charWidth > tex.width) break;
426+
ulong glyph = GetGlyph(c);
427+
if (glyph != 0)
428+
{
429+
for (int row = 0; row < 7; row++)
430+
{
431+
for (int col = 0; col < 5; col++)
432+
{
433+
bool on = ((glyph >> ((6 - row) * 5 + (4 - col))) & 1) == 1;
434+
if (!on) continue;
435+
// Scale the 5x7 glyph to charWidth x charHeight
436+
int px0 = x + col * charWidth / 5;
437+
int px1 = x + (col + 1) * charWidth / 5;
438+
int py0 = startY + (6 - row) * charHeight / 7;
439+
int py1 = startY + (7 - row) * charHeight / 7;
440+
for (int py = py0; py < py1 && py < tex.height; py++)
441+
for (int px = px0; px < px1 && px < tex.width; px++)
442+
tex.SetPixel(px, py, color);
443+
}
444+
}
445+
}
446+
x += charWidth + spacing;
447+
}
448+
}
449+
450+
private static ulong GetGlyph(char c)
451+
{
452+
// 5x7 pixel font stored as 35-bit values (row0=bits34-30 ... row6=bits4-0)
453+
// Each row is 5 wide, MSB=left. Row 0 is top.
454+
switch (char.ToUpperInvariant(c))
455+
{
456+
case 'A': return 0b01110_10001_10001_11111_10001_10001_10001UL;
457+
case 'B': return 0b11110_10001_10001_11110_10001_10001_11110UL;
458+
case 'C': return 0b01110_10001_10000_10000_10000_10001_01110UL;
459+
case 'D': return 0b11100_10010_10001_10001_10001_10010_11100UL;
460+
case 'E': return 0b11111_10000_10000_11110_10000_10000_11111UL;
461+
case 'F': return 0b11111_10000_10000_11110_10000_10000_10000UL;
462+
case 'G': return 0b01110_10001_10000_10111_10001_10001_01110UL;
463+
case 'H': return 0b10001_10001_10001_11111_10001_10001_10001UL;
464+
case 'I': return 0b01110_00100_00100_00100_00100_00100_01110UL;
465+
case 'K': return 0b10001_10010_10100_11000_10100_10010_10001UL;
466+
case 'L': return 0b10000_10000_10000_10000_10000_10000_11111UL;
467+
case 'M': return 0b10001_11011_10101_10101_10001_10001_10001UL;
468+
case 'N': return 0b10001_11001_10101_10011_10001_10001_10001UL;
469+
case 'O': return 0b01110_10001_10001_10001_10001_10001_01110UL;
470+
case 'R': return 0b11110_10001_10001_11110_10100_10010_10001UL;
471+
case 'S': return 0b01110_10001_10000_01110_00001_10001_01110UL;
472+
case 'T': return 0b11111_00100_00100_00100_00100_00100_00100UL;
473+
case 'U': return 0b10001_10001_10001_10001_10001_10001_01110UL;
474+
case 'V': return 0b10001_10001_10001_10001_01010_01010_00100UL;
475+
case 'W': return 0b10001_10001_10001_10101_10101_11011_10001UL;
476+
case 'Y': return 0b10001_10001_01010_00100_00100_00100_00100UL;
477+
case '0': return 0b01110_10011_10101_10101_10101_11001_01110UL;
478+
case '1': return 0b00100_01100_00100_00100_00100_00100_01110UL;
479+
case '2': return 0b01110_10001_00001_00010_00100_01000_11111UL;
480+
case '3': return 0b01110_10001_00001_00110_00001_10001_01110UL;
481+
case '4': return 0b00010_00110_01010_10010_11111_00010_00010UL;
482+
case '5': return 0b11111_10000_11110_00001_00001_10001_01110UL;
483+
case '6': return 0b01110_10001_10000_11110_10001_10001_01110UL;
484+
case '7': return 0b11111_00001_00010_00100_01000_01000_01000UL;
485+
case '8': return 0b01110_10001_10001_01110_10001_10001_01110UL;
486+
case '9': return 0b01110_10001_10001_01111_00001_10001_01110UL;
487+
case 'J': return 0b00111_00010_00010_00010_00010_10010_01100UL;
488+
case 'P': return 0b11110_10001_10001_11110_10000_10000_10000UL;
489+
case 'Q': return 0b01110_10001_10001_10001_10101_10010_01101UL;
490+
case 'X': return 0b10001_01010_00100_00100_00100_01010_10001UL;
491+
case 'Z': return 0b11111_00001_00010_00100_01000_10000_11111UL;
492+
case '-': return 0b00000_00000_00000_11111_00000_00000_00000UL;
493+
case '_': return 0b00000_00000_00000_00000_00000_00000_11111UL;
494+
case ' ': return 0UL;
495+
case '+': return 0b00000_00100_00100_11111_00100_00100_00000UL;
496+
default: return 0UL;
497+
}
498+
}
499+
273500
/// <summary>
274501
/// Downscales a Texture2D so that its longest edge is at most <paramref name="maxEdge"/> pixels.
275502
/// Uses bilinear filtering via a temporary RenderTexture blit.

‎MCPForUnity/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "com.coplaydev.unity-mcp",
3-
"version": "9.4.8-beta.7",
3+
"version": "9.4.8-beta.8",
44
"displayName": "MCP for Unity",
55
"description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4",
66
"unity": "2021.3",

0 commit comments

Comments
 (0)