using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; namespace VideoStage.EditorTools { /// /// ショート動画(縦 1080×1920)の背景にする「ロボットの作業部屋」を組んで撮る /// (2026-11-06・依頼99)。 /// /// ServiceSim の既存のシーン・設定には触れない。作るのは Assets/VideoStage/ の中だけ /// (VideoStage.unity と Materials/)。ビルド設定にも足さない。 /// /// コマンド /Applications/Unity/Hub/Editor/2022.3.62f3/Unity.app/Contents/MacOS/Unity \ /// -batchmode -quit -projectPath /Users/shimazueiichi/Robotics_UX/ServiceSim \ /// -executeMethod VideoStage.EditorTools.VideoStageBuilder.BuildAndShoot \ /// -stageOut <出力フォルダ> -stageFrames <ループのコマを置くフォルダ> \ /// -logFile <ログ> /// (-nographics は付けない。カメラで撮るので絵を描く装置が要る) /// /// 出すもの: stage_bg.png / stage_bg_off.png / stage_check.png / stage_meta.json と、 /// ループ用のコマ frame_0000.png〜frame_0179.png(mp4 にするのは ffmpeg で外から) /// public static class VideoStageBuilder { const int W = 1080, H = 1920; const int SuperSample = 2; // 2倍で描いて縮める(ふちのギザギザを減らす) // カメラ(円の中心から手前に CamDist、高さは床の円の楕円の中心が y=RingCenterPy に来るように解く) const float Pitch = 12f; // 俯角(度) const float VFov = 55f; // 縦の画角(度) const float CamDist = 6.0f; // 円の中心からカメラまでの水平の距離(m) const float RingCenterPy = 1560f; // 床の円(直径 3m)と、2人の足もと(円の中・手前寄りの左右) const float RingR = 1.5f; const float RingWidth = 0.10f; const int RingSegments = 64; static readonly Vector3 FootL = new Vector3(-0.75f, 0f, -1.0f); static readonly Vector3 FootR = new Vector3(0.75f, 0f, -1.0f); // 部屋 const float WallZ = 2.9f; // 奥の壁の面 const float RoomHalfW = 3.6f; const float CeilY = 4.2f; const float FrontZ = -6.5f; // モニターの光る部分を収める枠(画面の px・左上が原点)。依頼の目安 x 80〜1000・y 460〜1080 の内側 const float GlowLeft = 90f, GlowRight = 990f, GlowTop = 470f, GlowBottom = 1070f; const float Bezel = 0.035f; // ループ映像 const int LoopFps = 30; const float LoopSec = 6f; static readonly string Root = "Assets/VideoStage"; static readonly string MatDir = "Assets/VideoStage/Materials"; static readonly string ScenePath = "Assets/VideoStage/VideoStage.unity"; // 明るさのもと(ゆらぎで掛ける値) const float LampIntensity = 1.6f; const float GlowLightIntensity = 0.55f; static readonly Color ScreenOn = Hex("#0E1420"); class Stage { public Camera cam; public Light lamp, glow; public Material screenMat; public GameObject checkL, checkR; public Vector3[] glowCorners; // 左上・右上・右下・左下(世界の座標) } static Dictionary mats = new Dictionary(); [MenuItem("VideoStage/舞台を組んで撮る(出力先を選ぶ)")] public static void BuildAndShootMenu() { string dir = EditorUtility.OpenFolderPanel("出力先", "", ""); if (string.IsNullOrEmpty(dir)) return; Run(dir, Path.Combine(dir, "_frames")); } public static void BuildAndShoot() { string outDir = Arg("-stageOut"); string frameDir = Arg("-stageFrames"); if (string.IsNullOrEmpty(outDir)) throw new Exception("-stageOut がない"); if (string.IsNullOrEmpty(frameDir)) frameDir = Path.Combine(outDir, "_frames"); Run(outDir, frameDir); } static void Run(string outDir, string frameDir) { Directory.CreateDirectory(outDir); Directory.CreateDirectory(frameDir); var st = Build(); EditorSceneManager.SaveScene(EditorSceneManager.GetActiveScene(), ScenePath); AssetDatabase.SaveAssets(); SetMonitor(st, true, 1f); SetLamp(st, 1f); SavePng(Shoot(st.cam), Path.Combine(outDir, "stage_bg.png")); SetMonitor(st, false, 1f); SavePng(Shoot(st.cam), Path.Combine(outDir, "stage_bg_off.png")); SetMonitor(st, true, 1f); st.checkL.SetActive(true); st.checkR.SetActive(true); SavePng(Shoot(st.cam), Path.Combine(outDir, "stage_check.png")); st.checkL.SetActive(false); st.checkR.SetActive(false); File.WriteAllText(Path.Combine(outDir, "stage_meta.json"), Meta(st), new UTF8Encoding(false)); // ループ:周期 6 秒で割り切れる波だけを足すので、180 コマ目は 0 コマ目と同じになる int n = Mathf.RoundToInt(LoopFps * LoopSec); for (int i = 0; i < n; i++) { float t = i / (float)LoopFps; SetLamp(st, 1f + Wave(t, 3, 0.030f, 0f) + Wave(t, 7, 0.020f, 1.3f) + Wave(t, 13, 0.012f, 0.4f)); SetMonitor(st, true, 1f + Wave(t, 2, 0.020f, 0.7f) + Wave(t, 11, 0.008f, 2.1f)); SavePng(Shoot(st.cam), Path.Combine(frameDir, string.Format("frame_{0:0000}.png", i))); } SetLamp(st, 1f); SetMonitor(st, true, 1f); Debug.Log("[VideoStage] done out=" + outDir + " frames=" + n); } static float Wave(float t, int cycles, float amp, float phase) { return amp * Mathf.Sin(2f * Mathf.PI * cycles * t / LoopSec + phase); } // ---------------------------------------------------------------- 組み立て static Stage Build() { EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); if (!AssetDatabase.IsValidFolder(MatDir)) AssetDatabase.CreateFolder(Root, "Materials"); mats.Clear(); var st = new Stage(); RenderSettings.skybox = null; RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat; RenderSettings.ambientLight = new Color(0.035f, 0.032f, 0.030f); RenderSettings.defaultReflectionMode = UnityEngine.Rendering.DefaultReflectionMode.Custom; RenderSettings.reflectionIntensity = 0.2f; RenderSettings.fog = false; // 色(暗い茶・灰。派手なのはモニターのふちと床の黄色だけ) Mat("Floor", Hex("#2A2724"), 0.25f); Mat("Wall", Hex("#3A322B"), 0.05f); Mat("WallSide", Hex("#2E2925"), 0.05f); Mat("Ceil", Hex("#141210"), 0.0f); Mat("Wood", Hex("#4A3727"), 0.15f); Mat("Metal", Hex("#6A6A6C"), 0.45f, 0.5f); Mat("DarkMetal", Hex("#262628"), 0.4f, 0.5f); Mat("BoxA", Hex("#5A4632"), 0.1f); Mat("BoxB", Hex("#4A4E52"), 0.15f); Mat("BoxC", Hex("#3E3A36"), 0.1f); Mat("BoxD", Hex("#5E5248"), 0.1f); Mat("Bezel", Hex("#C4471F"), 0.35f, 0f, Hex("#C4471F") * 0.08f); Mat("MonitorBack", Hex("#101012"), 0.3f); Mat("RingY", Hex("#E2B21C"), 0.3f, 0f, Hex("#E2B21C") * 0.06f); Mat("RingK", Hex("#121212"), 0.3f); Mat("LampHead", Hex("#3A3530"), 0.4f, 0.5f, new Color(1f, 0.72f, 0.42f) * 0.6f); Mat("CheckGray", Hex("#8A8A8A"), 0.2f, 0f, Hex("#8A8A8A") * 0.25f); // 画面は明かりを受けない一様な色(中身をあとで重ねるので、映り込みを出さない) var screen = Mat("Screen", ScreenOn, 0f); screen.shader = Shader.Find("Unlit/Color"); screen.SetColor("_Color", ScreenOn); st.screenMat = screen; BuildRoom(); BuildShelves(); BuildRing(); st.cam = BuildCamera(); st.glowCorners = BuildMonitor(st.cam); st.lamp = BuildWorkbench(); st.glow = BuildLights(st.glowCorners); st.checkL = CheckCapsule("check_1.70m", FootL, 1.70f); st.checkR = CheckCapsule("check_1.15m", FootR, 1.15f); st.checkL.SetActive(false); st.checkR.SetActive(false); return st; } static void BuildRoom() { var room = new GameObject("Room").transform; float w = RoomHalfW * 2f, d = WallZ - FrontZ; Box("Floor", room, new Vector3(0, -0.05f, (WallZ + FrontZ) / 2f), new Vector3(w, 0.1f, d), "Floor"); Box("BackWall", room, new Vector3(0, CeilY / 2f, WallZ + 0.05f), new Vector3(w, CeilY, 0.1f), "Wall"); Box("WallL", room, new Vector3(-RoomHalfW - 0.05f, CeilY / 2f, (WallZ + FrontZ) / 2f), new Vector3(0.1f, CeilY, d), "WallSide"); Box("WallR", room, new Vector3(RoomHalfW + 0.05f, CeilY / 2f, (WallZ + FrontZ) / 2f), new Vector3(0.1f, CeilY, d), "WallSide"); Box("Ceiling", room, new Vector3(0, CeilY + 0.05f, (WallZ + FrontZ) / 2f), new Vector3(w, 0.1f, d), "Ceil"); // 壁の下の巾木と、天井の梁(暗いまま。上の見出しの所を暗く保つ) Box("Baseboard", room, new Vector3(0, 0.06f, WallZ - 0.01f), new Vector3(w, 0.12f, 0.03f), "DarkMetal"); for (int i = 0; i < 3; i++) Box("Beam" + i, room, new Vector3(0, CeilY - 0.15f, WallZ - 0.6f - i * 2.2f), new Vector3(w, 0.3f, 0.25f), "Ceil"); // 壁ぞいの配管 Cyl("Pipe", room, new Vector3(0, 3.55f, WallZ - 0.12f), new Vector3(0.08f, RoomHalfW, 0.08f), "DarkMetal", new Vector3(0, 0, 90)); } static void BuildShelves() { var root = new GameObject("Shelves").transform; var rng = new System.Random(99); string[] boxMats = { "BoxA", "BoxB", "BoxC", "BoxD" }; // 奥の壁の左右に背の高い棚(モニターの外側) foreach (float cx in new[] { -2.85f, 2.85f }) Shelf(root, new Vector3(cx, 0, WallZ - 0.25f), 1.3f, 2.3f, 0.45f, 5, rng, boxMats, 0f); // モニターの下の低い棚 Shelf(root, new Vector3(-0.9f, 0, WallZ - 0.22f), 1.7f, 0.62f, 0.4f, 2, rng, boxMats, 0f); Shelf(root, new Vector3(0.9f, 0, WallZ - 0.22f), 1.7f, 0.62f, 0.4f, 2, rng, boxMats, 0f); // 左右の壁ぞいの棚(奥から手前へ) Shelf(root, new Vector3(-RoomHalfW + 0.25f, 0, 1.2f), 1.6f, 2.1f, 0.45f, 4, rng, boxMats, 90f); Shelf(root, new Vector3(-RoomHalfW + 0.25f, 0, -0.6f), 1.6f, 2.1f, 0.45f, 4, rng, boxMats, 90f); Shelf(root, new Vector3(RoomHalfW - 0.25f, 0, -0.4f), 1.6f, 2.1f, 0.45f, 4, rng, boxMats, -90f); } static void Shelf(Transform parent, Vector3 pos, float width, float height, float depth, int levels, System.Random rng, string[] boxMats, float yaw) { var s = new GameObject("Shelf").transform; s.SetParent(parent, false); s.localPosition = pos; s.localRotation = Quaternion.Euler(0, yaw, 0); float post = 0.04f; foreach (float sx in new[] { -width / 2f + post / 2f, width / 2f - post / 2f }) foreach (float sz in new[] { -depth / 2f + post / 2f, depth / 2f - post / 2f }) Box("Post", s, new Vector3(sx, height / 2f, sz), new Vector3(post, height, post), "DarkMetal"); for (int i = 0; i < levels; i++) { float y = 0.08f + i * (height - 0.1f) / Mathf.Max(1, levels - 1); if (levels == 1) y = height; Box("Board", s, new Vector3(0, y, 0), new Vector3(width, 0.025f, depth), "Wood"); if (i == levels - 1 && height > 1f) continue; // いちばん上の板には何も置かない float space = (levels > 1 ? (height - 0.1f) / (levels - 1) : 0.4f) - 0.06f; float x = -width / 2f + 0.08f; while (true) { float bw = 0.18f + (float)rng.NextDouble() * 0.22f; if (x + bw > width / 2f - 0.08f) break; if (rng.NextDouble() < 0.82) { float bh = Mathf.Min(space, 0.12f + (float)rng.NextDouble() * 0.2f); float bd = depth * (0.6f + (float)rng.NextDouble() * 0.3f); Box("PartsBox", s, new Vector3(x + bw / 2f, y + 0.0125f + bh / 2f, (float)(rng.NextDouble() - 0.5) * 0.04f), new Vector3(bw, bh, bd), boxMats[rng.Next(boxMats.Length)]); } x += bw + 0.03f + (float)rng.NextDouble() * 0.05f; } } } static void BuildRing() { var ring = new GameObject("FloorRing").transform; float seg = 2f * Mathf.PI * RingR / RingSegments; for (int i = 0; i < RingSegments; i++) { float a = (i + 0.5f) * 2f * Mathf.PI / RingSegments; var p = new Vector3(Mathf.Cos(a) * RingR, 0.003f, Mathf.Sin(a) * RingR); var go = Box("Dash" + i, ring, p, new Vector3(RingWidth, 0.006f, seg * 1.02f), i % 2 == 0 ? "RingY" : "RingK"); go.transform.localRotation = Quaternion.Euler(0, -a * Mathf.Rad2Deg, 0); } } static Camera BuildCamera() { var go = new GameObject("StageCamera"); var cam = go.AddComponent(); cam.fieldOfView = VFov; cam.nearClipPlane = 0.1f; cam.farClipPlane = 40f; cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = Color.black; cam.renderingPath = RenderingPath.DeferredShading; // 明かりの数の上限(品質設定)を変えずに済む cam.allowMSAA = false; cam.aspect = W / (float)H; go.transform.rotation = Quaternion.Euler(Pitch, 0, 0); // 楕円の中心が y=RingCenterPy に来る高さを二分法で探す float lo = 0.3f, hi = 10f; for (int i = 0; i < 60; i++) { float h = (lo + hi) / 2f; go.transform.position = new Vector3(0, h, -CamDist); if (RingBox(cam).center.y > RingCenterPy) hi = h; else lo = h; } go.transform.position = new Vector3(0, (lo + hi) / 2f, -CamDist); return cam; } // モニターは画面の面がカメラの画面と平行になるように前へ傾けて掛ける(画面の上で長方形に見える) static Vector3[] BuildMonitor(Camera cam) { var fwd = cam.transform.forward; var centerRay = Ray(cam, (GlowLeft + GlowRight) / 2f, (GlowTop + GlowBottom) / 2f); float tc = (WallZ - 0.42f - centerRay.origin.z) / centerRay.direction.z; var planeP = centerRay.origin + centerRay.direction * tc; var plane = new Plane(-fwd, planeP); Vector3 Hit(float px, float py) { var r = Ray(cam, px, py); plane.Raycast(r, out float d); return r.origin + r.direction * d; } var c = new[] { Hit(GlowLeft, GlowTop), Hit(GlowRight, GlowTop), Hit(GlowRight, GlowBottom), Hit(GlowLeft, GlowBottom) }; float gw = Vector3.Distance(c[0], c[1]), gh = Vector3.Distance(c[0], c[3]); var center = (c[0] + c[2]) / 2f; var mon = new GameObject("WallMonitor").transform; mon.position = center; mon.rotation = cam.transform.rotation; Box("Body", mon, new Vector3(0, 0, 0.04f), new Vector3(gw + 2f * Bezel, gh + 2f * Bezel, 0.07f), "MonitorBack"); // ふち(オレンジ):4本の細い棒 Box("BezelTop", mon, new Vector3(0, gh / 2f + Bezel / 2f, -0.002f), new Vector3(gw + 2f * Bezel, Bezel, 0.012f), "Bezel"); Box("BezelBottom", mon, new Vector3(0, -gh / 2f - Bezel / 2f, -0.002f), new Vector3(gw + 2f * Bezel, Bezel, 0.012f), "Bezel"); Box("BezelL", mon, new Vector3(-gw / 2f - Bezel / 2f, 0, -0.002f), new Vector3(Bezel, gh, 0.012f), "Bezel"); Box("BezelR", mon, new Vector3(gw / 2f + Bezel / 2f, 0, -0.002f), new Vector3(Bezel, gh, 0.012f), "Bezel"); Box("Screen", mon, new Vector3(0, 0, 0f), new Vector3(gw, gh, 0.004f), "Screen"); // 壁へのアーム(上と下の2本。傾いた分の奥行きを埋める) var root = new GameObject("MonitorMount").transform; foreach (var y in new[] { center.y + gh * 0.3f, center.y - gh * 0.3f }) { float zFront = planeP.z + (y - planeP.y) * Mathf.Tan(Pitch * Mathf.Deg2Rad) + 0.08f; float len = Mathf.Max(0.05f, WallZ - zFront); Box("Arm", root, new Vector3(0, y, zFront + len / 2f), new Vector3(0.4f, 0.08f, len), "DarkMetal"); } // ふちの外側の4隅(光る部分の4隅は c) return c; } static Light BuildWorkbench() { var b = new GameObject("Workbench").transform; b.position = new Vector3(1.7f, 0, 0.95f); b.rotation = Quaternion.Euler(0, -15f, 0); float tw = 1.4f, td = 0.75f, th = 0.88f; Box("Top", b, new Vector3(0, th, 0), new Vector3(tw, 0.05f, td), "Wood"); foreach (float sx in new[] { -tw / 2f + 0.05f, tw / 2f - 0.05f }) foreach (float sz in new[] { -td / 2f + 0.05f, td / 2f - 0.05f }) Box("Leg", b, new Vector3(sx, th / 2f, sz), new Vector3(0.05f, th, 0.05f), "DarkMetal"); Box("LowShelf", b, new Vector3(0, 0.2f, 0), new Vector3(tw - 0.1f, 0.03f, td - 0.1f), "Wood"); Box("BinA", b, new Vector3(-0.35f, 0.3f, 0), new Vector3(0.4f, 0.17f, 0.45f), "BoxA"); Box("BinB", b, new Vector3(0.2f, 0.29f, 0.05f), new Vector3(0.35f, 0.15f, 0.4f), "BoxB"); // ロボットアームらしい形(台座・肩・上腕・ひじ・前腕・手) var arm = new GameObject("RobotArm").transform; arm.SetParent(b, false); arm.localPosition = new Vector3(0.12f, th + 0.025f, 0.1f); arm.localRotation = Quaternion.Euler(0, 150f, 0); arm.localScale = Vector3.one * 0.62f; // 卓上の小さいアーム(高さ約 0.37m。モニターにかからない) Cyl("Base", arm, new Vector3(0, 0.04f, 0), new Vector3(0.24f, 0.04f, 0.24f), "DarkMetal"); Cyl("Turret", arm, new Vector3(0, 0.13f, 0), new Vector3(0.15f, 0.06f, 0.15f), "Metal"); Cyl("Shoulder", arm, new Vector3(0, 0.22f, 0), new Vector3(0.12f, 0.07f, 0.12f), "DarkMetal", new Vector3(90, 0, 0)); var upper = Box("UpperArm", arm, Vector3.zero, new Vector3(0.08f, 0.36f, 0.08f), "Metal"); upper.transform.localPosition = new Vector3(0.07f, 0.22f + 0.17f, 0); upper.transform.localRotation = Quaternion.Euler(0, 0, -22f); Cyl("Elbow", arm, new Vector3(0.14f, 0.55f, 0), new Vector3(0.1f, 0.06f, 0.1f), "DarkMetal", new Vector3(90, 0, 0)); var fore = Box("Forearm", arm, Vector3.zero, new Vector3(0.3f, 0.065f, 0.065f), "Metal"); fore.transform.localPosition = new Vector3(0.27f, 0.5f, 0); fore.transform.localRotation = Quaternion.Euler(0, 0, -20f); Cyl("Wrist", arm, new Vector3(0.41f, 0.44f, 0), new Vector3(0.06f, 0.04f, 0.06f), "DarkMetal"); Box("FingerA", arm, new Vector3(0.40f, 0.37f, 0.025f), new Vector3(0.02f, 0.08f, 0.015f), "DarkMetal"); Box("FingerB", arm, new Vector3(0.40f, 0.37f, -0.025f), new Vector3(0.02f, 0.08f, 0.015f), "DarkMetal"); // 卓上ライト var lamp = new GameObject("DeskLamp").transform; lamp.SetParent(b, false); lamp.localPosition = new Vector3(-0.5f, th + 0.025f, -0.08f); Cyl("LampBase", lamp, new Vector3(0, 0.012f, 0), new Vector3(0.14f, 0.012f, 0.14f), "DarkMetal"); var pole = Box("LampPole", lamp, Vector3.zero, new Vector3(0.018f, 0.38f, 0.018f), "DarkMetal"); pole.transform.localPosition = new Vector3(-0.03f, 0.2f, 0); pole.transform.localRotation = Quaternion.Euler(0, 0, 12f); var head = Cyl("LampHead", lamp, new Vector3(-0.1f, 0.37f, 0), new Vector3(0.14f, 0.06f, 0.14f), "LampHead", new Vector3(0, 0, 30f)); Box("LampShade", lamp, new Vector3(-0.1f, 0.40f, 0), new Vector3(0.1f, 0.04f, 0.1f), "DarkMetal").transform.localRotation = Quaternion.Euler(0, 0, 30f); var lgo = new GameObject("DeskLampLight"); lgo.transform.SetParent(lamp, false); lgo.transform.localPosition = new Vector3(-0.12f, 0.3f, 0); var l = lgo.AddComponent(); l.type = LightType.Point; l.color = new Color(1f, 0.68f, 0.38f); l.intensity = LampIntensity; l.range = 2.6f; l.shadows = LightShadows.Soft; l.lightmapBakeType = LightmapBakeType.Realtime; return l; } static Light BuildLights(Vector3[] glow) { var root = new GameObject("Lights").transform; // 奥の暖色の明かり(棚のあたり・壁の下の方をぼんやり照らす) PointLight(root, "BackWarmL", new Vector3(-2.6f, 1.5f, WallZ - 0.9f), new Color(1f, 0.62f, 0.32f), 1.7f, 3.6f, true); PointLight(root, "BackWarmR", new Vector3(2.9f, 1.9f, WallZ - 0.9f), new Color(1f, 0.62f, 0.32f), 0.9f, 3.0f, false); PointLight(root, "UnderMonitor", new Vector3(0, 0.35f, WallZ - 0.7f), new Color(1f, 0.6f, 0.3f), 1.0f, 2.4f, false); PointLight(root, "SideWarmL", new Vector3(-RoomHalfW + 0.9f, 1.8f, 0.4f), new Color(1f, 0.6f, 0.32f), 0.45f, 2.6f, false); // 円の上の弱い明かり(手前は暗めのまま、円と立つ所だけ少し見える) var s = new GameObject("RingSpot"); s.transform.SetParent(root, false); s.transform.position = new Vector3(0, CeilY - 0.2f, -0.6f); s.transform.rotation = Quaternion.Euler(90, 0, 0); var sl = s.AddComponent(); sl.type = LightType.Spot; sl.spotAngle = 75f; sl.innerSpotAngle = 30f; sl.color = new Color(1f, 0.84f, 0.68f); sl.intensity = 0.9f; sl.range = 6f; sl.shadows = LightShadows.Soft; sl.lightmapBakeType = LightmapBakeType.Realtime; // モニターの光が前に落ちる(青み) var gc = (glow[0] + glow[2]) / 2f; return PointLight(root, "MonitorGlow", gc + new Vector3(0, -0.4f, -1.0f), new Color(0.35f, 0.45f, 0.85f), GlowLightIntensity, 4.0f, false); } static Light PointLight(Transform parent, string name, Vector3 pos, Color c, float intensity, float range, bool shadows) { var go = new GameObject(name); go.transform.SetParent(parent, false); go.transform.position = pos; var l = go.AddComponent(); l.type = LightType.Point; l.color = c; l.intensity = intensity; l.range = range; l.shadows = shadows ? LightShadows.Soft : LightShadows.None; l.lightmapBakeType = LightmapBakeType.Realtime; return l; } static GameObject CheckCapsule(string name, Vector3 foot, float height) { var go = GameObject.CreatePrimitive(PrimitiveType.Capsule); go.name = name; UnityEngine.Object.DestroyImmediate(go.GetComponent()); float r = height * 0.13f; go.transform.position = foot + new Vector3(0, height / 2f, 0); go.transform.localScale = new Vector3(r * 2f, height / 2f, r * 2f); go.GetComponent().sharedMaterial = mats["CheckGray"]; return go; } // ---------------------------------------------------------------- 撮る static void SetMonitor(Stage st, bool on, float k) { st.screenMat.SetColor("_Color", on ? ScreenOn * k : Color.black); st.glow.enabled = on; st.glow.intensity = GlowLightIntensity * k; } static void SetLamp(Stage st, float k) { st.lamp.intensity = LampIntensity * k; } static Texture2D Shoot(Camera cam) { var big = RenderTexture.GetTemporary(W * SuperSample, H * SuperSample, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default); var small = RenderTexture.GetTemporary(W, H, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default); big.filterMode = FilterMode.Bilinear; cam.targetTexture = big; cam.Render(); cam.targetTexture = null; Graphics.Blit(big, small); // 2倍→1倍。1倍の画素の中心は2×2の真ん中なので4つの平均になる var prev = RenderTexture.active; RenderTexture.active = small; var tex = new Texture2D(W, H, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(0, 0, W, H), 0, 0); tex.Apply(); RenderTexture.active = prev; RenderTexture.ReleaseTemporary(big); RenderTexture.ReleaseTemporary(small); return tex; } static void SavePng(Texture2D tex, string path) { File.WriteAllBytes(path, tex.EncodeToPNG()); UnityEngine.Object.DestroyImmediate(tex); } // ---------------------------------------------------------------- 数(画面の px・左上が原点) static Vector2 Px(Camera cam, Vector3 world) { var v = cam.WorldToViewportPoint(world); return new Vector2(v.x * W, (1f - v.y) * H); } // 床の円(線の真ん中の半径 1.5m)を 720 点で画面に写した外接の箱 static Rect RingBox(Camera cam) { float minX = 1e9f, maxX = -1e9f, minY = 1e9f, maxY = -1e9f; for (int i = 0; i < 720; i++) { float a = i * 2f * Mathf.PI / 720f; var p = Px(cam, new Vector3(Mathf.Cos(a) * RingR, 0, Mathf.Sin(a) * RingR)); minX = Mathf.Min(minX, p.x); maxX = Mathf.Max(maxX, p.x); minY = Mathf.Min(minY, p.y); maxY = Mathf.Max(maxY, p.y); } return Rect.MinMaxRect(minX, minY, maxX, maxY); } static Ray Ray(Camera cam, float px, float py) { return cam.ViewportPointToRay(new Vector3(px / W, 1f - py / H, 0)); } static string Meta(Stage st) { var cam = st.cam; var ci = CultureInfo.InvariantCulture; string F(float v) => v.ToString("0.0", ci); string F3(float v) => v.ToString("0.###", ci); string P(Vector2 p) => "[" + F(p.x) + ", " + F(p.y) + "]"; string V(Vector3 p) => "[" + F3(p.x) + ", " + F3(p.y) + ", " + F3(p.z) + "]"; var box = RingBox(cam); var ringC = box.center; var ringFloorC = Px(cam, Vector3.zero); float pxPerM = Px(cam, new Vector3(0.5f, 0, 0)).x - Px(cam, new Vector3(-0.5f, 0, 0)).x; float pxPerMFeet = Px(cam, FootR + new Vector3(0.5f, 0, 0)).x - Px(cam, FootR + new Vector3(-0.5f, 0, 0)).x; var fl = Px(cam, FootL); var fr = Px(cam, FootR); float hL170 = fl.y - Px(cam, FootL + Vector3.up * 1.70f).y; float hR115 = fr.y - Px(cam, FootR + Vector3.up * 1.15f).y; float hR170 = fr.y - Px(cam, FootR + Vector3.up * 1.70f).y; float hFov = 2f * Mathf.Atan(Mathf.Tan(VFov * Mathf.Deg2Rad / 2f) * W / H) * Mathf.Rad2Deg; var g = st.glowCorners; var sb = new StringBuilder(); sb.Append("{\n"); sb.Append(" \"image_size\": [" + W + ", " + H + "],\n"); sb.Append(" \"origin\": \"top-left, px\",\n"); sb.Append(" \"screen_corners\": [" + P(Px(cam, g[0])) + ", " + P(Px(cam, g[1])) + ", " + P(Px(cam, g[2])) + ", " + P(Px(cam, g[3])) + "],\n"); sb.Append(" \"screen_corners_order\": \"top-left, top-right, bottom-right, bottom-left\",\n"); sb.Append(" \"ring_center\": " + P(ringC) + ",\n"); sb.Append(" \"ring_rx\": " + F(box.width / 2f) + ",\n"); sb.Append(" \"ring_ry\": " + F(box.height / 2f) + ",\n"); sb.Append(" \"ring_center_floor_point\": " + P(ringFloorC) + ",\n"); sb.Append(" \"ring_note\": \"ring_center/rx/ry = bounding box of the projected ring (line center, radius 1.5 m, line width 0.10 m). ring_center_floor_point = projection of the ring's center on the floor\",\n"); sb.Append(" \"foot_left\": " + P(fl) + ",\n"); sb.Append(" \"foot_right\": " + P(fr) + ",\n"); sb.Append(" \"foot_world_m\": {\"left\": " + V(FootL) + ", \"right\": " + V(FootR) + "},\n"); sb.Append(" \"px_per_m_at_ring\": " + F(pxPerM) + ",\n"); sb.Append(" \"px_per_m_at_feet\": " + F(pxPerMFeet) + ",\n"); sb.Append(" \"person_height_px\": {\"left_1.70m\": " + F(hL170) + ", \"right_1.15m\": " + F(hR115) + ", \"right_1.70m\": " + F(hR170) + "},\n"); sb.Append(" \"camera\": {\n"); sb.Append(" \"position_m\": " + V(cam.transform.position) + ",\n"); sb.Append(" \"rotation_euler_deg\": " + V(cam.transform.eulerAngles) + ",\n"); sb.Append(" \"pitch_down_deg\": " + F3(Pitch) + ",\n"); sb.Append(" \"vertical_fov_deg\": " + F3(VFov) + ",\n"); sb.Append(" \"horizontal_fov_deg\": " + F3(hFov) + ",\n"); sb.Append(" \"near_far_m\": [" + F3(cam.nearClipPlane) + ", " + F3(cam.farClipPlane) + "],\n"); sb.Append(" \"axes\": \"Unity world, y up, z toward the back wall, meters. ring center at (0,0,0)\"\n"); sb.Append(" },\n"); sb.Append(" \"loop\": {\"fps\": " + LoopFps + ", \"seconds\": " + F3(LoopSec) + ", \"frames\": " + Mathf.RoundToInt(LoopFps * LoopSec) + "}\n"); sb.Append("}\n"); return sb.ToString(); } // ---------------------------------------------------------------- 小道具 static Material Mat(string name, Color albedo, float smooth, float metal = 0f, Color? emission = null) { string path = MatDir + "/" + name + ".mat"; var m = AssetDatabase.LoadAssetAtPath(path); if (m == null) { m = new Material(Shader.Find("Standard")); AssetDatabase.CreateAsset(m, path); } m.shader = Shader.Find("Standard"); m.SetColor("_Color", albedo); m.SetFloat("_Glossiness", smooth); m.SetFloat("_Metallic", metal); if (emission.HasValue) { m.EnableKeyword("_EMISSION"); m.globalIlluminationFlags = MaterialGlobalIlluminationFlags.None; m.SetColor("_EmissionColor", emission.Value); } else { m.DisableKeyword("_EMISSION"); m.SetColor("_EmissionColor", Color.black); } EditorUtility.SetDirty(m); mats[name] = m; return m; } static GameObject Box(string name, Transform parent, Vector3 localPos, Vector3 size, string mat) { return Prim(PrimitiveType.Cube, name, parent, localPos, size, mat, Vector3.zero); } static GameObject Cyl(string name, Transform parent, Vector3 localPos, Vector3 scale, string mat, Vector3? euler = null) { return Prim(PrimitiveType.Cylinder, name, parent, localPos, scale, mat, euler ?? Vector3.zero); } static GameObject Prim(PrimitiveType t, string name, Transform parent, Vector3 localPos, Vector3 scale, string mat, Vector3 euler) { var go = GameObject.CreatePrimitive(t); go.name = name; UnityEngine.Object.DestroyImmediate(go.GetComponent()); if (parent != null) go.transform.SetParent(parent, false); go.transform.localPosition = localPos; go.transform.localRotation = Quaternion.Euler(euler); go.transform.localScale = scale; go.GetComponent().sharedMaterial = mats[mat]; return go; } static Color Hex(string h) { ColorUtility.TryParseHtmlString(h, out var c); return c; } static string Arg(string name) { var a = Environment.GetCommandLineArgs(); for (int i = 0; i < a.Length - 1; i++) if (a[i] == name) return a[i + 1]; return null; } } }