Skip to content

Commit 56a14af

Browse files
KennerMinerclaudehappy-otter
committed
Fix game_view screenshots to capture UI Toolkit overlays
Use ScreenCapture.CaptureScreenshotAsTexture() for game_view screenshots when include_image=true and in Play mode. This captures the final composited frame including UI Toolkit overlays, which camera.Render() misses since UI Toolkit renders at the compositor level after camera rendering. The camera-based path is still used when a specific camera is requested or when not in Play mode. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
1 parent 2a6be79 commit 56a14af

2 files changed

Lines changed: 137 additions & 10 deletions

File tree

MCPForUnity/Editor/Tools/ManageScene.cs

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -589,22 +589,80 @@ private static object CaptureScreenshot(SceneCommand cmd)
589589
}
590590
}
591591

592-
// When a specific camera is requested or include_image is true, always use camera-based capture
593-
// (synchronous, gives us bytes in memory for base64).
594-
if (targetCamera != null || includeImage)
592+
// When include_image is requested but no specific camera, use composited capture
593+
// (ScreenCapture.CaptureScreenshotAsTexture) which captures UI Toolkit overlays.
594+
// When a specific camera IS requested, use camera-based capture.
595+
if (targetCamera != null)
595596
{
597+
if (!Application.isBatchMode) EnsureGameView();
598+
599+
ScreenshotCaptureResult result = ScreenshotUtility.CaptureFromCameraToAssetsFolder(
600+
targetCamera, fileName, resolvedSuperSize, ensureUniqueFileName: true,
601+
includeImage: includeImage, maxResolution: maxResolution);
602+
603+
AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
604+
string message = $"Screenshot captured to '{result.AssetsRelativePath}' (camera: {targetCamera.name}).";
605+
606+
var data = new Dictionary<string, object>
607+
{
608+
{ "path", result.AssetsRelativePath },
609+
{ "fullPath", result.FullPath },
610+
{ "superSize", result.SuperSize },
611+
{ "isAsync", false },
612+
{ "camera", targetCamera.name },
613+
{ "captureSource", "game_view" },
614+
};
615+
if (includeImage && result.ImageBase64 != null)
616+
{
617+
data["imageBase64"] = result.ImageBase64;
618+
data["imageWidth"] = result.ImageWidth;
619+
data["imageHeight"] = result.ImageHeight;
620+
}
621+
return new SuccessResponse(message, data);
622+
}
623+
624+
if (includeImage && Application.isPlaying)
625+
{
626+
if (!Application.isBatchMode) EnsureGameView();
627+
628+
ScreenshotCaptureResult result = ScreenshotUtility.CaptureComposited(
629+
fileName, resolvedSuperSize, ensureUniqueFileName: true,
630+
includeImage: true, maxResolution: maxResolution);
631+
632+
AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
633+
string cameraName = Camera.main != null ? Camera.main.name : "composited";
634+
string message = $"Screenshot captured to '{result.AssetsRelativePath}' (camera: {cameraName}).";
635+
636+
var data = new Dictionary<string, object>
637+
{
638+
{ "path", result.AssetsRelativePath },
639+
{ "fullPath", result.FullPath },
640+
{ "superSize", result.SuperSize },
641+
{ "isAsync", false },
642+
{ "camera", cameraName },
643+
{ "captureSource", "game_view" },
644+
};
645+
if (result.ImageBase64 != null)
646+
{
647+
data["imageBase64"] = result.ImageBase64;
648+
data["imageWidth"] = result.ImageWidth;
649+
data["imageHeight"] = result.ImageHeight;
650+
}
651+
return new SuccessResponse(message, data);
652+
}
653+
654+
if (includeImage)
655+
{
656+
// Not in play mode — fall back to camera-based capture
657+
targetCamera = Camera.main;
596658
if (targetCamera == null)
597659
{
598-
targetCamera = Camera.main;
599-
if (targetCamera == null)
600-
{
601-
var allCams = UnityFindObjectsCompat.FindAll<Camera>();
602-
targetCamera = allCams.Length > 0 ? allCams[0] : null;
603-
}
660+
var allCams = UnityFindObjectsCompat.FindAll<Camera>();
661+
targetCamera = allCams.Length > 0 ? allCams[0] : null;
604662
}
605663
if (targetCamera == null)
606664
{
607-
return new ErrorResponse("No camera found in the scene. Add a Camera to use screenshot with camera or include_image.");
665+
return new ErrorResponse("No camera found in the scene. Add a Camera to use screenshot with include_image outside of Play mode.");
608666
}
609667

610668
if (!Application.isBatchMode) EnsureGameView();

MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,75 @@ public static ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(
224224
return result;
225225
}
226226

227+
/// <summary>
228+
/// Captures a screenshot using ScreenCapture.CaptureScreenshotAsTexture, which captures the
229+
/// final composited frame including UI Toolkit overlays, post-processing, etc.
230+
/// Falls back to camera-based capture if ScreenCapture is unavailable.
231+
/// </summary>
232+
public static ScreenshotCaptureResult CaptureComposited(
233+
string fileName = null,
234+
int superSize = 1,
235+
bool ensureUniqueFileName = true,
236+
bool includeImage = false,
237+
int maxResolution = 0)
238+
{
239+
ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, isAsync: false);
240+
Texture2D tex = null;
241+
Texture2D downscaled = null;
242+
string imageBase64 = null;
243+
int imgW = 0, imgH = 0;
244+
try
245+
{
246+
tex = ScreenCapture.CaptureScreenshotAsTexture(result.SuperSize);
247+
if (tex == null)
248+
{
249+
// Fallback to camera-based if ScreenCapture fails
250+
var cam = Camera.main;
251+
if (cam != null)
252+
return CaptureFromCameraToAssetsFolder(cam, fileName, superSize, ensureUniqueFileName, includeImage, maxResolution);
253+
throw new InvalidOperationException("ScreenCapture.CaptureScreenshotAsTexture returned null and no fallback camera available.");
254+
}
255+
256+
int width = tex.width;
257+
int height = tex.height;
258+
259+
byte[] png = tex.EncodeToPNG();
260+
File.WriteAllBytes(result.FullPath, png);
261+
262+
if (includeImage)
263+
{
264+
int targetMax = maxResolution > 0 ? maxResolution : 640;
265+
if (width > targetMax || height > targetMax)
266+
{
267+
downscaled = DownscaleTexture(tex, targetMax);
268+
byte[] smallPng = downscaled.EncodeToPNG();
269+
imageBase64 = System.Convert.ToBase64String(smallPng);
270+
imgW = downscaled.width;
271+
imgH = downscaled.height;
272+
}
273+
else
274+
{
275+
imageBase64 = System.Convert.ToBase64String(png);
276+
imgW = width;
277+
imgH = height;
278+
}
279+
}
280+
}
281+
finally
282+
{
283+
DestroyTexture(tex);
284+
DestroyTexture(downscaled);
285+
}
286+
287+
if (includeImage && imageBase64 != null)
288+
{
289+
return new ScreenshotCaptureResult(
290+
result.FullPath, result.AssetsRelativePath, result.SuperSize, false,
291+
imageBase64, imgW, imgH);
292+
}
293+
return result;
294+
}
295+
227296
/// <summary>
228297
/// Renders a camera to a Texture2D without saving to disk. Used for multi-angle captures.
229298
/// Returns the base64-encoded PNG, downscaled to fit within <paramref name="maxResolution"/>.

0 commit comments

Comments
 (0)