diff --git a/Assets/VRM10_Samples/MToon10Showcase/Runtime/MToon10ShowcaseEntryPoint.cs b/Assets/VRM10_Samples/MToon10Showcase/Runtime/MToon10ShowcaseEntryPoint.cs index dc037f868..168737460 100644 --- a/Assets/VRM10_Samples/MToon10Showcase/Runtime/MToon10ShowcaseEntryPoint.cs +++ b/Assets/VRM10_Samples/MToon10Showcase/Runtime/MToon10ShowcaseEntryPoint.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using UniGLTF; using UnityEngine; using VRM10.MToon10; @@ -115,23 +117,51 @@ namespace VRM10.Samples.MToon10Showcase var width = (PrimitiveRadius * 2 + PrimitiveSpacing) * Columns - PrimitiveSpacing; var bottomLeft = _nextOrigin; + const float screenCenterXOffset = 2f; + var floorOffset = new Vector3(width / 2f + screenCenterXOffset, -1f, 0.0f); CreateFloor( - InitialOrigin + new Vector3(width / 2f, -1f, 0.0f), - bottomLeft + new Vector3(width / 2f, -1f, 0.0f), + InitialOrigin + floorOffset, + bottomLeft + floorOffset, dependencies.floorPrefab); const float cameraHeight = 5f; - + var cameraOffset = new Vector3(width / 2f + screenCenterXOffset, cameraHeight, 0); cameraScroller.Initialize( - InitialOrigin + new Vector3(width / 2f, cameraHeight, 0.0f), - bottomLeft + new Vector3(width / 2f, cameraHeight, 0.0f), + InitialOrigin + cameraOffset, + bottomLeft + cameraOffset, 0.5f); - const float captureSpacing = 0.1f; + const float padding = 0.1f; + var topLeft = InitialOrigin + new Vector3(-padding, 0, 0); + var bottomRight = bottomLeft + new Vector3(width + padding, 0, 0); + topDownCapture.Capture(topLeft, bottomRight); + } - topDownCapture.Capture( - InitialOrigin + new Vector3(-captureSpacing, cameraHeight, 0.0f), - bottomLeft + new Vector3(width + captureSpacing, cameraHeight, 0.0f)); + private void OnGUI() + { + if (GUI.Button(new Rect(10, 10, 150, 30), "Show Captured Image")) + { + topDownCapture.ShowCapturedImageNextToCaptureTarget(0.9f); + } + + if (GUI.Button(new Rect(10, 45, 150, 30), "Export Captured Image")) + { + var fileName = $"MToon10Showcase_{PackageVersion.VERSION}.png"; +#if UNITY_EDITOR + var path = UnityEditor.EditorUtility.SaveFilePanel("Export Screenshot", "", fileName, "png"); +#else + var path = Path.Combine(Application.dataPath, fileName); +#endif + if (string.IsNullOrEmpty(path)) + { + Debug.LogWarning("Export cancelled"); + return; + } + + topDownCapture.ExportCapturedImage(path); + Application.OpenURL(path); + Debug.Log($"Exported screenshot to {path}"); + } } private Transform CreateShowcase(T[] entries, Material baseMaterial, Action setup, @@ -176,7 +206,7 @@ namespace VRM10.Samples.MToon10Showcase private static GameObject CreateFloor(Vector3 startOrigin, Vector3 endOrigin, GameObject floorPrefab) { if (startOrigin.z < endOrigin.z) throw new ArgumentException(); - + var floors = new GameObject("Floors"); var nextFloorOrigin = startOrigin; var floorLength = 10f * floorPrefab.transform.localScale.y; diff --git a/Assets/VRM10_Samples/MToon10Showcase/Runtime/TopDownCapture.cs b/Assets/VRM10_Samples/MToon10Showcase/Runtime/TopDownCapture.cs index 12e45c80e..bdf2e98e3 100644 --- a/Assets/VRM10_Samples/MToon10Showcase/Runtime/TopDownCapture.cs +++ b/Assets/VRM10_Samples/MToon10Showcase/Runtime/TopDownCapture.cs @@ -1,40 +1,104 @@ using System; +using System.IO; using UnityEngine; +using UnityEngine.Rendering; namespace VRM10.Samples.MToon10Showcase { public sealed class TopDownCapture : MonoBehaviour { + private static readonly float CameraHeight = 5f; + private static readonly int MaxPixels = 4096 * 4096; + private static readonly int TextureWidth = 480; + [SerializeField] private Camera _camera; - [SerializeField] private RenderTexture _renderTarget; - - public RenderTexture RenderTarget => _renderTarget; + private RenderTexture _renderTarget; + private GameObject _quad; + private Vector3 _topLeftOfCaptureArea; + private Vector3 _bottomRightOfCaptureArea; public void Capture(Vector3 topLeft, Vector3 bottomRight) { if (topLeft.x > bottomRight.x) throw new ArgumentException("TopLeft.x must be less than BottomRight.x"); if (topLeft.z < bottomRight.z) throw new ArgumentException("TopLeft.z must be greater than BottomRight.z"); + // Set up render texture var width = bottomRight.x - topLeft.x; var height = topLeft.z - bottomRight.z; var aspect = width / height; - _camera.transform.position = new Vector3(topLeft.x + width / 2f, topLeft.y, topLeft.z - height / 2f); + var texHeight = (int)(TextureWidth / aspect); + if (TextureWidth * texHeight > MaxPixels) throw new Exception("Too many pixels to capture."); + if (_renderTarget != null) _renderTarget.Release(); + _renderTarget = new RenderTexture(TextureWidth, texHeight, 16, RenderTextureFormat.ARGB32); + + // Set up camera + _camera.transform.position = new Vector3(topLeft.x + width / 2f, CameraHeight, topLeft.z - height / 2f); _camera.transform.rotation = Quaternion.Euler(90f, 0f, 0f); _camera.orthographic = true; _camera.orthographicSize = height / 2f; - var texWidth = 480; - var texHeight = (int)(texWidth / aspect); - - if (texWidth * texHeight > 4096 * 4096) throw new Exception("Too many pixels to capture."); - - _renderTarget = new RenderTexture(texWidth, texHeight, 16, RenderTextureFormat.ARGB32); _camera.targetTexture = _renderTarget; - _camera.enabled = true; + + // Capture _camera.Render(); - _camera.enabled = false; - _camera.targetTexture = null; + // Save capture area for next step + _topLeftOfCaptureArea = topLeft; + _bottomRightOfCaptureArea = bottomRight; + } + + public void ShowCapturedImageNextToCaptureTarget(float xSpacing) + { + if (_renderTarget == null) + { + Debug.LogWarning("Call Capture() before ShowCaptureOnMesh()"); + return; + } + + if (_quad != null) + { + return; + } + + var offset = new Vector3(_bottomRightOfCaptureArea.x + xSpacing, 0, 0); + var topLeft = _topLeftOfCaptureArea + offset; + var bottomRight = _bottomRightOfCaptureArea + offset; + var width = bottomRight.x - topLeft.x; + var height = topLeft.z - bottomRight.z; + _quad = GameObject.CreatePrimitive(PrimitiveType.Quad); + _quad.gameObject.name = "Capture"; + _quad.transform.position = new Vector3(topLeft.x + width / 2f, topLeft.y, topLeft.z - height / 2f); + _quad.transform.rotation = Quaternion.Euler(90f, 0f, 0f); + _quad.transform.localScale = new Vector3(width, height, 1f); + var material = new Material(Shader.Find("Unlit/Texture")); + material.mainTexture = _renderTarget; + _quad.GetComponent().material = material; + } + + public void ExportCapturedImage(string path) + { + if (_renderTarget == null) + { + Debug.LogWarning("Call Capture() before ExportCapturedImage()"); + return; + } + + AsyncGPUReadback.Request(_renderTarget, 0, request => + { + if (request.hasError) + { + Debug.LogError("AsyncGPUReadback error"); + return; + } + + var texture = new Texture2D(_renderTarget.width, _renderTarget.height, TextureFormat.RGBA32, false); + var data = request.GetData(); + texture.LoadRawTextureData(data); + texture.Apply(); + var encodedData = texture.EncodeToPNG(); + File.WriteAllBytes(path, encodedData); + DestroyImmediate(texture); + }); } } } \ No newline at end of file diff --git a/Assets/VRM10_Samples/MToon10Showcase/Runtime/VRM10.Samples.MToon10Showcase.asmdef b/Assets/VRM10_Samples/MToon10Showcase/Runtime/VRM10.Samples.MToon10Showcase.asmdef index bc5764474..afd68a0d2 100644 --- a/Assets/VRM10_Samples/MToon10Showcase/Runtime/VRM10.Samples.MToon10Showcase.asmdef +++ b/Assets/VRM10_Samples/MToon10Showcase/Runtime/VRM10.Samples.MToon10Showcase.asmdef @@ -3,7 +3,8 @@ "rootNamespace": "", "references": [ "GUID:e47c917724578cc43b5506c17a27e9a0", - "GUID:0aaf403bd13871a44b7127aef2695ff8" + "GUID:0aaf403bd13871a44b7127aef2695ff8", + "GUID:8d76e605759c3f64a957d63ef96ada7c" ], "includePlatforms": [], "excludePlatforms": [],