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
{
    /// <summary>
    /// ショート動画（縦 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 で外から）
    /// </summary>
    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<string, Material> mats = new Dictionary<string, Material>();

        [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<Camera>();
            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<Light>();
            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<Light>();
            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<Light>();
            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<Collider>());
            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<Renderer>().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<Material>(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<Collider>());
            if (parent != null) go.transform.SetParent(parent, false);
            go.transform.localPosition = localPos;
            go.transform.localRotation = Quaternion.Euler(euler);
            go.transform.localScale = scale;
            go.GetComponent<Renderer>().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;
        }
    }
}
