Skip to content

Commit 5a2a59f

Browse files
authored
Merge pull request #1040 from KennerMiner/feature/game-view-uitoolkit-screenshot-capture
fix: include UI Toolkit overlays in game_view screenshots with include_image
2 parents 9b0a662 + a90ab06 commit 5a2a59f

2 files changed

Lines changed: 140 additions & 12 deletions

File tree

MCPForUnity/Editor/Tools/ManageScene.cs

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -591,22 +591,48 @@ private static object CaptureScreenshot(SceneCommand cmd)
591591
}
592592
}
593593

594-
// When a specific camera is requested or include_image is true, always use camera-based capture
595-
// (synchronous, gives us bytes in memory for base64).
596-
if (targetCamera != null || includeImage)
594+
// When include_image is requested but no specific camera, use composited capture
595+
// (ScreenCapture.CaptureScreenshotAsTexture) which captures UI Toolkit overlays.
596+
// When a specific camera IS requested, use camera-based capture.
597+
if (targetCamera != null)
597598
{
599+
if (!Application.isBatchMode) EnsureGameView();
600+
601+
ScreenshotCaptureResult result = ScreenshotUtility.CaptureFromCameraToAssetsFolder(
602+
targetCamera, fileName, resolvedSuperSize, ensureUniqueFileName: true,
603+
includeImage: includeImage, maxResolution: maxResolution);
604+
605+
AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
606+
string message = $"Screenshot captured to '{result.AssetsRelativePath}' (camera: {targetCamera.name}).";
607+
return new SuccessResponse(message, BuildScreenshotResponseData(result, targetCamera.name, includeImage));
608+
}
609+
610+
if (includeImage && Application.isPlaying)
611+
{
612+
if (!Application.isBatchMode) EnsureGameView();
613+
614+
ScreenshotCaptureResult result = ScreenshotUtility.CaptureComposited(
615+
fileName, resolvedSuperSize, ensureUniqueFileName: true,
616+
includeImage: true, maxResolution: maxResolution);
617+
618+
AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
619+
string cameraName = Camera.main != null ? Camera.main.name : "composited";
620+
string message = $"Screenshot captured to '{result.AssetsRelativePath}' (camera: {cameraName}).";
621+
return new SuccessResponse(message, BuildScreenshotResponseData(result, cameraName, includeImage: true));
622+
}
623+
624+
if (includeImage)
625+
{
626+
// Not in play mode — fall back to camera-based capture
627+
targetCamera = Camera.main;
598628
if (targetCamera == null)
599629
{
600-
targetCamera = Camera.main;
601-
if (targetCamera == null)
602-
{
603-
var allCams = UnityFindObjectsCompat.FindAll<Camera>();
604-
targetCamera = allCams.Length > 0 ? allCams[0] : null;
605-
}
630+
var allCams = UnityFindObjectsCompat.FindAll<Camera>();
631+
targetCamera = allCams.Length > 0 ? allCams[0] : null;
606632
}
607633
if (targetCamera == null)
608634
{
609-
return new ErrorResponse("No camera found in the scene. Add a Camera to use screenshot with camera or include_image.");
635+
return new ErrorResponse("No camera found in the scene. Add a Camera to use screenshot with include_image outside of Play mode.");
610636
}
611637

612638
if (!Application.isBatchMode) EnsureGameView();
@@ -624,7 +650,6 @@ private static object CaptureScreenshot(SceneCommand cmd)
624650
{
625651
return new ErrorResponse(ex.Message);
626652
}
627-
628653
if (ScreenshotUtility.IsUnderAssets(result.ProjectRelativePath))
629654
AssetDatabase.ImportAsset(result.ProjectRelativePath, ImportAssetOptions.ForceSynchronousImport);
630655
string message = $"Screenshot captured to '{result.ProjectRelativePath}' (camera: {targetCamera.name}).";
@@ -644,7 +669,7 @@ private static object CaptureScreenshot(SceneCommand cmd)
644669
data["imageWidth"] = result.ImageWidth;
645670
data["imageHeight"] = result.ImageHeight;
646671
}
647-
return new SuccessResponse(message, data);
672+
return new SuccessResponse(message, BuildScreenshotResponseData(result, targetCamera.name, includeImage));
648673
}
649674

650675
// Default path: use ScreenCapture API if available, camera fallback otherwise
@@ -715,6 +740,31 @@ private static object CaptureScreenshot(SceneCommand cmd)
715740
}
716741
}
717742

743+
private static Dictionary<string, object> BuildScreenshotResponseData(
744+
ScreenshotCaptureResult result,
745+
string cameraName,
746+
bool includeImage)
747+
{
748+
var data = new Dictionary<string, object>
749+
{
750+
{ "path", result.AssetsRelativePath },
751+
{ "fullPath", result.FullPath },
752+
{ "superSize", result.SuperSize },
753+
{ "isAsync", false },
754+
{ "camera", cameraName },
755+
{ "captureSource", "game_view" },
756+
};
757+
758+
if (includeImage && result.ImageBase64 != null)
759+
{
760+
data["imageBase64"] = result.ImageBase64;
761+
data["imageWidth"] = result.ImageWidth;
762+
data["imageHeight"] = result.ImageHeight;
763+
}
764+
765+
return data;
766+
}
767+
718768
private static object CaptureSceneViewScreenshot(
719769
SceneCommand cmd,
720770
string fileName,

MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,84 @@ public static ScreenshotCaptureResult CaptureFromCameraToProjectFolder(
236236
return result;
237237
}
238238

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

0 commit comments

Comments
 (0)