#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; /// Authors HomeCamp through Pipeline; never constructs the scene at runtime. public static class BuildHomeCamp { const string Root = "Assets/Bundles/Scenes/Town/HomeCamp"; const string Town = "Assets/Bundles/Scenes/Town/TownSquare"; const string PrefabPath = Root + "/HomeCamp.prefab"; const string ScenePath = Root + "/HomeCamp.unity"; const int Layer = 29; static readonly string[] Parts = { "ground", "mainhouse", "cottage", "fountain" }; static string Repo => Directory.GetParent(Application.dataPath).Parent.FullName; static string Review => Path.Combine(Repo, "Tools/output/homecamp_review"); [Serializable] class Receipt { public string status = ""; public int triangles = 0; public int targetTriangles = 0; } public static string Inspect() { return string.Join("\n", Enumerable.Range(0, SceneManager.sceneCount).Select(i => { var s = SceneManager.GetSceneAt(i); return s.path + " dirty=" + s.isDirty; })); } static void Folder(string path) { if (AssetDatabase.IsValidFolder(path)) return; var parent = path.Substring(0, path.LastIndexOf('/')); Folder(parent); AssetDatabase.CreateFolder(parent, Path.GetFileName(path)); } static Bounds BoundsOf(GameObject go) { var renderers = go.GetComponentsInChildren(true); if (renderers.Length == 0) throw new InvalidOperationException("No renderer: " + go.name); var b = renderers[0].bounds; foreach (var r in renderers.Skip(1)) b.Encapsulate(r.bounds); return b; } static void SetLayer(GameObject go) { foreach (var t in go.GetComponentsInChildren(true)) t.gameObject.layer = Layer; } static void Source(string source, string destination) { if (!File.Exists(source)) throw new FileNotFoundException(source); Folder(Path.GetDirectoryName(destination).Replace('\\', '/')); File.Copy(source, destination, true); AssetDatabase.ImportAsset(destination, ImportAssetOptions.ForceSynchronousImport); } static void TextureSettings(string path, bool srgb, bool backdrop = false) { var importer = (TextureImporter)AssetImporter.GetAtPath(path); importer.textureType = TextureImporterType.Default; importer.sRGBTexture = srgb; importer.alphaIsTransparency = false; importer.mipmapEnabled = !backdrop; importer.maxTextureSize = 2048; importer.textureCompression = TextureImporterCompression.Uncompressed; importer.wrapMode = TextureWrapMode.Clamp; importer.SaveAndReimport(); } static Material SaveMaterial(string path, string shaderName) { var shader = Shader.Find(shaderName); if (!shader) throw new InvalidOperationException("Missing shader " + shaderName); var mat = AssetDatabase.LoadAssetAtPath(path); if (!mat) { mat = new Material(shader); AssetDatabase.CreateAsset(mat, path); } mat.shader = shader; return mat; } public static string BuildAssets() { if (EditorApplication.isPlaying) throw new InvalidOperationException("Stop Play before authoring."); AssetDatabase.ImportAsset("Assets/Bundles/Shaders/XShader/HomeCampXPbr.shader", ImportAssetOptions.ForceSynchronousImport); // Validate all four receipts before importing any model. foreach (var part in Parts) { var path = Path.Combine(Repo, "Tools/output/homecamp_studio", part, "production.json"); var receipt = JsonUtility.FromJson(File.ReadAllText(path)); if (receipt.status != "ready_for_unity" || receipt.triangles * 10 > receipt.targetTriangles * 13) throw new InvalidOperationException("Unfinished model " + part); } foreach (var part in Parts) { var dir = Root + "/" + part; var source = Path.Combine(Repo, "Tools/output/homecamp_studio", part, "xpbr"); foreach (var suffix in new[] { ".fbx", "_base.png", "_mix.png" }) Source(Path.Combine(source, part + suffix), dir + "/" + part + suffix); var fbx = dir + "/" + part + ".fbx"; var importer = (ModelImporter)AssetImporter.GetAtPath(fbx); importer.globalScale = 1; importer.useFileScale = false; importer.bakeAxisConversion = true; importer.materialImportMode = ModelImporterMaterialImportMode.None; importer.meshCompression = ModelImporterMeshCompression.Off; importer.isReadable = part == "ground"; importer.addCollider = false; importer.SaveAndReimport(); TextureSettings(dir + "/" + part + "_base.png", true); TextureSettings(dir + "/" + part + "_mix.png", false); var mat = SaveMaterial(dir + "/" + part + ".mat", "Test/HomeCampXPbr"); mat.SetTexture("_MainTex", AssetDatabase.LoadAssetAtPath(dir + "/" + part + "_base.png")); mat.SetTexture("_MixTex", AssetDatabase.LoadAssetAtPath(dir + "/" + part + "_mix.png")); mat.SetColor("_BaseColor", Color.white); mat.SetFloat("_Metallic", 1); mat.SetFloat("_Roughness", 1); mat.SetFloat("_AO", 1); EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat); var scene = EditorSceneManager.NewPreviewScene(); try { var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(fbx), scene); go.name = part; go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one; foreach (var r in go.GetComponentsInChildren()) r.sharedMaterial = mat; if (part == "ground") foreach (var f in go.GetComponentsInChildren()) { var c = f.gameObject.AddComponent(); c.sharedMesh = f.sharedMesh; c.convex = false; } PrefabUtility.SaveAsPrefabAsset(go, dir + "/" + part + ".prefab"); } finally { EditorSceneManager.ClosePreviewScene(scene); } } Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp/backdrop.png"), Root + "/backdrop.png"); TextureSettings(Root + "/backdrop.png", true, true); var background = SaveMaterial(Root + "/backdrop.mat", "Universal Render Pipeline/Unlit"); background.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath(Root + "/backdrop.png")); background.SetColor("_BaseColor", Color.white); EditorUtility.SetDirty(background); AssetDatabase.SaveAssetIfDirty(background); foreach (var name in new[] { "tree_card", "flower_card" }) { Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp", name + ".png"), Root + "/" + name + ".png"); TextureSettings(Root + "/" + name + ".png", true); var ti = (TextureImporter)AssetImporter.GetAtPath(Root + "/" + name + ".png"); ti.alphaIsTransparency = true; ti.SaveAndReimport(); var card = SaveMaterial(Root + "/" + name + ".mat", "Universal Render Pipeline/Unlit"); card.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath(Root + "/" + name + ".png")); card.SetColor("_BaseColor", Color.white); card.SetFloat("_AlphaClip", 1); card.SetFloat("_Cutoff", .55f); card.EnableKeyword("_ALPHATEST_ON"); card.SetFloat("_Cull", 0); card.renderQueue = 2450; card.SetOverrideTag("RenderType", "TransparentCutout"); EditorUtility.SetDirty(card); AssetDatabase.SaveAssetIfDirty(card); } BuildGroundSurface(); return "Imported four Studio assets + XPbr, authored four component Prefabs and unlit background material."; } static void BuildGroundSurface() { Source(Path.Combine(Repo, "Doc/Design/Scene/HomeCamp/ground_surface.png"), Root + "/ground_surface.png"); TextureSettings(Root + "/ground_surface.png", true); var mat = SaveMaterial(Root + "/ground_surface.mat", "Universal Render Pipeline/Unlit"); mat.SetTexture("_BaseMap", AssetDatabase.LoadAssetAtPath(Root + "/ground_surface.png")); mat.SetColor("_BaseColor", Color.white); EditorUtility.SetDirty(mat); AssetDatabase.SaveAssetIfDirty(mat); var mesh = AssetDatabase.LoadAssetAtPath(Root + "/GroundSurface.asset"); if (!mesh) { mesh = new Mesh(); AssetDatabase.CreateAsset(mesh, Root + "/GroundSurface.asset"); } mesh.Clear(); mesh.name = "HomeCamp continuous ground surface"; mesh.vertices = new[] {new Vector3(-18,0,-21), new Vector3(-18,0,21), new Vector3(18,0,21), new Vector3(18,0,-21)}; mesh.uv = new[] {new Vector2(0,0),new Vector2(0,1),new Vector2(1,1),new Vector2(1,0)}; mesh.triangles = new[] {0,1,2,0,2,3}; mesh.RecalculateNormals(); mesh.RecalculateTangents(); mesh.RecalculateBounds(); EditorUtility.SetDirty(mesh); AssetDatabase.SaveAssetIfDirty(mesh); var scene = EditorSceneManager.NewPreviewScene(); try { var go = new GameObject("GroundSurface"); SceneManager.MoveGameObjectToScene(go, scene); go.AddComponent().sharedMesh = mesh; go.AddComponent().sharedMaterial = mat; go.AddComponent().sharedMesh = mesh; PrefabUtility.SaveAsPrefabAsset(go, Root + "/GroundSurface.prefab"); } finally { EditorSceneManager.ClosePreviewScene(scene); } } static GameObject Place(Transform parent, string path, string name, Vector3 pos, float size, bool width = false, bool faceCenter = false) { var prefab = AssetDatabase.LoadAssetAtPath(path); if (!prefab) throw new FileNotFoundException(path); var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent); go.name = name; go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one; var b = BoundsOf(go); var current = width ? Mathf.Max(b.size.x, b.size.z) : b.size.y; go.transform.localScale = Vector3.one * (size / current); if (faceCenter) { var away = new Vector3(pos.x, 0, pos.z - .8f); go.transform.localRotation = Quaternion.LookRotation(away, Vector3.up); } go.transform.position = pos; SetLayer(go); return go; } static string PartPath(string part) => Root + "/" + part + "/" + part + ".prefab"; static string ReusePath(string part) { if (part == "tree" || part == "bush") { var name = part == "bush" ? "shrub" : "tree"; return "Assets/Bundles/Scenes/Prototype/Battlefield/A/Components/" + name + "/" + name + ".prefab"; } return Town + "/" + part + "/" + part + ".prefab"; } static float GroundY(GameObject ground, float x, float z) { var ray = new Ray(new Vector3(x, 60, z), Vector3.down); var hits = ground.GetComponentsInChildren().Select(c => { RaycastHit hit; return c.Raycast(ray, out hit, 120) ? hit.point.y : float.NegativeInfinity; }).ToArray(); var y = hits.Max(); if (float.IsNegativeInfinity(y)) throw new InvalidOperationException("Ground ray missed at " + x + "," + z); return y; } static GameObject FoliageCard(Transform parent, string name, string image, Vector3 bottom, float height) { var go = GameObject.CreatePrimitive(PrimitiveType.Quad); go.name = name; go.transform.SetParent(parent, false); Object.DestroyImmediate(go.GetComponent()); var texture = AssetDatabase.LoadAssetAtPath(Root + "/" + image + ".png"); var rotation = Quaternion.LookRotation(new Vector3(0, 1.8f, 3) - new Vector3(0, 15, -27), Vector3.up); go.transform.rotation = rotation; go.transform.position = bottom + (rotation * Vector3.up) * height * .5f; go.transform.localScale = new Vector3(height * texture.width / texture.height, height, 1); var renderer = go.GetComponent(); renderer.sharedMaterial = AssetDatabase.LoadAssetAtPath(Root + "/" + image + ".mat"); renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = false; SetLayer(go); return go; } static Camera MakeCamera(Scene scene, Transform parent) { var go = new GameObject("HomeCampCamera"); SceneManager.MoveGameObjectToScene(go, scene); if (parent) go.transform.SetParent(parent, false); var camera = go.AddComponent(); go.transform.position = new Vector3(0, 15, -27); go.transform.LookAt(new Vector3(0, 1.8f, 3)); camera.fieldOfView = 48; camera.nearClipPlane = .1f; camera.farClipPlane = 180; camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(.46f, .72f, .88f); camera.cullingMask = 1 << Layer; camera.scene = scene; camera.allowHDR = false; camera.allowMSAA = true; return camera; } static void Light(Scene scene, Transform parent) { var go = new GameObject("HomeCampSun"); SceneManager.MoveGameObjectToScene(go, scene); if (parent) go.transform.SetParent(parent, false); var light = go.AddComponent(); light.type = LightType.Directional; light.color = new Color(1, .95f, .84f); light.intensity = 1.05f; light.shadows = LightShadows.Soft; light.cullingMask = 1 << Layer; go.transform.rotation = Quaternion.Euler(43, -32, 0); } static void Background(Transform parent, Camera camera) { var go = GameObject.CreatePrimitive(PrimitiveType.Quad); go.name = "DistantLakeWaterfallCastle"; go.transform.SetParent(parent, false); Object.DestroyImmediate(go.GetComponent()); var renderer = go.GetComponent(); renderer.sharedMaterial = AssetDatabase.LoadAssetAtPath(Root + "/backdrop.mat"); renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = false; const float distance = 75; var halfHeight = distance * Mathf.Tan(camera.fieldOfView * Mathf.Deg2Rad * .5f); var width = halfHeight * 2 * 1080f / 1920; var height = width / 1.5f; go.transform.position = camera.transform.position + camera.transform.forward * distance + camera.transform.up * (halfHeight - height * .5f); go.transform.rotation = camera.transform.rotation; go.transform.localScale = new Vector3(width * 1.04f, height * 1.04f, 1); SetLayer(go); } public static string Build() { if (EditorApplication.isPlaying) throw new InvalidOperationException("Author in Edit Mode."); var previous = SceneManager.GetActiveScene(); var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive); try { SceneManager.SetActiveScene(scene); var root = new GameObject("HomeCamp"); var ground = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(Root + "/GroundSurface.prefab"), root.transform); ground.name = "Ground"; SetLayer(ground); // Final source ground must be horizontal; reject a vertical generated plate. var gb = BoundsOf(ground); if (gb.size.y > Mathf.Min(gb.size.x, gb.size.z) * .3f) throw new InvalidOperationException("Ground requires offline orientation correction before assembly: " + gb.size); Physics.SyncTransforms(); var placements = new List(); void Put(string part, string name, float x, float z, float size, bool width = false, bool facing = false, bool reused = false) { var y = GroundY(ground, x, z); if (reused && (part == "tree" || part == "bush")) { FoliageCard(root.transform, name, part == "tree" ? "tree_card" : "flower_card", new Vector3(x,y,z), size); placements.Add(name + " card groundY=" + y.ToString("F3")); return; } var path = reused ? ReusePath(part) : PartPath(part); var go = Place(root.transform, path, name, new Vector3(x, y, z), size, width, facing); var bottom = BoundsOf(go).min.y; go.transform.position += Vector3.up * (y - bottom); placements.Add(name + " groundY=" + y.ToString("F3")); } Put("mainhouse", "MainHouse", 0, 7.2f, 6.5f, false, true); Put("mainhouse", "HouseLeft", -7.5f, 4, 4, false, true); Put("mainhouse", "HouseRight", 7.5f, 4, 4, false, true); Put("fountain", "Fountain", 0, .8f, 4.4f, true); Put("tree", "TreeBackLeft", -9, 8, 7, false, false, true); Put("tree", "TreeBackRight", 9, 9, 6.5f, false, false, true); Put("tree", "TreeRearLeft", -5.6f, 14, 4.8f, false, false, true); Put("tree", "TreeRearRight", 5.6f, 14, 4.8f, false, false, true); Put("tree", "TreeFrontLeft", -10, -4, 5, false, false, true); Put("lamp", "LampLeft", -5.2f, -.5f, 2.6f, false, false, true); Put("lamp", "LampRight", 5.2f, 1, 2.6f, false, false, true); Put("lamp", "LampForeground", 5.5f, -6, 2.7f, false, false, true); Put("bench", "BenchFrontLeft", -4.4f, -4, 2.2f, true, true, true); for (int i = 0; i < 12; i++) { var angle = (i * 30f) * Mathf.Deg2Rad; Put("bush", "FountainFlowers" + i, Mathf.Sin(angle) * 2.7f, .8f + Mathf.Cos(angle) * 2.7f, .55f, false, false, true); } foreach (var p in new[] { new Vector2(-7,-5), new Vector2(-5,-7), new Vector2(6,-6), new Vector2(8,-3), new Vector2(-9,2), new Vector2(9,2) }) Put("bush", "BorderFlowers", p.x, p.y, .9f, false, false, true); var camera = MakeCamera(scene, root.transform); Light(scene, root.transform); Background(root.transform, camera); SetLayer(root); RenderSettings.ambientMode = AmbientMode.Trilight; RenderSettings.ambientSkyColor = new Color(.68f, .75f, .82f); RenderSettings.ambientEquatorColor = new Color(.55f, .57f, .52f); RenderSettings.ambientGroundColor = new Color(.35f, .34f, .29f); PrefabUtility.SaveAsPrefabAsset(root, PrefabPath); EditorSceneManager.SaveScene(scene, ScenePath); Directory.CreateDirectory(Review); File.WriteAllText(Path.Combine(Review, "assembly.txt"), string.Join("\n", placements)); CaptureCamera(camera, 1080, 1920, "homecamp_portrait.png"); CaptureCamera(camera, 1080, 2400, "homecamp_tall.png"); return "Saved " + PrefabPath + " and " + ScenePath + "; " + placements.Count + " placements; portrait/tall rendered."; } finally { if (previous.IsValid() && previous.isLoaded) SceneManager.SetActiveScene(previous); EditorSceneManager.CloseScene(scene, true); } } static void CaptureCamera(Camera camera, int width, int height, string name) { Directory.CreateDirectory(Review); var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32); var old = RenderTexture.active; var oldTarget = camera.targetTexture; var oldAspect = camera.aspect; var oldAsync = ShaderUtil.allowAsyncCompilation; var texture = new Texture2D(width, height, TextureFormat.RGB24, false); try { ShaderUtil.allowAsyncCompilation = false; camera.aspect = (float)width / height; camera.targetTexture = rt; camera.Render(); camera.Render(); RenderTexture.active = rt; texture.ReadPixels(new Rect(0, 0, width, height), 0, 0); texture.Apply(); File.WriteAllBytes(Path.Combine(Review, name), texture.EncodeToPNG()); } finally { ShaderUtil.allowAsyncCompilation = oldAsync; camera.targetTexture = oldTarget; camera.aspect = oldAspect; RenderTexture.active = old; RenderTexture.ReleaseTemporary(rt); Object.DestroyImmediate(texture); } } public static string CaptureParts() => CaptureSet(Parts); public static string CaptureReuse() => CaptureSet(new[] { "tree", "lamp", "bench", "bush" }); static string CaptureSet(IEnumerable parts) { foreach (var part in parts) { var scene = EditorSceneManager.NewPreviewScene(); try { var path = Parts.Contains(part) ? PartPath(part) : ReusePath(part); var go = (GameObject)PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath(path), scene); SetLayer(go); Light(scene, null); var b = BoundsOf(go); var extent = Mathf.Max(b.size.x, Mathf.Max(b.size.y, b.size.z)); var camera = MakeCamera(scene, null); camera.orthographic = true; camera.orthographicSize = extent * .7f; camera.nearClipPlane = Mathf.Max(.00001f, extent * .01f); camera.farClipPlane = Mathf.Max(10, extent * 10); foreach (var direction in new[] { "front", "side", "back", "perspective" }) { var offset = direction == "front" ? new Vector3(0,.15f,-3) : direction == "side" ? new Vector3(3,.15f,0) : direction == "back" ? new Vector3(0,.15f,3) : new Vector3(2,1.8f,-3); camera.transform.position = b.center + offset * extent; camera.transform.LookAt(b.center); CaptureCamera(camera, 640, 640, part + "_" + direction + ".png"); } } finally { EditorSceneManager.ClosePreviewScene(scene); } } return "Rendered four views per component."; } public static string Verify() { var report = new List(); foreach (var part in Parts) { var p = AssetDatabase.LoadAssetAtPath(PartPath(part)); if (!p) throw new InvalidOperationException("Missing component " + part); var triangles = p.GetComponentsInChildren().Sum(f => (int)f.sharedMesh.GetIndexCount(0) / 3); var receipt = JsonUtility.FromJson(File.ReadAllText(Path.Combine(Repo, "Tools/output/homecamp_studio", part, "production.json"))); if (triangles > receipt.targetTriangles * 1.3f) throw new InvalidOperationException("Triangle budget " + part); foreach (var r in p.GetComponentsInChildren()) if (!r.sharedMaterial || r.sharedMaterial.shader.name != "Test/HomeCampXPbr") throw new InvalidOperationException("XPbr " + part); report.Add(part + "=" + triangles); } var root = AssetDatabase.LoadAssetAtPath(PrefabPath); if (!root || !File.Exists(ScenePath)) throw new InvalidOperationException("Missing assembled scene/prefab"); if (root.GetComponentsInChildren().Length == 0) throw new InvalidOperationException("Missing ground collider"); var preview = EditorSceneManager.NewPreviewScene(); var triangleCount = 0; try { var instance = (GameObject)PrefabUtility.InstantiatePrefab(root, preview); var ground = instance.transform.Find("Ground").gameObject; Physics.SyncTransforms(); for (int x=-8; x<=8; x+=4) for (int z=-8; z<=8; z+=4) GroundY(ground,x,z); triangleCount = instance.GetComponentsInChildren().Sum(f => (int)f.sharedMesh.GetIndexCount(0)/3); } finally { EditorSceneManager.ClosePreviewScene(preview); } var backdrop = root.transform.Find("DistantLakeWaterfallCastle"); if (!backdrop || backdrop.GetComponent()) throw new InvalidOperationException("Backdrop collider"); var bg = backdrop.GetComponent(); if (bg.sharedMaterial.shader.name != "Universal Render Pipeline/Unlit" || bg.shadowCastingMode != ShadowCastingMode.Off) throw new InvalidOperationException("Backdrop must be unlit without shadows"); if (EditorBuildSettings.scenes.Any(s => s.path == ScenePath)) throw new InvalidOperationException("Review scene unexpectedly added to build"); Directory.CreateDirectory(Review); File.WriteAllText(Path.Combine(Review, "verification.txt"), "PASS " + string.Join("; ", report) + "; assembledTriangles=" + triangleCount + "; ground collision 25/25; unlit quad backdrop; independent scene; cottage/Studio ground retained as review candidates and not placed"); return File.ReadAllText(Path.Combine(Review, "verification.txt")); } public static string OpenForReview() { var scene = SceneManager.GetSceneByPath(ScenePath); if (!scene.IsValid() || !scene.isLoaded) scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Additive); SceneManager.SetActiveScene(scene); var root = scene.GetRootGameObjects().Single(); Selection.activeGameObject = root; var camera = root.GetComponentInChildren(); if (SceneView.lastActiveSceneView != null) SceneView.lastActiveSceneView.AlignViewToObject(camera.transform); return "Opened HomeCamp additively; existing scenes preserved."; } } #endif