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;
}
}
}